History

Merge pull request #689 from RhysSullivan/fix/oauth-dcr-scope-registration

fix(oauth): declare requested scopes in DCR body

Rhys Sullivan committed 6 months ago

Commit b87fecc2 parents 64c2eb7 b4041082 files · +60 −1Browse files

Unified · Split

2 files
  1. packages/core/sdk/src/oauth-discovery.test.ts +57 −0
  2. packages/core/sdk/src/oauth-discovery.ts +3 −1
packages/core/sdk/src/oauth-discovery.test.ts+57−0
@@ -14,6 +14,7 @@ type Handler = (url: string, init: RequestInit) => Response | Promise<Response>; · 13 unmodified lines
1414 const DcrRequestBody = Schema.Struct({
1515 redirect_uris: Schema.Array(Schema.String),
1616 token_endpoint_auth_method: Schema.String,
17+ scope: Schema.optional(Schema.String),
1718 });
1819 const decodeDcrRequestBody = Schema.decodeUnknownSync(Schema.fromJsonString(DcrRequestBody));
1920
@@ -349,6 +350,62 @@ describe("beginDynamicAuthorization", () => { · 329 unmodified lines
349350 expect(result.state.resourceMetadata?.resource).toBe("https://backboard.railway.com");
350351 });
351352
353+ it("declares requested scopes in the DCR body so Auth0-style servers don't reject /authorize", async () => {
354+ const { calls } = installFetchRouter([
355+ {
356+ match: (u) => u === "https://mcp.grata.com/.well-known/oauth-protected-resource",
357+ handle: () => new Response(null, { status: 404 }),
358+ },
359+ {
360+ match: (u) => u === "https://mcp.grata.com/.well-known/oauth-authorization-server",
361+ handle: () =>
362+ new Response(
363+ JSON.stringify({
364+ issuer: "https://mcp.grata.com/",
365+ authorization_endpoint: "https://mcp.grata.com/authorize",
366+ token_endpoint: "https://mcp.grata.com/token",
367+ registration_endpoint: "https://mcp.grata.com/register",
368+ scopes_supported: ["openid", "profile", "email", "offline_access"],
369+ response_types_supported: ["code"],
370+ grant_types_supported: ["authorization_code", "refresh_token"],
371+ code_challenge_methods_supported: ["S256"],
372+ }),
373+ { status: 200, headers: { "content-type": "application/json" } },
374+ ),
375+ },
376+ {
377+ match: (u) => u === "https://mcp.grata.com/register",
378+ handle: () =>
379+ new Response(
380+ JSON.stringify({
381+ client_id: "grata-client-id",
382+ redirect_uris: ["https://app.example/cb"],
383+ token_endpoint_auth_method: "none",
384+ scope: "openid profile email offline_access",
385+ }),
386+ { status: 201, headers: { "content-type": "application/json" } },
387+ ),
388+ },
389+ ]);
390+
391+ const result = await Effect.runPromise(
392+ beginDynamicAuthorization({
393+ endpoint: "https://mcp.grata.com/",
394+ redirectUrl: "https://app.example/cb",
395+ state: "state-grata",
396+ }),
397+ );
398+
399+ const dcrCall = calls.find((c) => c.url === "https://mcp.grata.com/register");
400+ expect(dcrCall).toBeDefined();
401+ const body = decodeDcrRequestBody(String(dcrCall!.init.body));
402+ expect(body.scope).toBe("openid profile email offline_access");
403+
404+ const authUrl = new URL(result.authorizationUrl);
405+ expect(authUrl.searchParams.get("scope")).toBe("openid profile email offline_access");
406+ expect(authUrl.searchParams.get("client_id")).toBe("grata-client-id");
407+ });
408+
352409 it("skips discovery + DCR when previousState is provided", async () => {
353410 const { calls } = installFetchRouter([]);
354411
packages/core/sdk/src/oauth-discovery.ts+3−1
@@ -640,11 +640,14 @@ export const beginDynamicAuthorization = ( · 639 unmodified lines
640640 });
641641 }
642642
643+ const scopes = input.scopes ?? authServer.metadata.scopes_supported ?? [];
644+
643645 const baseClientMetadata: DynamicClientMetadata = {
644646 grant_types: ["authorization_code", "refresh_token"],
645647 response_types: ["code"],
646648 token_endpoint_auth_method: "none",
647649 client_name: "Executor",
650+ ...(scopes.length > 0 ? { scope: scopes.join(" ") } : {}),
648651 ...(input.clientMetadata ?? {}),
649652 redirect_uris: input.clientMetadata?.redirect_uris ?? [input.redirectUrl],
650653 };
@@ -669,7 +672,6 @@ export const beginDynamicAuthorization = ( · 18 unmodified lines
669672
670673 const codeVerifier = createPkceCodeVerifier();
671674 const codeChallenge = yield* Effect.promise(() => createPkceCodeChallenge(codeVerifier));
672− const scopes = input.scopes ?? authServer.metadata.scopes_supported ?? [];
673675
674676 const authorizationUrl = buildAuthorizationUrl({
675677 authorizationUrl: authServer.metadata.authorization_endpoint,