token_authority 0.2.1 → 0.3.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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -1
  3. data/README.md +52 -14
  4. data/app/controllers/concerns/token_authority/initial_access_token_authentication.rb +2 -2
  5. data/app/controllers/token_authority/protected_resource_metadata_controller.rb +39 -0
  6. data/app/helpers/token_authority/authorization_grants_helper.rb +2 -3
  7. data/app/models/concerns/token_authority/claim_validatable.rb +2 -2
  8. data/app/models/concerns/token_authority/resourceable.rb +2 -2
  9. data/app/models/token_authority/access_token.rb +2 -2
  10. data/app/models/token_authority/access_token_request.rb +1 -1
  11. data/app/models/token_authority/authorization_request.rb +2 -2
  12. data/app/models/token_authority/authorization_server_metadata.rb +4 -4
  13. data/app/models/token_authority/client.rb +4 -4
  14. data/app/models/token_authority/client_metadata_document.rb +2 -2
  15. data/app/models/token_authority/client_registration_request.rb +5 -5
  16. data/app/models/token_authority/jwks_fetcher.rb +1 -1
  17. data/app/models/token_authority/protected_resource_metadata.rb +110 -31
  18. data/app/models/token_authority/refresh_token.rb +2 -2
  19. data/app/models/token_authority/refresh_token_request.rb +1 -1
  20. data/lib/generators/token_authority/install/templates/token_authority.rb +100 -114
  21. data/lib/token_authority/configuration.rb +345 -175
  22. data/lib/token_authority/errors.rb +29 -0
  23. data/lib/token_authority/routing/constraints.rb +2 -2
  24. data/lib/token_authority/routing/routes.rb +74 -16
  25. data/lib/token_authority/version.rb +1 -1
  26. data/lib/token_authority.rb +2 -2
  27. metadata +2 -2
  28. data/app/controllers/token_authority/resource_metadata_controller.rb +0 -12
@@ -10,52 +10,55 @@ module TokenAuthority
10
10
  # Configuration is typically set in a Rails initializer using a configure block
11
11
  # that yields this configuration object.
12
12
  #
13
- # @example Basic configuration
13
+ # @example Minimal configuration
14
14
  # TokenAuthority.configure do |config|
15
15
  # config.secret_key = Rails.application.credentials.secret_key_base
16
- # config.user_class = "User"
17
- # config.rfc_9068_audience_url = "https://api.example.com"
18
- # config.rfc_9068_issuer_url = "https://example.com"
19
- # end
20
16
  #
21
- # @example Enabling scopes
22
- # TokenAuthority.configure do |config|
23
17
  # config.scopes = {
24
18
  # "read" => "Read access to your data",
25
19
  # "write" => "Write access to your data"
26
20
  # }
27
- # config.require_scope = true
21
+ #
22
+ # config.resources = {
23
+ # api: {
24
+ # resource: "https://example.com/api",
25
+ # resource_name: "My API",
26
+ # scopes_supported: %w[read write],
27
+ # authorization_servers: ["https://example.com"]
28
+ # }
29
+ # }
28
30
  # end
29
31
  #
30
32
  # @since 0.2.0
31
33
  class Configuration
34
+ # ==========================================================================
35
+ # General
36
+ # ==========================================================================
37
+
32
38
  # @!attribute [rw] secret_key
33
39
  # The secret key used for JWT signing and HMAC operations.
34
40
  # This should be a secure random string, typically derived from Rails credentials.
35
41
  # @return [String] the secret key
36
42
  attr_accessor :secret_key
37
43
 
38
- # @!attribute [rw] rfc_9068_audience_url
39
- # The default audience (aud) claim for JWT access tokens per RFC 9068.
40
- # Identifies the intended recipient of the token (typically the API server).
41
- # @return [String, nil] the audience URL
42
- attr_accessor :rfc_9068_audience_url
44
+ # @!attribute [rw] event_logging_enabled
45
+ # Enable structured event logging for OAuth flows and security events.
46
+ # @return [Boolean] true if enabled (default: true)
47
+ attr_accessor :event_logging_enabled
43
48
 
