keycardai-oauth 0.1.0 → 0.2.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 +141 -0
- data/lib/keycardai/oauth/token_verifier.rb +36 -2
- 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: b1149d6a6597b774774ced7f36e51ee6a2df28a1bfb94ad084ef2802db9827d8
|
|
4
|
+
data.tar.gz: e32a325dc4240903863b427be6ff808dc484954bbaff4128f4a49393fa2bc508
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4af98a839e2e21cff0948d5cb2b8cfa289252152699c9ca7a9bd409abf35defb935cc61cc836ba3fd7b5b30e3cfedafd1128232e4d8f6929b4608cc14282471f
|
|
7
|
+
data.tar.gz: 44ca0ff23a101c3c4e9f3a4527da545e2faa77af2a49354249602df6d0f7d936b14b3b1b9f9d104c6796b272f1ae85d3507fc43e57a28d41baecaf2bbadbe1d2
|
data/CHANGELOG.md
CHANGED
|
@@ -14,3 +14,144 @@ dynamic client registration (RFC 7591), authorization code with PKCE including
|
|
|
14
14
|
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
|
+
|
|
18
|
+
## 0.2.0-keycardai-oauth (2026-08-19)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
- feat(keycardai-oauth): expose the caller identity claims on the auth context (#24)
|
|
22
|
+
- Implements keycard-sdk-spec#44, which pins the auth-context identity
|
|
23
|
+
field set so the SDKs converge instead of each guessing.
|
|
24
|
+
- AccessToken gains subject_profile (the sub_profile claim) and
|
|
25
|
+
keycard_app_id, alongside the existing subject and client_id. The four
|
|
26
|
+
answer different questions and picking the wrong one is a bug, so the
|
|
27
|
+
class doc says which to key on: keycard_app_id is the stable
|
|
28
|
+
application identifier, client_id names the credential and rotates,
|
|
29
|
+
subject is the user on a user-present token and the application on an
|
|
30
|
+
application token, and subject_profile distinguishes those two directly
|
|
31
|
+
rather than leaving a consumer to infer it.
|
|
32
|
+
- Both Keycard claims are absent from a non-Keycard token, so they return
|
|
33
|
+
nil rather than raising, which the spec calls out as optional.
|
|
34
|
+
- Named subject_profile rather than sub_profile for consistency with the
|
|
35
|
+
existing subject accessor, which already reads sub. The spec treats
|
|
36
|
+
casing and naming as expression, and Go reads sub as Subject.
|
|
37
|
+
- Conformance row 4 in the bearer-middleware table now asserts the
|
|
38
|
+
identity fields, plus an application-token case where subject equals
|
|
39
|
+
keycard_app_id and a non-Keycard case where both are nil.
|
|
40
|
+
|
|
41
|
+
## 0.1.0-keycardai-oauth (2026-08-19)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
- feat(keycardai-oauth): authenticate loopback flow and challenge-driven entry (#7)
|
|
45
|
+
- Completes specs/oauth-client/authorization-code-pkce.md with the
|
|
46
|
+
high-level convenience layer:
|
|
47
|
+
- - authenticate: PKCE pair + CSRF state, authorize URL, shell-free
|
|
48
|
+
browser launch (open / xdg-open / cmd start), single-shot loopback
|
|
49
|
+
callback server (RFC 8252, converged defaults: port 8765, 300s
|
|
50
|
+
timeout, verifier length 128; port 0 binds ephemeral), state
|
|
51
|
+
validation (state_mismatch), redirect-carried OAuth errors typed,
|
|
52
|
+
InteractionTimeoutError on user inaction, then the code exchange
|
|
53
|
+
- resolve_issuer_from_challenge: RFC 9728 WWW-Authenticate
|
|
54
|
+
resource_metadata fetch to the first authorization server
|
|
55
|
+
- authenticate_from_challenge: challenge-driven entry to the same flow
|
|
56
|
+
- Tested against a real loopback server with a fake browser driving the
|
|
57
|
+
redirect (round trip, forged state, denied authorization, timeout).
|
|
58
|
+
- feat(keycardai-oauth): AccessContext, exchange orchestration, TokenVerifier, multi-zone (#6)
|
|
59
|
+
- Implements specs/delegated-access/access-context.md and
|
|
60
|
+
specs/multi-zone-and-ops/multi-zone-support.md, completing the oauth
|
|
61
|
+
core surface:
|
|
62
|
+
- - AccessContext: throwing access(resource) with typed
|
|
63
|
+
ResourceAccessError (global_error / resource_error / missing_token,
|
|
64
|
+
available_resources carried), non-throwing getters, status, mutators
|
|
65
|
+
for the grant layer, thread-safe, merge
|
|
66
|
+
- exchange_tokens_for_resources: non-throwing orchestration feeding an
|
|
67
|
+
AccessContext; per-resource scopes; impersonation path when
|
|
68
|
+
user_identifier is given
|
|
69
|
+
- TokenVerifier (server tier): JWTVerifier over a shared JWKSKeyring
|
|
70
|
+
returning AccessToken; verify_token_for_zone pins one zone issuer and
|
|
71
|
+
fails closed on unconfigured zones; caches keyed by issuer so no
|
|
72
|
+
cross-zone leakage
|
|
73
|
+
- TokenExchangeClient/ClientCredentialsClient accept an application
|
|
74
|
+
credential (exclusive with the raw pair, which now wraps into
|
|
75
|
+
ClientSecret) and a per-call issuer; token-endpoint cache is keyed by
|
|
76
|
+
issuer; credential-prepared params merge under caller overrides
|
|
77
|
+
- Conformance suites cover the full 16-row AccessContext table and the
|
|
78
|
+
6-row multi-zone table, plus orchestration coverage.
|
|
79
|
+
- feat(keycardai-oauth): application credentials (ClientSecret, WebIdentity, WorkloadIdentity) (#5)
|
|
80
|
+
- Implements specs/application-credentials/{client-secret,web-identity,
|
|
81
|
+
workload-identity}.md. The Credential interface is two methods:
|
|
82
|
+
authorization_header(issuer:) and prepare_token_exchange_request.
|
|
83
|
+
- - ClientSecret: client_secret_basic only; single pair or issuer-keyed
|
|
84
|
+
multi-zone map; construction rejects empty ids/secrets/maps; unknown
|
|
85
|
+
zones fail closed by raising (Python shape), never falling back
|
|
86
|
+
- PrivateKeyManager + FilePrivateKeyStorage: RSA-2048 generate/persist/
|
|
87
|
+
load (./server_keys default, ./mcp_keys legacy fallback), RFC 7523
|
|
88
|
+
create_client_assertion (iss=sub=client_id, aud=token_endpoint, jti,
|
|
89
|
+
iat, exp=iat+300), public JWKS export
|
|
90
|
+
- WebIdentity: key-id resolution (explicit, sanitized server_name,
|
|
91
|
+
UUID), lazy key bootstrap, fresh assertion per request, no Basic
|
|
92
|
+
header, public_jwks + client_jwks_url accessors
|
|
93
|
+
- WorkloadIdentity: pluggable IdentityTokenSource (bare callables
|
|
94
|
+
adapted), token fetched fresh every request, optional client_id form
|
|
95
|
+
param (KEP 108), typed WorkloadIdentity{Configuration,Runtime}Error
|
|
96
|
+
with source ids and cause chaining
|
|
97
|
+
- Token sources: FileTokenSource (the one blessed env read: 4-var
|
|
98
|
+
discovery list at exact cross-SDK parity, construction-time
|
|
99
|
+
validation), GCPMetadataTokenSource, FlyTokenSource (raw HTTP over
|
|
100
|
+
the Fly Unix socket)
|
|
101
|
+
- No deprecated EKS alias: Ruby is new and ships none; FileTokenSource
|
|
102
|
+
covers the EKS contract. Conformance suites map to all three spec
|
|
103
|
+
Testing tables (workload-identity row 10 n/a per the above).
|
|
104
|
+
- feat(keycardai-oauth): DCR, PKCE primitives, authorize URL, code exchange (#4)
|
|
105
|
+
- Implements specs/oauth-client/dynamic-client-registration.md and the
|
|
106
|
+
primitive + building-block layers of
|
|
107
|
+
specs/oauth-client/authorization-code-pkce.md:
|
|
108
|
+
- - register_client: RFC 7591 registration with RFC-minimal request
|
|
109
|
+
(only caller-supplied fields), additional_metadata bag with named
|
|
110
|
+
fields winning, initial_access_token Bearer auth, typed RFC 7591
|
|
111
|
+
3.2.2 error parsing, client_id required in the response
|
|
112
|
+
- PKCE module: verifier generation (43-128, unreserved charset, 128
|
|
113
|
+
default), S256/plain challenge derivation, generate_pair
|
|
114
|
+
- build_authorize_url: response_type=code plus PKCE, scope, state,
|
|
115
|
+
resource parameters
|
|
116
|
+
- exchange_authorization_code: public clients send client_id in the
|
|
117
|
+
body, confidential clients use HTTP Basic and omit it
|
|
118
|
+
- HTTP transport gains post_json; token-response parsing extracted to
|
|
119
|
+
TokenRequests module functions shared by the standalone exchange
|
|
120
|
+
- The high-level authenticate loopback flow (browser + callback server +
|
|
121
|
+
state validation) is the next slice; the spec unit tables do not cover
|
|
122
|
+
it. Conformance specs map one to one to both spec Testing tables.
|
|
123
|
+
- feat(keycardai-oauth): discovery, client credentials, token exchange, impersonation (#3)
|
|
124
|
+
- Implements specs/oauth-client/{authorization-server-discovery,
|
|
125
|
+
client-credentials,token-exchange}.md and
|
|
126
|
+
specs/delegated-access/impersonation.md:
|
|
127
|
+
- - fetch_authorization_server_metadata: RFC 8414 discovery with issuer
|
|
128
|
+
validation (issuer_mismatch), typed protocol errors, unknown fields
|
|
129
|
+
preserved; JWKSKeyring now discovers through this operation
|
|
130
|
+
- TokenResponse: shared token shape (scope parsed to array, Bearer
|
|
131
|
+
default, id_token, raw preserved), rejects a missing access_token
|
|
132
|
+
- TokenExchangeClient: RFC 8693 exchange with defaults, actor tokens,
|
|
133
|
+
client_assertion pass-through, Basic client auth, lazy cached token
|
|
134
|
+
endpoint, RFC 6749 5.2 typed OAuthError parsing
|
|
135
|
+
- impersonate: substitute-user exchange (vnd.kc.su+jwt unsigned subject
|
|
136
|
+
token, no actor_token, resource required client-side)
|
|
137
|
+
- ClientCredentialsClient: RFC 6749 4.4 grant on the same machinery
|
|
138
|
+
- Conformance specs map one to one to all four spec Testing tables.
|
|
139
|
+
- feat(keycardai-oauth): JWT/JWKS layer with conformance suites (#2)
|
|
140
|
+
- Implements specs/jwt-jwks/jwks-caching.md and
|
|
141
|
+
specs/jwt-jwks/jwt-signing-and-verification.md plus the shared
|
|
142
|
+
foundations they need:
|
|
143
|
+
- - Error taxonomy rooted at Keycardai::Error (configuration, network,
|
|
144
|
+
HTTP, invalid-token, JWKS quartet)
|
|
145
|
+
- Pluggable HTTP transport with a Net::HTTP default; no hidden retries
|
|
146
|
+
- JWKSKeyring: (issuer, kid) resolution with discovery + key TTLs,
|
|
147
|
+
same-origin jwks_uri enforcement, bounded cache with oldest-entry
|
|
148
|
+
eviction, per-issuer in-flight de-duplication
|
|
149
|
+
- JWTSigner: RS256, header {alg, kid}, default iss only when omitted,
|
|
150
|
+
temporal claims caller-managed
|
|
151
|
+
- JWTVerifier: fail-closed RFC 9068 verification, policy checks before
|
|
152
|
+
key resolution, trusted-issuer allowlist, kid required, alg allowlist
|
|
153
|
+
with none always rejected, clock_skew default 0 (exact comparison,
|
|
154
|
+
the canonical behavior per the spec Divergences; the types-table 60s
|
|
155
|
+
default looks stale and will be raised upstream)
|
|
156
|
+
- Conformance specs map one to one to the spec Testing tables; the two
|
|
157
|
+
live-zone integration rows are deferred to the E2E phase.
|
|
@@ -4,13 +4,47 @@ module Keycardai
|
|
|
4
4
|
module OAuth
|
|
5
5
|
# A verified bearer token: the raw compact JWT plus its verified claims,
|
|
6
6
|
# with convenience accessors for the RFC 9068 profile.
|
|
7
|
+
#
|
|
8
|
+
# Four accessors answer distinct questions about the caller, and picking
|
|
9
|
+
# the wrong one is a real bug rather than a style choice:
|
|
10
|
+
#
|
|
11
|
+
# - #client_id is the OAuth client that authenticated, so it names the
|
|
12
|
+
# credential. It rotates, so it does not stably identify an application.
|
|
13
|
+
# - #keycard_app_id is the stable Keycard application identifier. Key on
|
|
14
|
+
# this to answer "which application is calling", whatever the grant type
|
|
15
|
+
# or credential. For user agents it equals #client_id.
|
|
16
|
+
# - #subject is the user identifier on a user-present token and the
|
|
17
|
+
# application identifier on an application token.
|
|
18
|
+
# - #subject_profile distinguishes those two cases directly, rather than
|
|
19
|
+
# leaving a consumer to infer it from #subject against #client_id.
|
|
20
|
+
#
|
|
21
|
+
# #subject and #client_id are RFC 9068. The other two are Keycard claims
|
|
22
|
+
# and are absent from a non-Keycard token, so they return nil there.
|
|
7
23
|
AccessToken = Data.define(:token, :claims) do
|
|
8
|
-
# @return [String] the token's subject
|
|
24
|
+
# @return [String] the token's subject: the user for a user-present
|
|
25
|
+
# token, the application for an application token
|
|
9
26
|
def subject
|
|
10
27
|
claims["sub"]
|
|
11
28
|
end
|
|
12
29
|
|
|
13
|
-
#
|
|
30
|
+
# Whether a user authorized this access or an application is acting on
|
|
31
|
+
# its own behalf. Reads the Keycard `sub_profile` claim.
|
|
32
|
+
#
|
|
33
|
+
# @return ["user", "app", nil] nil on a non-Keycard token
|
|
34
|
+
def subject_profile
|
|
35
|
+
claims["sub_profile"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The stable Keycard application identifier, which is the claim to key
|
|
39
|
+
# on when identifying the calling application.
|
|
40
|
+
#
|
|
41
|
+
# @return [String, nil] nil on a non-Keycard token
|
|
42
|
+
def keycard_app_id
|
|
43
|
+
claims["keycard_app_id"]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [String] the OAuth client that authenticated, meaning the
|
|
47
|
+
# credential rather than the application
|
|
14
48
|
def client_id
|
|
15
49
|
claims["client_id"]
|
|
16
50
|
end
|