main
  1#!/usr/bin/env ruby
  2
  3require "bundler/inline"
  4
  5gemfile do
  6  source "https://rubygems.org"
  7
  8  gem "base64", "~> 0.1"
  9  gem "bcrypt", "~> 3.0"
 10  gem "csv", "~> 3.0"
 11  gem "declarative_policy", "~> 1.0"
 12  gem "erb", "~> 4.0"
 13  gem "globalid", "~> 1.0"
 14  gem "google-protobuf", "~> 3.0"
 15  gem "rack", "~> 3.0"
 16  gem "rack-session", "~> 2.0"
 17  gem "rackup", "~> 2.0"
 18  gem "saml-kit", "1.4.0"
 19  gem "twirp", "~> 1.0"
 20  gem "warden", "~> 1.0"
 21  gem "webrick", "~> 1.0"
 22end
 23
 24lib_path = Pathname.new(__FILE__).parent.parent.join('lib').realpath.to_s
 25$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
 26
 27require 'authx/rpc'
 28
 29$scheme = ENV.fetch("SCHEME", "http")
 30$port = ENV.fetch("PORT", 8282).to_i
 31$host = ENV.fetch("HOST", "localhost:#{$port}")
 32
 33DeclarativePolicy.configure do
 34  name_transformation do |name|
 35    "::Authz::#{name}Policy"
 36  end
 37end
 38
 39Warden::Manager.serialize_into_session do |user|
 40  user.id
 41end
 42
 43Warden::Manager.serialize_from_session do |id|
 44  ::Authn::User.find(id)
 45end
 46
 47Warden::Strategies.add(:password) do
 48  def valid?
 49    params['username'] && params['password']
 50  end
 51
 52  def authenticate!
 53    user = ::Authn::User.login(params.transform_keys(&:to_sym))
 54    user.nil? ? fail!("Could not log in") : success!(user)
 55  end
 56end
 57
 58module HTTPHelpers
 59  def current_user?(request)
 60    request.env['warden'].authenticated?
 61  end
 62
 63  def current_user(request)
 64    request.env['warden'].user
 65  end
 66
 67  def default_headers
 68    {
 69      'X-Powered-By' => 'IdP'
 70    }
 71  end
 72
 73  def http_not_found
 74    [404, default_headers, []]
 75  end
 76
 77  def http_ok(headers = {}, body = nil)
 78    [200, default_headers.merge(headers), [body]]
 79  end
 80
 81  def http_redirect_to(location)
 82    [302, { 'Location' => "#{$scheme}://#{$host}#{location}" }, []]
 83  end
 84end
 85
 86module Authn
 87  class User
 88    include ::BCrypt
 89
 90    class << self
 91      def all
 92        @all ||= ::CSV.read(File.join(__dir__, "../db/idp/users.csv"), headers: true).map do |row|
 93          new(row.to_h.transform_keys(&:to_sym))
 94        end
 95      end
 96
 97      def find(id)
 98        all.find do |user|
 99          user[:id] == id