44
- # @!attribute [rw] rfc_9068_issuer_url
45
- # The issuer (iss) claim for JWT access tokens per RFC 9068.
46
- # Identifies the authorization server that issued the token.
47
- # @return [String, nil] the issuer URL
48
- attr_accessor :rfc_9068_issuer_url
49
+ # @!attribute [rw] event_logging_debug_events
50
+ # Enable debug-level event logging for troubleshooting.
51
+ # @return [Boolean] true if enabled (default: false)
52
+ attr_accessor :event_logging_debug_events
49
53
 
50
- # @!attribute [rw] rfc_9068_default_access_token_duration
51
- # Default lifetime for access tokens in seconds.
52
- # @return [Integer] duration in seconds (default: 300)
53
- attr_accessor :rfc_9068_default_access_token_duration
54
+ # @!attribute [rw] instrumentation_enabled
55
+ # Enable ActiveSupport::Notifications instrumentation for performance monitoring.
56
+ # @return [Boolean] true if enabled (default: true)
57
+ attr_accessor :instrumentation_enabled
54
58
 
55
- # @!attribute [rw] rfc_9068_default_refresh_token_duration
56
- # Default lifetime for refresh tokens in seconds.
57
- # @return [Integer] duration in seconds (default: 1,209,600)
58
- attr_accessor :rfc_9068_default_refresh_token_duration
59
+ # ==========================================================================
60
+ # User Authentication
61
+ # ==========================================================================
59
62
 
60
63
  # @!attribute [rw] authenticatable_controller
61
64
  # The controller class name that provides authentication methods.
@@ -68,6 +71,10 @@ module TokenAuthority
68
71
  # @return [String] user class name (default: "User")
69
72
  attr_accessor :user_class
70
73
 
74
+ # ==========================================================================
75
+ # UI/Layout
76
+ # ==========================================================================
77
+
71
78
  # @!attribute [rw] consent_page_layout
72
79
  # The layout to use for the OAuth consent screen.
73
80
  # @return [String] layout name (default: "application")
@@ -78,10 +85,9 @@ module TokenAuthority
78
85
  # @return [String] layout name (default: "application")
79
86
  attr_accessor :error_page_layout
80
87
 
81
- # @!attribute [rw] rfc_8414_service_documentation
82
- # URL for service documentation in authorization server metadata per RFC 8414.
83
- # @return [String, nil] documentation URL
84
- attr_accessor :rfc_8414_service_documentation
88
+ # ==========================================================================
89
+ # Scopes
90
+ # ==========================================================================
85
91
 
86
92
  # @!attribute [rw] scopes
87
93
  # Hash mapping scope strings to human-readable descriptions.
@@ -96,109 +102,180 @@ module TokenAuthority
96
102
 
97
103
  # @!attribute [rw] require_scope
98
104
  # Whether clients must include a scope parameter in authorization requests.
99
- # @return [Boolean] true if scope is required (default: false)
105
+ # @return [Boolean] true if scope is required (default: true)
100
106
  attr_accessor :require_scope
101
107
 
102
- # @!attribute [rw] rfc_9728_resource
103
- # The resource URI for protected resource metadata per RFC 9728.
104
- # @return [String, nil] resource URI
105
- attr_accessor :rfc_9728_resource
106
-
107
- # @!attribute [rw] rfc_9728_scopes_supported
108
- # Array of OAuth scopes supported by the protected resource per RFC 9728.
109
- # @return [Array<String>, nil] supported scopes
110
- attr_accessor :rfc_9728_scopes_supported
111
-
112
- # @!attribute [rw] rfc_9728_authorization_servers
113
- # Array of authorization server issuer URLs per RFC 9728.
114
- # @return [Array<String>, nil] authorization server URLs
115
- attr_accessor :rfc_9728_authorization_servers
116
-
117
- # @!attribute [rw] rfc_9728_bearer_methods_supported
118
- # Array of bearer token methods supported per RFC 9728.
119
- # @return [Array<String>, nil] bearer methods
120
- attr_accessor :rfc_9728_bearer_methods_supported
121
-
122
- # @!attribute [rw] rfc_9728_jwks_uri
123
- # URL to the JWKS for protected resource per RFC 9728.
124
- # @return [String, nil] JWKS URI
125
- attr_accessor :rfc_9728_jwks_uri
126
-
127
- # @!attribute [rw] rfc_9728_resource_name
128
- # Human-readable name of the protected resource per RFC 9728.
129
- # @return [String, nil] resource name
130
- attr_accessor :rfc_9728_resource_name
131
-
132
- # @!attribute [rw] rfc_9728_resource_documentation
133
- # URL to documentation for the protected resource per RFC 9728.
134
- # @return [String, nil] documentation URL
135
- attr_accessor :rfc_9728_resource_documentation
108
+ # ==========================================================================
109
+ # Resources (RFC 9728 / RFC 8707)
110
+ # ==========================================================================
136
111
 
