keycardai-oauth 0.5.0 → 0.6.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 +26 -0
- data/README.md +20 -0
- data/lib/keycardai/oauth/authorization_code.rb +41 -0
- data/lib/keycardai/oauth/token_types.rb +1 -0
- data/lib/keycardai/oauth/token_verifier.rb +8 -1
- data/lib/keycardai/oauth/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: 3cd4432729ce7f9f1e54d2fe539c92cc86c69b6b8914abd78b407f9aa75cfe72
|
|
4
|
+
data.tar.gz: 65378c59aea04d972e50922379d5c45c01361cda5eb210421f00633093982388
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 964ed9c9d520da2470d0f31ca9427da6397752cf5d5265e7f9517367e67719e741d869f38a62042275f9af2049a2921b96d096a76171b7920c407c5be5e72189
|
|
7
|
+
data.tar.gz: b02efcd68eef6b896b7b1cfde8e29e0b537fcf86397bd33e1d3ac9d7322ddf8015319c5d544feec0766f4e1032eec5d6d679f4e11662797a5fb06dbe4c93946c
|
data/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,32 @@ the loopback flow (RFC 8252), JWT signing and verification with a caching JWKS
|
|
|
15
15
|
keyring, the three application credentials (ClientSecret with multi-zone,
|
|
16
16
|
WebIdentity, WorkloadIdentity with pluggable token sources), and AccessContext.
|
|
17
17
|
|
|
18
|
+
## 0.6.0-keycardai-oauth (2026-10-09)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
- feat(keycardai-oauth): refresh a grant from the authorization-code flow (SDK-15) (#42)
|
|
22
|
+
- Adds Keycardai::OAuth.refresh_authorization(issuer, refresh_token:, client_id:, client_secret:, resources:, scopes:), the refresh step from authorization-code-pkce.md spec-version 4. Client authentication follows exchange_authorization_code: HTTP Basic with no body client_id for a confidential client, body client_id for a public one. A rotated refresh_token comes back on the TokenResponse for the caller to store; invalid_grant raises OAuthError and means the user must authorize again.
|
|
23
|
+
|
|
24
|
+
## 0.5.1-keycardai-oauth (2026-09-17)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
- fix(keycardai-oauth): warn once when a TokenVerifier is built without audiences (SDK-4) (#39)
|
|
28
|
+
- * fix(keycardai-oauth): warn once when a TokenVerifier is built without audiences (SDK-4)
|
|
29
|
+
- Co-Authored-By: Larry Osakwe <larry@keycard.ai>
|
|
30
|
+
- * fix(keycardai-oauth): bind the example server's verifier to KEYCARD_RESOURCE_ID
|
|
31
|
+
- The example used KEYCARD_RESOURCE_ID as a display name and built its verifier
|
|
32
|
+
without audiences. It now keeps SERVER_NAME for display, passes the resource
|
|
33
|
+
identifier as audiences, and its selftest mints tokens for that identifier,
|
|
34
|
+
sends the wrong-scope token with the right aud so it reaches the scope check,
|
|
35
|
+
and asserts the proxied authorization_endpoint passes through unmodified
|
|
36
|
+
(the resource= rewrite it still expected was retired in #37).
|
|
37
|
+
- Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
|
38
|
+
- ---------
|
|
39
|
+
- Co-authored-by: devin-ai-keycard <devin-ai@keycard.ai>
|
|
40
|
+
Co-authored-by: Larry Osakwe <larry@keycard.ai>
|
|
41
|
+
Co-authored-by: Larry-Osakwe <larryosak@gmail.com>
|
|
42
|
+
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
|
|
43
|
+
|
|
18
44
|
## 0.5.0-keycardai-oauth (2026-09-08)
|
|
19
45
|
|
|
20
46
|
|
data/README.md
CHANGED
|
@@ -104,6 +104,26 @@ issuer's `userinfo_endpoint` rather than in the token. Pass `metadata:` to reuse
|
|
|
104
104
|
metadata you already discovered; nothing is cached, because group membership can
|
|
105
105
|
change server-side.
|
|
106
106
|
|
|
107
|
+
### Refresh a stored grant
|
|
108
|
+
|
|
109
|
+
A token response from the authorization-code flow may carry a `refresh_token`.
|
|
110
|
+
Redeem it with `refresh_authorization`, which authenticates the client the way
|
|
111
|
+
`exchange_authorization_code` does. The gem keeps no state: when the response
|
|
112
|
+
carries a new `refresh_token`, the server rotated it and you store it in place
|
|
113
|
+
of the old one. `OAuthError` with `error == "invalid_grant"` means the user must
|
|
114
|
+
authorize again.
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
token = Keycardai::OAuth.refresh_authorization(
|
|
118
|
+
"https://your-zone.keycard.cloud",
|
|
119
|
+
refresh_token: stored_refresh_token,
|
|
120
|
+
client_id: ENV.fetch("KEYCARD_CLIENT_ID"),
|
|
121
|
+
client_secret: ENV["KEYCARD_CLIENT_SECRET"], # omit for a public client
|
|
122
|
+
resources: ["https://api.acme.test"],
|
|
123
|
+
)
|
|
124
|
+
stored_refresh_token = token.refresh_token if token.refresh_token
|
|
125
|
+
```
|
|
126
|
+
|
|
107
127
|
### Act as a named user
|
|
108
128
|
|
|
109
129
|
```ruby
|
|
@@ -75,5 +75,46 @@ module Keycardai
|
|
|
75
75
|
|
|
76
76
|
TokenRequests.parse_response(http_client.post_form(endpoint, params, headers: headers, timeout: timeout))
|
|
77
77
|
end
|
|
78
|
+
|
|
79
|
+
# Redeem a refresh token for a new access token (RFC 6749 §6). Client
|
|
80
|
+
# authentication follows the code exchange: a confidential client
|
|
81
|
+
# authenticates with HTTP Basic and omits client_id from the body, a public
|
|
82
|
+
# client carries client_id in the body. The gem keeps no state: when the
|
|
83
|
+
# response carries a refresh_token the server rotated it, and the caller
|
|
84
|
+
# stores it in place of the old one.
|
|
85
|
+
#
|
|
86
|
+
# @param issuer [String] the zone's issuer URL; supplies the token endpoint
|
|
87
|
+
# @param refresh_token [String] the refresh token from an earlier token response
|
|
88
|
+
# @param client_id [String]
|
|
89
|
+
# @param client_secret [String, nil] confidential-client secret
|
|
90
|
+
# @param resources [Array<String>] RFC 8707 resource indicators, one resource
|
|
91
|
+
# parameter per entry
|
|
92
|
+
# @param scopes [Array<String>] scopes to request, space-joined into scope
|
|
93
|
+
# @param http_client [#get, #post_form] pluggable transport
|
|
94
|
+
# @param timeout [Numeric, nil]
|
|
95
|
+
# @return [TokenResponse]
|
|
96
|
+
# @raise [OAuthError] an RFC 6749 §5.2 error response; error == "invalid_grant"
|
|
97
|
+
# means the refresh token is expired, revoked, or unknown and the user must
|
|
98
|
+
# authorize again
|
|
99
|
+
# @raise [HTTPError] a non-2xx response with no OAuth error body (5xx, 429)
|
|
100
|
+
# @raise [NetworkError, ProtocolError]
|
|
101
|
+
def self.refresh_authorization(issuer, refresh_token:, client_id:, client_secret: nil, resources: [],
|
|
102
|
+
scopes: [], http_client: HTTP::NetHTTPClient.new, timeout: nil)
|
|
103
|
+
metadata = fetch_authorization_server_metadata(issuer, http_client: http_client, timeout: timeout)
|
|
104
|
+
endpoint = metadata.token_endpoint ||
|
|
105
|
+
raise(ProtocolError.new("metadata for #{issuer} has no token_endpoint", code: "invalid_metadata"))
|
|
106
|
+
|
|
107
|
+
params = {
|
|
108
|
+
"grant_type" => GrantType::REFRESH_TOKEN,
|
|
109
|
+
"refresh_token" => refresh_token,
|
|
110
|
+
"client_id" => client_secret ? nil : client_id,
|
|
111
|
+
"scope" => Array(scopes).empty? ? nil : Array(scopes).join(" ")
|
|
112
|
+
}.compact
|
|
113
|
+
params = params.to_a + Array(resources).map { |resource| ["resource", resource] }
|
|
114
|
+
headers = { "Accept" => "application/json" }
|
|
115
|
+
headers["Authorization"] = HTTP.basic_authorization(client_id, client_secret) if client_secret
|
|
116
|
+
|
|
117
|
+
TokenRequests.parse_response(http_client.post_form(endpoint, params, headers: headers, timeout: timeout))
|
|
118
|
+
end
|
|
78
119
|
end
|
|
79
120
|
end
|
|
@@ -77,7 +77,8 @@ module Keycardai
|
|
|
77
77
|
class TokenVerifier
|
|
78
78
|
# @param issuers [String, Array<String>] trusted zone issuer URL(s)
|
|
79
79
|
# @param audiences [String, Array<String>, nil] when set, tokens must
|
|
80
|
-
# carry an intersecting aud
|
|
80
|
+
# carry an intersecting aud; when nil, the verifier accepts a token
|
|
81
|
+
# minted for any resource in the zone and warns once at construction
|
|
81
82
|
# @param http_client [#get] pluggable transport
|
|
82
83
|
# @param key_ttl [Numeric] JWKS key cache lifetime in seconds
|
|
83
84
|
# @param discovery_ttl [Numeric] jwks_uri cache lifetime in seconds
|
|
@@ -90,6 +91,12 @@ module Keycardai
|
|
|
90
91
|
@issuers = Array(issuers).reject { |issuer| issuer.nil? || issuer.empty? }
|
|
91
92
|
raise ConfigurationError, "TokenVerifier requires at least one trusted issuer" if @issuers.empty?
|
|
92
93
|
|
|
94
|
+
if audiences.nil?
|
|
95
|
+
warn "Keycardai::OAuth::TokenVerifier has no audiences configured, so it accepts a token minted " \
|
|
96
|
+
"for any resource in the zone; pass audiences: with this server's registered resource identifier.",
|
|
97
|
+
uplevel: 1
|
|
98
|
+
end
|
|
99
|
+
|
|
93
100
|
@audiences = audiences
|
|
94
101
|
@clock = clock
|
|
95
102
|
@keyring = JWKSKeyring.new(http_client: http_client, key_ttl: key_ttl,
|