From b3d5ce8343a10d58aeace42c44c0216dffc854c3 Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 17 Apr 2026 18:21:01 -0400 Subject: [PATCH] fix(api): distinguish 401 (auth failure) from 403 (permissions) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backport of v1.x #1145 to main. Previously main returned the same "access denied / lacks required permissions" message for both 401 and 403, which was misleading when the user's real problem was a revoked/expired API token — it sent them chasing permissions instead of re-authenticating. Changes: * `utils/socket/api.mts` `getErrorMessageForHttpStatusCode` — split 401 and 403 into separate branches with distinct, actionable guidance (re-auth vs. check permissions). * `commands/scan/perform-reachability-analysis.mts` — when the enterprise-plan check fails with a 401, return "Authentication failed" + token-focused guidance instead of the generic "Unable to verify plan permissions" message. * Updated the matching unit test assertion for the 401 branch. Skipped from v1.x's version: the extra `logger.fail` in `fetch-organization-list.mts`. Main's `handleApiCall` already wires the cause into the returned CResult; a caller-level log would double-log in many paths. --- .../commands/scan/perform-reachability-analysis.mts | 10 ++++++++++ packages/cli/src/utils/socket/api.mts | 11 ++++++++++- packages/cli/test/unit/utils/socket/api.test.mts | 5 ++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/scan/perform-reachability-analysis.mts b/packages/cli/src/commands/scan/perform-reachability-analysis.mts index 6afc868d2..67399da3e 100644 --- a/packages/cli/src/commands/scan/perform-reachability-analysis.mts +++ b/packages/cli/src/commands/scan/perform-reachability-analysis.mts @@ -2,6 +2,7 @@ import path from 'node:path' import { getDefaultLogger } from '@socketsecurity/lib/logger' +import { HTTP_STATUS_UNAUTHORIZED } from '../../constants/http.mts' import { DOT_SOCKET_DOT_FACTS_JSON } from '../../constants/paths.mts' import { SOCKET_DEFAULT_BRANCH, @@ -81,6 +82,15 @@ export async function performReachabilityAnalysis( // Check if user has enterprise plan for reachability analysis. const orgsCResult = await fetchOrganization() if (!orgsCResult.ok) { + const httpCode = (orgsCResult as { data?: { code?: number } }).data?.code + if (httpCode === HTTP_STATUS_UNAUTHORIZED) { + return { + ok: false, + message: 'Authentication failed', + cause: + 'Your API token appears to be invalid, expired, or revoked. Please check your token and try again.', + } + } return { ok: false, message: 'Unable to verify plan permissions', diff --git a/packages/cli/src/utils/socket/api.mts b/packages/cli/src/utils/socket/api.mts index 858a7c6ca..7b4606dc9 100644 --- a/packages/cli/src/utils/socket/api.mts +++ b/packages/cli/src/utils/socket/api.mts @@ -163,7 +163,16 @@ export async function getErrorMessageForHttpStatusCode(code: number) { '💡 Try: Check your command syntax and parameter values.' ) } - if (code === HTTP_STATUS_FORBIDDEN || code === HTTP_STATUS_UNAUTHORIZED) { + if (code === HTTP_STATUS_UNAUTHORIZED) { + return ( + '❌ Authentication failed: Your Socket API token appears to be invalid, expired, or revoked.\n' + + '💡 Try:\n' + + ' • Run `socket whoami` to verify your current token\n' + + ' • Run `socket login` to re-authenticate\n' + + ` • Manage tokens at ${SOCKET_SETTINGS_API_TOKENS_URL}` + ) + } + if (code === HTTP_STATUS_FORBIDDEN) { return ( '❌ Access denied: Your API token lacks required permissions or organization access.\n' + '💡 Try:\n' + diff --git a/packages/cli/test/unit/utils/socket/api.test.mts b/packages/cli/test/unit/utils/socket/api.test.mts index 297a88a76..670782b4e 100644 --- a/packages/cli/test/unit/utils/socket/api.test.mts +++ b/packages/cli/test/unit/utils/socket/api.test.mts @@ -173,7 +173,10 @@ describe('api utilities', () => { it('returns message for 401 Unauthorized', async () => { const result = await getErrorMessageForHttpStatusCode(401) - expect(result).toContain('permissions') + // 401 is now distinct from 403: it's an auth/token problem, not + // a permissions problem. Callers get actionable "re-auth" guidance. + expect(result).toContain('Authentication failed') + expect(result).toContain('token') }) it('returns message for 403 Forbidden', async () => {