137
- # @!attribute [rw] rfc_9728_resource_policy_uri
138
- # URL to privacy policy for the protected resource per RFC 9728.
139
- # @return [String, nil] policy URI
140
- attr_accessor :rfc_9728_resource_policy_uri
112
+ # @!attribute [rw] resources
113
+ # Protected resource metadata keyed by resource identifier (RFC 9728).
114
+ #
115
+ # Maps resource symbols to metadata hashes. When a request arrives at
116
+ # the /.well-known/oauth-protected-resource endpoint, the controller extracts
117
+ # the subdomain and looks it up in this hash. If no match is found, the first
118
+ # resource in the hash is used as the default.
119
+ #
120
+ # For single-resource deployments, simply configure one entry - it will be used
121
+ # for all requests regardless of subdomain.
122
+ #
123
+ # Each entry must include the :resource field (required per RFC 9728). The
124
+ # validate! method raises ConfigurationError if any entry is missing this field.
125
+ # All other fields are optional and will be omitted from responses if not set.
126
+ #
127
+ # == Available Resource Options
128
+ #
129
+ # [resource] (Required) The protected resource URI. Used as the
130
+ # audience (aud) claim in JWT access tokens.
131
+ # [resource_name] Human-readable name shown on the consent screen.
132
+ # [scopes_supported] Array of scope strings this resource accepts.
133
+ # [authorization_servers] Array of authorization server URLs. The first entry
134
+ # is used as the issuer (iss) claim if token_issuer_url
135
+ # is not set. Also appears in RFC 9728 metadata responses.
136
+ # [bearer_methods_supported] Array of supported bearer token methods (e.g., ["header"]).
137
+ # [jwks_uri] URI for the JSON Web Key Set endpoint.
138
+ # [resource_documentation] URL for API documentation.
139
+ # [resource_policy_uri] URL for the privacy policy.
140
+ # [resource_tos_uri] URL for terms of service.
141
+ #
142
+ # @example Single resource with all options
143
+ # config.resources = {
144
+ # api: {
145
+ # resource: "https://example.com/api",
146
+ # resource_name: "Example API",
147
+ # scopes_supported: %w[read write admin],
148
+ # authorization_servers: ["https://example.com"],
149
+ # bearer_methods_supported: ["header"],
150
+ # jwks_uri: "https://example.com/.well-known/jwks.json",
151
+ # resource_documentation: "https://example.com/docs/api",
152
+ # resource_policy_uri: "https://example.com/privacy",
153
+ # resource_tos_uri: "https://example.com/terms"
154
+ # }
155
+ # }
156
+ #
157
+ # @example Multi-resource deployment with subdomains
158
+ # config.resources = {
159
+ # api: {
160
+ # resource: "https://api.example.com",
161
+ # resource_name: "REST API",
162
+ # scopes_supported: %w[api:read api:write],
163
+ # authorization_servers: ["https://auth.example.com"]
164
+ # },
165
+ # mcp: {
166
+ # resource: "https://mcp.example.com",
167
+ # resource_name: "MCP Server",
168
+ # scopes_supported: %w[mcp:tools mcp:prompts mcp:resources],
169
+ # authorization_servers: ["https://auth.example.com"]
170
+ # }
171
+ # }
172
+ #
173
+ # @return [Hash{Symbol => Hash}, nil] resource identifier to metadata mapping
174
+ attr_accessor :resources
141
175
 
