client/auth: register dpop and wif scenarios as extensions (#401)

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.


Claude-Session: https://claude.ai/code/session_012E7BriGaLimJxQr7B7ChiM

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
claude[bot] 2026-07-10 17:44:27 +01:00 • committed by GitHub
commit ce25103b1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 18 additions and 19 deletions

View file

@ -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();

View file

@ -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);

View file

@ -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()
];

View file

@ -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, ' +

View file

@ -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];