From 012afd1c33cd1a2a113b6f0c0b8309f05680edd6 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 16 Sep 2026 15:35:15 -0700 Subject: [PATCH] chore(auth): diagnose unexpected managed OAuth callback failures --- .../credential-groups/oauth-callback.test.ts | 86 ++++++++++++++++++- .../api/credential-groups/oauth-callback.ts | 52 ++++++++--- 2 files changed, 126 insertions(+), 12 deletions(-) diff --git a/apps/sim/app/api/credential-groups/oauth-callback.test.ts b/apps/sim/app/api/credential-groups/oauth-callback.test.ts index ba64bcf7698..c132e570485 100644 --- a/apps/sim/app/api/credential-groups/oauth-callback.test.ts +++ b/apps/sim/app/api/credential-groups/oauth-callback.test.ts @@ -1,4 +1,5 @@ /** @vitest-environment node */ +import { sha256Hex } from '@sim/security/hash' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' import { OrchestrationError } from '@/lib/core/orchestration/types' @@ -11,6 +12,7 @@ const mocks = vi.hoisted(() => ({ consumeAttempt: vi.fn(), logError: vi.fn(), completeSetupOAuth: vi.fn(), + authenticateSession: vi.fn(), })) vi.mock('@sim/logger', () => ({ @@ -21,7 +23,7 @@ vi.mock('@/lib/knowledge/application/github-setup', () => ({ })) vi.mock('@/lib/api/server/routes', () => ({ internalSessionAuth: { - authenticate: async () => ({ kind: 'session', userId: 'admin', sessionId: 'browser' }), + authenticate: mocks.authenticateSession, }, })) vi.mock('@/lib/core/utils/urls', () => ({ getBaseUrl: () => 'https://sim.test' })) @@ -127,6 +129,55 @@ describe('GitHub managed OAuth failure presentation', () => { provider: 'github-repositories', failure: 'failed', errorClass: 'unexpected', + stage: 'enrollment_completion', + errorType: 'Error', + fingerprint: sha256Hex('member@example.com ghu_token').slice(0, 12), + }) + }) + + it('identifies a wrapped database failure without logging SQL, parameters, or provider data', async () => { + const cause = Object.assign(new Error('duplicate key for member@example.com'), { + name: 'PostgresError', + code: '23505', + detail: 'ghu_private_token', + }) + mocks.consumeAttempt.mockResolvedValue(attempt) + mocks.completeOAuth.mockRejectedValueOnce( + new Error('Failed query: INSERT INTO credential\nparams: ghu_private_token', { cause }) + ) + const response = await completeCallback() + expect(response.headers.get('location')).toContain('oauth=failed') + expect(mocks.logError).toHaveBeenCalledExactlyOnceWith('Managed OAuth authorization failed', { + provider: 'github-repositories', + failure: 'failed', + errorClass: 'unexpected', + stage: 'enrollment_completion', + errorType: 'PostgresError', + databaseCode: '23505', + fingerprint: sha256Hex(cause.message).slice(0, 12), + }) + const logged = JSON.stringify(mocks.logError.mock.calls) + expect(logged).not.toContain('member@example.com') + expect(logged).not.toContain('ghu_private_token') + expect(logged).not.toContain('INSERT') + }) + + it('does not log arbitrary error names or codes as diagnostic metadata', async () => { + mocks.consumeAttempt.mockResolvedValue(attempt) + mocks.completeOAuth.mockRejectedValueOnce( + Object.assign(new Error('private provider response'), { + name: 'ghu_private_token', + code: 'client_secret=private', + }) + ) + await completeCallback() + expect(mocks.logError).toHaveBeenCalledExactlyOnceWith('Managed OAuth authorization failed', { + provider: 'github-repositories', + failure: 'failed', + errorClass: 'unexpected', + stage: 'enrollment_completion', + errorType: 'UnknownError', + fingerprint: sha256Hex('private provider response').slice(0, 12), }) }) @@ -149,7 +200,40 @@ describe('GitHub managed OAuth failure presentation', () => { describe('GitHub installation setup OAuth return target', () => { beforeEach(() => { vi.clearAllMocks() + mocks.authenticateSession.mockResolvedValue({ + kind: 'session', + userId: 'admin', + sessionId: 'browser', + }) }) + + it.each(['session_authentication', 'setup_completion'])( + 'identifies an unexpected failure during %s without exposing its message', + async (stage) => { + mocks.consumeAttempt.mockResolvedValue({ + ...attempt, + returnTo: 'github-installation', + organizationId: 'organization', + completionId, + }) + const error = new TypeError('private callback data') + if (stage === 'session_authentication') { + mocks.authenticateSession.mockRejectedValueOnce(error) + } else { + mocks.completeSetupOAuth.mockRejectedValueOnce(error) + } + const response = await completeCallback() + expect(response.headers.get('location')).toContain('oauth=failed') + expect(mocks.logError).toHaveBeenCalledExactlyOnceWith('Managed OAuth authorization failed', { + provider: 'github-repositories', + failure: 'failed', + errorClass: 'unexpected', + stage, + errorType: 'TypeError', + fingerprint: sha256Hex(error.message).slice(0, 12), + }) + } + ) it('resumes only the server-owned setup after the guarded OAuth completion', async () => { mocks.consumeAttempt.mockResolvedValue({ ...attempt, diff --git a/apps/sim/app/api/credential-groups/oauth-callback.ts b/apps/sim/app/api/credential-groups/oauth-callback.ts index 5808df6c42d..0d8b0240ac2 100644 --- a/apps/sim/app/api/credential-groups/oauth-callback.ts +++ b/apps/sim/app/api/credential-groups/oauth-callback.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' -import { getErrorMessage } from '@sim/utils/errors' +import { sha256Hex } from '@sim/security/hash' +import { describeError, getErrorMessage } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import type { CredentialGroupOAuthCallbackQuery } from '@/lib/api/contracts/credential-groups' import { internalSessionAuth } from '@/lib/api/server/routes' @@ -23,6 +24,18 @@ import { } from '@/app/api/credential-groups/enrollment-redirect' const logger = createLogger('CredentialGroupOAuthCallbackAPI') +const DIAGNOSTIC_ERROR_TYPES = new Set([ + 'Error', + 'TypeError', + 'ReferenceError', + 'SyntaxError', + 'RangeError', + 'ZodError', + 'PostgresError', + 'DrizzleQueryError', + 'InternalUnauthenticatedError', + 'ManagedOAuthCredentialError', +]) interface HandleCredentialGroupOAuthCallbackParams { request: NextRequest @@ -87,13 +100,17 @@ export async function handleCredentialGroupOAuthCallback({ return failureRedirect('failed') } + let stage = 'session_authentication' try { if (installationSetup) { const principal = await internalSessionAuth.authenticate() + stage = 'setup_completion' await completeGitHubSetupReaderOAuth.execute({ principal, input: { attempt, code }, request }) return setupRedirect() } + stage = 'enrollment_authentication' const principal = await credentialGroupOAuthAttemptPrincipal(attempt) + stage = 'enrollment_completion' await completePublicCredentialGroupOAuth.execute({ principal, input: { attempt, code }, @@ -137,19 +154,32 @@ export async function handleCredentialGroupOAuthCallback({ } } const applicationError = asOrchestrationError(error) + const errorClass = + error instanceof CredentialGroupInvitationUnavailableError + ? 'invitation_unavailable' + : error instanceof CredentialGroupOAuthError + ? 'credential_group_oauth' + : error instanceof CredentialGroupProviderConfigurationError + ? 'provider_configuration' + : applicationError + ? 'application' + : 'unexpected' + const unexpectedError = errorClass === 'unexpected' ? describeError(error) : undefined logger.error('Managed OAuth authorization failed', { provider, failure: status, - errorClass: - error instanceof CredentialGroupInvitationUnavailableError - ? 'invitation_unavailable' - : error instanceof CredentialGroupOAuthError - ? 'credential_group_oauth' - : error instanceof CredentialGroupProviderConfigurationError - ? 'provider_configuration' - : applicationError - ? 'application' - : 'unexpected', + errorClass, + /** Provider errors and SQL parameters may contain credentials; retain only bounded diagnostics. */ + ...(unexpectedError && { + stage, + errorType: DIAGNOSTIC_ERROR_TYPES.has(unexpectedError.name) + ? unexpectedError.name + : 'UnknownError', + fingerprint: sha256Hex(unexpectedError.message).slice(0, 12), + ...(unexpectedError.code && /^[0-9A-Z]{5}$/.test(unexpectedError.code) + ? { databaseCode: unexpectedError.code } + : {}), + }), ...(error instanceof CredentialGroupOAuthError && { statusCode: error.statusCode }), ...(error instanceof CredentialGroupProviderConfigurationError && { statusCode: 503 }), ...(applicationError && {