142
- # @!attribute [rw] rfc_9728_resource_tos_uri
143
- # URL to terms of service for the protected resource per RFC 9728.
144
- # @return [String, nil] TOS URI
145
- attr_accessor :rfc_9728_resource_tos_uri
176
+ # @!attribute [rw] require_resource
177
+ # Whether clients must include a resource parameter in authorization requests.
178
+ # @return [Boolean] true if resource is required (default: true)
179
+ attr_accessor :require_resource
146
180
 
147
- # @!attribute [rw] rfc_7591_enabled
181
+ # ==========================================================================
182
+ # JWT Access Tokens (RFC 9068)
183
+ # ==========================================================================
184
+
185
+ # @!attribute [rw] token_audience_url
186
+ # The default audience (aud) claim for JWT access tokens per RFC 9068.
187
+ # Identifies the intended recipient of the token (typically the API server).
188
+ # @return [String, nil] the audience URL
189
+ attr_accessor :token_audience_url
190
+
191
+ # @!attribute [rw] token_issuer_url
192
+ # The issuer (iss) claim for JWT access tokens per RFC 9068.
193
+ # Identifies the authorization server that issued the token.
194
+ # @return [String, nil] the issuer URL
195
+ attr_accessor :token_issuer_url
196
+
197
+ # @!attribute [rw] default_access_token_duration
198
+ # Default lifetime for access tokens in seconds.
199
+ # @return [Integer] duration in seconds (default: 300)
200
+ attr_accessor :default_access_token_duration
201
+
202
+ # @!attribute [rw] default_refresh_token_duration
203
+ # Default lifetime for refresh tokens in seconds.
204
+ # @return [Integer] duration in seconds (default: 1,209,600)
205
+ attr_accessor :default_refresh_token_duration
206
+
207
+ # ==========================================================================
208
+ # Server Metadata (RFC 8414)
209
+ # ==========================================================================
210
+
211
+ # @!attribute [rw] authorization_server_documentation
212
+ # URL for service documentation in authorization server metadata per RFC 8414.
213
+ # @return [String, nil] documentation URL
214
+ attr_accessor :authorization_server_documentation
215
+
216
+ # ==========================================================================
217
+ # Dynamic Client Registration (RFC 7591)
218
+ # ==========================================================================
219
+
220
+ # @!attribute [rw] dcr_enabled
148
221
  # Enable dynamic client registration per RFC 7591.
149
- # @return [Boolean] true if enabled (default: false)
150
- attr_accessor :rfc_7591_enabled
222
+ # @return [Boolean] true if enabled (default: true)
223
+ attr_accessor :dcr_enabled
151
224
 
152
- # @!attribute [rw] rfc_7591_require_initial_access_token
225
+ # @!attribute [rw] dcr_require_initial_access_token
153
226
  # Require initial access token for client registration per RFC 7591.
154
227
  # @return [Boolean] true if required (default: false)
155
- attr_accessor :rfc_7591_require_initial_access_token
228
+ attr_accessor :dcr_require_initial_access_token
156
229
 
157
- # @!attribute [rw] rfc_7591_initial_access_token_validator
230
+ # @!attribute [rw] dcr_initial_access_token_validator
158
231
  # Callable object to validate initial access tokens.
159
232
  # Should accept a token string and return true/false.
160
233
  # @return [Proc, nil] validator callable
161
- attr_accessor :rfc_7591_initial_access_token_validator
234
+ attr_accessor :dcr_initial_access_token_validator
162
235
 
163
- # @!attribute [rw] rfc_7591_allowed_grant_types
236
+ # @!attribute [rw] dcr_allowed_grant_types
164
237
  # Array of grant types allowed during client registration per RFC 7591.
165
238
  # @return [Array<String>] allowed grant types
166
- attr_accessor :rfc_7591_allowed_grant_types
239
+ attr_accessor :dcr_allowed_grant_types
167
240
 
168
- # @!attribute [rw] rfc_7591_allowed_response_types
241
+ # @!attribute [rw] dcr_allowed_response_types
169
242
  # Array of response types allowed during client registration per RFC 7591.
170
243
  # @return [Array<String>] allowed response types
171
- attr_accessor :rfc_7591_allowed_response_types
244
+ attr_accessor :dcr_allowed_response_types
172
245
 
