himari 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -0
  3. data/lib/himari/access_token.rb +72 -4
  4. data/lib/himari/access_token_jwt.rb +46 -0
  5. data/lib/himari/app.rb +102 -28
  6. data/lib/himari/authorization_code.rb +18 -4
  7. data/lib/himari/client_registration.rb +70 -4
  8. data/lib/himari/config.rb +8 -3
  9. data/lib/himari/decisions/authentication.rb +18 -2
  10. data/lib/himari/decisions/authorization.rb +18 -7
  11. data/lib/himari/decisions/base.rb +7 -3
  12. data/lib/himari/decisions/claims.rb +14 -9
  13. data/lib/himari/dynamic_client_registration.rb +255 -0
  14. data/lib/himari/id_token.rb +15 -28
  15. data/lib/himari/item_provider.rb +3 -1
  16. data/lib/himari/item_providers/oauth_client_metadata.rb +222 -0
  17. data/lib/himari/item_providers/static.rb +2 -0
  18. data/lib/himari/item_providers/storage.rb +33 -0
  19. data/lib/himari/jwt_token.rb +50 -0
  20. data/lib/himari/lifetime_value.rb +5 -3
  21. data/lib/himari/log_line.rb +2 -0
  22. data/lib/himari/middlewares/authentication_rule.rb +2 -0
  23. data/lib/himari/middlewares/authorization_rule.rb +2 -0
  24. data/lib/himari/middlewares/claims_rule.rb +2 -0
  25. data/lib/himari/middlewares/client.rb +2 -0
  26. data/lib/himari/middlewares/config.rb +2 -0
  27. data/lib/himari/middlewares/dynamic_clients.rb +55 -0
  28. data/lib/himari/middlewares/metadata_clients.rb +121 -0
  29. data/lib/himari/middlewares/signing_key.rb +2 -0
  30. data/lib/himari/provider_chain.rb +3 -1
  31. data/lib/himari/rack_oauth2_ext.rb +58 -0
  32. data/lib/himari/refresh_token.rb +93 -0
  33. data/lib/himari/rule.rb +2 -0
  34. data/lib/himari/rule_processor.rb +3 -0
  35. data/lib/himari/services/client_registration_endpoint.rb +78 -0
  36. data/lib/himari/services/downstream_authorization.rb +22 -7
  37. data/lib/himari/services/jwks_endpoint.rb +3 -1
  38. data/lib/himari/services/oidc_authorization_endpoint.rb +63 -3
  39. data/lib/himari/services/oidc_provider_metadata_endpoint.rb +31 -7
  40. data/lib/himari/services/oidc_token_endpoint.rb +225 -46
  41. data/lib/himari/services/oidc_userinfo_endpoint.rb +13 -7
  42. data/lib/himari/services/upstream_authentication.rb +62 -14
  43. data/lib/himari/session_data.rb +31 -2
  44. data/lib/himari/signing_key.rb +17 -14
  45. data/lib/himari/storages/base.rb +45 -1
  46. data/lib/himari/storages/filesystem.rb +14 -3
  47. data/lib/himari/storages/memory.rb +10 -2
  48. data/lib/himari/token_string.rb +40 -4
  49. data/lib/himari/version.rb +1 -1
  50. data/public/public/index.css +18 -0
  51. data/views/consent.erb +59 -0
  52. metadata +50 -14
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'himari/token_string'
5
+
6
+ module Himari
7
+ class RefreshToken
8
+ include TokenString
9
+
10
+ def self.magic_header
11
+ 'hmrt'
12
+ end
13
+
14
+ def self.default_lifetime
15
+ raise ArgumentError, "RefreshToken requires an explicit lifetime:"
16
+ end
17
+
18
+ def initialize(handle:, client_id:, claims:, session_handle:, expiry:, openid: false, scopes: [], secret: nil, secret_hash: nil, secret_hash_prev: nil, version: 1, updated_at: nil)
19
+ @handle = handle
20
+ @client_id = client_id
21
+ @claims = claims
22
+ @session_handle = session_handle
23
+ @openid = openid
24
+ @scopes = scopes
25
+ @expiry = expiry
26
+
27
+ @secret = secret
28
+ @secret_hash = secret_hash
29
+ @secret_hash_prev = secret_hash_prev
30
+ @version = version
31
+ @updated_at = updated_at || Time.now.to_i
32
+ @verification = nil
33
+ end
34
+
35
+ attr_reader :handle, :client_id, :claims, :session_handle, :openid, :scopes, :expiry, :version, :updated_at
36
+
37
+ # Rotate the token in place (same handle): mint a new current secret while keeping the
38
+ # just-presented secret valid as the previous one, so a client whose rotation response is
39
+ # lost can retry with the secret it still holds. The secret to keep is the hash verify!
40
+ # matched (TokenString#verification) — whichever slot the client used; rotate is therefore
41
+ # only valid after a successful verify!. version is bumped so a concurrent refresh against
42
+ # the version we read fails the conditional update. expiry is preserved: the initial
43
+ # lifetime is an absolute cap on the rotation chain, not slid forward on each refresh.
44
+ def rotate(claims:, openid:, now: Time.now)
45
+ raise TokenString::SecretMissing, "rotate requires a verified secret; call verify! first" unless verification
46
+
47
+ self.class.new(
48
+ handle:,
49
+ client_id:,
50
+ session_handle:,
51
+ claims:,
52
+ openid:,
53
+ scopes:,
54
+ secret: SecureRandom.urlsafe_base64(48),
55
+ secret_hash_prev: verification.secret_hash,
56
+ version: version + 1,
57
+ updated_at: now.to_i,
58
+ expiry:,
59
+ )
60
+ end
61
+
62
+ def as_log
63
+ {
64
+ handle: handle,
65
+ client_id: client_id,
66
+ claims: claims,
67
+ session_handle: session_handle,
68
+ openid: openid,
69
+ scopes: scopes,
70
+ expiry: expiry,
71
+ version: version,
72
+ updated_at: updated_at,
73
+ prev_secret_set: !secret_hash_prev.nil?,
74
+ }
75
+ end
76
+
77
+ def as_json
78
+ {
79
+ handle: handle,
80
+ secret_hash: secret_hash,
81
+ secret_hash_prev: secret_hash_prev,
82
+ client_id: client_id,
83
+ claims: claims,
84
+ session_handle: session_handle,
85
+ openid: openid,
86
+ scopes: scopes,
87
+ expiry: expiry.to_i,
88
+ version: version,
89
+ updated_at: updated_at.to_i,
90
+ }
91
+ end
92
+ end
93
+ end
data/lib/himari/rule.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Himari
2
4
  Rule = Struct.new(:name, :block, keyword_init: true) do
