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?("&") ? "&" : "&"
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