diff --git a/.changeset/quiet-figma-oauth-scopes.md b/.changeset/quiet-figma-oauth-scopes.md new file mode 100644 index 000000000..71f9d6e56 --- /dev/null +++ b/.changeset/quiet-figma-oauth-scopes.md @@ -0,0 +1,5 @@ +--- +"@executor-js/plugin-openapi": patch +--- + +Do not add unadvertised OpenID Connect identity scopes to OAuth authorization requests derived from OpenAPI specifications. diff --git a/packages/plugins/openapi/src/sdk/derive-auth.test.ts b/packages/plugins/openapi/src/sdk/derive-auth.test.ts new file mode 100644 index 000000000..ba22b1eac --- /dev/null +++ b/packages/plugins/openapi/src/sdk/derive-auth.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { resolvedOAuthScopes } from "./derive-auth"; + +describe("resolvedOAuthScopes", () => { + it("does not synthesize OIDC scopes for a plain OAuth provider", () => { + expect(resolvedOAuthScopes(["current_user:read", "files:read"], "auto")).toEqual([ + "current_user:read", + "files:read", + ]); + }); + + it("preserves advertised OIDC scopes in auto mode", () => { + expect(resolvedOAuthScopes(["read", "openid", "profile"], "auto")).toEqual([ + "read", + "openid", + "profile", + ]); + }); + + it("merges explicitly configured identity scopes", () => { + expect(resolvedOAuthScopes(["read", "email"], ["openid", "email"])).toEqual([ + "read", + "email", + "openid", + ]); + }); +}); diff --git a/packages/plugins/openapi/src/sdk/derive-auth.ts b/packages/plugins/openapi/src/sdk/derive-auth.ts index 553c8e8ca..ac7629572 100644 --- a/packages/plugins/openapi/src/sdk/derive-auth.ts +++ b/packages/plugins/openapi/src/sdk/derive-auth.ts @@ -41,9 +41,12 @@ const standardOidcIdentityScopes = ["openid", "email", "profile"] as const; const identityScopesForPreset = ( identityScopes: OAuth2Preset["identityScopes"], + advertisedScopes: ReadonlySet, ): readonly string[] => { if (identityScopes === false) return []; - return identityScopes === "auto" ? standardOidcIdentityScopes : identityScopes; + return identityScopes === "auto" + ? standardOidcIdentityScopes.filter((scope) => advertisedScopes.has(scope)) + : identityScopes; }; export const resolvedOAuthScopes = ( @@ -51,7 +54,7 @@ export const resolvedOAuthScopes = ( identityScopes: OAuth2Preset["identityScopes"], ): string[] => { const merged = new Set(apiScopes); - for (const scope of identityScopesForPreset(identityScopes)) merged.add(scope); + for (const scope of identityScopesForPreset(identityScopes, merged)) merged.add(scope); return [...merged]; }; @@ -141,9 +144,10 @@ const oauthTemplateFromPreset = ( // --------------------------------------------------------------------------- // All spec-detected auth methods → the union of stored `Authentication` // templates. Header presets become apiKey templates; each oauth2 preset becomes -// an oauth template (with its declared API scopes plus, for auth-code flows, -// the standard identity scopes). Slugs stay deterministic per method so the -// stored template is stable across previews of the same spec. +// an oauth template with its declared scopes. Auto identity detection only +// preserves standard OIDC scopes the provider advertised; it never invents +// scopes unsupported by a plain OAuth provider. Slugs stay deterministic per +// method so the stored template is stable across previews of the same spec. // --------------------------------------------------------------------------- export const detectedAuthenticationTemplates = (