3
5
  def call(context, decision)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Himari
2
4
  class RuleProcessor
3
5
  class MissingDecisionError < StandardError; end
@@ -40,6 +42,7 @@ module Himari
40
42
 
41
43
  rule.call(context, decision)
42
44
  raise MissingDecisionError, "rule '#{rule.name}' returned no decision; rule must use one of decision.allow!, deny!, continue!, skip!" unless decision.effect
45
+
43
46
  result.decision_log.push(decision)
44
47
 
45
48
  case decision.effect
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'rack/request'
5
+ require 'himari/log_line'
6
+ require 'himari/dynamic_client_registration'
7
+
8
+ module Himari
9
+ module Services
10
+ # RFC 7591 OAuth 2.0 Dynamic Client Registration endpoint. Accepts a JSON client metadata
11
+ # document via POST, persists a Himari::DynamicClientRegistration, and returns the client
12
+ # information response (including a one-time client_secret for confidential clients).
13
+ class ClientRegistrationEndpoint
14
+ # @param storage [Himari::Storages::Base]
15
+ # @param registration_lifetime [Integer] seconds a registration stays valid
16
+ # @param ignore_localhost_redirect_uri_port [Boolean] relax loopback redirect_uri ports for
17
+ # registered clients (default true; see RFC 8252 §7.3)
18
+ # @param logger [Logger, nil]
19
+ def initialize(storage:, registration_lifetime: Himari::DynamicClientRegistration::REGISTRATION_LIFETIME, ignore_localhost_redirect_uri_port: true, logger: nil)
20
+ @storage = storage
21
+ @registration_lifetime = registration_lifetime
22
+ @ignore_localhost_redirect_uri_port = ignore_localhost_redirect_uri_port
23
+ @logger = logger
24
+ end
25
+
26
+ def app
27
+ self
28
+ end
29
+
30
+ def call(env)
31
+ request = Rack::Request.new(env)
32
+ return error_response(405, :invalid_request, 'method not allowed') unless request.post?
33
+
34
+ metadata = parse_body(request)
35
+ return error_response(400, :invalid_client_metadata, 'request body must be a JSON object') unless metadata
36
+
37
+ client = Himari::DynamicClientRegistration.register(
38
+ metadata: metadata,
39
+ lifetime: @registration_lifetime,
40
+ ignore_localhost_redirect_uri_port: @ignore_localhost_redirect_uri_port,
41
+ registration_ip: request.ip,
42
+ registration_remote_addr: env['REMOTE_ADDR'],
43
+ registration_x_forwarded_for: env['HTTP_X_FORWARDED_FOR'],
44
+ )
45
+ @storage.put_dynamic_client(client)
46
+
47
+ @logger&.info(Himari::LogLine.new('ClientRegistrationEndpoint: registered', req: env['himari.request_as_log'], client: client.as_log))
48
+
49
+ json_response(201, client.registration_response)
50
+ rescue Himari::DynamicClientRegistration::ValidationError => e
51
+ @logger&.warn(Himari::LogLine.new('ClientRegistrationEndpoint: rejected', req: env['himari.request_as_log'], err: e.error_code, message: e.message))
52
+ error_response(400, e.error_code, e.message)
53
+ end
54
+
55
+ private def parse_body(request)
56
+ return unless request.media_type == 'application/json'
57
+
58
+ body = request.body.read
59
+ parsed = JSON.parse(body, symbolize_names: true)
60
+ parsed.is_a?(Hash) ? parsed : nil
61
+ rescue JSON::ParserError
62
+ nil
63
+ end
64
+
65
+ private def json_response(status, body)
66
+ [
67
+ status,
68
+ {'Content-Type' => 'application/json', 'Cache-Control' => 'no-store', 'Pragma' => 'no-cache'},
69
+ [JSON.generate(body), "\n"],
70
+ ]
71
+ end
72
+
73
+ private def error_response(status, error, description)
74
+ json_response(status, {error: error, error_description: description})
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'himari/decisions/authorization'
2
4
  require 'himari/middlewares/authorization_rule'