173
- # @!attribute [rw] rfc_7591_allowed_scopes
246
+ # @!attribute [rw] dcr_allowed_scopes
174
247
  # Array of scopes allowed during client registration per RFC 7591.
175
248
  # @return [Array<String>, nil] allowed scopes
176
- attr_accessor :rfc_7591_allowed_scopes
249
+ attr_accessor :dcr_allowed_scopes
177
250
 
178
- # @!attribute [rw] rfc_7591_allowed_token_endpoint_auth_methods
251
+ # @!attribute [rw] dcr_allowed_token_endpoint_auth_methods
179
252
  # Array of token endpoint authentication methods allowed per RFC 7591.
180
253
  # @return [Array<String>] allowed auth methods
181
- attr_accessor :rfc_7591_allowed_token_endpoint_auth_methods
254
+ attr_accessor :dcr_allowed_token_endpoint_auth_methods
182
255
 
183
- # @!attribute [rw] rfc_7591_client_secret_expiration
256
+ # @!attribute [rw] dcr_client_secret_expiration
184
257
  # Duration in seconds before client secrets expire, or nil for no expiration.
185
258
  # @return [Integer, nil] expiration duration in seconds
186
- attr_accessor :rfc_7591_client_secret_expiration
259
+ attr_accessor :dcr_client_secret_expiration
187
260
 
188
- # @!attribute [rw] rfc_7591_software_statement_jwks
261
+ # @!attribute [rw] dcr_software_statement_jwks
189
262
  # JWKS for verifying software statements during registration per RFC 7591.
190
263
  # @return [Hash, nil] JWKS
191
- attr_accessor :rfc_7591_software_statement_jwks
264
+ attr_accessor :dcr_software_statement_jwks
192
265
 
193
- # @!attribute [rw] rfc_7591_software_statement_required
266
+ # @!attribute [rw] dcr_software_statement_required
194
267
  # Require software statements during client registration per RFC 7591.
195
268
  # @return [Boolean] true if required (default: false)
196
- attr_accessor :rfc_7591_software_statement_required
269
+ attr_accessor :dcr_software_statement_required
197
270
 
198
- # @!attribute [rw] rfc_7591_jwks_cache_ttl
271
+ # @!attribute [rw] dcr_jwks_cache_ttl
199
272
  # Time-to-live for cached JWKS in seconds.
200
273
  # @return [Integer] TTL in seconds (default: 3600)
201
- attr_accessor :rfc_7591_jwks_cache_ttl
274
+ attr_accessor :dcr_jwks_cache_ttl
275
+
276
+ # ==========================================================================
277
+ # Client Metadata Document (draft-ietf-oauth-client-id-metadata-document)
278
+ # ==========================================================================
202
279
 
203
280
  # @!attribute [rw] client_metadata_document_enabled
204
281
  # Enable support for client metadata documents (URL-based client IDs).
@@ -235,40 +312,12 @@ module TokenAuthority
235
312
  # @return [Integer] timeout in seconds (default: 5)
236
313
  attr_accessor :client_metadata_document_read_timeout
237
314
 
238
- # @!attribute [rw] rfc_8707_resources
239
- # Hash mapping resource URIs to human-readable descriptions per RFC 8707.
240
- # Set to nil or empty hash to disable resource indicators.
241
- # @example
242
- # config.rfc_8707_resources = {
243
- # "https://api.example.com" => "Main API",
244
- # "https://files.example.com" => "File Storage API"
245
- # }
246
- # @return [Hash{String => String}, nil] resource mappings
247
- attr_accessor :rfc_8707_resources
248
-
249
- # @!attribute [rw] rfc_8707_require_resource
250
- # Whether clients must include a resource parameter per RFC 8707.
251
- # @return [Boolean] true if required (default: false)
252
- attr_accessor :rfc_8707_require_resource
253
-
254
- # @!attribute [rw] event_logging_enabled
255
- # Enable structured event logging for OAuth flows and security events.
256
- # @return [Boolean] true if enabled (default: true)
257
- attr_accessor :event_logging_enabled
258
-
259
- # @!attribute [rw] event_logging_debug_events
260
- # Enable debug-level event logging for troubleshooting.
261
- # @return [Boolean] true if enabled (default: false)
262
- attr_accessor :event_logging_debug_events
263
-
264
- # @!attribute [rw] instrumentation_enabled
265
- # Enable ActiveSupport::Notifications instrumentation for performance monitoring.
266
- # @return [Boolean] true if enabled (default: true)
267
- attr_accessor :instrumentation_enabled
268
-
269
315
  def initialize
