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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33aa8b51bc6809128ee7546762842456ad9e204d0e60655217e879cb498a0b63
4
- data.tar.gz: ee37eb4012f1bf46e01058476c6ce4abd4ba23be66c83841fb5114241ec365c4
3
+ metadata.gz: b1149d6a6597b774774ced7f36e51ee6a2df28a1bfb94ad084ef2802db9827d8
4
+ data.tar.gz: e32a325dc4240903863b427be6ff808dc484954bbaff4128f4a49393fa2bc508
5
5
  SHA512:
6
- metadata.gz: 6f150494708f23dddb34ecdcb7bdaaac837cd9d13f6ca1e0730df19536cec26db3d497474b8484cfad13f4d640dbb1831295c5172f6203a4e8c67b6e0683adc5
7
- data.tar.gz: 9edbb0dda964b5f65eb88fae046bcb68b29e0b11f447633c27d0af9178b78ed377af2d7c42a2dcfb2fb75fbcffcda0e5c194e984a6ed8fc332ec5ecdc6e021ac
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
- # @return [String]
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Keycardai
4
4
  module OAuth
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keycardai-oauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keycard