3
5
  require 'himari/rule_processor'
@@ -21,11 +23,13 @@ module Himari
21
23
  end
22
24
  end
23
25
 
24
- Result = Struct.new(:client, :claims, :lifetime, :authz_result) do
26
+ Result = Struct.new(:client, :claims, :scopes, :lifetime, :mint_jwt_access_token, :authz_result) do
25
27
  def as_log
26
28
  {
27
29
  client: client.as_log,
28
30
  claims: claims,
31
+ scopes: scopes,
32
+ mint_jwt_access_token: mint_jwt_access_token,
29
33
  decision: {
30
34
  authorization: authz_result.as_log,
31
35
  },
@@ -35,13 +39,19 @@ module Himari
35
39
 
36
40
  # @param session [Himari::SessionData]
37
41
  # @param client [Himari::ClientRegistration]
38
- # @param request [Rack::Request]
42
+ # @param request [Rack::Request] exposed to rules as context.request (an escape hatch); the
43
+ # engine never reads it, so requested scopes are supplied explicitly, never derived from it.
44
+ # @param requested_scopes [Array<String>] scopes asked for, before the client's allow-list
45
+ # filter. The caller supplies them from the appropriate source: the authorization endpoint
46
+ # passes the request's parsed scope, the refresh flow the scopes recorded on the grant.
39
47
  # @param authz_rules [Array<Himari::Rule>] Authorization Rules
40
48
  # @param logger [Logger]
41
- def initialize(session:, client:, request: nil, authz_rules: [], logger: nil)
49
+ def initialize(session:, client:, requested_scopes:, grant_type: :initial, request: nil, authz_rules: [], logger: nil)
42
50
  @session = session
43
51
  @client = client
52
+ @grant_type = grant_type
44
53
  @request = request
54
+ @requested_scopes = requested_scopes
45
55
  @authz_rules = authz_rules
46
56
  @logger = logger
47
57
  end
@@ -49,25 +59,30 @@ module Himari
49
59
  # @param session [Himari::SessionData]
50
60
  # @param client [Himari::ClientRegistration]
51
61
  # @param request [Rack::Request]
52
- def self.from_request(session:, client:, request:)
62
+ # @param requested_scopes [Array<String>] see #initialize; always supplied by the caller
63
+ def self.from_request(session:, client:, request:, requested_scopes:, grant_type: :initial)
53
64
  new(
54
65
  session: session,
55
66
  client: client,
67
+ grant_type: grant_type,
56
68
  request: request,
69
+ requested_scopes: requested_scopes,
57
70
  authz_rules: Himari::ProviderChain.new(request.env[Himari::Middlewares::AuthorizationRule::RACK_KEY] || []).collect,
58
71
  logger: request.env['rack.logger'],
59
72
  )
60
73
  end
61
74
 
62
75
  def perform
63
- context = Himari::Decisions::Authorization::Context.new(claims: @session.claims, user_data: @session.user_data, request: @request, client: @client).freeze
76
+ scopes = @client.filter_scopes(@requested_scopes)
77
+ context = Himari::Decisions::Authorization::Context.new(claims: @session.claims, user_data: @session.user_data, request: @request, client: @client, scopes: scopes, grant_type: @grant_type).freeze
64
78
 
65
79
  authorization = Himari::RuleProcessor.new(context, Himari::Decisions::Authorization.new(claims: @session.claims.dup)).run(@authz_rules)
66
- raise ForbiddenError.new(Result.new(@client, nil, nil, authorization)) unless authorization.allowed
80
+ raise ForbiddenError.new(Result.new(@client, nil, scopes, nil, nil, authorization)) unless authorization.allowed
67
81
 
68
82
  claims = authorization.decision.output_claims
69
83
  lifetime = authorization.decision.lifetime
70
- Result.new(@client, claims, lifetime, authorization)
84
+ mint_jwt_access_token = authorization.decision.mint_jwt_access_token
85
+ Result.new(@client, claims, scopes, lifetime, mint_jwt_access_token, authorization)
71
86
  end
72
87
  end
73
88
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Himari
2
4
  module Services
3
5
  class JwksEndpoint
@@ -26,7 +28,7 @@ module Himari
26
28
  # https://www.rfc-editor.org/rfc/rfc7517#section-5
27
29
  return [404, {'Content-Type' => 'application/json'}, ['{"error": "not_found"}']] unless @env['REQUEST_METHOD'] == 'GET'
28
30
 
29
- signing_keys = @signing_key_provider.collect()
31
+ signing_keys = @signing_key_provider.collect
30
32
 
31
33
  [
32
34
  200,
@@ -1,22 +1,42 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack/oauth2'
2
4
  require 'digest/sha2'
3
5
  require 'openid_connect'
4
6
 
7
+ require 'himari/rack_oauth2_ext'
8
+
5
9
  module Himari
6
10
  module Services
7
11
  class OidcAuthorizationEndpoint
8
12
  class ReauthenticationRequired < StandardError; end
9
13
 
14
+ # Raised when the user must be shown the consent page before a code is granted. Carries the
15
+ # data the page renders (the requesting client and the requested scopes); app.rb rescues it.
16
+ class ConsentRequired < StandardError
17
+ def initialize(client:, scopes:)
18
+ @client = client
19
+ @scopes = scopes
20
+ super('consent required')
21
+ end
22
+
23
+ attr_reader :client, :scopes
24
+ end
25
+
10
26
  SUPPORTED_RESPONSE_TYPES = ['code'] # TODO: share with oidc metadata
11
27
 
12
28
  # @param authz [Himari::AuthorizationCode] pending (unpersisted) authz data
13
29
  # @param client [Himari::ClientRegistration]
14
30
  # @param storage [Himari::Storages::Base]
31
+ # @param issuer [String] issuer identifier, returned to the client as the RFC 9207 `iss` parameter
32
+ # @param consent [:approve, :deny, nil] the user's consent decision (nil = not yet asked)
15
33
  # @param logger [Logger]
16
- def initialize(authz:, client:, storage:, logger: nil)
34
+ def initialize(authz:, client:, storage:, issuer:, consent: nil, logger: nil)
17
35
  @authz = authz
18
36
  @client = client
19
37
  @storage = storage
38
+ @issuer = issuer
39
+ @consent = consent
20
40
  @logger = logger
21
41
  end
22
42
 
@@ -31,19 +51,57 @@ module Himari
31
51
 
32
52
  def app(env)
33
53
  Rack::OAuth2::Server::Authorize.new do |req, res|
54
+ # RFC 9207: hand the issuer to rack-oauth2 so both the grant response and any error it
55
+ # redirects back to the client carry the `iss` parameter (see Himari::RackOAuth2Ext).
56
+ req.iss = @issuer
57
+ res.iss = @issuer
58
+
34
59
  # sanity check
35
60
  unless @client.id == req.client_id
36
61
  @logger&.warn(Himari::LogLine.new('OidcAuthorizationEndpoint: @client.id != req.client_id', req: env['himari.request_as_log'], known_client: @client.id, given_client: req.client_id))
37
62
  next req.bad_request!
38
63
  end
39
64
  raise "[BUG] client.id != authz.cilent_id" unless @authz.client_id == @client.id
40
- res.redirect_uri = req.verify_redirect_uri!(@client.redirect_uris)
65
+
66
+ given_redirect_uri = req.redirect_uri&.to_s
67
+ res.redirect_uri = if given_redirect_uri && !given_redirect_uri.empty?
68
+ # Raise before recording the redirect_uri so we never redirect errors to an unverified URI.
69
+ next req.bad_request!(:invalid_request, '"redirect_uri" mismatch') unless @client.redirect_uri_covers?(given_redirect_uri)
70
+
71
+ given_redirect_uri
72
+ elsif @client.redirect_uris.size == 1 && @client.redirect_uris.first.is_a?(String)
73
+ @client.redirect_uris.first
74
+ else
75
+ next req.bad_request!(:invalid_request, '"redirect_uri" missing')
76
+ end
77
+ # rack-oauth2 redirects subsequent errors back to the verified redirect_uri via this accessor.
78
+ req.verified_redirect_uri = res.redirect_uri
41
79
 
42
80
  req.unsupported_response_type! if res.protocol_params_location == :fragment
43
81
  req.bad_request!(:request_uri_not_supported, "Request Object is not implemented") if req.request_uri || req.request
44
82
  req.bad_request!(:invalid_request, 'prompt=none should not contain any other value') if req.prompt.include?('none') && req.prompt.any? { |x| x != 'none' }
45
83
  raise ReauthenticationRequired if req.prompt.include?('login') || req.prompt.include?('select_account')
46
84
 
85
+ # Drop scopes this client does not recognise before they reach consent or the grant.
86
+ scopes = @client.filter_scopes(req.scope)
87
+
88
+ # Consent gate. Clients granted skip_consent (the default for dynamically/metadata-
89
+ # registered clients) bypass it; prompt=consent forces the page regardless.
90
+ if !@client.skip_consent || req.prompt.include?('consent')
91
+ case @consent
92
+ when :approve
93
+ # consent given; fall through and grant
94
+ when :deny
95
+ next req.access_denied!
96
+ else
97
+ # prompt=none forbids interaction (OIDC §3.1.2.1), so surface the error via redirect
98
+ # instead of rendering the page.
99
+ next req.consent_required! if req.prompt.include?('none')
100
+
101
+ raise ConsentRequired.new(client: @client, scopes: scopes)
102
+ end
103
+ end
104
+
47
105
  requested_response_types = [*req.response_type]
48
106
  unless SUPPORTED_RESPONSE_TYPES.include?(requested_response_types.map(&:to_s).join(' '))
49
107
  next req.unsupported_response_type!
@@ -53,7 +111,9 @@ module Himari
53
111
  @authz.redirect_uri = res.redirect_uri
54
112
  @authz.nonce = req.nonce
55
113
 
56
- @authz.openid = req.scope.include?('openid')
114
+ @authz.scopes = scopes
115
+ @authz.openid = scopes.include?('openid')
116
+ @authz.offline_access = scopes.include?('offline_access')
57
117
  if req.code_challenge && req.code_challenge_method
58
118
  @authz.code_challenge = req.code_challenge
59
119
  @authz.code_challenge_method = req.code_challenge_method || 'plain'
@@ -1,10 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Himari
2
4
  module Services
3
5
  class OidcProviderMetadataEndpoint
6
+ # Scopes and claims Himari always advertises; configured values are merged on top.
7
+ DEFAULT_SCOPES_SUPPORTED = %w(openid offline_access).freeze
8
+ DEFAULT_CLAIMS_SUPPORTED = %w(sub iss iat nbf exp).freeze
9
+
4
10
  # @param signing_key_provider [Himari::ProviderChain<Himari::SigningKey>]
5
- def initialize(signing_key_provider:, issuer:)
11
+ # @param registration_endpoint [String, nil] advertised when Dynamic Client Registration is enabled
12
+ # @param client_id_metadata_document_supported [Boolean] advertised when OAuth Client ID Metadata Document support is enabled
13
+ # @param scopes_supported [Array<String>] extra scopes to advertise alongside the defaults
14
+ # @param claims_supported [Array<String>] extra claims to advertise alongside the defaults
15
+ def initialize(signing_key_provider:, issuer:, registration_endpoint: nil, client_id_metadata_document_supported: false, scopes_supported: [], claims_supported: [])
6
16
  @signing_key_provider = signing_key_provider
7
17
  @issuer = issuer
18
+ @registration_endpoint = registration_endpoint
19
+ @client_id_metadata_document_supported = client_id_metadata_document_supported
20
+ @scopes_supported = scopes_supported
21
+ @claims_supported = claims_supported
8
22
  end
9
23
 
10
24
  def app
@@ -12,32 +26,42 @@ module Himari
12
26
  end
13
27
 
14
28
  def call(env)
15
- Handler.new(signing_key_provider: @signing_key_provider, issuer: @issuer, env: env).response
29
+ Handler.new(signing_key_provider: @signing_key_provider, issuer: @issuer, registration_endpoint: @registration_endpoint, client_id_metadata_document_supported: @client_id_metadata_document_supported, scopes_supported: @scopes_supported, claims_supported: @claims_supported, env: env).response
16
30
  end
17
31
 
18
32
  class Handler
19
33
  class InvalidToken < StandardError; end
20
34
 
21
- def initialize(signing_key_provider:, issuer:, env:)
35
+ def initialize(signing_key_provider:, issuer:, env:, registration_endpoint: nil, client_id_metadata_document_supported: false, scopes_supported: [], claims_supported: [])
22
36
  @signing_key_provider = signing_key_provider
23
37
  @issuer = issuer
38
+ @registration_endpoint = registration_endpoint
39
+ @client_id_metadata_document_supported = client_id_metadata_document_supported
40
+ @scopes_supported = scopes_supported
41
+ @claims_supported = claims_supported
24
42
  @env = env
25
43
  end
26
44
 
27
45
  def metadata
28
- signing_keys = @signing_key_provider.collect()
46
+ signing_keys = @signing_key_provider.collect
29
47
  {
30
48
  issuer: @issuer,
31
49
  authorization_endpoint: "#{@issuer}/oidc/authorize",
32
50
  token_endpoint: "#{@issuer}/public/oidc/token",
33
51
  userinfo_endpoint: "#{@issuer}/public/oidc/userinfo",
34
52
  jwks_uri: "#{@issuer}/public/jwks",
35
- scopes_supported: %w(openid),
53
+ registration_endpoint: @registration_endpoint,
54
+ client_id_metadata_document_supported: @client_id_metadata_document_supported ? true : nil,
55
+ scopes_supported: (DEFAULT_SCOPES_SUPPORTED + @scopes_supported).uniq,
36
56
  response_types_supported: ['code'], # violation: dynamic OpenID Provider MUST support code, id_token, token+id_token
57
+ grant_types_supported: %w(authorization_code refresh_token),
58
+ token_endpoint_auth_methods_supported: %w(client_secret_basic client_secret_post none),
59
+ code_challenge_methods_supported: %w(S256 plain),
37
60
  subject_types_supported: ['public'],
38
61
  id_token_signing_alg_values_supported: signing_keys.map(&:alg).uniq.sort,
39
- claims_supported: %w(sub iss iat nbf exp),
40
- }
62
+ claims_supported: (DEFAULT_CLAIMS_SUPPORTED + @claims_supported).uniq,
63
+ authorization_response_iss_parameter_supported: true, # RFC 9207
64
+ }.compact
41
65
  end
42
66
 
43
67
  def response