token_authority 0.1.0 → 0.2.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +199 -7
  4. data/app/controllers/concerns/token_authority/client_authentication.rb +141 -0
  5. data/app/controllers/concerns/token_authority/controller_event_logging.rb +98 -0
  6. data/app/controllers/concerns/token_authority/initial_access_token_authentication.rb +35 -0
  7. data/app/controllers/concerns/token_authority/token_authentication.rb +128 -0
  8. data/app/controllers/token_authority/authorization_grants_controller.rb +119 -0
  9. data/app/controllers/token_authority/authorizations_controller.rb +105 -0
  10. data/app/controllers/token_authority/clients_controller.rb +99 -0
  11. data/app/controllers/token_authority/metadata_controller.rb +12 -0
  12. data/app/controllers/token_authority/resource_metadata_controller.rb +12 -0
  13. data/app/controllers/token_authority/sessions_controller.rb +228 -0
  14. data/app/helpers/token_authority/authorization_grants_helper.rb +27 -0
  15. data/app/models/concerns/token_authority/claim_validatable.rb +95 -0
  16. data/app/models/concerns/token_authority/event_logging.rb +144 -0
  17. data/app/models/concerns/token_authority/resourceable.rb +111 -0
  18. data/app/models/concerns/token_authority/scopeable.rb +105 -0
  19. data/app/models/concerns/token_authority/session_creatable.rb +101 -0
  20. data/app/models/token_authority/access_token.rb +127 -0
  21. data/app/models/token_authority/access_token_request.rb +193 -0
  22. data/app/models/token_authority/authorization_grant.rb +119 -0
  23. data/app/models/token_authority/authorization_request.rb +276 -0
  24. data/app/models/token_authority/authorization_server_metadata.rb +101 -0
  25. data/app/models/token_authority/client.rb +263 -0
  26. data/app/models/token_authority/client_id_resolver.rb +114 -0
  27. data/app/models/token_authority/client_metadata_document.rb +164 -0
  28. data/app/models/token_authority/client_metadata_document_cache.rb +33 -0
  29. data/app/models/token_authority/client_metadata_document_fetcher.rb +266 -0
  30. data/app/models/token_authority/client_registration_request.rb +214 -0
  31. data/app/models/token_authority/client_registration_response.rb +58 -0
  32. data/app/models/token_authority/jwks_cache.rb +37 -0
  33. data/app/models/token_authority/jwks_fetcher.rb +70 -0
  34. data/app/models/token_authority/protected_resource_metadata.rb +74 -0
  35. data/app/models/token_authority/refresh_token.rb +110 -0
  36. data/app/models/token_authority/refresh_token_request.rb +116 -0
  37. data/app/models/token_authority/session.rb +193 -0
  38. data/app/models/token_authority/software_statement.rb +70 -0
  39. data/app/views/token_authority/authorization_grants/new.html.erb +25 -0
  40. data/app/views/token_authority/client_error.html.erb +8 -0
  41. data/config/locales/token_authority.en.yml +248 -0
  42. data/config/routes.rb +29 -0
  43. data/lib/generators/token_authority/install/install_generator.rb +61 -0
  44. data/lib/generators/token_authority/install/templates/create_token_authority_tables.rb.erb +116 -0
  45. data/lib/generators/token_authority/install/templates/token_authority.rb +247 -0
  46. data/lib/token_authority/configuration.rb +397 -0
  47. data/lib/token_authority/engine.rb +34 -0
  48. data/lib/token_authority/errors.rb +221 -0
  49. data/lib/token_authority/instrumentation.rb +80 -0
  50. data/lib/token_authority/instrumentation_log_subscriber.rb +62 -0
  51. data/lib/token_authority/json_web_token.rb +78 -0
  52. data/lib/token_authority/log_event_subscriber.rb +43 -0
  53. data/lib/token_authority/routing/constraints.rb +71 -0
  54. data/lib/token_authority/routing/routes.rb +39 -0
  55. data/lib/token_authority/version.rb +4 -1
  56. data/lib/token_authority.rb +30 -1
  57. metadata +65 -5
  58. data/app/assets/stylesheets/token_authority/application.css +0 -15
  59. data/app/controllers/token_authority/application_controller.rb +0 -4
  60. data/app/helpers/token_authority/application_helper.rb +0 -4
  61. data/app/views/layouts/token_authority/application.html.erb +0 -17
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ ##
5
+ # Models a access token request
6
+ class AccessTokenRequest
7
+ include ActiveModel::Model
8
+ include ActiveModel::Validations
9
+ include TokenAuthority::Resourceable
10
+ include TokenAuthority::Scopeable
11
+ include TokenAuthority::EventLogging
12
+
13
+ attr_accessor :code_verifier, :token_authority_authorization_grant, :redirect_uri
14
+
15
+ validate :token_authority_authorization_grant_must_be_valid
16
+ validate :code_verifier_must_be_valid
17
+ validate :redirect_uri_must_be_valid
18
+ validate :resources_must_be_valid
19
+ validate :scope_must_be_valid
20
+
21
+ # Returns the effective resources (requested or falls back to grant's resources)
22
+ def effective_resources
23
+ return resources if resources.any?
24
+
25
+ token_authority_authorization_grant&.resources || []
26
+ end
27
+
28
+ # Returns the effective scopes (requested or falls back to grant's scopes)
29
+ def effective_scopes
30
+ return scope if scope.present?
31
+
32
+ token_authority_authorization_grant&.scopes || []
33
+ end
34
+
35
+ private
36
+
37
+ def token_authority_authorization_grant_must_be_valid
38
+ errors.add(:token_authority_authorization_grant, :invalid) and return unless token_authority_authorization_grant_present?
39
+
40
+ errors.add(:token_authority_authorization_grant, :redeemed) if token_authority_authorization_grant.redeemed?
41
+ end
42
+
43
+ def code_verifier_must_be_valid
44
+ return unless token_authority_authorization_grant_present?
45
+
46
+ if token_authority_client.public_client_type?
47
+ validate_code_verifier_for_public
48
+ else
49
+ validate_code_verifier_for_confidential
50
+ end
51
+ end
52
+
53
+ def validate_code_verifier_for_public
54
+ debug_event("validation.pkce.started",
55
+ client_type: "public",
56
+ has_code_verifier: code_verifier.present?)
57
+
58
+ errors.add(:code_verifier, :blank) and return if code_verifier.blank?
59
+
60
+ validate_code_verifier_matches_code_challenge
61
+ end
62
+
63
+ def validate_code_verifier_for_confidential
64
+ return unless challenge_required?
65
+
66
+ debug_event("validation.pkce.started",
67
+ client_type: "confidential",
68
+ has_code_verifier: code_verifier.present?,
69
+ challenge_params_present: challenge_params_present_in_authorize?)
70
+
71
+ if code_verifier.blank? && challenge_params_present_in_authorize?
72
+ errors.add(:code_verifier, :present_in_authorize) and return
73
+ end
74
+
75
+ validate_code_verifier_matches_code_challenge
76
+ end
77
+
78
+ def challenge_required?
79
+ code_verifier.present? || challenge_params_present_in_authorize?
80
+ end
81
+
82
+ def challenge_params_present_in_authorize?
83
+ token_authority_authorization_grant.code_challenge.present? || token_authority_authorization_grant.code_challenge_method.present?
84
+ end
85
+
86
+ def validate_code_verifier_matches_code_challenge
87
+ challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)
88
+ valid = token_authority_authorization_grant.code_challenge == challenge
89
+
90
+ debug_event("validation.pkce.completed",
91
+ valid: valid,
92
+ code_challenge_method: token_authority_authorization_grant.code_challenge_method)
93
+
94
+ errors.add(:code_verifier, :does_not_validate_code_challenge) unless valid
95
+ end
96
+
97
+ def redirect_uri_must_be_valid
98
+ return unless token_authority_authorization_grant_present?
99
+
100
+ if token_authority_client.public_client_type?
101
+ validate_redirect_uri_for_public
102
+ else
103
+ validate_redirect_uri_for_confidential
104
+ end
105
+ end
106
+
107
+ def validate_redirect_uri_for_public
108
+ errors.add(:redirect_uri, :blank) and return if redirect_uri.blank?
109
+
110
+ validate_redirect_uri_matches_authorize_param
111
+ end
112
+
113
+ def validate_redirect_uri_for_confidential
114
+ return unless redirect_uri.present? || redirect_uri_param_present_in_authorize?
115
+
116
+ errors.add(:redirect_uri, :present_in_authorize) and return if redirect_uri.blank?
117
+
118
+ validate_redirect_uri_matches_authorize_param
119
+ end
120
+
121
+ def validate_redirect_uri_matches_authorize_param
122
+ errors.add(:redirect_uri, :mismatched) unless token_authority_authorization_grant.redirect_uri == redirect_uri
123
+ end
124
+
125
+ def redirect_uri_param_present_in_authorize?
126
+ token_authority_authorization_grant.redirect_uri.present?
127
+ end
128
+
129
+ def token_authority_authorization_grant_present?
130
+ return @token_authority_authorization_grant_present if defined?(@token_authority_authorization_grant_present)
131
+
132
+ @token_authority_authorization_grant_present = token_authority_authorization_grant&.is_a?(TokenAuthority::AuthorizationGrant)
133
+ end
134
+
135
+ def token_authority_client
136
+ @token_authority_client ||= token_authority_authorization_grant.resolved_client
137
+ end
138
+
139
+ def resources_must_be_valid
140
+ return unless token_authority_authorization_grant_present?
141
+ return if resources.empty?
142
+
143
+ # If resources are provided but feature is disabled, reject them
144
+ unless TokenAuthority.config.rfc_8707_enabled?
145
+ errors.add(:resources, :not_allowed)
146
+ return
147
+ end
148
+
149
+ # Validate all resource URIs
150
+ unless valid_resource_uris?
151
+ errors.add(:resources, :invalid_uri)
152
+ return
153
+ end
154
+
155
+ # Check against allowed resources list
156
+ unless allowed_resources?
157
+ errors.add(:resources, :not_allowed)
158
+ return
159
+ end
160
+
161
+ # Check that requested resources are a subset of granted resources (downscoping)
162
+ granted_resources = token_authority_authorization_grant.resources
163
+ errors.add(:resources, :not_subset) unless resources_subset_of?(granted_resources)
164
+ end
165
+
166
+ def scope_must_be_valid
167
+ return unless token_authority_authorization_grant_present?
168
+ return if scope.blank?
169
+
170
+ # If scopes are provided but feature is disabled, reject them
171
+ unless TokenAuthority.config.scopes_enabled?
172
+ errors.add(:scope, :not_allowed)
173
+ return
174
+ end
175
+
176
+ # Validate all scope tokens
177
+ unless valid_scope_tokens?
178
+ errors.add(:scope, :invalid)
179
+ return
180
+ end
181
+
182
+ # Check against allowed scopes list
183
+ unless allowed_scopes?
184
+ errors.add(:scope, :not_allowed)
185
+ return
186
+ end
187
+
188
+ # Check that requested scopes are a subset of granted scopes (downscoping)
189
+ granted_scopes = token_authority_authorization_grant.scopes || []
190
+ errors.add(:scope, :not_subset) unless scopes_subset_of?(granted_scopes)
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ # Represents an OAuth authorization grant (authorization code).
5
+ #
6
+ # An authorization grant is created after a user consents to allow a client
7
+ # application access. It contains a one-time-use authorization code and PKCE
8
+ # challenge parameters for public clients.
9
+ #
10
+ # The grant can be associated with either:
11
+ # - A registered Client (stored in database)
12
+ # - A URL-based ClientMetadataDocument (fetched via client_id_url)
13
+ #
14
+ # Grants have a short expiration time (5 minutes by default) and can only
15
+ # be redeemed once. After redemption, they create a Session with access
16
+ # and refresh tokens.
17
+ #
18
+ # @example Redeeming a grant
19
+ # session = authorization_grant.redeem(
20
+ # resources: ["https://api.example.com"],
21
+ # scopes: ["read", "write"]
22
+ # )
23
+ #
24
+ # @since 0.2.0
25
+ class AuthorizationGrant < ApplicationRecord
26
+ include TokenAuthority::SessionCreatable
27
+
28
+ VALID_CODE_CHALLENGE_METHODS = %w[S256].freeze
29
+
30
+ belongs_to :user, class_name: TokenAuthority.config.user_class
31
+ belongs_to :token_authority_client, class_name: "TokenAuthority::Client", optional: true
32
+ has_many :token_authority_sessions,
33
+ class_name: "TokenAuthority::Session",
34
+ foreign_key: :token_authority_authorization_grant_id,
35
+ inverse_of: :token_authority_authorization_grant,
36
+ dependent: :destroy
37
+
38
+ validates :code_challenge_method, inclusion: {in: VALID_CODE_CHALLENGE_METHODS}, allow_nil: true
39
+ validate :must_have_client_identifier
40
+
41
+ before_validation :generate_expires_at
42
+ before_create :generate_public_id
43
+
44
+ # Returns the client associated with this grant.
45
+ #
46
+ # Resolves to either a registered Client or a URL-based ClientMetadataDocument
47
+ # depending on which association is populated. URL-based clients are fetched
48
+ # and cached on first access.
49
+ #
50
+ # @return [TokenAuthority::Client, TokenAuthority::ClientMetadataDocument, nil]
51
+ # the client object, or nil if neither association exists
52
+ #
53
+ # @see TokenAuthority::ClientIdResolver
54
+ def resolved_client
55
+ return token_authority_client if token_authority_client.present?
56
+ return nil if client_id_url.blank?
57
+
58
+ @resolved_client ||= TokenAuthority::ClientIdResolver.resolve(client_id_url)
59
+ end
60
+
61
+ # Returns the most recent active session for this grant.
62
+ #
63
+ # After a grant is redeemed, subsequent refresh operations create new sessions
64
+ # with "created" status while marking old sessions as "refreshed". This method
65
+ # returns the current active session.
66
+ #
67
+ # @return [TokenAuthority::Session, nil] the active session, or nil if none exists
68
+ def active_token_authority_session
69
+ token_authority_sessions.created_status.order(created_at: :desc).first
70
+ end
71
+
72
+ # Redeems the authorization code to create a new token session.
73
+ #
74
+ # This is the final step in the authorization code flow. The grant must not
75
+ # have been redeemed previously, and PKCE verification (if applicable) must
76
+ # pass before redemption succeeds.
77
+ #
78
+ # After successful redemption, the grant is marked as redeemed and a new
79
+ # Session is created with access and refresh tokens.
80
+ #
81
+ # @param resources [Array<String>] resource indicators for the token session
82
+ # @param scopes [Array<String>] scopes for the token session
83
+ #
84
+ # @return [TokenAuthority::Session] the newly created session
85
+ #
86
+ # @raise [TokenAuthority::ServerError] if session creation fails
87
+ #
88
+ # @example
89
+ # session = grant.redeem(
90
+ # resources: ["https://api.example.com"],
91
+ # scopes: ["read", "write"]
92
+ # )
93
+ def redeem(resources: [], scopes: [])
94
+ instrument("grant.redeem") do
95
+ create_token_authority_session(grant: self, resources:, scopes:) do
96
+ update(redeemed: true)
97
+ end
98
+ end
99
+ rescue TokenAuthority::ServerError => error
100
+ raise TokenAuthority::ServerError, error.message
101
+ end
102
+
103
+ private
104
+
105
+ def must_have_client_identifier
106
+ return if token_authority_client.present? || client_id_url.present?
107
+
108
+ errors.add(:base, :must_have_client_identifier)
109
+ end
110
+
111
+ def generate_expires_at
112
+ self.expires_at ||= 5.minutes.from_now
113
+ end
114
+
115
+ def generate_public_id
116
+ self.public_id = SecureRandom.uuid
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ # Validates OAuth 2.1 authorization requests.
5
+ #
6
+ # This service object validates all parameters of an authorization request
7
+ # according to OAuth 2.1 specifications, including PKCE requirements, redirect
8
+ # URI validation, resource indicators (RFC 8707), and scope validation.
9
+ #
10
+ # It enforces different validation rules for public vs confidential clients:
11
+ # - Public clients MUST include PKCE parameters
12
+ # - Confidential clients MAY include PKCE parameters
13
+ # - Public clients MUST include redirect_uri
14
+ # - Confidential clients MAY omit redirect_uri if only one is registered
15
+ #
16
+ # After validation, the request can be serialized to an internal state token
17
+ # (JWT) for storage during the consent flow, then deserialized when processing
18
+ # the user's approval or denial.
19
+ #
20
+ # @example Validating an authorization request
21
+ # request = AuthorizationRequest.new(
22
+ # token_authority_client: client,
23
+ # client_id: "abc123",
24
+ # redirect_uri: "https://app.example.com/callback",
25
+ # response_type: "code",
26
+ # state: "xyz",
27
+ # code_challenge: "E9Melhoa...",
28
+ # code_challenge_method: "S256"
29
+ # )
30
+ # if request.valid?
31
+ # # Process authorization
32
+ # end
33
+ #
34
+ # @since 0.2.0
35
+ class AuthorizationRequest
36
+ include ActiveModel::Model
37
+ include ActiveModel::Validations
38
+ include TokenAuthority::Resourceable
39
+ include TokenAuthority::Scopeable
40
+
41
+ # Valid PKCE code challenge methods per OAuth 2.1.
42
+ # Only S256 (SHA-256) is supported; plain is not allowed.
43
+ VALID_CODE_CHALLENGE_METHODS = ["S256"].freeze
44
+
45
+ # Valid response types. Currently only "code" is supported.
46
+ VALID_RESPONSE_TYPES = ["code"].freeze
47
+
48
+ # @!attribute [rw] token_authority_client
49
+ # The client making the authorization request.
50
+ # @return [TokenAuthority::Client, TokenAuthority::ClientMetadataDocument]
51
+ attr_accessor :token_authority_client
52
+
53
+ # @!attribute [rw] client_id
54
+ # The client identifier.
55
+ # @return [String]
56
+ attr_accessor :client_id
57
+
58
+ # @!attribute [rw] code_challenge
59
+ # The PKCE code challenge.
60
+ # @return [String, nil]
61
+ attr_accessor :code_challenge
62
+
63
+ # @!attribute [rw] code_challenge_method
64
+ # The PKCE code challenge method (S256).
65
+ # @return [String, nil]
66
+ attr_accessor :code_challenge_method
67
+
68
+ # @!attribute [rw] redirect_uri
69
+ # The URI to redirect to after authorization.
70
+ # @return [String, nil]
71
+ attr_accessor :redirect_uri
72
+
73
+ # @!attribute [rw] response_type
74
+ # The OAuth response type (code).
75
+ # @return [String]
76
+ attr_accessor :response_type
77
+
78
+ # @!attribute [rw] state
79
+ # The state parameter for CSRF protection.
80
+ # @return [String, nil]
81
+ attr_accessor :state
82
+
83
+ validates :response_type, presence: true, inclusion: {in: VALID_RESPONSE_TYPES}
84
+
85
+ validate :token_authority_client_must_be_valid
86
+ validate :client_id_must_be_valid
87
+ validate :pkce_params_must_be_valid
88
+ validate :redirect_uri_must_be_valid
89
+ validate :resources_must_be_valid
90
+ validate :scope_must_be_valid
91
+
92
+ # Deserializes an authorization request from an internal state token.
93
+ #
94
+ # The state token is a JWT that preserves the authorization request parameters
95
+ # during the consent flow. This allows the request to survive redirects to
96
+ # the consent screen and back.
97
+ #
98
+ # @param token [String] the JWT state token
99
+ #
100
+ # @return [TokenAuthority::AuthorizationRequest] the deserialized request
101
+ #
102
+ # @note If the client cannot be resolved, token_authority_client will be nil
103
+ # and validation will fail.
104
+ def self.from_internal_state_token(token)
105
+ attributes = TokenAuthority::JsonWebToken.decode(token)
106
+ token_authority_client = TokenAuthority::ClientIdResolver.resolve(attributes[:token_authority_client])
107
+ new(
108
+ **attributes.except(:token_authority_client, :exp).merge(token_authority_client:)
109
+ )
110
+ rescue TokenAuthority::ClientNotFoundError
111
+ new(**attributes.except(:token_authority_client, :exp).merge(token_authority_client: nil))
112
+ end
113
+
114
+ # Converts the authorization request to a hash for serialization.
115
+ #
116
+ # @return [Hash] the request parameters
117
+ def to_h
118
+ {
119
+ token_authority_client: token_authority_client.public_id,
120
+ client_id:,
121
+ state:,
122
+ code_challenge:,
123
+ code_challenge_method:,
124
+ redirect_uri:,
125
+ response_type:,
126
+ resources:,
127
+ scope:
128
+ }
129
+ end
130
+
131
+ # Serializes the authorization request to an internal state token.
132
+ #
133
+ # The state token is used to preserve authorization request parameters
134
+ # during the consent flow without storing them in the session.
135
+ #
136
+ # @return [String] the JWT state token
137
+ def to_internal_state_token
138
+ TokenAuthority::JsonWebToken.encode(to_h)
139
+ end
140
+
141
+ private
142
+
143
+ def token_authority_client_must_be_valid
144
+ errors.add(:token_authority_client, :invalid) unless valid_token_authority_client?
145
+ end
146
+
147
+ def client_id_must_be_valid
148
+ return unless valid_token_authority_client?
149
+
150
+ # For URL-based clients, client_id must match the URL
151
+ if token_authority_client.is_a?(TokenAuthority::ClientMetadataDocument)
152
+ errors.add(:client_id, :blank) and return if client_id.blank?
153
+ errors.add(:client_id, :mismatched) and return if client_id != token_authority_client.public_id
154
+ return
155
+ end
156
+
157
+ # For registered clients
158
+ return if token_authority_client.confidential_client_type? && client_id.blank?
159
+
160
+ errors.add(:client_id, :blank) and return if client_id.blank?
161
+
162
+ client = TokenAuthority::Client.find_by(public_id: client_id)
163
+ errors.add(:client_id, :unregistered_client) unless client
164
+ end
165
+
166
+ def pkce_params_must_be_valid
167
+ return unless valid_token_authority_client?
168
+
169
+ # URL-based clients are always public and must use PKCE
170
+ if token_authority_client.public_client_type?
171
+ validate_public_pkce_params
172
+ else
173
+ validate_confidential_pkce_params
174
+ end
175
+ end
176
+
177
+ def validate_public_pkce_params
178
+ return unless valid_token_authority_client?
179
+ return unless token_authority_client.public_client_type?
180
+
181
+ errors.add(:code_challenge, :required_for_public_clients) if code_challenge.blank?
182
+ errors.add(:code_challenge_method, :required_for_public_clients) if code_challenge_method.blank?
183
+ errors.add(:code_challenge_method, :invalid) unless code_challenge_method.in?(VALID_CODE_CHALLENGE_METHODS)
184
+ end
185
+
186
+ def validate_confidential_pkce_params
187
+ return unless valid_token_authority_client?
188
+ return unless token_authority_client.confidential_client_type?
189
+ return unless code_challenge.present? || code_challenge_method.present?
190
+
191
+ errors.add(:code_challenge, :required_if_other_pkce_params_present) if code_challenge.blank?
192
+ errors.add(:code_challenge_method, :required_if_other_pkce_params_present) if code_challenge_method.blank?
193
+ errors.add(:code_challenge_method, :invalid) unless code_challenge_method.in?(VALID_CODE_CHALLENGE_METHODS)
194
+ end
195
+
196
+ def redirect_uri_must_be_valid
197
+ return unless valid_token_authority_client?
198
+
199
+ if token_authority_client.public_client_type?
200
+ validate_public_client_redirect_uri
201
+ else
202
+ validate_confidential_client_redirect_uri
203
+ end
204
+ end
205
+
206
+ def validate_public_client_redirect_uri
207
+ errors.add(:redirect_uri, :blank) if redirect_uri.blank?
208
+ validate_redirect_uris_match
209
+ end
210
+
211
+ def validate_confidential_client_redirect_uri
212
+ return if redirect_uri.blank?
213
+
214
+ validate_redirect_uris_match
215
+ end
216
+
217
+ def validate_redirect_uris_match
218
+ errors.add(:redirect_uri, :invalid) unless token_authority_client.redirect_uri_registered?(redirect_uri)
219
+ end
220
+
221
+ def valid_token_authority_client?
222
+ token_authority_client.is_a?(TokenAuthority::Client) ||
223
+ token_authority_client.is_a?(TokenAuthority::ClientMetadataDocument)
224
+ end
225
+
226
+ def resources_must_be_valid
227
+ # Check if resource is required
228
+ if TokenAuthority.config.rfc_8707_require_resource && resources.empty?
229
+ errors.add(:resources, :required)
230
+ return
231
+ end
232
+
233
+ return if resources.empty?
234
+
235
+ # If resources are provided but feature is disabled, reject them
236
+ unless TokenAuthority.config.rfc_8707_enabled?
237
+ errors.add(:resources, :not_allowed)
238
+ return
239
+ end
240
+
241
+ # Validate all resource URIs
242
+ unless valid_resource_uris?
243
+ errors.add(:resources, :invalid_uri)
244
+ return
245
+ end
246
+
247
+ # Check against allowed resources list
248
+ errors.add(:resources, :not_allowed) unless allowed_resources?
249
+ end
250
+
251
+ def scope_must_be_valid
252
+ # Check if scope is required
253
+ if TokenAuthority.config.require_scope && scope.blank?
254
+ errors.add(:scope, :required)
255
+ return
256
+ end
257
+
258
+ return if scope.blank?
259
+
260
+ # If scopes are provided but feature is disabled, reject them
261
+ unless TokenAuthority.config.scopes_enabled?
262
+ errors.add(:scope, :not_allowed)
263
+ return
264
+ end
265
+
266
+ # Validate all scope tokens
267
+ unless valid_scope_tokens?
268
+ errors.add(:scope, :invalid)
269
+ return
270
+ end
271
+
272
+ # Check against allowed scopes list
273
+ errors.add(:scope, :not_allowed) unless allowed_scopes?
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ # Builds OAuth 2.0 Authorization Server Metadata per RFC 8414.
5
+ #
6
+ # This class generates the metadata document that clients use to discover
7
+ # the authorization server's capabilities and endpoint locations. The metadata
8
+ # is typically served at /.well-known/oauth-authorization-server.
9
+ #
10
+ # The metadata includes:
11
+ # - Endpoint URLs (authorization, token, revocation, registration)
12
+ # - Supported grant types and response types
13
+ # - Supported token endpoint authentication methods
14
+ # - PKCE code challenge methods
15
+ # - Optional scopes and service documentation
16
+ #
17
+ # @example Building metadata
18
+ # metadata = AuthorizationServerMetadata.new(mount_path: "/oauth")
19
+ # metadata.to_h
20
+ # # => {
21
+ # # issuer: "https://example.com",
22
+ # # authorization_endpoint: "https://example.com/oauth/authorize",
23
+ # # ...
24
+ # # }
25
+ #
26
+ # @see https://www.rfc-editor.org/rfc/rfc8414.html RFC 8414: OAuth 2.0 Authorization Server Metadata
27
+ # @since 0.2.0
28
+ class AuthorizationServerMetadata
29
+ # Creates a new metadata builder.
30
+ #
31
+ # @param mount_path [String] the path where the OAuth engine is mounted
32
+ # (e.g., "/oauth")
33
+ def initialize(mount_path:)
34
+ @mount_path = mount_path
35
+ end
36
+
37
+ # Converts the metadata to a hash for JSON serialization.
38
+ #
39
+ # Builds the complete metadata document including all required and optional
40
+ # fields based on the current configuration. The registration_endpoint is
41
+ # only included when dynamic client registration is enabled.
42
+ #
43
+ # @return [Hash] the authorization server metadata
44
+ #
45
+ # @example
46
+ # metadata = AuthorizationServerMetadata.new(mount_path: "/oauth")
47
+ # json = metadata.to_h.to_json
48
+ def to_h
49
+ metadata = {
50
+ issuer: issuer,
51
+ authorization_endpoint: "#{issuer}#{@mount_path}/authorize",
52
+ token_endpoint: "#{issuer}#{@mount_path}/token",
53
+ revocation_endpoint: "#{issuer}#{@mount_path}/revoke",
54
+ response_types_supported: ["code"],
55
+ grant_types_supported: ["authorization_code", "refresh_token"],
56
+ token_endpoint_auth_methods_supported: token_endpoint_auth_methods_supported,
57
+ code_challenge_methods_supported: ["S256"]
58
+ }
59
+
60
+ metadata[:scopes_supported] = scopes_supported if scopes_supported.any?
61
+ metadata[:service_documentation] = service_documentation if service_documentation.present?
62
+
63
+ # RFC 7591 Dynamic Client Registration
64
+ if TokenAuthority.config.rfc_7591_enabled
65
+ metadata[:registration_endpoint] = "#{issuer}#{@mount_path}/register"
66
+ end
67
+
68
+ metadata
69
+ end
70
+
71
+ private
72
+
73
+ # Returns the issuer URL from configuration with trailing slashes removed.
74
+ # @return [String]
75
+ # @api private
76
+ def issuer
77
+ TokenAuthority.config.rfc_9068_issuer_url.to_s.chomp("/")
78
+ end
79
+
80
+ # Returns the list of supported scopes from configuration.
81
+ # @return [Array<String>]
82
+ # @api private
83
+ def scopes_supported
84
+ TokenAuthority.config.scopes&.keys || []
85
+ end
86
+
87
+ # Returns the service documentation URL from configuration.
88
+ # @return [String, nil]
89
+ # @api private
90
+ def service_documentation
91
+ TokenAuthority.config.rfc_8414_service_documentation
92
+ end
93
+
94
+ # Returns the supported token endpoint authentication methods.
95
+ # @return [Array<String>]
96
+ # @api private
97
+ def token_endpoint_auth_methods_supported
98
+ TokenAuthority.config.rfc_7591_allowed_token_endpoint_auth_methods
99
+ end
100
+ end
101
+ end