* feat(auth): add JWT-bearer helper for WIF client conformance (SEP-1933) Adds createWorkloadJwt and generateWorkloadKeypair to provide reusable, tested JWT signing infrastructure for the upcoming wif-jwt-bearer scenario (PR #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> * feat(auth): add JWT-bearer helper for WIF client conformance (SEP-1933) Adds createWorkloadJwt and generateWorkloadKeypair to provide reusable, tested JWT signing infrastructure for the upcoming wif-jwt-bearer scenario (PR #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> * feat(auth): add WIF JWT-bearer scenario and negative tests (SEP-1933) 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> * style: apply prettier formatting to wif-jwt-bearer.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(auth): add expired-assertion negative test; clarify wif-jwt-bearer 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> * fix(wif-jwt-bearer): disable DCR, detect retries, improve getChecks description - 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> * style: prettier formatting on everything-client.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(wif-jwt-bearer): let errors propagate from broken example clients 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> * fix(wif-jwt-bearer): tag as draft, add scope-rejected check, add SEP 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> * style: prettier formatting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(wif-jwt-bearer): add wif-grant-fallback check for unauthorized_client 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> * style: rename unused scope param in WifGrantFallbackProvider Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(wif-jwt-bearer): use realistic SPIFFE and K8s PSAT token formats 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> * style: prettier formatting on wif-jwt-bearer.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(wif-jwt-bearer): drop informational k8s_issuer/k8s_subject from 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> * fix(wif-jwt-bearer): address pre-merge review feedback - 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> * fix(wif-jwt-bearer): drop decorative fields and neutralise JWT fixtures - 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> * fix(wif-jwt-bearer): remove workaround comment, drop signing_algorithm, 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> * fix(wif-jwt-bearer): export scope constants, remove unused AS field, 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> * fix(wif-jwt-bearer): address pcarleton review comments - Move all broken WIF provider classes and runners to wif-broken-clients.ts; everything-client.ts now contains only the conformant WifJwtBearerProvider - Add sentinel in getChecks() for !tokenRequestReceived to prevent a silent pass when the client never sends a request - Drop wif-assertion-scope-rejected check; the no-retry behaviour after invalid_scope is already covered by wif-no-retry, and sep-1933.yaml updated accordingly - Remove audience from context schema and scenario return; the field was unused by any client implementation - Expand scenario description to tell a client implementer what to implement - Apply suggested description fix to wif-grant-fallback (does not assume unauthorized_client was the specific error that preceded the fallback) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(wif-jwt-bearer): drop number type from expiresIn, use jose duration strings Jose supports negative duration strings ('-60s', '60 seconds ago') that produce already-expired tokens without needing absolute epoch arithmetic. Drop the number overload, update the expired_jwt call site to '-60s', and update the unit test accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
35 lines
2.9 KiB
YAML
35 lines
2.9 KiB
YAML
sep: 1933
|
|
spec_url: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1933
|
|
requirements:
|
|
- check: wif-grant-type
|
|
text: 'The request includes grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer (RFC 7523 §2.1 — clients MUST use this grant type for JWT-bearer authorization grants)'
|
|
|
|
- check: wif-assertion-missing
|
|
text: 'The request includes assertion: <workload-jwt> (RFC 7523 §2.1 — the assertion parameter is REQUIRED and MUST contain the JWT)'
|
|
|
|
- check: wif-assertion-verified
|
|
text: 'Client successfully authenticates to the MCP authorization server using a JWT-bearer grant with a valid workload identity JWT assertion'
|
|
|
|
- check: wif-assertion-expired
|
|
text: 'The JWT MUST contain an exp claim and the authorization server MUST reject assertions whose exp has passed (RFC 7523 §3 — client MUST surface this error and not silently ignore it)'
|
|
|
|
- check: wif-assertion-audience
|
|
text: 'The JWT MUST contain an aud claim identifying the authorization server (RFC 7523 §3 — client MUST surface invalid audience errors and not silently ignore them)'
|
|
|
|
- check: wif-assertion-malformed
|
|
text: 'The authorization server MUST reject JWTs with invalid signatures or malformed claims (RFC 7523 §3 — client MUST surface verification failures)'
|
|
|
|
- check: wif-no-retry
|
|
text: 'Clients should not retry a JWT-bearer token request after the authorization server has rejected the assertion, including after invalid_scope responses (WARNING until SEP-1933 lands normative spec text; RFC 7523 is silent on client retry behaviour)'
|
|
|
|
- check: wif-grant-fallback
|
|
text: 'When the authorization server returns unauthorized_client for a JWT-bearer token request, the client should not fall back to a different grant type (WARNING until SEP-1933 lands normative spec text; RFC 7523 §2.1 defines the grant but does not forbid grant-type switching after failure)'
|
|
|
|
- text: 'The JWT MUST include an iss (issuer) claim identifying the workload identity provider (RFC 7523 §3)'
|
|
excluded: 'Issuer validation is AS policy; the client presents a pre-signed token and cannot vary the iss value. The scenario does not expose issuer in context because it is not a client-observable parameter — the client has no mechanism to select or alter the iss of an assertion it did not construct.'
|
|
|
|
- text: 'The JWT MUST include a sub (subject) claim identifying the workload (RFC 7523 §3)'
|
|
excluded: 'Subject allowlist enforcement is AS policy; the client has no control over the subject in a pre-signed IdP token. From the client perspective this collapses to a generic invalid_grant response, already covered by existing checks.'
|
|
|
|
- text: 'The JWT MUST include a jti (JWT ID) claim to prevent replay (RFC 7523 §3, recommended)'
|
|
excluded: 'Replay detection is AS policy; the client cannot influence jti uniqueness across requests. No client-observable protocol difference from other invalid_grant responses.'
|