From b67b69b100297ad6bdcff6dc1161b266913458c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 14:40:09 +0000 Subject: [PATCH] client/auth: register dpop and wif scenarios as extensions Move the DPoP scenarios (auth/dpop, auth/dpop-nonce) and the WIF JWT-bearer scenario (auth/wif-jwt-bearer) off the draft spec timeline and register them as the io.modelcontextprotocol/auth/dpop and io.modelcontextprotocol/auth/wif protocol extensions (SEP-2133 reverse-DNS ids), so they run under --suite extensions rather than at --spec-version draft. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012E7BriGaLimJxQr7B7ChiM --- src/scenarios/client/auth/dpop.ts | 6 ++++-- src/scenarios/client/auth/index.test.ts | 6 +++--- src/scenarios/client/auth/index.ts | 10 +++++----- src/scenarios/client/auth/wif-jwt-bearer.ts | 13 ++++--------- src/types.ts | 2 ++ 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/scenarios/client/auth/dpop.ts b/src/scenarios/client/auth/dpop.ts index 1d7b37b0..e993369d 100644 --- a/src/scenarios/client/auth/dpop.ts +++ b/src/scenarios/client/auth/dpop.ts @@ -5,7 +5,7 @@ import type { CheckStatus, SpecReference } from '../../../types'; -import { ScenarioUrls, DRAFT_PROTOCOL_VERSION } from '../../../types'; +import { ScenarioUrls } from '../../../types'; import { createAuthServer, type DpopTokenRequestObservation @@ -145,7 +145,9 @@ export function collapseDuplicateChecks( export class DPoPClientScenario implements Scenario { readonly name: string; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { + extensionId: 'io.modelcontextprotocol/auth/dpop' + } as const; readonly description: string; private authServer = new ServerLifecycle(); diff --git a/src/scenarios/client/auth/index.test.ts b/src/scenarios/client/auth/index.test.ts index f37bf926..57ff27ac 100644 --- a/src/scenarios/client/auth/index.test.ts +++ b/src/scenarios/client/auth/index.test.ts @@ -348,7 +348,7 @@ describe('WIF JWT-bearer negative tests', () => { }); // DPoP (SEP-1932): the compliant paths for both postures (auth/dpop and -// auth/dpop-nonce) are covered by the Client Draft Scenarios loop above; these +// auth/dpop-nonce) are covered by the Client Extension Scenarios loop above; these // are the negative cases, via deliberately-broken example clients. The baseline // checks (scheme, replay, token-request-proof) are nonce-independent, so those // negatives run against auth/dpop; the two nonce checks only fire when a @@ -419,8 +419,8 @@ describe('DPoP client negative tests (SEP-1932)', () => { // DPoP nonce-less baseline (SEP-1932): a client that implements NO nonce // handling still completes DPoP successfully when the server does not require a // nonce (the common case — server nonces are OPTIONAL, RFC 9449 §8/§9). The -// nonce-capable compliant path is covered by the Client Draft Scenarios loop -// above (auth/dpop is in draftScenariosList). +// nonce-capable compliant path is covered by the Client Extension Scenarios +// loop above (auth/dpop is in extensionScenariosList). describe('DPoP client nonce-less baseline (SEP-1932)', () => { test('auth/dpop: nonce-incapable client passes the baseline', async () => { const runner = new InlineClientRunner(dpopNoNonceClient); diff --git a/src/scenarios/client/auth/index.ts b/src/scenarios/client/auth/index.ts index a46a872a..7daec39b 100644 --- a/src/scenarios/client/auth/index.ts +++ b/src/scenarios/client/auth/index.ts @@ -66,7 +66,10 @@ export const backcompatScenariosList: Scenario[] = [ export const extensionScenariosList: Scenario[] = [ new ClientCredentialsJwtScenario(), new ClientCredentialsBasicScenario(), - new EnterpriseManagedAuthorizationScenario() + new EnterpriseManagedAuthorizationScenario(), + new DPoPClientScenario(false), // auth/dpop — nonce-less baseline (common case) + new DPoPClientScenario(true), // auth/dpop-nonce — server-required nonce (§8/§9) + new WifJwtBearerScenario() ]; // Draft scenarios (informational - not scored for tier assessment) @@ -81,8 +84,5 @@ export const draftScenariosList: Scenario[] = [ new IssParameterWrongIssuerScenario(), new IssParameterUnexpectedScenario(), new IssParameterNormalizedVariantScenario(), - new MetadataIssuerMismatchScenario(), - new WifJwtBearerScenario(), - new DPoPClientScenario(false), // auth/dpop — nonce-less baseline (common case) - new DPoPClientScenario(true) // auth/dpop-nonce — server-required nonce (§8/§9) + new MetadataIssuerMismatchScenario() ]; diff --git a/src/scenarios/client/auth/wif-jwt-bearer.ts b/src/scenarios/client/auth/wif-jwt-bearer.ts index 5c65fba3..4db0bd9c 100644 --- a/src/scenarios/client/auth/wif-jwt-bearer.ts +++ b/src/scenarios/client/auth/wif-jwt-bearer.ts @@ -1,12 +1,6 @@ import * as jose from 'jose'; -import type { - Scenario, - ConformanceCheck, - ScenarioUrls, - SpecVersion -} from '../../../types'; +import type { Scenario, ConformanceCheck, ScenarioUrls } from '../../../types'; import type { ScenarioContext } from '../../../mock-server'; -import { DRAFT_PROTOCOL_VERSION } from '../../../types'; import { createAuthServer } from './helpers/createAuthServer'; import { createServer } from './helpers/createServer'; import { MockTokenVerifier } from './helpers/mockTokenVerifier'; @@ -26,8 +20,9 @@ const WIF_CLIENT_ID = 'conformance-wif-workload'; export class WifJwtBearerScenario implements Scenario { name = 'auth/wif-jwt-bearer'; - specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION]; - readonly source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const; + readonly source = { + extensionId: 'io.modelcontextprotocol/auth/wif' + } as const; description = 'Tests the RFC 7523 JWT-bearer grant for workload identity federation (SEP-1933). ' + 'The client must: use grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, ' + diff --git a/src/types.ts b/src/types.ts index eaa364b3..a6e02251 100644 --- a/src/types.ts +++ b/src/types.ts @@ -95,6 +95,8 @@ export type ScenarioSpecTag = SpecVersion | 'extension'; export const EXTENSION_IDS = [ 'io.modelcontextprotocol/oauth-client-credentials', 'io.modelcontextprotocol/enterprise-managed-authorization', + 'io.modelcontextprotocol/auth/dpop', + 'io.modelcontextprotocol/auth/wif', 'io.modelcontextprotocol/tasks' ] as const; export type ExtensionId = (typeof EXTENSION_IDS)[number];