270
316
  # General
271
317
  @secret_key = nil
318
+ @event_logging_enabled = true
319
+ @event_logging_debug_events = false
320
+ @instrumentation_enabled = true
272
321
 
273
322
  # User Authentication
274
323
  @authenticatable_controller = "ApplicationController"
@@ -279,41 +328,34 @@ module TokenAuthority
279
328
  @error_page_layout = "application"
280
329
 
281
330
  # Scopes
282
- @scopes = nil
283
- @require_scope = false
331
+ @scopes = {}
332
+ @require_scope = true
333
+
334
+ # Resources
335
+ @resources = {}
336
+ @require_resource = true
284
337
 
285
338
  # JWT Access Tokens (RFC 9068)
286
- @rfc_9068_audience_url = nil
287
- @rfc_9068_issuer_url = nil
288
- @rfc_9068_default_access_token_duration = 300 # 5 minutes in seconds
289
- @rfc_9068_default_refresh_token_duration = 1_209_600 # 14 days in seconds
339
+ @token_audience_url = nil
340
+ @token_issuer_url = nil
341
+ @default_access_token_duration = 300 # 5 minutes in seconds
342
+ @default_refresh_token_duration = 1_209_600 # 14 days in seconds
290
343
 
291
344
  # Server Metadata (RFC 8414)
292
- @rfc_8414_service_documentation = nil
293
-
294
- # Protected Resource Metadata (RFC 9728)
295
- @rfc_9728_resource = nil
296
- @rfc_9728_scopes_supported = nil
297
- @rfc_9728_authorization_servers = nil
298
- @rfc_9728_bearer_methods_supported = nil
299
- @rfc_9728_jwks_uri = nil
300
- @rfc_9728_resource_name = nil
301
- @rfc_9728_resource_documentation = nil
302
- @rfc_9728_resource_policy_uri = nil
303
- @rfc_9728_resource_tos_uri = nil
345
+ @authorization_server_documentation = nil
304
346
 
305
347
  # Dynamic Client Registration (RFC 7591)
306
- @rfc_7591_enabled = false
307
- @rfc_7591_require_initial_access_token = false
308
- @rfc_7591_initial_access_token_validator = nil
309
- @rfc_7591_allowed_grant_types = %w[authorization_code refresh_token]
310
- @rfc_7591_allowed_response_types = %w[code]
311
- @rfc_7591_allowed_scopes = nil
312
- @rfc_7591_allowed_token_endpoint_auth_methods = %w[none client_secret_basic client_secret_post client_secret_jwt private_key_jwt]
313
- @rfc_7591_client_secret_expiration = nil
314
- @rfc_7591_software_statement_jwks = nil
315
- @rfc_7591_software_statement_required = false
316
- @rfc_7591_jwks_cache_ttl = 3600
348
+ @dcr_enabled = true
349
+ @dcr_require_initial_access_token = false
350
+ @dcr_initial_access_token_validator = nil
351
+ @dcr_allowed_grant_types = %w[authorization_code refresh_token]
352
+ @dcr_allowed_response_types = %w[code]
353
+ @dcr_allowed_scopes = nil
354
+ @dcr_allowed_token_endpoint_auth_methods = %w[none client_secret_basic client_secret_post client_secret_jwt private_key_jwt]
355
+ @dcr_client_secret_expiration = nil
356
+ @dcr_software_statement_jwks = nil
357
+ @dcr_software_statement_required = false
358
+ @dcr_jwks_cache_ttl = 3600
317
359
 
318
360
  # Client Metadata Document (draft-ietf-oauth-client-id-metadata-document)
319
361
  @client_metadata_document_enabled = true
@@ -323,17 +365,6 @@ module TokenAuthority
323
365
  @client_metadata_document_blocked_hosts = []
