summaryrefslogtreecommitdiff
path: root/bin/ui
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-05-02 14:29:41 -0600
committermo khan <mo@mokhan.ca>2025-05-02 14:29:41 -0600
commitc583bcd1473205104a1e1af812ed4976d30c7baa (patch)
tree933edf78a4ac8aea55256e42641e56bbb4c58834 /bin/ui
parent91defaefca47e9cebbe92c6abf33c4423df9bc7d (diff)
refactor: remove anything unrelated to the authz daemon
Diffstat (limited to 'bin/ui')
-rwxr-xr-xbin/ui503
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>&nbsp;</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