100        end
101      end
102
103      def find_by
104        all.find do |user|
105          yield user
106        end
107      end
108
109      def find_by_email(email)
110        find_by do |user|
111          user[:email] == email
112        end
113      end
114
115      def find_by_username(username)
116        find_by do |user|
117          user[:username] == username
118        end
119      end
120
121      def login(params = {})
122        user = find_by_username(params[:username])
123        user&.valid_password?(params[:password]) ? user : nil
124      end
125    end
126
127    attr_reader :id
128
129    def initialize(attributes)
130      @attributes = attributes
131      @id = self[:id]
132    end
133
134    def [](attribute)
135      @attributes.fetch(attribute.to_sym)
136    end
137
138    def name_id_for(name_id_format)
139      if name_id_format == Saml::Kit::Namespaces::EMAIL_ADDRESS
140        self[:email]
141      else
142        self[:id]
143      end
144    end
145
146    def create_access_token
147      ::Authz::JWT.new(
148        sub: to_global_id.to_s,
149        auth_time: Time.now.to_i,
150        email: self[:email],
151        username: self[:username],
152      )
153    end
154
155    def create_id_token
156      ::Authz::JWT.new(sub: to_global_id.to_s)
157    end
158
159    def assertion_attributes_for(request)
160      {
161        email: self[:email],
162      }
163    end
164
165    def valid_password?(entered_password)
166      ::BCrypt::Password.new(self[:password_digest]) == entered_password
167    end
168
169    def to_global_id
170      ::GlobalID.new(
171        ::URI::GID.build(
172          app: "example",
173          model_name: "User",
174          model_id: id,
175          params: {}
176        )
177      ).to_s
178    end
179  end
180
181  class OnDemandRegistry < Saml::Kit::DefaultRegistry
182    def metadata_for(entity_id)
183      found = super(entity_id)
184      return found if found
185
186      register_url(entity_id, verify_ssl: false)
187      super(entity_id)
188    end
189  end
190
191  class SessionsController
192    include ::HTTPHelpers
193
194    def call(env)
195      request = Rack::Request.new(env)
196      case request.request_method
197      when Rack::GET
198        case request.path
199        when '/sessions/new'
200          return get_login(request)
201        end
202      when Rack::POST
203        case request.path
204        when '/sessions'
205          if (user = env['warden'].authenticate(:password))
206            path = request.params["redirect_back"] ? request.params["redirect_back"] : "/"
207            return http_redirect_to(path)
208          else
209            return http_redirect_to("/sessions/new")
210          end
211        when '/sessions/delete'
212          request.env['warden'].logout
213          return http_redirect_to('/')
214        end
215      end
216
217      http_not_found
218    end
219
220    private
221
222    def get_login(request)
223      template = <<~ERB
224      <!doctype html>
225      <html lang="en" data-theme="light">
226        <head>
227          <title>IdP</title>
228          <meta charset="utf-8">
229          <meta name="viewport" content="width=device-width, initial-scale=1">
230          <meta name="color-scheme" content="light dark">
231          <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
232        </head>
233        <body>
234          <main class="container">
235            <nav>
236              <ul>
237                <li><strong>IdP</strong></li>
238                <li><a href="http://ui.example.com:8080/">UI</a></li>
239              </ul>
240            </nav>
241
242            <form id="login-form" action="/sessions" method="post">
243              <fieldset>
244                <label>
245                  Username
246                  <input type="input" placeholder="Username" id="username" name="username" value="" />
247                </label>
248                <label>
249                  Password
250                  <input type="password" placeholder="Password" id="password" name="password" value="" />
251                </label>
252              </fieldset>
253
254              <input type="hidden" name="redirect_back" value="<%= request.params["redirect_back"] %>" />
255              <input type="submit" id="login-button" value="Login" />
256            </form>
257          </main>
258        </body>
259      </html>
260      ERB
261      html = ERB.new(template, trim_mode: '-').result(binding)
262      [200, { 'Content-Type' => "text/html" }, [html]]
263    end
264  end
265
266  class SAMLController
267    include ::HTTPHelpers
268
269    def initialize(scheme, host)
270      Saml::Kit.configure do |x|
271        x.entity_id = "#{$scheme}://#{$host}/saml/metadata.xml"
272        x.registry = OnDemandRegistry.new
273        x.logger = Logger.new("/dev/stderr")
274      end
275
276      @saml_metadata = Saml::Kit::Metadata.build do |builder|
277        builder.contact_email = 'hi@example.com'
278        builder.organization_name = "Acme, Inc"
279        builder.organization_url = "#{scheme}://#{host}"
280        builder.build_identity_provider do |x|
281          x.add_single_sign_on_service("#{scheme}://#{host}/saml/new", binding: :http_post)
282          x.name_id_formats = [Saml::Kit::Namespaces::PERSISTENT, Saml::Kit::Namespaces::EMAIL_ADDRESS]
283          x.attributes << :email
284        end
285      end
286    end
287
288    def call(env)
289      request = Rack::Request.new(env)
290      case request.request_method
291      when Rack::GET
292        case request.path
293        when "/saml/continue"
294          if current_user?(request)
295            saml_params = request.session[:saml_params]
296            return saml_post_back(request, current_user(request), saml_params)
297          else
298            return http_redirect_to("/sessions/new?redirect_back=/saml/continue")
299          end
300        when "/saml/metadata.xml"
301          return http_ok(
302            { 'Content-Type' => "application/samlmetadata+xml" },
303            saml_metadata.to_xml(pretty: true)
304          )
305        end
306      when Rack::POST
307        case request.path
308        when "/saml/new"
309          saml_params = saml_params_from(request)
310
311          if current_user?(request)
312            return saml_post_back(request, current_user(request), saml_params)
313          else
314            request.session[:saml_params] = saml_params
315            return http_redirect_to("/sessions/new?redirect_back=/saml/continue")
316          end
317        end
318      end
319
320      http_not_found
321    end
322
323    private
324
325    attr_reader :saml_metadata
326
327    def saml_post_back(request, user, saml_params)
328      saml_request = binding_for(request).deserialize(saml_params)
329
330      @builder = nil
331      url, saml_params = saml_request.response_for(
332        user,
333        binding: :http_post,
334        relay_state: saml_params[:RelayState]
335      ) { |builder| @builder = builder }
336      template = <<~ERB
337      <!doctype html>
338      <html lang="en" data-theme="light">
339        <head>
340          <title>IdP</title>
341          <meta charset="utf-8">
342          <meta name="viewport" content="width=device-width, initial-scale=1">
343          <meta name="color-scheme" content="light dark">
344          <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
345        </head>
346        <body>
347          <main class="container">
348            <nav>
349              <ul>
350                <li><strong>IdP</strong></li>
351                <li><a href="http://ui.example.com:8080/">UI</a></li>
352              </ul>
353            </nav>
354
355            <h2>Recieved SAML Request</h2>
356            <textarea readonly="readonly" disabled="disabled" cols=225 rows=8><%=- saml_request.to_xml(pretty: true) -%></textarea>
357
358            <h2>Sending SAML Response (IdP -> SP)</h2>
359            <textarea readonly="readonly" disabled="disabled" cols=225 rows=32><%=- @builder.build.to_xml(pretty: true) -%></textarea>
360            <form id="postback-form" action="<%= url %>" method="post">
361              <%- saml_params.each do |(key, value)| -%>
362                <input type="hidden" name="<%= key %>" value="<%= value %>" />
363              <%- end -%>
364              <input id="submit-button" type="submit" value="Continue" />
365            </form>
366          </main>
367        </body>
368      </html>
369      ERB
370      erb = ERB.new(template, trim_mode: '-')
371      html = erb.result(binding)
372      [200, { 'Content-Type' => "text/html" }, [html]]
373    end
374
375    def saml_params_from(request)
376      if request.post?
377        {
378          "SAMLRequest" => request.params["SAMLRequest"],
379          "RelayState" => request.params["RelayState"],
380        }
381      else
382        query_string = request.query_string
383        on = query_string.include?("&amp;") ? "&amp;" : "&"
384        Hash[query_string.split(on).map { |x| x.split("=", 2) }].symbolize_keys
385      end
386    end
387
388    def binding_for(request)
389      Saml::Kit::Bindings::HttpPost.new(location: "#{$scheme}://#{$host}/saml/new")
390    end
391  end
392end
393
394class Organization
395  class << self
396    def find(id)
397      new
398    end
399  end
400end
401
402module Authz
403  class OrganizationPolicy < DeclarativePolicy::Base
404    condition(:owner) { true }
405
406    rule { owner }.enable :read_project
407    rule { owner }.enable :read_group
408    rule { owner }.enable :create_project
409  end
410
411  class JWT
412    class << self
413      # TODO:: validate signature
414      def decode(encoded)
415        _header, body, _signature = encoded
416          .split('.', 3)
417          .map { |x| JSON.parse(Base64.strict_decode64(x), symbolize_names: true) rescue {} }
418        new(body)
419      end
420    end
421
422    attr_reader :claims
423
424    def initialize(claims)
425      now = Time.now.to_i
426      @claims = {
427        iss: "#{$scheme}://#{$host}",
428        iat: now,
429        aud: "",
430        nbf: now,
431        jti: SecureRandom.uuid,
432        exp: now + 3600,
433      }.merge(claims)
434    end
435
436    def [](claim)
437      claims.fetch(claim)
438    end
439
440    def active?
441      now = Time.now.to_i
442      self[:nbf] <= now && now < self[:exp]
443    end
444
445    def to_jwt
446      [
447        Base64.strict_encode64(JSON.generate(alg: "none")),
448        Base64.strict_encode64(JSON.generate(claims)),
449        ""
450      ].join(".")
451    end
452  end
453
454  module Rpc
455    class Ability
456      def allowed(request, env)
457        {
458          result: can?(request)
459        }
460      end
461
462      private
463
464      def can?(request)
465        subject = subject_of(request.subject)
466        resource = resource_from(request.resource)
467        permission = request.permission.to_sym
468
469        policy = DeclarativePolicy.policy_for(subject, resource)
470        policy.can?(permission)
471      rescue StandardError => error
472        puts error.inspect
473        false
474      end
475
476      def subject_of(encoded_token)
477        token = ::Authz::JWT.decode(encoded_token)
478        token&.claims[:sub]
479      end
480
481      def resource_from(global_id)
482        GlobalID::Locator.locate(global_id)
483      end
484    end
485  end
486
487  class AuthorizationGrant
488    class << self
489      def all
490        @all ||= []
491      end
492
493      def find_by(params)
494        case params[:grant_type]
495        when 'authorization_code'
496          authorization_code_grant(params[:code], params[:code_verifier])
497        when 'refresh_token'
498          refresh_grant(params[:refresh_token])
499        when 'client_credentials'
500          client_credentials_grant(params)
501        when 'password'
502          password_grant(params[:username], params[:password])
503        when "urn:ietf:params:oauth:grant-type:saml2-bearer" # RFC-7522
504          saml_assertion_grant(params[:assertion])
505        when 'urn:ietf:params:oauth:grant-type:jwt-bearer' # RFC7523
506          jwt_bearer_grant(params)
507        end
508      end
509
510      # TODO:: implement `code_verifier` param
511      def authorization_code_grant(code, code_verifier)
512        all.find do |grant|
513          grant.active? && grant.code == code
514        end
515      end
516
517      def refresh_grant(refresh_token)
518        raise NotImplementedError
519      end
520
521      def client_credential_grant(params)
522        raise NotImplementedError
523      end
524
525      def password_grant(username, password)
526        raise NotImplementedError
527      end
528
529      def saml_assertion_grant(encoded_saml_assertion)
530        xml = Base64.decode64(encoded_saml_assertion)
531        # TODO:: Validate signature and prevent assertion reuse
532        saml_assertion = Saml::Kit::Document.to_saml_document(xml)
533
534        user = ::Authn::User.find_by_email(saml_assertion.name_id) ||
535          ::Authn::User.find(saml_assertion.name_id)
536        new(user, saml_assertion: saml_assertion)
537      end
538
539      def jwt_bearer_grant(params)
540        raise NotImplementedError
541      end
542
543      def create!(user, params = {})
544        new(user, params).tap do |grant|
545          all << grant
546        end
547      end
548    end
549
550    attr_reader :code, :user, :params
551
552    def initialize(user, params = {})
553      @user = user
554      @params = params
555      @code = SecureRandom.uuid
556      @exchanged_at = nil
557    end
558
559    def active?
560      @exchanged_at.nil?
561    end
562
563    def inactive?
564      !active?
565    end
566
567    def create_access_token
568      raise "Invalid code" unless active?
569
570      user.create_access_token.tap do
571        @exchanged_at = Time.now
572      end
573    end
574
575    def exchange
576      {
577        access_token: create_access_token.to_jwt,
578        token_type: "Bearer",
579        issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
580        expires_in: 3600,
581        refresh_token: SecureRandom.hex(32)
582      }.tap do |body|
583        if params["scope"]&.include?("openid")
584          body[:id_token] = user.create_id_token.to_jwt
585        end
586      end
587    rescue StandardError => error
588      {
589        error: error.message,
590        error_description: error.backtrace,
591      }
592    end
593  end
594
595  class OAuthController
596    include ::HTTPHelpers
597
598    def call(env)
599      request = Rack::Request.new(env)
600
601      case request.request_method
602      when Rack::GET
603        case request.path
604        when "/oauth/authorize/continue"
605          if current_user?(request)
606            return get_authorize(request.session[:oauth_params])
607          end
608        when "/oauth/authorize" # RFC-6749
609          oauth_params = request.params.slice('client_id', 'scope', 'redirect_uri', 'response_mode', 'response_type', 'state', 'code_challenge_method', 'code_challenge')
610          if current_user?(request)
611            return get_authorize(oauth_params)
612          else
613            request.session[:oauth_params] = oauth_params
614            return http_redirect_to("/sessions/new?redirect_back=/oauth/authorize/continue")
615          end
616        else
617          return http_not_found
618        end
619      when Rack::POST
620        case request.path
621        when "/oauth/authorize" # RFC-6749
622          return post_authorize(request)
623        when "/oauth/introspect" # RFC-7662
624          params = request.content_type == "application/json" ? JSON.parse(request.body.read, symbolize_names: true) : Hash[URI.decode_www_form(request.body.read)].transform_keys(&:to_sym)
625          return post_introspect(params.slice(:token, :token_type_hint))
626        when "/oauth/token" # RFC-6749
627          params = request.content_type == "application/json" ? JSON.parse(request.body.read, symbolize_names: true) : Hash[URI.decode_www_form(request.body.read)].transform_keys(&:to_sym)
628          grant = AuthorizationGrant.find_by(params)
629
630          return [404, { "Content-Type" => "application/json" }, [JSON.pretty_generate(error: 404, error_description: "Not Found")]] if grant.nil? || grant.inactive?
631          return [200, { "Content-Type" => "application/json" }, [JSON.pretty_generate(grant.exchange)]]
632        when "/oauth/revoke" # RFC-7009
633          # TODO:: Revoke the JWT token and make it ineligible for usage
634          return http_not_found
635        else
636          return http_not_found
637        end
638      end
639      http_not_found
640    end
641
642    private
643
644    def post_introspect(params)
645      token = ::Authz::JWT.decode(params[:token])
646      return [200, { "Content-Type" => "application/json" }, [JSON.pretty_generate(token.claims.merge(active: token.active?))]]
647    end
648
649    def get_authorize(oauth_params)
650      template = <<~ERB
651      <!doctype html>
652      <html lang="en" data-theme="light">
653        <head>
654          <title>IdP</title>
655          <meta charset="utf-8">
656          <meta name="viewport" content="width=device-width, initial-scale=1">
657          <meta name="color-scheme" content="light dark">
658          <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
659        </head>
660        <body>
661          <main class="container">
662            <nav>
663              <ul>
664                <li><strong>IdP</strong></li>
665                <li><a href="http://ui.example.com:8080/">UI</a></li>
666              </ul>
667            </nav>
668
669            <h1>Authorize?</h1>
670            <p>Client ID: <%= oauth_params['client_id'] %></p>
671            <form id="authorize-form" action="/oauth/authorize" method="post">
672              <input type="hidden" name="client_id" value="<%= oauth_params['client_id'] %>" />
673              <input type="hidden" name="scope" value="<%= oauth_params['scope'] %>" />
674              <input type="hidden" name="redirect_uri" value="<%= oauth_params['redirect_uri'] %>" />
675              <input type="hidden" name="response_mode" value="<%= oauth_params['response_mode'] %>" />
676              <input type="hidden" name="response_type" value="<%= oauth_params['response_type'] %>" />
677              <input type="hidden" name="state" value="<%= oauth_params['state'] %>" />
678              <input type="hidden" name="code_challenge_method" value="<%= oauth_params['code_challenge_method'] %>" />
679              <input type="hidden" name="code_challenge" value="<%= oauth_params['code_challenge'] %>" />
680              <input id="submit-button" type="submit" value="Authorize" />
681            </form>
682          </main>
683        </body>
684      </html>
685      ERB
686      html = ERB.new(template, trim_mode: '-').result(binding)
687      [200, { 'Content-Type' => "text/html" }, [html]]
688    end
689
690    def post_authorize(request)
691      params = request.params.slice('client_id', 'redirect_uri', 'response_type', 'response_mode', 'state', 'code_challenge_method', 'code_challenge', 'scope')
692      grant = AuthorizationGrant.create!(current_user(request), params)
693      case params['response_type']
694      when 'code'
695        case params['response_mode']
696        when 'fragment'
697          return [302, { 'Location' => "#{params['redirect_uri']}#code=#{grant.code}&state=#{params['state']}" }, []]
698        when 'query'
699          return [302, { 'Location' => "#{params['redirect_uri']}?code=#{grant.code}&state=#{params['state']}" }, []]
700        else
701          # TODO:: form post
702        end
703      when 'token'
704        return http_not_found
705      else
706        return http_not_found
707      end
708    end
709  end
710end
711
712class IdentityProvider
713  include ::HTTPHelpers
714
715  def call(env)
716    request = Rack::Request.new(env)
717
718    case request.request_method
719    when Rack::GET
720      case request.path
721      when '/'
722        if current_user?(request)
723          return get_dashboard(request)
724        else
725          return http_redirect_to("/sessions/new")
726        end
727      when '/.well-known/openid-configuration'
728        return openid_metadata
729      when '/.well-known/oauth-authorization-server'
730        return oauth_metadata
731      when '/.well-known/webfinger' # RFC-7033
732        return http_not_found
733      else
734        return http_not_found
735      end
736    end
737    http_not_found
738  end
739
740  private
741
742  def get_dashboard(request)
743    template = <<~ERB
744      <!doctype html>
745      <html lang="en" data-theme="light">
746        <head>
747          <title>IdP</title>
748          <meta charset="utf-8">
749          <meta name="viewport" content="width=device-width, initial-scale=1">
750          <meta name="color-scheme" content="light dark">
751          <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
752        </head>
753        <body>
754          <main class="container">
755            <nav>
756              <ul>
757                <li><strong>IdP</strong></li>
758                <li><a href="http://ui.example.com:8080/">UI</a></li>
759              </ul>
760              <ul>
761                <li><a href="/">Home</a></li>
762                <li><a href="http://ui.example.com:8080/groups.html">Groups</a></li>
763                <li>
764                  <form action="/sessions/delete" method="post">
765                    <input type="submit" value="logout" />
766                  </form>
767                </li>
768              </ul>
769            </nav>
770
771            <h1> Hello, <%= current_user(request)[:username] %></h1>
772          </main>
773        </body>
774      </html>
775    ERB
776    erb = ERB.new(template, trim_mode: '-')
777    html = erb.result(binding)
778    [200, { 'Content-Type' => "text/html" }, [html]]
779  end
780
781  # GET /.well-known/oauth-authorization-server
782  def oauth_metadata
783    [200, { 'Content-Type' => "application/json" }, [JSON.pretty_generate({
784      issuer: "#{$scheme}://#{$host}/.well-known/oauth-authorization-server",
785      authorization_endpoint: "#{$scheme}://#{$host}/oauth/authorize",
786      token_endpoint: "#{$scheme}://#{$host}/oauth/token",
787      jwks_uri: "", # RFC-7517
788      registration_endpoint: "", # RFC-7591
789      scopes_supported: ["openid", "profile", "email"],
790      response_types_supported: ["code", "code id_token", "id_token", "token id_token"],
791      response_modes_supported: ["query", "fragment", "form_post"],
792      grant_types_supported: ["authorization_code", "implicit"], # RFC-7591
793      token_endpoint_auth_methods_supported: ["client_secret_basic"], # RFC-7591
794      token_endpoint_auth_signing_alg_values_supported: ["RS256"],
795      service_documentation: "",
796      ui_locales_supported: ["en-US"],
797      op_policy_uri: "",
798      op_tos_uri: "",
799      revocation_endpoint: "#{$scheme}://#{$host}/oauth/revoke", # RFC-7009
800      revocation_endpoint_auth_methods_supported: ["client_secret_basic"],
801      revocation_endpoint_auth_signing_alg_values_supported: ["RS256"],
802      introspection_endpoint: "#{$scheme}://#{$host}/oauth/introspect", # RFC-7662
803      introspection_endpoint_auth_methods_supported: ["client_secret_basic"],
804      introspection_endpoint_auth_signing_alg_values_supported: ["RS256"],
805      code_challenge_methods_supported: [], # RFC-7636
806    })]]
807  end
808
809  # GET /.well-known/openid-configuration
810  def openid_metadata
811    [200, { 'Content-Type' => "application/json" }, [JSON.pretty_generate({
812      issuer: "#{$scheme}://#{$host}/.well-known/oauth-authorization-server",
813      authorization_endpoint: "#{$scheme}://#{$host}/oauth/authorize",
814      token_endpoint: "#{$scheme}://#{$host}/oauth/token",
815      userinfo_endpoint: "#{$scheme}://#{$host}/oidc/user/",
816      jwks_uri: "", # RFC-7517
817      registration_endpoint: nil,
818      scopes_supported: ["openid", "profile", "email"],
819      response_types_supported: ["code", "code id_token", "id_token", "token id_token"],
820      response_modes_supported: ["query", "fragment", "form_post"],
821      grant_types_supported: ["authorization_code", "implicit"], # RFC-7591
822      acr_values_supported: [],
823      subject_types_supported: ["pairwise", "public"],
824      id_token_signing_alg_values_supported: ["RS256"],
825      id_token_encryption_alg_values_supported: [],
826      id_token_encryption_enc_values_supported: [],
827      userinfo_signing_alg_values_supported: ["RS256"],
828      userinfo_encryption_alg_values_supported: [],
829      userinfo_encryption_enc_values_supported: [],
830      request_object_signing_alg_values_supported: ["none", "RS256"],
831      request_object_encryption_alg_values_supported: [],
832      request_object_encryption_enc_values_supported: [],
833      token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic", "client_secret_jwt", "private_key_jwt"],
834      token_endpoint_auth_signing_alg_values_supported: [],
835      display_values_supported: [],
836      claim_types_supported: ["normal", "aggregated", "distributed"],
837      claims_supported: [
838        "acr",
839        "auth_time",
840        "email",
841        "email_verified",
842        "family_name",
843        "given_name",
844        "iss",
845        "locale",
846        "name",
847        "nickname",
848        "picture",
849        "profile",
850        "sub",
851        "website"
852      ],
853      service_documentation: nil,
854      claims_locales_supported: [],
855      ui_locales_supported: ["en-US"],
856      claims_parameter_supported: false,
857      request_parameter_supported: false,
858      request_uri_paramater_supported: false,
859      require_request_uri_registration: false,
860      op_policy_uri: "",
861      op_tos_uri: "",
862    })]]
863  end
864end
865
866if __FILE__ == $0
867  app = Rack::Builder.new do
868    use Rack::CommonLogger
869    use Rack::Reloader
870    use Rack::Session::Cookie, { domain: $host.split(":", 2)[0], path: "/", secret: SecureRandom.hex(64) }
871    use ::Warden::Manager do |config|
872      config.default_scope = :user
873      config.default_strategies :password
874    end
875
876    map "/twirp" do
877      # https://github.com/arthurnn/twirp-ruby/wiki/Service-Handlers
878      run ::Authx::Rpc::AbilityService.new(::Authz::Rpc::Ability.new)
879    end
880    map "/oauth" do
881      run ::Authz::OAuthController.new
882    end
883
884    map "/saml" do
885      run Authn::SAMLController.new($scheme, $host)
886    end
887
888    map "/sessions" do
889      run Authn::SessionsController.new
890    end
891    run IdentityProvider.new
892  end.to_app
893
894  Rackup::Server.start(app: app, Port: $port)
895end