feat(auth): client conformance for Workload Identity Federation (SEP-1933)#268
Open
coding-bobo wants to merge 18 commits into
Open
feat(auth): client conformance for Workload Identity Federation (SEP-1933)#268coding-bobo wants to merge 18 commits into
coding-bobo wants to merge 18 commits into
Conversation
This was referenced May 12, 2026
5f756a9 to
7e41fee
Compare
Adds createWorkloadJwt and generateWorkloadKeypair to provide reusable, tested JWT signing infrastructure for the upcoming wif-jwt-bearer scenario (PR modelcontextprotocol#2). Also extracts JWT_BEARER_GRANT_TYPE constant and migrates cross-app-access.ts to use it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the auth/wif-jwt-bearer client conformance scenario using the RFC 7523 JWT-bearer grant (urn:ietf:params:oauth:grant-type:jwt-bearer). The scenario pre-signs valid, wrong-audience, and expired JWTs on start() to simulate cloud workload identity tokens. The conformance AS verifies the assertion and emits per-class checks (wif-assertion-verified, wif-assertion-missing, wif-assertion-audience, wif-assertion-expired, wif-assertion-malformed). Broken example clients exercise the missing-assertion and wrong-audience failure paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r comments - Add runWifJwtBearerExpiredAssertion + auth-test-wif-expired-assertion.ts to exercise the wif-assertion-expired check path (was dead code) - Clarify WifAssertionVerified description to note iss is not validated - Add comment explaining clockTolerance: 5 is intentional (same-run keypair) - Add comment explaining numeric expiresIn is absolute epoch seconds Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…escription - Add WIF_CLIENT_ID constant; pre-seed provider's clientInformation() so the SDK skips Dynamic Client Registration entirely (disableDynamicRegistration on the auth server + pre-seeded client_id on the provider side) - Add failedOnce/tokenRequestReceived tracking on the scenario; reject and record wif-no-retry FAILURE if the client attempts a second token request after the first fails - Add hasAttempted guard in WifJwtBearerProvider.prepareTokenRequest() to throw on retry from the client side - Fix getChecks() sentinel description: distinguish "no request made" from "request made but verification failed" - Add client_id to context and Zod schema Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The try/catch blocks were silently swallowing auth failures, so the negative tests passed purely because expectedFailureSlugs found the AS-emitted check — not because client error-surfacing was verified. allowClientError: true on the test cases handles the non-zero exit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…traceability
- Change specVersions from ['extension'] to [DRAFT_PROTOCOL_VERSION] and add
source = { introducedIn: DRAFT_PROTOCOL_VERSION } so the scenario is
reachable via --spec-version draft (extension tag excluded it from all
spec-version runs)
- Move registration from extensionScenariosList to draftScenariosList in
index.ts to match the tag/list convention enforced by spec-version.test.ts
- Add wif-assertion-scope-rejected check: AS returns invalid_scope for a
valid JWT when the client requests the reserved 'wif.rejected' scope;
verify client surfaces the error and does not retry
- Add runWifJwtBearerScopeRejected to everything-client.ts; add optional
scope param to WifJwtBearerProvider; add CLI entry point and vitest case
- Add src/seps/sep-1933.yaml with requirements mapped to each check ID,
covering the three deferred checks (iss, sub, jti) with exclusion rationale
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lient fallback Client bug class: WIF client receives unauthorized_client from JWT-bearer grant and silently switches to authorization_code instead of surfacing the error. The MCP SDK retries after UnauthorizedClientError (auth.js:152-154), calling prepareTokenRequest() a second time. WifGrantFallbackProvider exploits this by returning authorization_code params on the second call. Spec anchor: RFC 7523 §2.1 — clients MUST use the JWT-bearer grant type; silent grant-type switching hides authentication failures. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
valid_jwt uses a SPIFFE JWT-SVID subject (spiffe://conformance-test.local/...) issued by a SPIRE-style issuer. wrong_audience_jwt uses a Kubernetes PSAT subject and kubernetes.io claims, mirroring how K8s projected service-account tokens look in production. Token verification and client behaviour are unchanged; the formats ground the scenario in real workload identity platforms. k8s_issuer and k8s_subject added to context and schema for external clients that want to construct their own K8s-style assertions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… context These fields were not used by any client handler and are redundant since the K8s claims are already baked into wrong_audience_jwt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Downgrade wif-no-retry, wif-grant-fallback, and wif-assertion-scope-rejected to WARNING; RFC 7523 is silent on client retry and grant-type switching, so FAILURE status was not traceable to spec text - Add WifRetryProvider broken client that re-sends JWT-bearer after unauthorized_client, making wif-no-retry actually exercisable as a WARNING - Drop getChecks() sentinel (non-standard pattern that overloaded the wif-assertion-verified check ID); test harness detects empty check sets - Add comment on DRAFT_PROTOCOL_VERSION workaround for runner limitation - Add comment explaining why both slash/no-slash audience forms are accepted - Tighten schema comments for audience URL constraint and ES256 literal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove issuer and subject from context schema and scenario return value; neither is validated by the AS or used by the client, making them misleading. The iss exclusion rationale is updated to explain why. - Replace SPIFFE/K8s-PSAT constants and the kubernetes.io additionalClaims with neutral values across all three JWT fixtures; no check inspects token format, so format flavour was decorative and invited false assumptions about fixture semantics. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…m, annotate retry provider - Remove self-admitted workaround comment on DRAFT_PROTOCOL_VERSION; the classification stands on its own and the comment was inviting challenge - Drop signing_algorithm from context schema and scenario return; the field was redundant once the value was fixed to ES256, and the assertion is opaque to the client regardless - Add comment to WifRetryProvider explaining it deliberately omits the hasAttempted guard so the SDK retry reaches the AS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…annotate state machine - Export WIF_TRIGGER_UNAUTHORIZED_SCOPE and WIF_REJECTED_SCOPE from createWorkloadJwt.ts and import them in everything-client.ts; removes duplicate bare-string declarations that could silently diverge - Remove tokenEndpointAuthSigningAlgValuesSupported from mock AS config; the SDK does not consume this field in the JWT-bearer flow - Add comment to onTokenRequest clarifying wif-no-retry and wif-grant-fallback fire on any second request after any failure, not only post-unauthorized_client Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
c5b736e to
e5e13c4
Compare
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.
Motivation
As autonomous workloads increasingly interact with MCP servers, the existing authorization mechanisms - primarily designed around human-centric OAuth flows or statically registered client credentials - impose operational friction and do not align with how modern platforms provision identity. Workloads running under Kubernetes, SPIFFE/SPIRE, or cloud-native runtimes already receive short-lived, cryptographically verifiable JWTs that reflect their runtime identity. Requiring those workloads to additionally register as OAuth clients or manage long-lived secrets adds complexity and weakens security posture.
SEP-1933 addresses this by letting MCP directly leverage platform-provided credentials via the JWT Bearer grant (RFC 7523), eliminating a separate client identity lifecycle while preserving strong authentication guarantees. This PR adds client conformance coverage for that flow, so any MCP client that claims SEP-1933 support can be checked against the same behavioural contract regardless of the underlying workload identity platform.
Scope
Conformance tests are scoped to client behaviour. From the client's perspective, it receives a pre-issued JWT assertion (via scenario context) and must present it correctly at the token endpoint. OIDC discovery, signing-key resolution, issuer-allowlist policy, and
jtireplay detection are authorization-server concerns and are deliberately deferred to AS conformance work - they don't exercise any new client code path.What's included
Signing helper
helpers/createWorkloadJwt.ts:generateWorkloadKeypair(alg?)andcreateWorkloadJwt(opts), plusJWT_BEARER_GRANT_TYPEandDEFAULT_WORKLOAD_JWT_ALGconstants. Keypair generation is intentionally separated from signing so the scenario can sign with a key the AS does not trust without API gymnastics.expiresInaccepts either a duration string or an absolute epoch number, so already-expired assertions can be constructed deterministically for negative tests.helpers/createWorkloadJwt.test.ts: 13 unit tests (round-trip sign+verify, default lifetime, expired-token construction via numeric epoch, uniquejti, array audience preservation per RFC 7519 §4.1.3, reserved-claim protection, caller-suppliedjti/notBefore, algorithm override).cross-app-access.ts: two bare URN literals replaced withJWT_BEARER_GRANT_TYPE. No behaviour change.API shape mirrors existing patterns in
client-credentials.ts(keypair + PEM export) andcross-app-access.ts(SignJWT chain).Scenario and clients
src/scenarios/client/auth/wif-jwt-bearer.ts: generates an ES256 keypair perstart(), pre-signs three JWT variants (valid_jwt,wrong_audience_jwt,expired_jwt) with neutral fixture values, passes them via context, and configures the conformance AS withgrant_types: [jwt-bearer]andtoken_endpoint_auth_method: none. DCR is disabled and the client is pre-seeded withWIF_CLIENT_ID, matching the SEP-1933 model where trust is established at the issuer level rather than per-client.src/scenarios/client/auth/spec-references.ts:RFC_7523_JWT_BEARERandSEP_1933_WIFreferences.src/seps/sep-1933.yaml: requirement traceability.src/schemas/context.ts:auth/wif-jwt-bearercontext schema.examples/clients/typescript/everything-client.ts:WifJwtBearerProvider,runWifJwtBearer, and six broken-client runners.examples/clients/typescript/auth-test-wif-*.ts: CLI entrypoints for each broken variant.src/scenarios/client/auth/index.ts: scenario registration indraftScenariosList.src/scenarios/client/auth/index.test.ts: happy-path via draft loop + 6 negative tests.Checks implemented (9)
wif-grant-typegrant_type=urn:ietf:params:oauth:grant-type:jwt-bearerwif-assertion-missingassertionparameterwif-assertion-verifiedwif-assertion-expiredinvalid_grantfor an expired assertionwif-assertion-audienceinvalid_grantfor a wrong-audience assertionwif-assertion-malformedinvalid_grantfor a bad-signature assertionwif-no-retrywif-assertion-scope-rejectedinvalid_scopeand does not retrywif-grant-fallbackauthorization_codeafterunauthorized_clientwif-no-retry,wif-grant-fallback, andwif-assertion-scope-rejectedare WARNING because RFC 7523 is silent on client retry and grant-type switching behaviour; they will be upgraded to FAILURE once SEP-1933 lands normative spec text.Three requirements are explicitly deferred in
src/seps/sep-1933.yaml(iss,sub,jti); all are AS policy decisions. The scenario does not exposeissorsubin context because the client presents a pre-signed token and has no mechanism to vary those claims.Coverage vs. issue #223
Issue #223 lists seven negative checks. This PR covers all seven, plus additional client-observable checks:
wif-assertion-expired(expiredexp),wif-assertion-audience(wrongaud),wif-assertion-missing(missing assertion),wif-assertion-scope-rejected(invalid_scope),wif-grant-fallback(unauthorized_clientwithout grant-type fallback).wif-grant-type,wif-assertion-malformed,wif-no-retry,wif-assertion-verified.The remaining three issue checks (
invalid_grant/unauthorisedsub,invalid_grant/replayedjti, issuer allowlist) exercise AS policy rather than client code paths and are deferred to AS conformance work.Testing
npx vitest run src/scenarios/client/auth/helpers/createWorkloadJwt.test.ts- 13 tests passnpx vitest run src/scenarios/client/auth/index.test.ts- 49 tests pass (6 WIF negative tests)npx vitest run- full suite passes (194 tests, 16 files, no regressions)npm run check- typecheck and lint cleanCloses / relates
modelcontextprotocol/conformance#223modelcontextprotocol/modelcontextprotocol#1933