[auth] Rename basic-dcr scenarios to metadata variants (#41)

* merge metadata tests

* [auth] Rename basic-dcr scenarios to metadata variants

Consolidate auth metadata discovery scenarios with consistent naming:
- AuthBasicDCRScenario → AuthMetadataDefaultScenario (auth/metadata-default)
- AuthBasicMetadataVar1Scenario → AuthMetadataVar1Scenario (auth/metadata-var1)
- AuthBasicMetadataVar2Scenario → AuthMetadataVar2Scenario (auth/metadata-var2)
- AuthBasicMetadataVar3Scenario → AuthMetadataVar3Scenario (auth/metadata-var3)

Also updates descriptions to use consistent markdown formatting with
bold headers for Registration, PRM, and OAuth metadata fields.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Paul Carleton 2025-11-20 13:14:01 +00:00 • committed by GitHub
commit f14895ca0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 228 additions and 317 deletions

View file

@ -1,93 +0,0 @@
import type { Scenario, ConformanceCheck } from '../../../types.js';
import { ScenarioUrls } from '../../../types.js';
import { createAuthServer } from './helpers/createAuthServer.js';
import { createServer } from './helpers/createServer.js';
import { ServerLifecycle } from './helpers/serverLifecycle.js';
import { Request, Response } from 'express';
import { SpecReferences } from './spec-references.js';
export class AuthBasicDCRScenario implements Scenario {
name = 'auth/basic-dcr';
description =
'Tests Basic OAuth flow with DCR, PRM at path-based location, OAuth metadata at root location, and no scopes required';
private authServer = new ServerLifecycle();
private server = new ServerLifecycle();
private checks: ConformanceCheck[] = [];
async start(): Promise<ScenarioUrls> {
this.checks = [];
const authApp = createAuthServer(this.checks, this.authServer.getUrl);
await this.authServer.start(authApp);
const app = createServer(
this.checks,
this.server.getUrl,
this.authServer.getUrl
);
// For this scenario, reject PRM requests at root location since we have the path-based PRM.
app.get(
'/.well-known/oauth-protected-resource',
(req: Request, res: Response) => {
this.checks.push({
id: 'prm-priority-order',
name: 'PRM Priority Order',
description:
'Client requested PRM metadata at root location on a server with path-based PRM',
status: 'FAILURE',
timestamp: new Date().toISOString(),
specReferences: [
SpecReferences.RFC_PRM_DISCOVERY,
SpecReferences.MCP_PRM_DISCOVERY
],
details: {
url: req.url,
path: req.path
}
});
// Return 404 to indicate PRM is not available at root location
res.status(404).json({
error: 'not_found',
error_description: 'PRM metadata not available at root location'
});
}
);
await this.server.start(app);
return { serverUrl: `${this.server.getUrl()}/mcp` };
}
async stop() {
await this.authServer.stop();
await this.server.stop();
}
getChecks(): ConformanceCheck[] {
const expectedSlugs = [
'prm-pathbased-requested',
'authorization-server-metadata',
'client-registration',
'authorization-request',
'token-request'
];
for (const slug of expectedSlugs) {
if (!this.checks.find((c) => c.id === slug)) {
this.checks.push({
id: slug,
// TODO: these are redundant...
name: `Expected Check Missing: ${slug}`,
description: `Expected Check Missing: ${slug}`,
status: 'FAILURE',
timestamp: new Date().toISOString()
// TODO: ideally we'd add the spec references
});
}
}
return this.checks;
}
}

View file

@ -1,210 +0,0 @@
import type { Scenario, ConformanceCheck } from '../../../types.js';
import { ScenarioUrls } from '../../../types.js';
import { createAuthServer } from './helpers/createAuthServer.js';
import { createServer } from './helpers/createServer.js';
import { ServerLifecycle } from './helpers/serverLifecycle.js';
import { SpecReferences } from './spec-references.js';
export class AuthBasicMetadataVar1Scenario implements Scenario {
name = 'auth/basic-metadata-var1';
description =
'Tests Basic OAuth flow with DCR, PRM at root location, OAuth metadata at OpenID discovery path, and no scopes required';
private authServer = new ServerLifecycle();
private server = new ServerLifecycle();
private checks: ConformanceCheck[] = [];
async start(): Promise<ScenarioUrls> {
this.checks = [];
const authApp = createAuthServer(this.checks, this.authServer.getUrl, {
metadataPath: '/.well-known/openid-configuration',
isOpenIdConfiguration: true
});
await this.authServer.start(authApp);
const app = createServer(
this.checks,
this.server.getUrl,
this.authServer.getUrl,
{
prmPath: '/.well-known/oauth-protected-resource'
}
);
await this.server.start(app);
return { serverUrl: `${this.server.getUrl()}/mcp` };
}
async stop() {
await this.authServer.stop();
await this.server.stop();
}
getChecks(): ConformanceCheck[] {
const expectedSlugs = [
'authorization-server-metadata',
'client-registration',
'authorization-request',
'token-request'
];
for (const slug of expectedSlugs) {
if (!this.checks.find((c) => c.id === slug)) {
this.checks.push({
id: slug,
name: `Expected Check Missing: ${slug}`,
description: `Expected Check Missing: ${slug}`,
status: 'FAILURE',
timestamp: new Date().toISOString()
});
}
}
return this.checks;
}
}
export class AuthBasicMetadataVar2Scenario implements Scenario {
name = 'auth/basic-metadata-var2';
description =
'Tests Basic OAuth flow with DCR, PRM at root location, OAuth metadata at path-based OAuth discovery path';
private authServer = new ServerLifecycle();
private server = new ServerLifecycle();
private checks: ConformanceCheck[] = [];
async start(): Promise<ScenarioUrls> {
this.checks = [];
const authApp = createAuthServer(this.checks, this.authServer.getUrl, {
metadataPath: '/tenant1/.well-known/openid-configuration',
isOpenIdConfiguration: true,
routePrefix: '/tenant1'
});
authApp.get('/.well-known/oauth-authorization-server', (req, res) => {
this.checks.push({
id: 'authorization-server-metadata-wrong-path',
name: 'AuthorizationServerMetadataWrongPath',
description:
'Client requested authorization server at the root path when the AS URL has a path-based location',
status: 'FAILURE',
timestamp: new Date().toISOString(),
specReferences: [
SpecReferences.RFC_AUTH_SERVER_METADATA_REQUEST,
SpecReferences.MCP_AUTH_DISCOVERY
],
details: {
url: req.url
}
});
res.status(404).send('Not Found');
});
await this.authServer.start(authApp);
const app = createServer(
this.checks,
this.server.getUrl,
() => `${this.authServer.getUrl()}/tenant1`,
{
prmPath: '/.well-known/oauth-protected-resource'
}
);
await this.server.start(app);
return { serverUrl: `${this.server.getUrl()}/mcp` };
}
async stop() {
await this.authServer.stop();
await this.server.stop();
}
getChecks(): ConformanceCheck[] {
const expectedSlugs = [
'authorization-server-metadata',
'client-registration',
'authorization-request',
'token-request'
];
for (const slug of expectedSlugs) {
if (!this.checks.find((c) => c.id === slug)) {
this.checks.push({
id: slug,
name: `Expected Check Missing: ${slug}`,
description: `Expected Check Missing: ${slug}`,
status: 'FAILURE',
timestamp: new Date().toISOString()
});
}
}
return this.checks;
}
}
export class AuthBasicMetadataVar3Scenario implements Scenario {
name = 'auth/basic-metadata-var3';
description =
'Tests Basic OAuth flow with DCR, PRM at custom location listed in WWW-Authenticate header, OAuth metadata is at nested OpenID discovery path, and no scopes required';
private authServer = new ServerLifecycle();
private server = new ServerLifecycle();
private checks: ConformanceCheck[] = [];
async start(): Promise<ScenarioUrls> {
this.checks = [];
const authApp = createAuthServer(this.checks, this.authServer.getUrl, {
metadataPath: '/tenant1/.well-known/openid-configuration',
isOpenIdConfiguration: true,
routePrefix: '/tenant1'
});
await this.authServer.start(authApp);
const app = createServer(
this.checks,
this.server.getUrl,
() => {
return `${this.authServer.getUrl()}/tenant1`;
},
{
// This is a custom path, so unable to get via probing, it's only available
// via following the `resource_metadata_url` in the WWW-Authenticate header.
// The resource must match the original request URL per RFC 9728.
prmPath: '/custom/metadata/location.json'
}
);
await this.server.start(app);
return { serverUrl: `${this.server.getUrl()}/mcp` };
}
async stop() {
await this.authServer.stop();
await this.server.stop();
}
getChecks(): ConformanceCheck[] {
const expectedSlugs = [
'authorization-server-metadata',
'client-registration',
'authorization-request',
'token-request'
];
for (const slug of expectedSlugs) {
if (!this.checks.find((c) => c.id === slug)) {
this.checks.push({
id: slug,
name: `Expected Check Missing: ${slug}`,
description: `Expected Check Missing: ${slug}`,
status: 'FAILURE',
timestamp: new Date().toISOString()
});
}
}
return this.checks;
}
}

View file

@ -0,0 +1,219 @@
/**
* OAuth Metadata Discovery Scenarios
*
* These scenarios test different combinations of PRM and OAuth metadata locations.
* The configurations are defined in SCENARIO_CONFIGS below and scenarios are
* generated from them.
*/
import type { Scenario, ConformanceCheck } from '../../../types.js';
import { ScenarioUrls } from '../../../types.js';
import { createAuthServer } from './helpers/createAuthServer.js';
import { createServer } from './helpers/createServer.js';
import { ServerLifecycle } from './helpers/serverLifecycle.js';
import { SpecReferences } from './spec-references.js';
import { Request, Response } from 'express';
/**
* Configuration for a metadata discovery scenario.
*/
interface MetadataScenarioConfig {
name: string;
prmLocation: string;
inWwwAuth: boolean;
oauthMetadataLocation: string;
/** Route prefix for the auth server (e.g., '/tenant1') */
authRoutePrefix?: string;
/** If true, add a trap for root PRM requests */
trapRootPrm?: boolean;
}
/**
* Scenario configurations table:
*
* | Scenario | PRM Location | In WWW-Auth | OAuth Metadata Location |
* |------------------|-------------------------------------------|-------------|------------------------------------------------|
* | metadata-default | /.well-known/oauth-protected-resource/mcp | Yes | /.well-known/oauth-authorization-server |
* | metadata-var1 | /.well-known/oauth-protected-resource/mcp | No | /.well-known/openid-configuration |
* | metadata-var2 | /.well-known/oauth-protected-resource | No | /.well-known/oauth-authorization-server/tenant1|
* | metadata-var3 | /custom/metadata/location.json | Yes | /tenant1/.well-known/openid-configuration |
*/
const SCENARIO_CONFIGS: MetadataScenarioConfig[] = [
{
name: 'metadata-default',
prmLocation: '/.well-known/oauth-protected-resource/mcp',
inWwwAuth: true,
oauthMetadataLocation: '/.well-known/oauth-authorization-server',
trapRootPrm: true
},
{
name: 'metadata-var1',
prmLocation: '/.well-known/oauth-protected-resource/mcp',
inWwwAuth: false,
oauthMetadataLocation: '/.well-known/openid-configuration'
},
{
name: 'metadata-var2',
prmLocation: '/.well-known/oauth-protected-resource',
inWwwAuth: false,
oauthMetadataLocation: '/.well-known/oauth-authorization-server/tenant1',
authRoutePrefix: '/tenant1'
},
{
name: 'metadata-var3',
prmLocation: '/custom/metadata/location.json',
inWwwAuth: true,
oauthMetadataLocation: '/tenant1/.well-known/openid-configuration',
authRoutePrefix: '/tenant1'
}
];
/**
* Creates a metadata discovery scenario from configuration.
*/
function createMetadataScenario(config: MetadataScenarioConfig): Scenario {
const authServer = new ServerLifecycle();
const server = new ServerLifecycle();
let checks: ConformanceCheck[] = [];
const routePrefix = config.authRoutePrefix || '';
const isOpenIdConfiguration = config.oauthMetadataLocation.includes(
'openid-configuration'
);
// Determine if PRM is at path-based location
const isPathBasedPrm =
config.prmLocation === '/.well-known/oauth-protected-resource/mcp';
return {
name: `auth/${config.name}`,
description: `Tests Basic OAuth metadata discovery flow.
**PRM:** ${config.prmLocation}${config.inWwwAuth ? '' : ' (not in WWW-Authenticate)'}
**OAuth metadata:** ${config.oauthMetadataLocation}
`,
async start(): Promise<ScenarioUrls> {
checks = [];
const authApp = createAuthServer(checks, authServer.getUrl, {
metadataPath: config.oauthMetadataLocation,
isOpenIdConfiguration,
...(routePrefix && { routePrefix })
});
// If path-based OAuth metadata, trap root requests
if (routePrefix) {
authApp.get('/.well-known/oauth-authorization-server', (req, res) => {
checks.push({
id: 'authorization-server-metadata-wrong-path',
name: 'AuthorizationServerMetadataWrongPath',
description:
'Client requested authorization server at the root path when the AS URL has a path-based location',
status: 'FAILURE',
timestamp: new Date().toISOString(),
specReferences: [
SpecReferences.RFC_AUTH_SERVER_METADATA_REQUEST,
SpecReferences.MCP_AUTH_DISCOVERY
],
details: {
url: req.url
}
});
res.status(404).send('Not Found');
});
}
await authServer.start(authApp);
const getAuthServerUrl = routePrefix
? () => `${authServer.getUrl()}${routePrefix}`
: authServer.getUrl;
const app = createServer(checks, server.getUrl, getAuthServerUrl, {
prmPath: config.prmLocation,
includePrmInWwwAuth: config.inWwwAuth
});
// Add trap for root PRM requests if configured
if (config.trapRootPrm) {
app.get(
'/.well-known/oauth-protected-resource',
(req: Request, res: Response) => {
checks.push({
id: 'prm-priority-order',
name: 'PRM Priority Order',
description:
'Client requested PRM metadata at root location on a server with path-based PRM',
status: 'FAILURE',
timestamp: new Date().toISOString(),
specReferences: [
SpecReferences.RFC_PRM_DISCOVERY,
SpecReferences.MCP_PRM_DISCOVERY
],
details: {
url: req.url,
path: req.path
}
});
res.status(404).json({
error: 'not_found',
error_description: 'PRM metadata not available at root location'
});
}
);
}
await server.start(app);
return { serverUrl: `${server.getUrl()}/mcp` };
},
async stop() {
await authServer.stop();
await server.stop();
},
getChecks(): ConformanceCheck[] {
const expectedSlugs = [
...(isPathBasedPrm ? ['prm-pathbased-requested'] : []),
'authorization-server-metadata',
'client-registration',
'authorization-request',
'token-request'
];
for (const slug of expectedSlugs) {
if (!checks.find((c) => c.id === slug)) {
checks.push({
id: slug,
name: `Expected Check Missing: ${slug}`,
description: `Expected Check Missing: ${slug}`,
status: 'FAILURE',
timestamp: new Date().toISOString()
});
}
}
return checks;
}
};
}
// Generate scenario instances from configurations
export const AuthMetadataDefaultScenario = createMetadataScenario(
SCENARIO_CONFIGS[0]
);
export const AuthMetadataVar1Scenario = createMetadataScenario(
SCENARIO_CONFIGS[1]
);
export const AuthMetadataVar2Scenario = createMetadataScenario(
SCENARIO_CONFIGS[2]
);
export const AuthMetadataVar3Scenario = createMetadataScenario(
SCENARIO_CONFIGS[3]
);
// Export all scenarios as an array for convenience
export const metadataScenarios = SCENARIO_CONFIGS.map(createMetadataScenario);

View file

@ -18,6 +18,7 @@ export interface ServerOptions {
prmPath?: string | null;
requiredScopes?: string[];
scopesSupported?: string[];
includePrmInWwwAuth?: boolean;
includeScopeInWwwAuth?: boolean;
authMiddleware?: express.RequestHandler;
tokenVerifier?: MockTokenVerifier;
@ -33,6 +34,7 @@ export function createServer(
prmPath = '/.well-known/oauth-protected-resource/mcp',
requiredScopes = [],
scopesSupported,
includePrmInWwwAuth = true,
includeScopeInWwwAuth = false,
tokenVerifier
} = options;
@ -136,9 +138,10 @@ export function createServer(
verifier,
// Only pass requiredScopes if we want them in the WWW-Authenticate header
requiredScopes: includeScopeInWwwAuth ? requiredScopes : [],
...(prmPath !== null && {
resourceMetadataUrl: `${getBaseUrl()}${prmPath}`
})
...(includePrmInWwwAuth &&
prmPath !== null && {
resourceMetadataUrl: `${getBaseUrl()}${prmPath}`
})
});
authMiddleware(req, res, async (err?: any) => {

View file

@ -42,7 +42,7 @@ describe('Client Auth Scenarios', () => {
describe('Negative tests', () => {
test('bad client requests root PRM location', async () => {
const runner = new InlineClientRunner(badPrmClient);
await runClientAgainstScenario(runner, 'auth/basic-dcr', [
await runClientAgainstScenario(runner, 'auth/metadata-default', [
'prm-priority-order'
]);
});

View file

@ -1,10 +1,5 @@
import { Scenario } from '../../../types';
import { AuthBasicDCRScenario } from './basic-dcr.js';
import {
AuthBasicMetadataVar1Scenario,
AuthBasicMetadataVar2Scenario,
AuthBasicMetadataVar3Scenario
} from './basic-metadata.js';
import { metadataScenarios } from './discovery-metadata.js';
import {
Auth20250326OAuthMetadataBackcompatScenario,
Auth20250326OEndpointFallbackScenario
@ -17,10 +12,7 @@ import {
} from './scope-handling.js';
export const authScenariosList: Scenario[] = [
new AuthBasicDCRScenario(),
new AuthBasicMetadataVar1Scenario(),
new AuthBasicMetadataVar2Scenario(),
new AuthBasicMetadataVar3Scenario(),
...metadataScenarios,
new Auth20250326OAuthMetadataBackcompatScenario(),
new Auth20250326OEndpointFallbackScenario(),
new ScopeFromWwwAuthenticateScenario(),