From 6e09f4dba1e31db180a36e0571368300fb418209 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Sat, 13 Jun 2026 01:06:17 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=90=9B=20cli=20auth=20for=20the=20?= =?UTF-8?q?last=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/auth/src/oauth/leafOAuth.ts | 11 ++++++-- .../cli/handlers/handleCreateOAuthApiKeys.ts | 18 +++++++++++-- .../unit/auth/registerMcpOAuthClient.test.ts | 27 +++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/packages/auth/src/oauth/leafOAuth.ts b/packages/auth/src/oauth/leafOAuth.ts index 0b3d9dda2..1bd92ebfc 100644 --- a/packages/auth/src/oauth/leafOAuth.ts +++ b/packages/auth/src/oauth/leafOAuth.ts @@ -1,5 +1,8 @@ import { LEAF_OAUTH_SCOPES } from "@autumn/shared/leafOAuthScopes"; -import { OPENID_SCOPES } from "@autumn/shared/scopeDefinitions"; +import { + LEGACY_SCOPE_ALIASES, + OPENID_SCOPES, +} from "@autumn/shared/scopeDefinitions"; const leafScopeSet = new Set(LEAF_OAUTH_SCOPES); const oauthPassthroughScopeSet = new Set(["offline_access"]); @@ -11,8 +14,12 @@ export const getDefaultOAuthScopes = (requestedScopes?: string[] | null) => { ? requestedScopes : [...LEAF_OAUTH_SCOPES, ...oauthPassthroughScopeSet]; + // Issued scopes must echo the client's request verbatim (better-auth + // rejects rewrites), so legacy CRUDL aliases are used for filtering only. return [...new Set(requested)].filter( - (scope) => leafScopeSet.has(scope) || oauthPassthroughScopeSet.has(scope), + (scope) => + leafScopeSet.has(LEGACY_SCOPE_ALIASES[scope] ?? scope) || + oauthPassthroughScopeSet.has(scope), ); }; diff --git a/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts b/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts index 08ab45a13..b6836a175 100644 --- a/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts +++ b/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts @@ -1,4 +1,10 @@ -import { AppEnv, ErrCode, RecaseError, Scopes } from "@autumn/shared"; +import { + AppEnv, + ErrCode, + LEGACY_SCOPE_ALIASES, + RecaseError, + Scopes, +} from "@autumn/shared"; import { createRoute } from "@/honoMiddlewares/routeHandler.js"; import { isMcpOAuthClientId } from "@/internal/auth/oauth/mcpOAuthScopes.js"; import { @@ -84,7 +90,15 @@ export const handleCreateOAuthApiKeys = createRoute({ statusCode: 401, }); } - const apiKeyScopes = requestedScopes ?? tokenRecord.scopes; + // Tokens may carry legacy CRUDL scopes (old CLI); store modern R/W on keys. + const apiKeyScopes = [ + ...new Set( + (requestedScopes ?? tokenRecord.scopes).map( + (scope) => LEGACY_SCOPE_ALIASES[scope] ?? scope, + ), + ), + ]; + if (await isMcpOAuthClientId({ clientId, ctx })) { throw new RecaseError({ message: "MCP OAuth clients must use OAuth access tokens directly", diff --git a/server/tests/unit/auth/registerMcpOAuthClient.test.ts b/server/tests/unit/auth/registerMcpOAuthClient.test.ts index d5ca9d064..b437a53cd 100644 --- a/server/tests/unit/auth/registerMcpOAuthClient.test.ts +++ b/server/tests/unit/auth/registerMcpOAuthClient.test.ts @@ -64,6 +64,33 @@ describe("getRequestedScopesForMcpClient", () => { ]); }); + test("keeps legacy CRUDL scopes (old CLI) whose alias is leaf-allowed, verbatim", () => { + expect( + getDefaultOAuthScopes([ + "customers:create", + "customers:read", + "customers:list", + "customers:update", + "customers:delete", + "features:create", + "features:read", + "plans:update", + "apiKeys:create", + "organisation:read", + ]), + ).toEqual([ + "customers:create", + "customers:read", + "customers:list", + "customers:update", + "customers:delete", + "features:create", + "features:read", + "plans:update", + "organisation:read", + ]); + }); + test("strips OAuth protocol scopes from resource scopes", () => { expect( getOAuthResourceScopes([