324
366
  @client_metadata_document_connect_timeout = 5
325
367
  @client_metadata_document_read_timeout = 5
326
-
327
- # Resource Indicators (RFC 8707)
328
- @rfc_8707_resources = nil
329
- @rfc_8707_require_resource = false
330
-
331
- # Event Logging
332
- @event_logging_enabled = true
333
- @event_logging_debug_events = false
334
-
335
- # Instrumentation
336
- @instrumentation_enabled = true
337
368
  end
338
369
 
339
370
  # Checks whether the scopes feature is enabled.
@@ -344,28 +375,167 @@ module TokenAuthority
344
375
  scopes.is_a?(Hash) && scopes.any?
345
376
  end
346
377
 
347
- # Checks whether RFC 8707 resource indicators are enabled.
348
- # Resource indicators are considered enabled when rfc_8707_resources is a non-empty hash.
378
+ # Checks whether resources are configured.
379
+ # Resources are enabled when at least one resource is configured.
380
+ #
381
+ # @return [Boolean] true if resources are enabled
382
+ def resources_enabled?
383
+ resource_registry.any?
384
+ end
385
+
386
+ # Builds a mapping of resource URIs to display names from resource configuration.
387
+ #
388
+ # This derives the RFC 8707 resource allowlist from the resources configuration.
389
+ # Each configured resource's :resource URI becomes a key, with its :resource_name
390
+ # (or the URI itself) as the display name.
391
+ #
392
+ # The result is used for:
393
+ # - Validating resource indicators in authorization requests
394
+ # - Displaying resource names on the consent screen
395
+ #
396
+ # @return [Hash{String => String}] mapping of resource URIs to display names
349
397
  #
350
- # @return [Boolean] true if resource indicators are enabled
351
- def rfc_8707_enabled?
352
- rfc_8707_resources.is_a?(Hash) && rfc_8707_resources.any?
398
+ # @example With resources configured
399
+ # config.resources = {
400
+ # api: { resource: "https://api.example.com", resource_name: "REST API" },
401
+ # mcp: { resource: "https://mcp.example.com", resource_name: "MCP Server" }
402
+ # }
403
+ # config.resource_registry
404
+ # # => { "https://api.example.com" => "REST API", "https://mcp.example.com" => "MCP Server" }
405
+ def resource_registry
406
+ return {} unless resources.is_a?(Hash)
407
+
408
+ resources.each_with_object({}) do |(_key, config), registry|
409
+ next unless config.is_a?(Hash) && config[:resource].present?
410
+
411
+ uri = config[:resource]
412
+ registry[uri] = config[:resource_name] || uri
413
+ end
353
414
  end
354
415
 
355
416
  # Validates the configuration for internal consistency.
356
417
  # Ensures that required features are properly configured before use.
357
418
  #
358
419
  # @raise [ConfigurationError] if require_scope is true but scopes are not configured
359
- # @raise [ConfigurationError] if rfc_8707_require_resource is true but resources are not configured
420
+ # @raise [ConfigurationError] if require_resource is true but no resources are configured
421
+ # @raise [ConfigurationError] if any resource entry is missing the required :resource field
422
+ # @raise [ConfigurationError] if no issuer URL is available
360
423
  # @return [void]
361
424
  def validate!
362
425
  if require_scope && !scopes_enabled?
363
426
  raise ConfigurationError, "require_scope is true but no scopes are configured"
364
427
  end
365
428
 
366
- if rfc_8707_require_resource && !rfc_8707_enabled?
367
- raise ConfigurationError, "rfc_8707_require_resource is true but no rfc_8707_resources are configured"
429
+ # Validate resource entries first (before checking if any valid resources exist)
430
+ if resources.is_a?(Hash)
431
+ resources.each do |key, config|
432
+ next unless config.is_a?(Hash)
433
+
434
+ if config[:resource].blank?
435
+ raise ConfigurationError, "resource :#{key} is missing the required :resource field"
436
+ end
437
+ end
368
438
  end
