Skip to content

cimd: support private_key_jwt CIMD clients (#118)#119

Merged
BorisTyshkevich merged 3 commits into
mainfrom
feature/cimd-private-key-jwt-118
May 15, 2026
Merged

cimd: support private_key_jwt CIMD clients (#118)#119
BorisTyshkevich merged 3 commits into
mainfrom
feature/cimd-private-key-jwt-118

Conversation

@BorisTyshkevich
Copy link
Copy Markdown
Collaborator

Closes #118.

Why

ChatGPT publishes CIMD documents with token_endpoint_auth_method=private_key_jwt and a jwks_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/mcp reproduced the failure end-to-end. Pod log:

cimd: invalid metadata document: token_endpoint_auth_method must be "none" (got "private_key_jwt")

What

  • parseCIMDMetadata accepts none (PKCE-only) and private_key_jwt; the latter requires a jwks_uri that passes the same shape rules as the CIMD client_id URL (https, no userinfo/fragment, port 443, IDNA-clean host).
  • New cmd/altinity-mcp/client_assertion.go:
    • JWKS fetcher reuses cimdResolver's SSRF-safe transport; separate FIFO+TTL cache keyspace; kid-miss invalidates + refreshes once.
    • verifyClientAssertion does RFC 7523 §3 validation — signature against the published JWKS, iss == sub == client_id, aud includes our /oauth/token URL, exp/nbf/iat with 60s skew, exp − iat ≤ 10m. Asymmetric algorithms only (RS/PS/ES/EdDSA).
  • /oauth/token dispatches on client.TokenEndpointAuthMethod. none clients still reject any client_assertion; private_key_jwt clients must supply a valid jwt-bearer assertion.
  • AS metadata advertises both methods + token_endpoint_auth_signing_alg_values_supported.

Replay protection

No pod-local jti cache. The downstream JWE authorization code already enforces single-use via the HA replay model (upstream IdP invalid_grant on 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, revisit jti.

Tests

9 new cases in client_assertion_test.go:

  • parser: private_key_jwt OK; reject missing jwks_uri; reject bad jwks_uri (http / userinfo / empty); accept loopback at parse (SSRF caught at dial)
  • /token: happy path; reject wrong iss / sub / aud / expired / over-lifetime
  • signature tamper rejected
  • missing jwks_uri rejected at /token time
  • kid rotation: signs with unknown kid → cache invalidate + refresh → final rejection

claude.ai's none+PKCE path stays covered by TestParseCIMDMetadata_OK and the HA replay regression suite — all green.

ok  	github.com/altinity/altinity-mcp/cmd/altinity-mcp	11.709s
ok  	github.com/altinity/altinity-mcp/pkg/clickhouse	1.967s
ok  	github.com/altinity/altinity-mcp/pkg/config	0.016s
ok  	github.com/altinity/altinity-mcp/pkg/jwe_auth	0.009s
ok  	github.com/altinity/altinity-mcp/pkg/server	16.680s

Test plan

  • Rebuild image (scripts/build-mcp-image.sh cimd-pkj-<sha>).
  • Roll to otel-google-mcp, otel-google-gating-mcp, github-mcp, antalya-mcp.
  • claude.ai E2E on all four — no regression on none+PKCE path.
  • ChatGPT dev-mode app E2E on all four — fresh registration → CIMD discovery → OAuth flow → execute_query SELECT currentUser(), 1+1.

BorisTyshkevich and others added 3 commits May 15, 2026 20:52
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>
@BorisTyshkevich BorisTyshkevich merged commit 59b67e3 into main May 15, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CIMD: support private_key_jwt (ChatGPT dev-mode apps blocked)

1 participant