diff options
| author | mo khan <mo@mokhan.ca> | 2025-05-02 14:29:41 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-05-02 14:29:41 -0600 |
| commit | c583bcd1473205104a1e1af812ed4976d30c7baa (patch) | |
| tree | 933edf78a4ac8aea55256e42641e56bbb4c58834 /bin/ui | |
| parent | 91defaefca47e9cebbe92c6abf33c4423df9bc7d (diff) | |
refactor: remove anything unrelated to the authz daemon
Diffstat (limited to 'bin/ui')
| -rwxr-xr-x | bin/ui | 503 |
1 files changed, 0 insertions, 503 deletions
diff --git a/bin/ui b/bin/ui deleted file mode 100755 index 0c1e4c4c..00000000 --- a/bin/ui +++ /dev/null @@ -1,503 +0,0 @@ -#!/usr/bin/env ruby - -require "bundler/inline" - -gemfile do - source "https://rubygems.org" - - gem "base64", "~> 0.1" - gem "erb", "~> 4.0" - gem "net-hippie", "~> 1.0" - gem "rack", "~> 3.0" - gem "rack-session", "~> 2.0" - gem "rackup", "~> 2.0" - gem "saml-kit", "1.4.0" - gem "webrick", "~> 1.0" -end - -$scheme = ENV.fetch("SCHEME", "http") -$port = ENV.fetch("PORT", 8283).to_i -$host = ENV.fetch("HOST", "localhost:#{$port}") -$idp_host = ENV.fetch("IDP_HOST", "localhost:8282") - -Net::Hippie.logger = Logger.new($stdout, level: :debug) - -class OnDemandRegistry < Saml::Kit::DefaultRegistry - def metadata_for(entity_id) - found = super(entity_id) - return found if found - - register_url(entity_id, verify_ssl: false) - super(entity_id) - end -end - -Saml::Kit.configure do |x| - x.entity_id = "#{$scheme}://#{$host}/saml/metadata.xml" - x.registry = OnDemandRegistry.new - x.logger = Logger.new("/dev/stderr") -end - -module OAuth - class Client - attr_reader :client_id, :client_secret, :http, :authz_host - - def initialize(authz_host, client_id, client_secret) - @authz_host = authz_host - @client_id = client_id - @client_secret = client_secret - @http = Net::Hippie::Client.new(headers: ::Net::Hippie::Client::DEFAULT_HEADERS.merge({ - 'Authorization' => Net::Hippie.basic_auth(client_id, client_secret), - })) - end - - def [](key) - server_metadata.fetch(key) - end - - def authorize_uri(redirect_uri:, state: SecureRandom.uuid, response_type: "code", response_mode: "query", scope: "openid") - [ - self[:authorization_endpoint], - to_query( - client_id: client_id, - state: state, - redirect_uri: redirect_uri, - response_mode: response_mode, - response_type: response_type, - scope: scope, - ) - ].join("?") - end - - def exchange(grant_type, params = {}) - with_http do |client| - client.post(self[:token_endpoint], body: body_for(grant_type, params)) - end - end - - private - - def body_for(grant_type, params) - case grant_type - when "authorization_code" - { - grant_type: grant_type, - code: params.fetch(:code), - code_verifier: params.fetch(:code_verifier, "not_implemented"), - } - when "urn:ietf:params:oauth:grant-type:saml2-bearer" - { - grant_type: grant_type, - assertion: params.fetch(:assertion), - } - else - raise NotImplementedError.new(grant_type) - end - end - - def to_query(params = {}) - params.map do |(key, value)| - [key, value].join("=") - end.join("&") - end - - def with_http - http.with_retry do |client| - yield client - end - end - - def server_metadata - @server_metadata ||= - with_http do |client| - response = client.get("http://#{authz_host}/.well-known/oauth-authorization-server") - JSON.parse(response.body, symbolize_names: true) - end - end - end -end - -module HTTPHelpers - def current_user?(request) - request.session[:access_token] - end - - def not_found - [404, { 'X-Backend-Server' => 'UI' }, []] - end - - def redirect_to(location) - if location.start_with?("http") - [302, { 'Location' => location }, []] - else - [302, { 'Location' => "#{$scheme}://#{$host}#{location}" }, []] - end - end - - def with_layout(bind) - template = <<~ERB - <!doctype html> - <html lang="en"> - <head> - <title>UI</title> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <meta name="color-scheme" content="light dark"> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.colors.min.css"> - </head> - <body> - <main class="container"> - #{yield} - </main> - </body> - </html> - ERB - ERB.new(template, trim_mode: '-').result(bind) - end -end - -class UI - include ::HTTPHelpers - - attr_reader :oauth_client - - def initialize(oauth_client) - @oauth_client = oauth_client - end - - def call(env) - request = Rack::Request.new(env) - case request.request_method - when Rack::GET - case request.path - when "/index.html" - return get_index(request) - when "/groups.html" - if current_user?(request) - return get_groups(request) - else - return redirect_to("/oidc/new") - end - when /\A\/groups\/\d+\/projects.html\z/ - if current_user?(request) - return get_projects(request) - else - return redirect_to("/oidc/new") - end - when "/oauth/callback" - return oauth_callback(Rack::Request.new(env)) - when "/oidc/new" - return redirect_to(oauth_client.authorize_uri( - redirect_uri: "#{request.base_url}/oauth/callback" - )) - when "/saml/metadata.xml" - return metadata - when "/saml/new" - return saml_post_to_idp(Rack::Request.new(env)) - else - return redirect_to("/index.html") - end - when Rack::POST - case request.path - when "/logout" - request.session.delete(:access_token) - request.session.delete(:id_token) - request.session.delete(:refresh_token) - return redirect_to("/") - when "/saml/assertions" - return saml_assertions(Rack::Request.new(env)) - else - return not_found - end - end - not_found - end - - private - - def metadata - xml = Saml::Kit::Metadata.build_xml do |builder| - builder.embed_signature = false - builder.contact_email = 'ui@example.com' - builder.organization_name = "Acme, Inc" - builder.organization_url = "https://example.com" - builder.build_service_provider do |x| - x.name_id_formats = [Saml::Kit::Namespaces::PERSISTENT] - x.add_assertion_consumer_service("#{$scheme}://#{$host}/saml/assertions", binding: :http_post) - end - end - - [200, { 'Content-Type' => "application/samlmetadata+xml" }, [xml]] - end - - def get_index(request) - html = with_layout(binding) do - <<~ERB - <%- if current_user?(request) -%> - <nav> - <ul> - <li><a href="http://#{$idp_host}/">IdP</a></li> - <li><strong>UI</strong></li> - </ul> - <ul> - <li><a href="/index.html">Home</a></li> - <li><a href="/groups.html">Groups</a></li> - <li> - <form action="/logout" method="post"> - <input type="submit" value="Logout" /> - </form> - </li> - </ul> - </nav> - <h1>Access Token</h1> - <pre><%= request.session[:access_token] %></pre> - - <h1>ID Token</h1> - <pre><%= request.session[:id_token] %></pre> - <%- else -%> - <nav> - <ul> - <li><a href="http://#{$idp_host}/">IdP</a></li> - <li><strong>UI</strong></li> - </ul> - <ul> - <li><a href="/saml/new">SAML Login</a></li> - <li><a href="/oidc/new">OIDC Login</a></li> - </ul> - </nav> - <%- end -%> - ERB - end - [200, { "Content-Type" => "text/html" }, [html]] - end - - def oauth_callback(request) - response = oauth_client.exchange("authorization_code", code: request.params['code']) - if response.code == "200" - tokens = JSON.parse(response.body, symbolize_names: true) - request.session[:access_token] = tokens[:access_token] - request.session[:id_token] = tokens[:id_token] - request.session[:refresh_token] = tokens[:access_token] - - html = with_layout(binding) do - <<~ERB - <nav> - <ul> - <li><a href="http://#{$idp_host}/">IdP</a></li> - <li><strong>UI</strong></li> - </ul> - <ul> - <li><a href="/index.html">Home</a></li> - <li><a href="/groups.html">Groups</a></li> - <li> - <form action="/logout" method="post"> - <input type="submit" value="Logout" /> - </form> - </li> - </ul> - </nav> - <h1>Access Token</h1> - <pre style="display: none;"><%= response.body %></pre> - <pre><%= JSON.pretty_generate(request.session[:access_token]) %></pre> - ERB - end - [200, { 'Content-Type' => "text/html" }, [html]] - else - [response.code, response.header, [response.body]] - end - end - - def get_groups(request) - http = Net::Hippie::Client.new(headers: ::Net::Hippie::Client::DEFAULT_HEADERS.merge({ - 'Authorization' => Net::Hippie.bearer_auth(request.session[:access_token]) - })) - - response = http.get("http://api.example.com:8080/groups.json") - if response.code == "200" - groups = JSON.parse(response.body, symbolize_names: true) - html = with_layout(binding) do - <<~ERB - <nav> - <ul> - <li><a href="http://#{$idp_host}/">IdP</a></li> - <li><strong>UI</strong></li> - </ul> - <ul> - <li><a href="/index.html">Home</a></li> - <li><a href="/groups.html">Groups</a></li> - <li> - <form action="/logout" method="post"> - <input type="submit" value="Logout" /> - </form> - </li> - </ul> - </nav> - - <h1>Groups</h1> - <table> - <thead> - <tr> - <th>ID</th> - <th>Name</th> - <th>Organization ID</th> - <th>Parent ID</th> - <th> </th> - </tr> - </thead> - <tbody> - <%- groups.each do |group| -%> - <tr> - <td><%= group[:id] %></td> - <td><%= group[:name] %></td> - <td><%= group[:organization_id] %></td> - <td><%= group[:parent_id] %></td> - <td><a href="/groups/<%= group[:id] %>/projects.html">Projects</a></td> - </tr> - <%- end -%> - </tbody> - </table> - ERB - end - [200, { 'Content-Type' => "text/html" }, [html]] - else - [response.code, response.header, [response.body]] - end - end - - def get_projects(request) - http = Net::Hippie::Client.new(headers: ::Net::Hippie::Client::DEFAULT_HEADERS.merge({ - 'Authorization' => Net::Hippie.bearer_auth(request.session[:access_token]) - })) - - response = http.get("http://api.example.com:8080/projects.json") - if response.code == "200" - projects = JSON.parse(response.body, symbolize_names: true) - - html = with_layout(binding) do - <<~ERB - <nav> - <ul> - <li><a href="http://#{$idp_host}/">IdP</a></li> - <li><strong>UI</strong></li> - </ul> - <ul> - <li><a href="/index.html">Home</a></li> - <li><a href="/groups.html">Groups</a></li> - <li> - <form action="/logout" method="post"> - <input type="submit" value="Logout" /> - </form> - </li> - </ul> - </nav> - - <h1>Projects</h1> - <table> - <thead> - <tr> - <th>Name</th> - <th>Group ID</th> - </tr> - </thead> - <tbody> - <%- projects.each do |project| -%> - <tr> - <td><%= project[:name] %></td> - <td><%= project[:group_id] %></td> - </tr> - <%- end -%> - </tbody> - </table> - ERB - end - [200, { 'Content-Type' => "text/html" }, [html]] - else - [response.code, response.header, [response.body]] - end - end - - def saml_post_to_idp(request) - idp = Saml::Kit.registry.metadata_for("http://#{$idp_host}/saml/metadata.xml") - relay_state = Base64.strict_encode64(JSON.generate(redirect_to: '/dashboard')) - - @saml_builder = nil - uri, saml_params = idp.login_request_for(binding: :http_post, relay_state: relay_state) do |builder| - @saml_builder = builder - end - - html = with_layout(binding) do - <<~ERB - <h2>Sending SAML Request (SP -> IdP)</h2> - <textarea readonly="readonly" disabled="disabled" cols=225 rows=8><%=- @saml_builder.to_xml(pretty: true) -%></textarea> - - <form id="idp-form" action="<%= uri %>" method="post"> - <%- saml_params.each do |(key, value)| -%> - <input type="hidden" name="<%= key %>" value="<%= value %>" /> - <%- end -%> - <input id="submit-button" type="submit" value="Continue" /> - </form> - ERB - end - [200, { 'Content-Type' => "text/html" }, [html]] - end - - def saml_assertions(request) - sp = Saml::Kit.registry.metadata_for("#{request.base_url}/saml/metadata.xml") - saml_binding = sp.assertion_consumer_service_for(binding: :http_post) - saml_response = saml_binding.deserialize(request.params) - raise saml_response.errors unless saml_response.valid? - - assertion = Base64.strict_encode64(saml_response.assertion.to_xml) - response = oauth_client.exchange( - "urn:ietf:params:oauth:grant-type:saml2-bearer", - assertion: assertion, - ) - if response.code == "200" - tokens = JSON.parse(response.body, symbolize_names: true) - request.session[:access_token] = tokens[:access_token] - request.session[:refresh_token] = tokens[:access_token] - - html = with_layout(binding) do - <<~ERB - <nav> - <ul> - <li><a href="http://#{$idp_host}/">IdP</a></li> - <li><strong>UI</strong></li> - </ul> - <ul> - <li><a href="/index.html">Home</a></li> - <li><a href="/groups.html">Groups</a></li> - <li> - <form action="/logout" method="post"> - <input type="submit" value="Logout" /> - </form> - </li> - </ul> - </nav> - - <h2>Received SAML Response</h2> - <textarea readonly="readonly" disabled="disabled" cols=220 rows=40><%=- saml_response.to_xml(pretty: true) -%></textarea> - <pre id="raw-saml-response" style="display: none;"><%= request.params["SAMLResponse"] %></pre> - <pre id="xml-saml-assertion" style="display: none;"><%= saml_response.assertion.to_xml(pretty: true) %></pre> - <pre id="access-token" style="display: none;"><%= JSON.pretty_generate(request.session[:access_token]) %></pre> - ERB - end - [200, { 'Content-Type' => "text/html" }, [html]] - else - [response.code, response.header, [response.body]] - end - end -end - -if __FILE__ == $0 - app = Rack::Builder.new do - use Rack::CommonLogger - use Rack::Reloader - use Rack::Session::Cookie, { domain: $host.split(":", 2)[0], path: "/", secret: SecureRandom.hex(64) } - - run UI.new(::OAuth::Client.new($idp_host, 'client_id', 'client_secret')) - end.to_app - - Rackup::Server.start(app: app, Port: $port) -end |