439
+
440
+ if require_resource && !resources_enabled?
441
+ raise ConfigurationError, "require_resource is true but no resources are configured"
442
+ end
443
+
444
+ if issuer_url.blank?
445
+ raise ConfigurationError,
446
+ "no issuer URL configured: set token_issuer_url or add authorization_servers to a resource"
447
+ end
448
+ end
449
+
450
+ # Resolves protected resource configuration using subdomain-aware lookup.
451
+ #
452
+ # Lookup strategy:
453
+ # 1. If resource_key is present, look it up as a symbol in resources
454
+ # 2. If not found or resource_key is blank, use the first resource in the hash
455
+ # 3. If resources is empty, return nil (controller will 404)
456
+ #
457
+ # @param resource_key [String, nil] the subdomain or lookup key
458
+ # @return [Hash, nil] the resource metadata, or nil if not configured
459
+ #
460
+ # @example Subdomain-specific lookup
461
+ # config.resources = { api: { resource: "https://api.example.com" } }
462
+ # config.protected_resource_for("api") # => { resource: "https://api.example.com" }
463
+ #
464
+ # @example Fallback to first resource
465
+ # config.resources = { api: { resource: "https://api.example.com" } }
466
+ # config.protected_resource_for("unknown") # => { resource: "https://api.example.com" }
467
+ #
468
+ # @example Not configured
469
+ # config.resources = {}
470
+ # config.protected_resource_for(nil) # => nil (will cause 404)
471
+ def protected_resource_for(resource_key)
472
+ return nil unless resources.is_a?(Hash) && resources.any?
473
+
474
+ # Try subdomain lookup first, fall back to first resource
475
+ if resource_key.present?
476
+ resources[resource_key.to_sym] || resources.values.first
477
+ else
478
+ resources.values.first
479
+ end
480
+ end
481
+
482
+ # Returns the effective audience URL for JWT tokens.
483
+ #
484
+ # The audience URL is determined as follows:
485
+ # 1. If token_audience_url is set, use that value
486
+ # 2. Otherwise, derive from the first resource's :resource URL
487
+ #
488
+ # @return [String, nil] the audience URL, or nil if not configured
489
+ #
490
+ # @example Explicit audience URL
491
+ # config.token_audience_url = "https://api.example.com"
492
+ # config.audience_url # => "https://api.example.com"
493
+ #
494
+ # @example Derived from resources
495
+ # config.token_audience_url = nil
496
+ # config.resources = { api: { resource: "https://api.example.com" } }
497
+ # config.audience_url # => "https://api.example.com"
498
+ def audience_url
499
+ return token_audience_url if token_audience_url.present?
500
+
501
+ # Derive from first resource's :resource URL
502
+ return nil unless resources.is_a?(Hash) && resources.any?
503
+
504
+ first_resource = resources.values.first
505
+ return nil unless first_resource.is_a?(Hash)
506
+
507
+ first_resource[:resource]
508
+ end
509
+
510
+ # Returns the effective issuer URL for JWT tokens.
511
+ #
512
+ # The issuer URL is determined as follows:
513
+ # 1. If token_issuer_url is set, use that value
514
+ # 2. Otherwise, derive from the first resource's authorization_servers
515
+ #
516
+ # @return [String, nil] the issuer URL, or nil if not configured
517
+ #
518
+ # @example Explicit issuer URL
519
+ # config.token_issuer_url = "https://auth.example.com"
520
+ # config.issuer_url # => "https://auth.example.com"
521
+ #
522
+ # @example Derived from authorization_servers
523
+ # config.token_issuer_url = nil
524
+ # config.resources = { api: { authorization_servers: ["https://auth.example.com"] } }
525
+ # config.issuer_url # => "https://auth.example.com"
526
+ def issuer_url
527
+ return token_issuer_url if token_issuer_url.present?
528
+
529
+ # Derive from first resource's authorization_servers
530
+ return nil unless resources.is_a?(Hash) && resources.any?
531
+
532
+ first_resource = resources.values.first
533
+ return nil unless first_resource.is_a?(Hash)
534
+
535
+ auth_servers = first_resource[:authorization_servers]
536
+ return nil unless auth_servers.is_a?(Array) && auth_servers.any?
537
+
538
+ auth_servers.first
369
539
  end
370
540
  end
371
541