Skip to content

Commit 85367f8

Browse files
committed
fail early if AS doens't support CIMD
1 parent 50d6708 commit 85367f8

2 files changed

Lines changed: 89 additions & 0 deletions

File tree

sdk/src/oauth/state-machines/debug-oauth-2025-11-25.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,6 +1234,23 @@ export const createDebugOAuthStateMachine = (
12341234

12351235
// Check registration strategy - 2025-11-25 priority: CIMD > Pre-registered > DCR
12361236
if (registrationStrategy === "cimd") {
1237+
// Per spec: clients SHOULD check client_id_metadata_document_supported
1238+
// before using CIMD. Fail early instead of redirecting to an AS that
1239+
// will reject the URL-formatted client_id.
1240+
if (
1241+
state.authorizationServerMetadata
1242+
.client_id_metadata_document_supported !== true
1243+
) {
1244+
updateState({
1245+
error:
1246+
"The authorization server does not support Client ID Metadata Documents " +
1247+
"(client_id_metadata_document_supported is not advertised in AS metadata). " +
1248+
"Try using 'dcr' or 'preregistered' registration strategy instead.",
1249+
isInitiatingAuth: false,
1250+
});
1251+
return;
1252+
}
1253+
12371254
// CIMD Step 1: Prepare client_id URL
12381255
updateState({
12391256
currentStep: "cimd_prepare",

sdk/tests/oauth-conformance/runner.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,78 @@ describe("OAuthConformanceTest", () => {
133133
]);
134134
});
135135

136+
it("fails early when CIMD is requested but AS does not advertise client_id_metadata_document_supported", async () => {
137+
const serverUrl = "https://mcp.example.com/mcp";
138+
const resourceMetadataUrl =
139+
"https://mcp.example.com/.well-known/oauth-protected-resource/mcp";
140+
const authServerUrl = "https://auth.example.com";
141+
142+
const fetchFn: typeof fetch = jest.fn(async (input, init) => {
143+
const url = String(input);
144+
const headers = new Headers(init?.headers);
145+
146+
if (url === serverUrl && !headers.get("Authorization")) {
147+
return new Response(null, {
148+
status: 401,
149+
headers: {
150+
"WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl}"`,
151+
},
152+
});
153+
}
154+
155+
if (url === resourceMetadataUrl) {
156+
return jsonResponse({
157+
resource: serverUrl,
158+
authorization_servers: [authServerUrl],
159+
});
160+
}
161+
162+
if (url === `${authServerUrl}/.well-known/oauth-authorization-server`) {
163+
return jsonResponse({
164+
issuer: authServerUrl,
165+
authorization_endpoint: `${authServerUrl}/authorize`,
166+
token_endpoint: `${authServerUrl}/token`,
167+
registration_endpoint: `${authServerUrl}/register`,
168+
response_types_supported: ["code"],
169+
grant_types_supported: ["authorization_code", "refresh_token"],
170+
code_challenge_methods_supported: ["S256"],
171+
// NOTE: client_id_metadata_document_supported is NOT set
172+
});
173+
}
174+
175+
return jsonResponse({ error: "not found" }, 404);
176+
}) as typeof fetch;
177+
178+
const test = new OAuthConformanceTest(
179+
{
180+
serverUrl,
181+
protocolVersion: "2025-11-25",
182+
registrationStrategy: "cimd",
183+
auth: { mode: "headless" },
184+
fetchFn,
185+
},
186+
{
187+
completeHeadlessAuthorization: jest.fn(),
188+
},
189+
);
190+
191+
const result = await test.run();
192+
193+
expect(result.passed).toBe(false);
194+
const failedStep = result.steps.find((step) => step.status === "failed");
195+
expect(failedStep).toMatchObject({
196+
status: "failed",
197+
error: {
198+
message: expect.stringContaining("client_id_metadata_document_supported"),
199+
},
200+
});
201+
// Should NOT reach cimd_prepare or authorization_request
202+
const stepNames = result.steps.map((step) => step.step);
203+
expect(stepNames).toContain("received_authorization_server_metadata");
204+
expect(stepNames).not.toContain("cimd_prepare");
205+
expect(stepNames).not.toContain("authorization_request");
206+
});
207+
136208
it("captures multiple authorization server metadata attempts for the 2025-03-26 fallback flow", async () => {
137209
const serverUrl = "https://legacy.example.com/mcp";
138210
const rootMetadataUrl =

0 commit comments

Comments
 (0)