token_authority 0.2.0 → 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 +54 -1
- data/README.md +53 -15
- data/app/controllers/concerns/token_authority/initial_access_token_authentication.rb +2 -2
- data/app/controllers/concerns/token_authority/token_authentication.rb +1 -1
- 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 +4 -4
- data/app/models/concerns/token_authority/resourceable.rb +2 -2
- data/app/models/concerns/token_authority/session_creatable.rb +1 -1
- data/app/models/token_authority/access_token.rb +26 -18
- 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/app/views/token_authority/authorization_grants/new.html.erb +2 -2
- 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,56 @@
|
|
|
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
|
+
|
|
43
|
+
## [0.2.1] - 2025-01-24
|
|
44
|
+
|
|
45
|
+
### Fixes
|
|
46
|
+
|
|
47
|
+
- Implemented support for all mandatory access token JWT claims
|
|
48
|
+
- Disable turbo on consent screen to allow redirects
|
|
49
|
+
|
|
50
|
+
### Documentation
|
|
51
|
+
|
|
52
|
+
- Update README to include link to MCP Quickstart guide.
|
|
53
|
+
|
|
3
54
|
## [0.2.0] - 2025-01-23
|
|
4
55
|
|
|
5
56
|
- Implemented support for OAuth 2.1 authorization flows and JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens (RFC 9068).
|
|
@@ -18,6 +69,8 @@
|
|
|
18
69
|
|
|
19
70
|
- Initial release
|
|
20
71
|
|
|
21
|
-
[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
|
|
74
|
+
[0.2.1]: https://github.com/dickdavis/token_authority/compare/v0.2.0...v0.2.1
|
|
22
75
|
[0.2.0]: https://github.com/dickdavis/token_authority/compare/v0.1.0...v0.2.0
|
|
23
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
|
|--------|----------|
|
|
@@ -16,7 +14,7 @@ This project aims to implement the OAuth standards specified in the [MCP Authori
|
|
|
16
14
|
|
|
17
15
|
## Usage
|
|
18
16
|
|
|
19
|
-
TokenAuthority is simple to install and configure.
|
|
17
|
+
TokenAuthority is simple to install and configure. For MCP server developers, see the [MCP Quickstart](https://github.com/dickdavis/token_authority/wiki/MCP-Quickstart) guide for a complete working example.
|
|
20
18
|
|
|
21
19
|
### Installation
|
|
22
20
|
|
|
@@ -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
|
|
|
@@ -84,7 +84,7 @@ module TokenAuthority
|
|
|
84
84
|
# @return [User] the user from the configured user_class
|
|
85
85
|
# @api private
|
|
86
86
|
def token_user
|
|
87
|
-
@token_user ||= TokenAuthority.config.user_class.constantize.find(@decoded_token.
|
|
87
|
+
@token_user ||= TokenAuthority.config.user_class.constantize.find(@decoded_token.sub)
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
# Returns the scopes granted in the authenticated token.
|
|
@@ -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.
|
|
@@ -21,8 +21,8 @@ module TokenAuthority
|
|
|
21
21
|
extend ActiveSupport::Concern
|
|
22
22
|
|
|
23
23
|
# JWT claims that should trigger session revocation when invalid.
|
|
24
|
-
# These represent security violations like wrong audience or
|
|
25
|
-
REVOCABLE_CLAIMS = %i[aud iss
|
|
24
|
+
# These represent security violations like wrong audience, issuer, or subject.
|
|
25
|
+
REVOCABLE_CLAIMS = %i[aud iss sub].freeze
|
|
26
26
|
|
|
27
27
|
# JWT claims that should trigger session expiration when invalid.
|
|
28
28
|
# Currently only includes the exp (expiration time) claim.
|
|
@@ -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.
|
|
@@ -78,7 +78,7 @@ module TokenAuthority
|
|
|
78
78
|
|
|
79
79
|
instrument("session.create") do
|
|
80
80
|
access_token_expiration = client.access_token_duration.seconds.from_now.to_i
|
|
81
|
-
access_token = TokenAuthority::AccessToken.default(user_id:, exp: access_token_expiration, resources:, scopes:)
|
|
81
|
+
access_token = TokenAuthority::AccessToken.default(user_id:, client_id: client.public_id, exp: access_token_expiration, resources:, scopes:)
|
|
82
82
|
|
|
83
83
|
refresh_token_expiration = client.refresh_token_duration.seconds.from_now.to_i
|
|
84
84
|
refresh_token = TokenAuthority::RefreshToken.default(exp: refresh_token_expiration, resources:, scopes:)
|
|
@@ -8,8 +8,7 @@ module TokenAuthority
|
|
|
8
8
|
# (JWT ID) is stored in the Session model for revocation lookups.
|
|
9
9
|
#
|
|
10
10
|
# The tokens follow RFC 9068 JWT Profile for OAuth Access Tokens, including
|
|
11
|
-
# standard claims (iss, aud, exp, iat, jti) plus
|
|
12
|
-
# and scope.
|
|
11
|
+
# standard claims (iss, sub, aud, exp, iat, jti, client_id) plus the scope claim.
|
|
13
12
|
#
|
|
14
13
|
# This is an ActiveModel object (not ActiveRecord) that provides validation
|
|
15
14
|
# and serialization of JWT claims without database persistence.
|
|
@@ -18,6 +17,7 @@ module TokenAuthority
|
|
|
18
17
|
# token = TokenAuthority::AccessToken.default(
|
|
19
18
|
# exp: 5.minutes.from_now,
|
|
20
19
|
# user_id: 42,
|
|
20
|
+
# client_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
21
21
|
# resources: ["https://api.example.com"],
|
|
22
22
|
# scopes: ["read", "write"]
|
|
23
23
|
# )
|
|
@@ -31,17 +31,22 @@ module TokenAuthority
|
|
|
31
31
|
class AccessToken
|
|
32
32
|
include TokenAuthority::ClaimValidatable
|
|
33
33
|
|
|
34
|
-
# @!attribute [rw]
|
|
35
|
-
# The
|
|
36
|
-
# @return [
|
|
37
|
-
attr_accessor :
|
|
34
|
+
# @!attribute [rw] sub
|
|
35
|
+
# The subject identifier (resource owner) per RFC 9068.
|
|
36
|
+
# @return [String]
|
|
37
|
+
attr_accessor :sub
|
|
38
|
+
|
|
39
|
+
# @!attribute [rw] client_id
|
|
40
|
+
# The OAuth client identifier per RFC 9068/RFC 8693.
|
|
41
|
+
# @return [String]
|
|
42
|
+
attr_accessor :client_id
|
|
38
43
|
|
|
39
44
|
# @!attribute [rw] scope
|
|
40
45
|
# Space-separated list of OAuth scopes granted to this token.
|
|
41
46
|
# @return [String, nil]
|
|
42
47
|
attr_accessor :scope
|
|
43
48
|
|
|
44
|
-
validates :
|
|
49
|
+
validates :sub, presence: true, comparison: {equal_to: :sub_from_token_authority_session}
|
|
45
50
|
|
|
46
51
|
# Creates a new access token with default claims per RFC 9068.
|
|
47
52
|
#
|
|
@@ -49,7 +54,8 @@ module TokenAuthority
|
|
|
49
54
|
# otherwise falls back to the configured default audience URL.
|
|
50
55
|
#
|
|
51
56
|
# @param exp [Time, Integer] token expiration time
|
|
52
|
-
# @param user_id [Integer] the user ID
|
|
57
|
+
# @param user_id [Integer] the user ID (converted to string for sub claim)
|
|
58
|
+
# @param client_id [String] the OAuth client identifier
|
|
53
59
|
# @param resources [Array<String>] resource indicators (RFC 8707)
|
|
54
60
|
# @param scopes [Array<String>] OAuth scopes to include
|
|
55
61
|
#
|
|
@@ -59,15 +65,16 @@ module TokenAuthority
|
|
|
59
65
|
# token = AccessToken.default(
|
|
60
66
|
# exp: 5.minutes.from_now,
|
|
61
67
|
# user_id: 123,
|
|
68
|
+
# client_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
62
69
|
# resources: ["https://api.example.com"],
|
|
63
70
|
# scopes: ["read", "write"]
|
|
64
71
|
# )
|
|
65
|
-
def self.default(exp:, user_id:, resources: [], scopes: [])
|
|
72
|
+
def self.default(exp:, user_id:, client_id:, resources: [], scopes: [])
|
|
66
73
|
# Use resources for aud claim if provided, otherwise fall back to config
|
|
67
74
|
aud = if resources.any?
|
|
68
75
|
(resources.size == 1) ? resources.first : resources
|
|
69
76
|
else
|
|
70
|
-
TokenAuthority.config.
|
|
77
|
+
TokenAuthority.config.audience_url
|
|
71
78
|
end
|
|
72
79
|
|
|
73
80
|
scope_claim = scopes.any? ? scopes.join(" ") : nil
|
|
@@ -76,9 +83,10 @@ module TokenAuthority
|
|
|
76
83
|
aud:,
|
|
77
84
|
exp:,
|
|
78
85
|
iat: Time.zone.now.to_i,
|
|
79
|
-
iss: TokenAuthority.config.
|
|
86
|
+
iss: TokenAuthority.config.issuer_url,
|
|
80
87
|
jti: SecureRandom.uuid,
|
|
81
|
-
user_id
|
|
88
|
+
sub: user_id.to_s,
|
|
89
|
+
client_id:,
|
|
82
90
|
scope: scope_claim
|
|
83
91
|
)
|
|
84
92
|
end
|
|
@@ -93,7 +101,7 @@ module TokenAuthority
|
|
|
93
101
|
#
|
|
94
102
|
# @example
|
|
95
103
|
# token = AccessToken.from_token(jwt_string)
|
|
96
|
-
#
|
|
104
|
+
# subject = token.sub
|
|
97
105
|
def self.from_token(token)
|
|
98
106
|
new(TokenAuthority::JsonWebToken.decode(token))
|
|
99
107
|
end
|
|
@@ -104,7 +112,7 @@ module TokenAuthority
|
|
|
104
112
|
#
|
|
105
113
|
# @return [Hash] the JWT claims
|
|
106
114
|
def to_h
|
|
107
|
-
{aud:, exp:, iat:, iss:, jti:,
|
|
115
|
+
{aud:, exp:, iat:, iss:, jti:, sub:, client_id:, scope:}.compact
|
|
108
116
|
end
|
|
109
117
|
|
|
110
118
|
# Encodes the token as a signed JWT string.
|
|
@@ -116,12 +124,12 @@ module TokenAuthority
|
|
|
116
124
|
|
|
117
125
|
private
|
|
118
126
|
|
|
119
|
-
# Returns the user_id from the associated session for validation.
|
|
127
|
+
# Returns the subject (user_id as string) from the associated session for validation.
|
|
120
128
|
#
|
|
121
|
-
# @return [
|
|
129
|
+
# @return [String, nil]
|
|
122
130
|
# @api private
|
|
123
|
-
def
|
|
124
|
-
token_authority_session&.user_id
|
|
131
|
+
def sub_from_token_authority_session
|
|
132
|
+
token_authority_session&.user_id&.to_s
|
|
125
133
|
end
|
|
126
134
|
end
|
|
127
135
|
end
|
|
@@ -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|
|