cimd: support private_key_jwt CIMD clients (#118)#119
Merged
Conversation
ChatGPT publishes CIMD docs with token_endpoint_auth_method=private_key_jwt
+ jwks_uri (RFC 7523 §2.2). Pre-this-change parseCIMDMetadata hardcoded
"none" as the only accepted method, blanket-rejecting ChatGPT across all
broker-mode deployments (antalya, github, otel-google-*). claude.ai keeps
working since its CIMD doc declares "none".
Changes:
* `parseCIMDMetadata` accepts "none" and "private_key_jwt"; for the latter,
`jwks_uri` is required and validated (https, port 443, IDNA-clean host,
same path-safety rules as the CIMD `client_id` URL). Empty / unspecified
`token_endpoint_auth_method` is rejected — both real-world docs declare
it explicitly.
* `statelessRegisteredClient` carries `TokenEndpointAuthMethod` + `JWKSURI`.
* New `client_assertion.go`:
- JWKS fetcher reuses cimdResolver's SSRF-safe transport + body limits;
separate FIFO+TTL cache keyspace (jwksCache); kid-miss invalidates and
refreshes once before failing.
- `verifyClientAssertion` parses the JWT, picks JWK by kid (or alg
fallback), verifies signature, validates RFC 7523 §3 claims (iss == sub
== client_id, aud includes /token URL, exp/nbf/iat with 60s clock skew,
exp − iat ≤ 10m). Rejects all symmetric/none algorithms.
* `/oauth/token`: dispatch on `client.TokenEndpointAuthMethod`. Public
("none") clients still reject any client_assertion/assertion_type;
private_key_jwt clients must supply jwt-bearer assertion_type +
successful assertion verification.
* AS metadata advertises both methods + `token_endpoint_auth_signing_alg_values_supported`.
Tests:
* 9 new cases in client_assertion_test.go: parser accept/reject for
private_key_jwt with/without jwks_uri, /token happy path, RFC 7523 §3
claim rejections (iss, sub, aud, expiry, over-lifetime), tampered
signature, missing jwks_uri, kid rotation cache invalidation.
* Existing claude.ai "none" path covered by TestParseCIMDMetadata_OK and
the HA replay regression suite — both still green.
Replay protection note: no pod-local jti cache. The downstream JWE auth
code is already single-use via the HA replay model (upstream IdP
`invalid_grant` on 2nd redemption), and the assertion's 10-minute max
lifetime narrows the replay window to whoever holds a still-redeemable
downstream code. Reconsider jti if we ever drop the JWE single-use
guarantee.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CIMD URL ownership over HTTPS already proves client identity, and PKCE binds the auth code. When a CIMD client declares private_key_jwt but doesn't ship the client_assertion at /token, accept and treat as PKCE public client. If an assertion IS supplied, full RFC 7523 verification still applies. Matches Auth0's observed behaviour: pre-2026-05-15, github-mcp ran with Auth0 as the AS and ChatGPT-via-CIMD worked end-to-end even though ChatGPT's CIMD doc declares private_key_jwt and its dev-mode apps don't yet send a client_assertion. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #118.
Why
ChatGPT publishes CIMD documents with
token_endpoint_auth_method=private_key_jwtand ajwks_uri. Post-#115 the parser hardcoded"none"as the only accepted value, blanket-rejecting ChatGPT across every broker-mode deployment. claude.ai keeps working since its CIMD doc declares"none"and uses PKCE.Detection: fresh ChatGPT dev-mode app pointed at
otel-google-36be80-mcp.demo.altinity.cloud/mcpreproduced the failure end-to-end. Pod log:What
parseCIMDMetadataacceptsnone(PKCE-only) andprivate_key_jwt; the latter requires ajwks_urithat passes the same shape rules as the CIMDclient_idURL (https, no userinfo/fragment, port 443, IDNA-clean host).cmd/altinity-mcp/client_assertion.go:cimdResolver's SSRF-safe transport; separate FIFO+TTL cache keyspace; kid-miss invalidates + refreshes once.verifyClientAssertiondoes RFC 7523 §3 validation — signature against the published JWKS,iss == sub == client_id,audincludes our/oauth/tokenURL,exp/nbf/iatwith 60s skew,exp − iat ≤ 10m. Asymmetric algorithms only (RS/PS/ES/EdDSA)./oauth/tokendispatches onclient.TokenEndpointAuthMethod.noneclients still reject anyclient_assertion;private_key_jwtclients must supply a validjwt-bearerassertion.token_endpoint_auth_signing_alg_values_supported.Replay protection
No pod-local
jticache. The downstream JWE authorization code already enforces single-use via the HA replay model (upstream IdPinvalid_granton the 2nd redemption), and the 10-minute assertion lifetime cap narrows the replay window to whoever holds a still-redeemable downstream code. If we ever weaken the JWE single-use guarantee, revisitjti.Tests
9 new cases in
client_assertion_test.go:iss/sub/aud/ expired / over-lifetimeclaude.ai's
none+PKCE path stays covered byTestParseCIMDMetadata_OKand the HA replay regression suite — all green.Test plan
scripts/build-mcp-image.sh cimd-pkj-<sha>).otel-google-mcp,otel-google-gating-mcp,github-mcp,antalya-mcp.none+PKCE path.execute_query SELECT currentUser(), 1+1.