Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/scenarios/authorization-server/auth/spec-references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ export const SpecReferences: { [key: string]: SpecReference } = {
id: 'MCP-Authorization-metadata-discovery',
url: 'https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#authorization-server-metadata-discovery'
},
MCP_AUTH_RESPONSE_VALIDATION: {
id: 'MCP-Authorization-response-validation',
url: 'https://modelcontextprotocol.io/specification/draft/basic/authorization#authorization-response-validation'
},
RFC_9207_ISS_PARAMETER: {
id: 'RFC-9207-iss-parameter',
url: 'https://www.rfc-editor.org/rfc/rfc9207.html#section-2'
},
RFC_9207_AS_METADATA: {
id: 'RFC-9207-authorization-server-metadata',
url: 'https://www.rfc-editor.org/rfc/rfc9207.html#section-2.3'
},
OAUTH_2_1_AUTHORIZATION_CODE_GRANT: {
id: 'OAUTH-2.1-authorization-code-grant',
url: 'https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-13.html#section-4.1'
Expand Down
128 changes: 119 additions & 9 deletions src/scenarios/authorization-server/authorization-code-grant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ describe('AuthorizationCodeGrantScenario', () => {

const checks = await scenario.run(OPTIONS, DETAILS);

expect(checks).toHaveLength(1);
expect(checks).toHaveLength(3);

const check = checks[0];

Expand Down Expand Up @@ -132,7 +132,7 @@ describe('AuthorizationCodeGrantScenario', () => {

const checks = await scenario.run(OPTIONS, DETAILS);

expect(checks).toHaveLength(1);
expect(checks).toHaveLength(3);

const check = checks[0];

Expand All @@ -154,7 +154,7 @@ describe('AuthorizationCodeGrantScenario', () => {

const checks = await scenario.run(OPTIONS, DETAILS);

expect(checks).toHaveLength(1);
expect(checks).toHaveLength(3);

const check = checks[0];

Expand All @@ -173,7 +173,7 @@ describe('AuthorizationCodeGrantScenario', () => {

const checks = await scenario.run(OPTIONS, DETAILS);

expect(checks).toHaveLength(1);
expect(checks).toHaveLength(3);

const check = checks[0];

Expand All @@ -197,7 +197,7 @@ describe('AuthorizationCodeGrantScenario', () => {

const checks = await scenario.run(OPTIONS, DETAILS);

expect(checks).toHaveLength(1);
expect(checks).toHaveLength(3);

const check = checks[0];

Expand All @@ -219,7 +219,7 @@ describe('AuthorizationCodeGrantScenario', () => {

const checks = await scenario.run(OPTIONS, DETAILS);

expect(checks).toHaveLength(1);
expect(checks).toHaveLength(3);

const check = checks[0];

Expand All @@ -241,7 +241,7 @@ describe('AuthorizationCodeGrantScenario', () => {

const checks = await scenario.run(OPTIONS, DETAILS);

expect(checks).toHaveLength(1);
expect(checks).toHaveLength(3);

const check = checks[0];

Expand Down Expand Up @@ -273,7 +273,7 @@ describe('AuthorizationCodeGrantScenario', () => {

const checks = await scenario.run(OPTIONS, DETAILS);

expect(checks).toHaveLength(1);
expect(checks).toHaveLength(3);

const check = checks[0];

Expand Down Expand Up @@ -305,7 +305,7 @@ describe('AuthorizationCodeGrantScenario', () => {

const checks = await scenario.run(OPTIONS, DETAILS);

expect(checks).toHaveLength(1);
expect(checks).toHaveLength(3);

const check = checks[0];

Expand Down Expand Up @@ -426,4 +426,114 @@ describe('AuthorizationCodeGrantScenario', () => {

expect(check.status).toBe('SKIPPED');
});

describe('SEP-2468 iss parameter (2026-07-28)', () => {
const DETAILS_ISS_SUPPORTED = {
'authorization-server-metadata-endpoint': {
body: {
...METADATA,
authorization_response_iss_parameter_supported: true
}
}
};

const statusOf = (checks: { id: string; status: string }[], id: string) =>
checks.find((c) => c.id === id)?.status;

beforeEach(() => {
mockTokenResponse({ access_token: 'access-token', token_type: 'Bearer' });
});

it('passes when iss is included and advertised in metadata', async () => {
const scenario = new AuthorizationCodeGrantScenario();
mockCallbackServer(
scenario,
(state) =>
`http://127.0.0.1:3000/callback?code=abc&state=${state}&iss=${SERVER_URL}`
);

const checks = await scenario.run(OPTIONS, DETAILS_ISS_SUPPORTED);

expect(statusOf(checks, 'authorization-code-grant')).toBe('SUCCESS');
expect(statusOf(checks, 'sep-2468-as-include-iss')).toBe('SUCCESS');
expect(statusOf(checks, 'sep-2468-as-advertise-iss-supported')).toBe(
'SUCCESS'
);
for (const c of checks.slice(1)) {
expect(c.source).toEqual({ introducedIn: '2026-07-28' });
}
});

it('warns when iss is omitted and skips the advertisement check', async () => {
const scenario = new AuthorizationCodeGrantScenario();
mockCallbackServer(
scenario,
(state) => `http://127.0.0.1:3000/callback?code=abc&state=${state}`
);

const checks = await scenario.run(OPTIONS, DETAILS_ISS_SUPPORTED);

expect(statusOf(checks, 'sep-2468-as-include-iss')).toBe('WARNING');
expect(statusOf(checks, 'sep-2468-as-advertise-iss-supported')).toBe(
'SKIPPED'
);
});

it('fails when iss is included but not advertised in metadata', async () => {
const scenario = new AuthorizationCodeGrantScenario();
mockCallbackServer(
scenario,
(state) =>
`http://127.0.0.1:3000/callback?code=abc&state=${state}&iss=${SERVER_URL}`
);

const checks = await scenario.run(OPTIONS, DETAILS);

expect(statusOf(checks, 'sep-2468-as-include-iss')).toBe('SUCCESS');
const advertise = checks.find(
(c) => c.id === 'sep-2468-as-advertise-iss-supported'
);
expect(advertise?.status).toBe('FAILURE');
expect(advertise?.errorMessage).toContain(
'authorization_response_iss_parameter_supported'
);
});

it('fails when iss is included but advertised as false', async () => {
const scenario = new AuthorizationCodeGrantScenario();
mockCallbackServer(
scenario,
(state) =>
`http://127.0.0.1:3000/callback?code=abc&state=${state}&iss=${SERVER_URL}`
);

const checks = await scenario.run(OPTIONS, {
'authorization-server-metadata-endpoint': {
body: {
...METADATA,
authorization_response_iss_parameter_supported: false
}
}
});

expect(statusOf(checks, 'sep-2468-as-advertise-iss-supported')).toBe(
'FAILURE'
);
});

it('evaluates iss on error responses too', async () => {
const scenario = new AuthorizationCodeGrantScenario();
mockCallbackServer(
scenario,
() => 'http://127.0.0.1:3000/callback?error=access_denied'
);

const checks = await scenario.run(OPTIONS, DETAILS_ISS_SUPPORTED);

expect(statusOf(checks, 'authorization-code-grant')).toBe('FAILURE');
const includeIss = checks.find((c) => c.id === 'sep-2468-as-include-iss');
expect(includeIss?.status).toBe('WARNING');
expect(includeIss?.errorMessage).toContain('error response');
});
});
});
91 changes: 86 additions & 5 deletions src/scenarios/authorization-server/authorization-code-grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
*/
import {
ClientScenarioForAuthorizationServer,
ConformanceCheck
ConformanceCheck,
DRAFT_PROTOCOL_VERSION
} from '../../types';
import { startCallbackServer } from '../authorization-server/auth/helpers/createCallbackServer';
import { request } from 'undici';
Expand Down Expand Up @@ -58,12 +59,15 @@ export class AuthorizationCodeGrantScenario implements ClientScenarioForAuthoriz
- HTTP response status code of token response MUST be 200 OK
- Content-Type header of token response MUST be application/json
- Cache-Control header of token response MUST be no-store
- Token response MUST return a JSON response including access_token and token_type`;
- Token response MUST return a JSON response including access_token and token_type
- (2026-07-28+, SEP-2468) SHOULD include the iss parameter in authorization responses, including error responses
- (2026-07-28+, SEP-2468) If the iss parameter is included, MUST set authorization_response_iss_parameter_supported to true in metadata`;

async run(
options: AuthorizationServerOptions,
details: Record<string, unknown>
): Promise<ConformanceCheck[]> {
let issChecks: ConformanceCheck[] = [];
try {
if (!options.clientId) {
return [
Expand Down Expand Up @@ -116,6 +120,7 @@ export class AuthorizationCodeGrantScenario implements ClientScenarioForAuthoriz

const authorizationResponseUrl =
await callback.waitForCallback(300_000);
issChecks = this.checkIssParameter(authorizationResponseUrl, metadata);

const errors: string[] = [];
const code = this.validateAuthorizationResponse(
Expand All @@ -134,24 +139,100 @@ export class AuthorizationCodeGrantScenario implements ClientScenarioForAuthoriz
this.validateTokenResponse(tokenResponse, errors);

if (errors.length > 0) {
return [this.failureCheck(errors.join(', '))];
return [this.failureCheck(errors.join(', ')), ...issChecks];
}

return [
this.successCheck({
authorizationRequest,
authorizationResponseUrl,
body: redactTokens(tokenResponse.body)
})
}),
...issChecks
];
} finally {
callback.close();
}
} catch (error) {
return [this.failureCheck(error)];
return [this.failureCheck(error), ...issChecks];
}
}

// SEP-2468: evaluated on every authorization response, including error responses.
private checkIssParameter(
responseUrl: string,
metadata: Record<string, unknown>
): ConformanceCheck[] {
const url = new URL(responseUrl);
const iss = url.searchParams.getAll('iss');
const issPresent = iss.length > 0;
const isErrorResponse = url.searchParams.has('error');
const issSupported =
metadata.authorization_response_iss_parameter_supported;
const source = { introducedIn: DRAFT_PROTOCOL_VERSION } as const;
const timestamp = new Date().toISOString();

const includeIss: ConformanceCheck = {
id: 'sep-2468-as-include-iss',
name: 'AuthorizationResponseIncludesIss',
description:
'Authorization server includes the iss parameter in authorization responses, including error responses',
status: issPresent ? 'SUCCESS' : 'WARNING',
source,
timestamp,
...(issPresent
? {}
: {
errorMessage: `Authorization ${isErrorResponse ? 'error ' : ''}response does not include the iss parameter`
}),
specReferences: [
SpecReferences.MCP_AUTH_RESPONSE_VALIDATION,
SpecReferences.RFC_9207_ISS_PARAMETER
],
details: { iss, isErrorResponse }
};

const advertiseCheck = {
id: 'sep-2468-as-advertise-iss-supported',
name: 'AuthorizationServerAdvertisesIssSupported',
description:
'Authorization server that includes iss sets authorization_response_iss_parameter_supported to true in its metadata',
source,
timestamp,
specReferences: [
SpecReferences.MCP_AUTH_RESPONSE_VALIDATION,
SpecReferences.RFC_9207_AS_METADATA
],
details: {
iss,
authorization_response_iss_parameter_supported: issSupported
}
};
let advertiseIss: ConformanceCheck;
if (!issPresent) {
// The MUST is conditional on the AS including iss.
advertiseIss = {
...advertiseCheck,
status: 'SKIPPED',
errorMessage:
'Not applicable: authorization response does not include the iss parameter'
};
} else if (issSupported === true) {
advertiseIss = { ...advertiseCheck, status: 'SUCCESS' };
} else {
advertiseIss = {
...advertiseCheck,
status: 'FAILURE',
errorMessage:
issSupported === undefined
? 'Authorization response includes iss but metadata does not include "authorization_response_iss_parameter_supported"'
: `Authorization response includes iss but "authorization_response_iss_parameter_supported" is ${JSON.stringify(issSupported)}, expected true`
};
}

return [includeIss, advertiseIss];
}

private buildAuthorizationRequest(
metadata: any,
options: AuthorizationServerOptions
Expand Down
10 changes: 4 additions & 6 deletions src/seps/sep-2468.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@ requirements:
- check: sep-2468-client-no-normalization
text: 'After decoding the iss value from the application/x-www-form-urlencoded response per RFC 9207 Section 2.4, clients MUST NOT apply scheme or host case folding, default-port elision, trailing-slash, or percent-encoding normalization (RFC 3986 Sections 6.2.2-6.2.3) before comparison.'

- text: 'MCP authorization servers SHOULD include the iss parameter in authorization responses, including error responses, as defined in RFC9207 Section 2.'
excluded: 'Targets the authorization server under test; observing iss in an authorization response requires driving an Authorization Code Grant against the AS, which the authorization-server suite does not implement yet (it only probes the metadata endpoint).'
issue: 'https://github.com/modelcontextprotocol/conformance/issues/208'
- text: 'Authorization servers that include the iss parameter MUST advertise this by setting authorization_response_iss_parameter_supported to true in their metadata (RFC9207 Section 2.3).'
excluded: 'Conditional on the AS actually including iss in an authorization response, which requires driving an Authorization Code Grant against the AS under test; the authorization-server suite does not implement that yet.'
issue: 'https://github.com/modelcontextprotocol/conformance/issues/208'
- check: sep-2468-as-include-iss
text: 'MCP authorization servers SHOULD include the iss parameter in authorization responses, including error responses, as defined in RFC9207 Section 2.'
- check: sep-2468-as-advertise-iss-supported
text: 'Authorization servers that include the iss parameter MUST advertise this by setting authorization_response_iss_parameter_supported to true in their metadata (RFC9207 Section 2.3).'
- text: 'This validation applies equally to error responses - on mismatch the client MUST NOT act on or display error, error_description, or error_uri.'
excluded: 'display is UI-facing; act-on has no protocol-observable signal beyond the existing reject-on-mismatch checks'
Loading