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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -1
- data/README.md +52 -14
- data/app/controllers/concerns/token_authority/initial_access_token_authentication.rb +2 -2
- data/app/controllers/token_authority/protected_resource_metadata_controller.rb +39 -0
- data/app/helpers/token_authority/authorization_grants_helper.rb +2 -3
- data/app/models/concerns/token_authority/claim_validatable.rb +2 -2
- data/app/models/concerns/token_authority/resourceable.rb +2 -2
- data/app/models/token_authority/access_token.rb +2 -2
- data/app/models/token_authority/access_token_request.rb +1 -1
- data/app/models/token_authority/authorization_request.rb +2 -2
- data/app/models/token_authority/authorization_server_metadata.rb +4 -4
- data/app/models/token_authority/client.rb +4 -4
- data/app/models/token_authority/client_metadata_document.rb +2 -2
- data/app/models/token_authority/client_registration_request.rb +5 -5
- data/app/models/token_authority/jwks_fetcher.rb +1 -1
- data/app/models/token_authority/protected_resource_metadata.rb +110 -31
- data/app/models/token_authority/refresh_token.rb +2 -2
- data/app/models/token_authority/refresh_token_request.rb +1 -1
- data/lib/generators/token_authority/install/templates/token_authority.rb +100 -114
- data/lib/token_authority/configuration.rb +345 -175
- data/lib/token_authority/errors.rb +29 -0
- data/lib/token_authority/routing/constraints.rb +2 -2
- data/lib/token_authority/routing/routes.rb +74 -16
- data/lib/token_authority/version.rb +1 -1
- data/lib/token_authority.rb +2 -2
- metadata +2 -2
- data/app/controllers/token_authority/resource_metadata_controller.rb +0 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8989a407b702176b74b3e121e4463dfc28e858097716c4cf6752f2ab54c78871
|
|
4
|
+
data.tar.gz: 1ecdecd8785d3f214e64359b4ab7e43ecb8630eb2af9237bb5a4d8a604d5227d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3b4bbc0e464d6781255efe9b523d753abbe78843c5bec77c5057861eef157b112e099c4afbd8a073ef0f1d1f69fff1946f27dddb6d0ce95a9a91bb3e0bc6c34
|
|
7
|
+
data.tar.gz: 0615a3d781b577ebb433bc5e01cae57827a8fe5bad6a6866092ea44438526e81090c28a39b708120150610c34a544fb4a405f8d9331f16e18f7943a27e55df3a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2025-01-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added support for multiple protected resources; applications can now define multiple protected resources under different subdomain constraints.
|
|
8
|
+
- Added `issuer_url` method that returns either `token_issuer_url` or derives from `authorization_servers`
|
|
9
|
+
- Added validation requiring either `token_issuer_url` or `:authorization_servers` on at least one resource
|
|
10
|
+
|
|
11
|
+
### Breaking
|
|
12
|
+
|
|
13
|
+
- Replaced `rfc_9728_*` config options with `config.resources` hash (keyed by symbol)
|
|
14
|
+
- Replaced `token_authority_routes` with `token_authority_auth_server_routes` and `token_authority_protected_resource_route`
|
|
15
|
+
- Removed `rfc_8707_resources` config option; resource allowlist is now derived from `config.resources`
|
|
16
|
+
- Renamed `rfc_8707_require_resource` to `require_resource`
|
|
17
|
+
- Renamed `rfc_8707_enabled?` method to `resources_enabled?`
|
|
18
|
+
- Changed default for `require_scope` from `false` to `true`
|
|
19
|
+
- Changed default for `require_resource` from `false` to `true`
|
|
20
|
+
- Changed default for `token_audience_url` from application URL to `nil`
|
|
21
|
+
- Changed default for `dcr_enabled` from `false` to `true`
|
|
22
|
+
- Changed default for `token_issuer_url` from required to `nil`
|
|
23
|
+
- When `token_audience_url` is nil, the `:resource` URL is used as the audience claim
|
|
24
|
+
- When `token_issuer_url` is nil, it's derived from the first resource's `:authorization_servers`
|
|
25
|
+
- Renamed configuration options to remove RFC number prefixes:
|
|
26
|
+
- `rfc_9068_audience_url` → `token_audience_url`
|
|
27
|
+
- `rfc_9068_issuer_url` → `token_issuer_url`
|
|
28
|
+
- `rfc_9068_default_access_token_duration` → `default_access_token_duration`
|
|
29
|
+
- `rfc_9068_default_refresh_token_duration` → `default_refresh_token_duration`
|
|
30
|
+
- `rfc_8414_service_documentation` → `authorization_server_documentation`
|
|
31
|
+
- `rfc_7591_enabled` → `dcr_enabled`
|
|
32
|
+
- `rfc_7591_require_initial_access_token` → `dcr_require_initial_access_token`
|
|
33
|
+
- `rfc_7591_initial_access_token_validator` → `dcr_initial_access_token_validator`
|
|
34
|
+
- `rfc_7591_allowed_grant_types` → `dcr_allowed_grant_types`
|
|
35
|
+
- `rfc_7591_allowed_response_types` → `dcr_allowed_response_types`
|
|
36
|
+
- `rfc_7591_allowed_scopes` → `dcr_allowed_scopes`
|
|
37
|
+
- `rfc_7591_allowed_token_endpoint_auth_methods` → `dcr_allowed_token_endpoint_auth_methods`
|
|
38
|
+
- `rfc_7591_client_secret_expiration` → `dcr_client_secret_expiration`
|
|
39
|
+
- `rfc_7591_software_statement_jwks` → `dcr_software_statement_jwks`
|
|
40
|
+
- `rfc_7591_software_statement_required` → `dcr_software_statement_required`
|
|
41
|
+
- `rfc_7591_jwks_cache_ttl` → `dcr_jwks_cache_ttl`
|
|
42
|
+
|
|
3
43
|
## [0.2.1] - 2025-01-24
|
|
4
44
|
|
|
5
45
|
### Fixes
|
|
@@ -29,7 +69,8 @@
|
|
|
29
69
|
|
|
30
70
|
- Initial release
|
|
31
71
|
|
|
32
|
-
[Unreleased]: https://github.com/dickdavis/token_authority/compare/v0.
|
|
72
|
+
[Unreleased]: https://github.com/dickdavis/token_authority/compare/v0.3.0...HEAD
|
|
73
|
+
[0.2.1]: https://github.com/dickdavis/token_authority/compare/v0.2.1...v0.3.0
|
|
33
74
|
[0.2.1]: https://github.com/dickdavis/token_authority/compare/v0.2.0...v0.2.1
|
|
34
75
|
[0.2.0]: https://github.com/dickdavis/token_authority/compare/v0.1.0...v0.2.0
|
|
35
76
|
[0.1.0]: https://github.com/dickdavis/token_authority/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# TokenAuthority
|
|
2
2
|
|
|
3
|
-
Rails engine allowing apps to act as their own OAuth 2.1 provider. The goal of this project is to make authorization
|
|
4
|
-
|
|
5
|
-
This project aims to implement the OAuth standards specified in the [MCP Authorization Specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#standards-compliance).
|
|
3
|
+
Rails engine allowing apps to act as their own OAuth 2.1 provider. The goal of this project is to make standards-based authorization as simple as possible.
|
|
6
4
|
|
|
7
5
|
| Status | Standard |
|
|
8
6
|
|--------|----------|
|
|
@@ -38,22 +36,35 @@ See the [Installation Guide](https://github.com/dickdavis/token_authority/wiki/I
|
|
|
38
36
|
|
|
39
37
|
### Configuration
|
|
40
38
|
|
|
41
|
-
Configure TokenAuthority in the generated initializer. The following represents a minimal configuration:
|
|
39
|
+
Configure TokenAuthority in the generated initializer. TokenAuthority is configured with dynamic client registration, client metadata documents, and resource indicators enabled by default. The following represents a minimal configuration:
|
|
42
40
|
|
|
43
41
|
```ruby
|
|
44
42
|
# config/initializers/token_authority.rb
|
|
45
43
|
TokenAuthority.configure do |config|
|
|
46
|
-
# The secret key used for
|
|
44
|
+
# The secret key used for signing JWT tokens
|
|
47
45
|
config.secret_key = Rails.application.credentials.secret_key_base
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
# The URI for the authorization server (to be included in tokens and metadata)
|
|
51
|
-
config.rfc_9068_issuer_url = "https://example.com/"
|
|
52
|
-
# Define available scopes and their descriptions (shown on consent screen)
|
|
46
|
+
|
|
47
|
+
# Define available scopes (required by default)
|
|
53
48
|
config.scopes = {
|
|
54
49
|
"read" => "Read your data",
|
|
55
|
-
"write" => "Create and modify your data"
|
|
56
|
-
|
|
50
|
+
"write" => "Create and modify your data"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Define protected resources (required by default)
|
|
54
|
+
# :resource is used as the audience (aud) claim in tokens
|
|
55
|
+
# :authorization_servers provides the issuer (iss) claim
|
|
56
|
+
config.resources = {
|
|
57
|
+
api: {
|
|
58
|
+
resource: "https://example.com/api",
|
|
59
|
+
resource_name: "My API",
|
|
60
|
+
scopes_supported: %w[read write],
|
|
61
|
+
authorization_servers: ["https://example.com"],
|
|
62
|
+
bearer_methods_supported: ["header"],
|
|
63
|
+
jwks_uri: "https://example.com/.well-known/jwks.json",
|
|
64
|
+
resource_documentation: "https://example.com/docs/api",
|
|
65
|
+
resource_policy_uri: "https://example.com/privacy",
|
|
66
|
+
resource_tos_uri: "https://example.com/terms"
|
|
67
|
+
}
|
|
57
68
|
}
|
|
58
69
|
end
|
|
59
70
|
```
|
|
@@ -66,7 +77,8 @@ Add the engine routes to your `config/routes.rb`:
|
|
|
66
77
|
|
|
67
78
|
```ruby
|
|
68
79
|
Rails.application.routes.draw do
|
|
69
|
-
|
|
80
|
+
token_authority_auth_server_routes
|
|
81
|
+
token_authority_protected_resource_route
|
|
70
82
|
end
|
|
71
83
|
```
|
|
72
84
|
|
|
@@ -79,10 +91,36 @@ To mount the engine at a different path, use the `at` option:
|
|
|
79
91
|
|
|
80
92
|
```ruby
|
|
81
93
|
Rails.application.routes.draw do
|
|
82
|
-
|
|
94
|
+
token_authority_auth_server_routes(at: "/auth")
|
|
95
|
+
token_authority_protected_resource_route
|
|
83
96
|
end
|
|
84
97
|
```
|
|
85
98
|
|
|
99
|
+
For applications with multiple protected resources, each resource must be on its own subdomain. This is because RFC 9728 defines a fixed well-known path (`/.well-known/oauth-protected-resource`) that can only exist once per host:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
Rails.application.routes.draw do
|
|
103
|
+
token_authority_auth_server_routes
|
|
104
|
+
|
|
105
|
+
constraints subdomain: "api" do
|
|
106
|
+
token_authority_protected_resource_route
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
constraints subdomain: "mcp" do
|
|
110
|
+
token_authority_protected_resource_route
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
> **Development Note:** Rails subdomain constraints require a real domain with proper DNS resolution. Use `lvh.me` (which resolves to `127.0.0.1`) for local development: `mcp.lvh.me:3000`, `api.lvh.me:3000`, etc. You'll also need to allow these hosts in your development config:
|
|
116
|
+
>
|
|
117
|
+
> ```ruby
|
|
118
|
+
> # config/environments/development.rb
|
|
119
|
+
> config.hosts << /.*\.lvh\.me/
|
|
120
|
+
> ```
|
|
121
|
+
>
|
|
122
|
+
> See the [Installation Guide](https://github.com/dickdavis/token_authority/wiki/Installation-Guide#subdomain-development-setup) for details.
|
|
123
|
+
|
|
86
124
|
### User Consent
|
|
87
125
|
|
|
88
126
|
Before issuing authorization codes, TokenAuthority displays a consent screen where users can approve or deny access to OAuth clients. The consent views are fully customizable and the layout is configurable—see [Customizing Views](https://github.com/dickdavis/token_authority/wiki/Customizing-Views) for details.
|
|
@@ -13,14 +13,14 @@ module TokenAuthority
|
|
|
13
13
|
private
|
|
14
14
|
|
|
15
15
|
def initial_access_token_required?
|
|
16
|
-
TokenAuthority.config.
|
|
16
|
+
TokenAuthority.config.dcr_require_initial_access_token
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def authenticate_initial_access_token
|
|
20
20
|
token = extract_bearer_token
|
|
21
21
|
raise TokenAuthority::InvalidInitialAccessTokenError if token.blank?
|
|
22
22
|
|
|
23
|
-
validator = TokenAuthority.config.
|
|
23
|
+
validator = TokenAuthority.config.dcr_initial_access_token_validator
|
|
24
24
|
raise TokenAuthority::InvalidInitialAccessTokenError unless validator&.call(token)
|
|
25
25
|
end
|
|
26
26
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TokenAuthority
|
|
4
|
+
##
|
|
5
|
+
# Serves RFC 9728 Protected Resource Metadata at /.well-known/oauth-protected-resource
|
|
6
|
+
#
|
|
7
|
+
# This controller enables OAuth clients to discover metadata about protected resources
|
|
8
|
+
# without requiring manual configuration. Clients can query this endpoint to learn:
|
|
9
|
+
# - Which authorization servers issue tokens for this resource
|
|
10
|
+
# - What scopes are supported
|
|
11
|
+
# - How to present bearer tokens (header vs body)
|
|
12
|
+
# - Where to find public keys for token verification
|
|
13
|
+
#
|
|
14
|
+
# The subdomain extraction strategy supports multi-tenant deployments where different
|
|
15
|
+
# subdomains represent different protected resources (e.g., api.example.com vs
|
|
16
|
+
# mcp.example.com). Returns 404 if no configuration exists for the subdomain.
|
|
17
|
+
#
|
|
18
|
+
# @see https://www.rfc-editor.org/rfc/rfc9728.html RFC 9728
|
|
19
|
+
# @since 0.3.0
|
|
20
|
+
class ProtectedResourceMetadataController < ActionController::API
|
|
21
|
+
# Returns metadata for the protected resource identified by request subdomain.
|
|
22
|
+
#
|
|
23
|
+
# The subdomain is extracted from the request and used to look up configuration.
|
|
24
|
+
# For requests without a subdomain (bare domain), uses the default protected_resource
|
|
25
|
+
# configuration if present. This allows both single-resource and multi-resource
|
|
26
|
+
# deployments with the same code.
|
|
27
|
+
#
|
|
28
|
+
# @return [JSON] RFC 9728 compliant metadata response
|
|
29
|
+
# @return [HTTP 404] if no configuration exists for this subdomain
|
|
30
|
+
def show
|
|
31
|
+
metadata = ProtectedResourceMetadata.new(resource: request.subdomain)
|
|
32
|
+
render json: metadata.to_h
|
|
33
|
+
rescue ResourceNotConfiguredError
|
|
34
|
+
# Return 404 rather than 500 because this is a client error: they're querying
|
|
35
|
+
# a subdomain that hasn't been configured as a protected resource
|
|
36
|
+
head :not_found
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
module TokenAuthority
|
|
4
4
|
module AuthorizationGrantsHelper
|
|
5
5
|
# Returns a human-friendly display name for a resource URI.
|
|
6
|
-
# Looks up the URI in the
|
|
6
|
+
# Looks up the URI in the resource registry derived from protected resources.
|
|
7
7
|
# Falls back to the URI itself if no mapping is configured.
|
|
8
8
|
#
|
|
9
9
|
# @param resource_uri [String] The resource URI
|
|
10
10
|
# @return [String] The display name or the URI if no mapping exists
|
|
11
11
|
def resource_display_name(resource_uri)
|
|
12
|
-
|
|
13
|
-
resources[resource_uri] || resource_uri
|
|
12
|
+
TokenAuthority.config.resource_registry[resource_uri] || resource_uri
|
|
14
13
|
end
|
|
15
14
|
|
|
16
15
|
# Returns a human-friendly display name for a scope.
|
|
@@ -37,7 +37,7 @@ module TokenAuthority
|
|
|
37
37
|
|
|
38
38
|
validates :jti, presence: true
|
|
39
39
|
|
|
40
|
-
validates :aud, presence: true, format: {with: /\A#{TokenAuthority.config.
|
|
40
|
+
validates :aud, presence: true, format: {with: /\A#{TokenAuthority.config.audience_url}*/}
|
|
41
41
|
|
|
42
42
|
validates :exp, presence: true
|
|
43
43
|
validate do
|
|
@@ -46,7 +46,7 @@ module TokenAuthority
|
|
|
46
46
|
errors.add(:exp, :expired) if Time.zone.now > Time.zone.at(exp)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
validates :iss, presence: true, format: {with: /\A#{TokenAuthority.config.
|
|
49
|
+
validates :iss, presence: true, format: {with: /\A#{TokenAuthority.config.issuer_url}\z/}
|
|
50
50
|
|
|
51
51
|
after_validation :expire_token_authority_session, if: :errors_for_expirable_claims?
|
|
52
52
|
after_validation :revoke_token_authority_session, if: :errors_for_revocable_claims?
|
|
@@ -88,9 +88,9 @@ module TokenAuthority
|
|
|
88
88
|
# @return [Boolean] true if all resources are allowed
|
|
89
89
|
# @api private
|
|
90
90
|
def allowed_resources?
|
|
91
|
-
return true unless TokenAuthority.config.
|
|
91
|
+
return true unless TokenAuthority.config.resources_enabled?
|
|
92
92
|
|
|
93
|
-
resources.all? { |uri| TokenAuthority.config.
|
|
93
|
+
resources.all? { |uri| TokenAuthority.config.resource_registry.key?(uri) }
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
# Checks if the current resources are a subset of the granted resources.
|
|
@@ -74,7 +74,7 @@ module TokenAuthority
|
|
|
74
74
|
aud = if resources.any?
|
|
75
75
|
(resources.size == 1) ? resources.first : resources
|
|
76
76
|
else
|
|
77
|
-
TokenAuthority.config.
|
|
77
|
+
TokenAuthority.config.audience_url
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
scope_claim = scopes.any? ? scopes.join(" ") : nil
|
|
@@ -83,7 +83,7 @@ module TokenAuthority
|
|
|
83
83
|
aud:,
|
|
84
84
|
exp:,
|
|
85
85
|
iat: Time.zone.now.to_i,
|
|
86
|
-
iss: TokenAuthority.config.
|
|
86
|
+
iss: TokenAuthority.config.issuer_url,
|
|
87
87
|
jti: SecureRandom.uuid,
|
|
88
88
|
sub: user_id.to_s,
|
|
89
89
|
client_id:,
|
|
@@ -141,7 +141,7 @@ module TokenAuthority
|
|
|
141
141
|
return if resources.empty?
|
|
142
142
|
|
|
143
143
|
# If resources are provided but feature is disabled, reject them
|
|
144
|
-
unless TokenAuthority.config.
|
|
144
|
+
unless TokenAuthority.config.resources_enabled?
|
|
145
145
|
errors.add(:resources, :not_allowed)
|
|
146
146
|
return
|
|
147
147
|
end
|
|
@@ -225,7 +225,7 @@ module TokenAuthority
|
|
|
225
225
|
|
|
226
226
|
def resources_must_be_valid
|
|
227
227
|
# Check if resource is required
|
|
228
|
-
if TokenAuthority.config.
|
|
228
|
+
if TokenAuthority.config.require_resource && resources.empty?
|
|
229
229
|
errors.add(:resources, :required)
|
|
230
230
|
return
|
|
231
231
|
end
|
|
@@ -233,7 +233,7 @@ module TokenAuthority
|
|
|
233
233
|
return if resources.empty?
|
|
234
234
|
|
|
235
235
|
# If resources are provided but feature is disabled, reject them
|
|
236
|
-
unless TokenAuthority.config.
|
|
236
|
+
unless TokenAuthority.config.resources_enabled?
|
|
237
237
|
errors.add(:resources, :not_allowed)
|
|
238
238
|
return
|
|
239
239
|
end
|
|
@@ -61,7 +61,7 @@ module TokenAuthority
|
|
|
61
61
|
metadata[:service_documentation] = service_documentation if service_documentation.present?
|
|
62
62
|
|
|
63
63
|
# RFC 7591 Dynamic Client Registration
|
|
64
|
-
if TokenAuthority.config.
|
|
64
|
+
if TokenAuthority.config.dcr_enabled
|
|
65
65
|
metadata[:registration_endpoint] = "#{issuer}#{@mount_path}/register"
|
|
66
66
|
end
|
|
67
67
|
|
|
@@ -74,7 +74,7 @@ module TokenAuthority
|
|
|
74
74
|
# @return [String]
|
|
75
75
|
# @api private
|
|
76
76
|
def issuer
|
|
77
|
-
TokenAuthority.config.
|
|
77
|
+
TokenAuthority.config.issuer_url.to_s.chomp("/")
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
# Returns the list of supported scopes from configuration.
|
|
@@ -88,14 +88,14 @@ module TokenAuthority
|
|
|
88
88
|
# @return [String, nil]
|
|
89
89
|
# @api private
|
|
90
90
|
def service_documentation
|
|
91
|
-
TokenAuthority.config.
|
|
91
|
+
TokenAuthority.config.authorization_server_documentation
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
# Returns the supported token endpoint authentication methods.
|
|
95
95
|
# @return [Array<String>]
|
|
96
96
|
# @api private
|
|
97
97
|
def token_endpoint_auth_methods_supported
|
|
98
|
-
TokenAuthority.config.
|
|
98
|
+
TokenAuthority.config.dcr_allowed_token_endpoint_auth_methods
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
end
|
|
@@ -241,8 +241,8 @@ module TokenAuthority
|
|
|
241
241
|
end
|
|
242
242
|
|
|
243
243
|
def set_default_durations
|
|
244
|
-
self.access_token_duration ||= TokenAuthority.config.
|
|
245
|
-
self.refresh_token_duration ||= TokenAuthority.config.
|
|
244
|
+
self.access_token_duration ||= TokenAuthority.config.default_access_token_duration
|
|
245
|
+
self.refresh_token_duration ||= TokenAuthority.config.default_refresh_token_duration
|
|
246
246
|
end
|
|
247
247
|
|
|
248
248
|
def set_client_id_issued_at
|
|
@@ -251,9 +251,9 @@ module TokenAuthority
|
|
|
251
251
|
|
|
252
252
|
def set_client_secret_expiration
|
|
253
253
|
return if client_type == "public"
|
|
254
|
-
return unless TokenAuthority.config.
|
|
254
|
+
return unless TokenAuthority.config.dcr_client_secret_expiration
|
|
255
255
|
|
|
256
|
-
self.client_secret_expires_at = Time.current + TokenAuthority.config.
|
|
256
|
+
self.client_secret_expires_at = Time.current + TokenAuthority.config.dcr_client_secret_expiration
|
|
257
257
|
end
|
|
258
258
|
|
|
259
259
|
def generate_client_secret_for(secret_id)
|
|
@@ -47,11 +47,11 @@ module TokenAuthority
|
|
|
47
47
|
|
|
48
48
|
# URL-based clients use default token durations from config
|
|
49
49
|
def access_token_duration
|
|
50
|
-
TokenAuthority.config.
|
|
50
|
+
TokenAuthority.config.default_access_token_duration
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def refresh_token_duration
|
|
54
|
-
TokenAuthority.config.
|
|
54
|
+
TokenAuthority.config.default_refresh_token_duration
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# URL-based clients cannot have secrets
|
|
@@ -72,7 +72,7 @@ module TokenAuthority
|
|
|
72
72
|
def token_endpoint_auth_method_is_allowed
|
|
73
73
|
return if token_endpoint_auth_method.blank?
|
|
74
74
|
|
|
75
|
-
allowed = TokenAuthority.config.
|
|
75
|
+
allowed = TokenAuthority.config.dcr_allowed_token_endpoint_auth_methods
|
|
76
76
|
unless allowed.include?(token_endpoint_auth_method)
|
|
77
77
|
errors.add(:token_endpoint_auth_method, "is not allowed: #{token_endpoint_auth_method}")
|
|
78
78
|
end
|
|
@@ -82,7 +82,7 @@ module TokenAuthority
|
|
|
82
82
|
return if grant_types.blank?
|
|
83
83
|
return unless grant_types.is_a?(Array)
|
|
84
84
|
|
|
85
|
-
allowed = TokenAuthority.config.
|
|
85
|
+
allowed = TokenAuthority.config.dcr_allowed_grant_types
|
|
86
86
|
disallowed = grant_types - allowed
|
|
87
87
|
errors.add(:grant_types, "contains disallowed types: #{disallowed.join(", ")}") if disallowed.any?
|
|
88
88
|
end
|
|
@@ -98,7 +98,7 @@ module TokenAuthority
|
|
|
98
98
|
def scopes_are_allowed
|
|
99
99
|
return if scope.blank?
|
|
100
100
|
|
|
101
|
-
allowed = TokenAuthority.config.
|
|
101
|
+
allowed = TokenAuthority.config.dcr_allowed_scopes
|
|
102
102
|
return if allowed.blank? # If no restrictions configured, allow all
|
|
103
103
|
|
|
104
104
|
requested_scopes = scope.to_s.split(/\s+/).reject(&:blank?)
|
|
@@ -146,10 +146,10 @@ module TokenAuthority
|
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
def parse_software_statement
|
|
149
|
-
jwks = TokenAuthority.config.
|
|
149
|
+
jwks = TokenAuthority.config.dcr_software_statement_jwks
|
|
150
150
|
if jwks.present?
|
|
151
151
|
SoftwareStatement.decode_and_verify(software_statement, jwks: jwks)
|
|
152
|
-
elsif TokenAuthority.config.
|
|
152
|
+
elsif TokenAuthority.config.dcr_software_statement_required
|
|
153
153
|
raise TokenAuthority::UnapprovedSoftwareStatementError
|
|
154
154
|
else
|
|
155
155
|
SoftwareStatement.decode(software_statement)
|
|
@@ -55,7 +55,7 @@ module TokenAuthority
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def store_in_cache(uri, jwks_data)
|
|
58
|
-
ttl = TokenAuthority.config.
|
|
58
|
+
ttl = TokenAuthority.config.dcr_jwks_cache_ttl
|
|
59
59
|
uri_hash = JwksCache.hash_uri(uri)
|
|
60
60
|
|
|
61
61
|
JwksCache.find_or_initialize_by(uri_hash: uri_hash).tap do |cache|
|
|
@@ -2,73 +2,152 @@
|
|
|
2
2
|
|
|
3
3
|
module TokenAuthority
|
|
4
4
|
##
|
|
5
|
-
#
|
|
5
|
+
# Assembles RFC 9728 Protected Resource Metadata responses for OAuth clients.
|
|
6
|
+
#
|
|
7
|
+
# This class transforms configured resource metadata into standardized JSON responses
|
|
8
|
+
# that clients use to discover how to interact with protected resources. The metadata
|
|
9
|
+
# includes supported scopes, bearer token methods, and links to authorization servers.
|
|
10
|
+
#
|
|
11
|
+
# The lookup strategy supports multi-tenant deployments: the resource_key parameter
|
|
12
|
+
# (typically extracted from the request subdomain) is converted to a symbol and used
|
|
13
|
+
# to look up the configuration in config.resources. If no match is found, the first
|
|
14
|
+
# resource in the hash is used as the fallback.
|
|
15
|
+
#
|
|
16
|
+
# @example Single resource deployment
|
|
17
|
+
# metadata = ProtectedResourceMetadata.new(resource: nil)
|
|
18
|
+
# metadata.to_h
|
|
19
|
+
# # => Returns first entry from config.resources
|
|
20
|
+
#
|
|
21
|
+
# @example Multi-tenant with subdomain-specific config
|
|
22
|
+
# metadata = ProtectedResourceMetadata.new(resource: "api")
|
|
23
|
+
# metadata.to_h
|
|
24
|
+
# # => Returns config.resources[:api]
|
|
25
|
+
#
|
|
26
|
+
# @see https://www.rfc-editor.org/rfc/rfc9728.html RFC 9728 OAuth 2.0 Protected Resource Metadata
|
|
27
|
+
# @since 0.3.0
|
|
6
28
|
class ProtectedResourceMetadata
|
|
7
|
-
|
|
8
|
-
|
|
29
|
+
# RFC 9728 defines these standard metadata fields for protected resources.
|
|
30
|
+
# This constant serves as documentation of the complete metadata schema and
|
|
31
|
+
# ensures field ordering matches the RFC for consistency.
|
|
32
|
+
ATTRIBUTES = %i[
|
|
33
|
+
resource
|
|
34
|
+
authorization_servers
|
|
35
|
+
scopes_supported
|
|
36
|
+
bearer_methods_supported
|
|
37
|
+
resource_name
|
|
38
|
+
resource_documentation
|
|
39
|
+
resource_policy_uri
|
|
40
|
+
resource_tos_uri
|
|
41
|
+
jwks_uri
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
44
|
+
# @return [String, nil] the lookup key used to find configuration (typically subdomain)
|
|
45
|
+
attr_reader :resource_key
|
|
46
|
+
|
|
47
|
+
# Initializes metadata builder for a specific resource.
|
|
48
|
+
#
|
|
49
|
+
# The resource parameter acts as a lookup key into the configuration system.
|
|
50
|
+
# In typical deployments, this is the request subdomain (e.g., "api", "mcp").
|
|
51
|
+
# When nil or blank, uses the default protected_resource configuration.
|
|
52
|
+
#
|
|
53
|
+
# @param resource [String, nil] the configuration key to look up
|
|
54
|
+
def initialize(resource:)
|
|
55
|
+
@resource_key = resource
|
|
9
56
|
end
|
|
10
57
|
|
|
58
|
+
# Generates the RFC 9728 metadata response as a hash.
|
|
59
|
+
#
|
|
60
|
+
# Returns only the fields that are configured; optional fields with nil or blank
|
|
61
|
+
# values are omitted to reduce response size and comply with RFC requirements.
|
|
62
|
+
# The authorization_servers field defaults to the OAuth issuer URL if not explicitly
|
|
63
|
+
# configured, ensuring clients always know where to obtain tokens.
|
|
64
|
+
#
|
|
65
|
+
# @return [Hash{Symbol => Object}] metadata hash with snake_case keys
|
|
66
|
+
# @raise [ResourceNotConfiguredError] if no configuration exists for the resource_key
|
|
11
67
|
def to_h
|
|
12
|
-
|
|
68
|
+
{
|
|
13
69
|
resource: resource,
|
|
14
|
-
authorization_servers: authorization_servers
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
metadata[:resource_tos_uri] = resource_tos_uri if resource_tos_uri.present?
|
|
24
|
-
|
|
25
|
-
metadata
|
|
70
|
+
authorization_servers: authorization_servers,
|
|
71
|
+
scopes_supported: scopes_supported,
|
|
72
|
+
bearer_methods_supported: bearer_methods_supported,
|
|
73
|
+
jwks_uri: jwks_uri,
|
|
74
|
+
resource_name: resource_name,
|
|
75
|
+
resource_documentation: resource_documentation,
|
|
76
|
+
resource_policy_uri: resource_policy_uri,
|
|
77
|
+
resource_tos_uri: resource_tos_uri
|
|
78
|
+
}.compact_blank
|
|
26
79
|
end
|
|
27
80
|
|
|
28
81
|
private
|
|
29
82
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
83
|
+
# Retrieves the configuration hash for this resource.
|
|
84
|
+
#
|
|
85
|
+
# Delegates to Configuration#protected_resource_for which implements the
|
|
86
|
+
# fallback strategy (subdomain-specific -> default -> nil). Memoizes the
|
|
87
|
+
# result since configuration doesn't change during a request.
|
|
88
|
+
#
|
|
89
|
+
# @return [Hash] the resource configuration hash
|
|
90
|
+
# @raise [ResourceNotConfiguredError] if no config exists for the resource key
|
|
91
|
+
def resource_config
|
|
92
|
+
@resource_config ||= TokenAuthority.config.protected_resource_for(resource_key) ||
|
|
93
|
+
raise(ResourceNotConfiguredError)
|
|
36
94
|
end
|
|
37
95
|
|
|
96
|
+
# The URI identifying this protected resource.
|
|
97
|
+
# This is the only required field in RFC 9728 metadata.
|
|
98
|
+
#
|
|
99
|
+
# @return [String] resource identifier URI
|
|
38
100
|
def resource
|
|
39
|
-
|
|
101
|
+
resource_config[:resource]
|
|
40
102
|
end
|
|
41
103
|
|
|
104
|
+
# Array of authorization server URLs that can issue tokens for this resource.
|
|
105
|
+
#
|
|
106
|
+
# Defaults to the configured issuer URL if not explicitly set, which is correct
|
|
107
|
+
# for the common case where the authorization server and protected resource
|
|
108
|
+
# share the same deployment. Multi-AS scenarios can override this.
|
|
109
|
+
#
|
|
110
|
+
# @return [Array<String>] authorization server URLs
|
|
42
111
|
def authorization_servers
|
|
43
|
-
|
|
112
|
+
resource_config[:authorization_servers].presence || [issuer]
|
|
44
113
|
end
|
|
45
114
|
|
|
46
115
|
def scopes_supported
|
|
47
|
-
|
|
116
|
+
resource_config[:scopes_supported]
|
|
48
117
|
end
|
|
49
118
|
|
|
50
119
|
def bearer_methods_supported
|
|
51
|
-
|
|
120
|
+
resource_config[:bearer_methods_supported]
|
|
52
121
|
end
|
|
53
122
|
|
|
54
123
|
def jwks_uri
|
|
55
|
-
|
|
124
|
+
resource_config[:jwks_uri]
|
|
56
125
|
end
|
|
57
126
|
|
|
58
127
|
def resource_name
|
|
59
|
-
|
|
128
|
+
resource_config[:resource_name]
|
|
60
129
|
end
|
|
61
130
|
|
|
62
131
|
def resource_documentation
|
|
63
|
-
|
|
132
|
+
resource_config[:resource_documentation]
|
|
64
133
|
end
|
|
65
134
|
|
|
66
135
|
def resource_policy_uri
|
|
67
|
-
|
|
136
|
+
resource_config[:resource_policy_uri]
|
|
68
137
|
end
|
|
69
138
|
|
|
70
139
|
def resource_tos_uri
|
|
71
|
-
|
|
140
|
+
resource_config[:resource_tos_uri]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The authorization server issuer URL, used as a default for authorization_servers.
|
|
144
|
+
#
|
|
145
|
+
# Strips trailing slash for consistency with OAuth discovery URL formats, which
|
|
146
|
+
# typically omit trailing slashes in issuer identifiers.
|
|
147
|
+
#
|
|
148
|
+
# @return [String] issuer URL without trailing slash
|
|
149
|
+
def issuer
|
|
150
|
+
TokenAuthority.config.issuer_url.to_s.chomp("/")
|
|
72
151
|
end
|
|
73
152
|
end
|
|
74
153
|
end
|
|
@@ -60,7 +60,7 @@ module TokenAuthority
|
|
|
60
60
|
aud = if resources.any?
|
|
61
61
|
(resources.size == 1) ? resources.first : resources
|
|
62
62
|
else
|
|
63
|
-
TokenAuthority.config.
|
|
63
|
+
TokenAuthority.config.audience_url
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
# Only include scope if scopes are provided
|
|
@@ -70,7 +70,7 @@ module TokenAuthority
|
|
|
70
70
|
aud:,
|
|
71
71
|
exp:,
|
|
72
72
|
iat: Time.zone.now.to_i,
|
|
73
|
-
iss: TokenAuthority.config.
|
|
73
|
+
iss: TokenAuthority.config.issuer_url,
|
|
74
74
|
jti: SecureRandom.uuid,
|
|
75
75
|
scope: scope_claim
|
|
76
76
|
)
|