token_authority 0.2.0 → 0.2.1
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 +13 -1
- data/README.md +1 -1
- data/app/controllers/concerns/token_authority/token_authentication.rb +1 -1
- data/app/models/concerns/token_authority/claim_validatable.rb +2 -2
- data/app/models/concerns/token_authority/session_creatable.rb +1 -1
- data/app/models/token_authority/access_token.rb +24 -16
- data/app/views/token_authority/authorization_grants/new.html.erb +2 -2
- data/lib/token_authority/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b6b1981cf717ab9e179f87aaea998642ad74315333c25ea3ea1e8e267d3a293
|
|
4
|
+
data.tar.gz: 25b6bb154158120808d02ab4f8c939c1d7f731f57db9e380167fe414ecaba7e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ce50536b44c5ef5063160808e7c93bc757f01b01f81f62c1ed0d3ba86da56d675c0d97e6fb3d944da4ac822925e5a7efc81dfef2c1adbc2094f9a43a4801af7
|
|
7
|
+
data.tar.gz: 476f43e644e3bf3b10aa57c3cff10e0bf2d1a73712b9cc0f2b9c89d306ec187e8663e3efd58582f5032f8f81f0ef8b5f46343daeb415b8e17a32ad047e2a38ee
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.1] - 2025-01-24
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- Implemented support for all mandatory access token JWT claims
|
|
8
|
+
- Disable turbo on consent screen to allow redirects
|
|
9
|
+
|
|
10
|
+
### Documentation
|
|
11
|
+
|
|
12
|
+
- Update README to include link to MCP Quickstart guide.
|
|
13
|
+
|
|
3
14
|
## [0.2.0] - 2025-01-23
|
|
4
15
|
|
|
5
16
|
- Implemented support for OAuth 2.1 authorization flows and JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens (RFC 9068).
|
|
@@ -18,6 +29,7 @@
|
|
|
18
29
|
|
|
19
30
|
- Initial release
|
|
20
31
|
|
|
21
|
-
[Unreleased]: https://github.com/dickdavis/token_authority/compare/v0.2.
|
|
32
|
+
[Unreleased]: https://github.com/dickdavis/token_authority/compare/v0.2.1...HEAD
|
|
33
|
+
[0.2.1]: https://github.com/dickdavis/token_authority/compare/v0.2.0...v0.2.1
|
|
22
34
|
[0.2.0]: https://github.com/dickdavis/token_authority/compare/v0.1.0...v0.2.0
|
|
23
35
|
[0.1.0]: https://github.com/dickdavis/token_authority/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -16,7 +16,7 @@ This project aims to implement the OAuth standards specified in the [MCP Authori
|
|
|
16
16
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
|
-
TokenAuthority is simple to install and configure.
|
|
19
|
+
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
20
|
|
|
21
21
|
### Installation
|
|
22
22
|
|
|
@@ -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.
|
|
@@ -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.
|
|
@@ -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,10 +65,11 @@ 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
|
|
@@ -78,7 +85,8 @@ module TokenAuthority
|
|
|
78
85
|
iat: Time.zone.now.to_i,
|
|
79
86
|
iss: TokenAuthority.config.rfc_9068_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
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
</ul>
|
|
20
20
|
<% end %>
|
|
21
21
|
<div>
|
|
22
|
-
<%= button_to t('token_authority.authorization_grants.new.reject_cta'), authorization_grants_path, params: { approve: false } %>
|
|
23
|
-
<%= button_to t('token_authority.authorization_grants.new.approve_cta'), authorization_grants_path, params: { approve: true } %>
|
|
22
|
+
<%= button_to t('token_authority.authorization_grants.new.reject_cta'), authorization_grants_path, params: { approve: false }, data: { turbo: false } %>
|
|
23
|
+
<%= button_to t('token_authority.authorization_grants.new.approve_cta'), authorization_grants_path, params: { approve: true }, data: { turbo: false } %>
|
|
24
24
|
</div>
|
|
25
25
|
</div>
|