import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { oauthTokenGetContract, oauthTokenPostContract, } from '@/lib/api/contracts/oauth-connections' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' import { getCredential, getOAuthToken, refreshTokenIfNeeded, resolveOAuthAccountId, resolveServiceAccountToken, } from '@/app/api/auth/oauth/utils' export const dynamic = 'force-dynamic' const logger = createLogger('OAuthTokenAPI') const SALESFORCE_INSTANCE_URL_REGEX = /__sf_instance__:([^\s]+)/ /** * Get an access token for a specific credential * Supports both session-based authentication (for client-side requests) * and workflow-based authentication (for server-side requests) */ export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() logger.info(`[${requestId}] OAuth token API POST request received`) try { const parsed = await parseRequest( oauthTokenPostContract, request, {}, { validationErrorResponse: (error) => { logger.warn(`[${requestId}] Invalid token request`, { errors: error.issues }) return NextResponse.json( { error: getValidationErrorMessage(error, 'Validation failed') }, { status: 400 } ) }, } ) if (!parsed.success) return parsed.response const { credentialId, credentialAccountUserId, providerId, workflowId, scopes, impersonateEmail, } = parsed.data.body const callerUserId = parsed.data.query.userId if (credentialAccountUserId && providerId) { logger.info(`[${requestId}] Fetching token by credentialAccountUserId + providerId`, { credentialAccountUserId, providerId, }) const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!auth.success || auth.authType !== AuthType.SESSION || !auth.userId) { logger.warn(`[${requestId}] Unauthorized request for credentialAccountUserId path`, { success: auth.success, authType: auth.authType, }) return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) } if (auth.userId !== credentialAccountUserId) { logger.warn( `[${requestId}] User ${auth.userId} attempted to access credentials for ${credentialAccountUserId}` ) return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) } try { const accessToken = await getOAuthToken(credentialAccountUserId, providerId) if (!accessToken) { return NextResponse.json( { error: `No credential found for user ${credentialAccountUserId} and provider ${providerId}`, }, { status: 404 } ) } recordAudit({ actorId: auth.userId, action: AuditAction.CREDENTIAL_ACCESSED, resourceType: AuditResourceType.CREDENTIAL, resourceId: providerId, description: `Accessed OAuth credential for provider ${providerId}`, metadata: { provider: providerId, credentialType: 'oauth', credentialAccountUserId, }, request, }) captureServerEvent(auth.userId, 'credential_used', { credential_type: 'oauth', provider_id: providerId, }) return NextResponse.json({ accessToken }, { status: 200 }) } catch (error) { const message = getErrorMessage(error, 'Failed to get OAuth token') logger.warn(`[${requestId}] OAuth token error: ${message}`) return NextResponse.json({ error: message }, { status: 403 }) } } if (!credentialId) { return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 }) } const resolved = await resolveOAuthAccountId(credentialId) if (resolved?.credentialType === 'service_account' && resolved.credentialId) { const authz = await authorizeCredentialUse(request, { credentialId, workflowId: workflowId ?? undefined, requireWorkflowIdForInternal: false, callerUserId, }) if (!authz.ok) { return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) } const saActorId = authz.requesterUserId const saWorkspaceId = resolved.workspaceId ?? authz.workspaceId ?? null const emitServiceAccountAccess = () => { if (!saActorId) return recordAudit({ workspaceId: saWorkspaceId, actorId: saActorId, action: AuditAction.CREDENTIAL_ACCESSED, resourceType: AuditResourceType.CREDENTIAL, resourceId: resolved.credentialId ?? credentialId, description: `Accessed service account credential for provider ${resolved.providerId ?? 'unknown'}`, metadata: { provider: resolved.providerId, credentialType: 'service_account', }, request, }) captureServerEvent( saActorId, 'credential_used', { credential_type: 'service_account', provider_id: resolved.providerId ?? 'unknown', ...(saWorkspaceId ? { workspace_id: saWorkspaceId } : {}), }, saWorkspaceId ? { groups: { workspace: saWorkspaceId } } : undefined ) } try { const result = await resolveServiceAccountToken( resolved.credentialId, resolved.providerId, scopes ?? [], impersonateEmail ) emitServiceAccountAccess() return NextResponse.json( { accessToken: result.accessToken, cloudId: result.cloudId, domain: result.domain, }, { status: 200 } ) } catch (error) { logger.error(`[${requestId}] Service account token error:`, error) return NextResponse.json({ error: 'Failed to get service account token' }, { status: 401 }) } } const authz = await authorizeCredentialUse(request, { credentialId, workflowId: workflowId ?? undefined, requireWorkflowIdForInternal: false, callerUserId, }) if (!authz.ok || !authz.credentialOwnerUserId) { return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) } const resolvedCredentialId = authz.resolvedCredentialId || credentialId const credential = await getCredential( requestId, resolvedCredentialId, authz.credentialOwnerUserId ) if (!credential) { return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) } const oauthActorId = authz.requesterUserId const oauthWorkspaceId = authz.workspaceId ?? null try { const { accessToken } = await refreshTokenIfNeeded( requestId, credential, resolvedCredentialId ) if (oauthActorId) { recordAudit({ workspaceId: oauthWorkspaceId, actorId: oauthActorId, action: AuditAction.CREDENTIAL_ACCESSED, resourceType: AuditResourceType.CREDENTIAL, resourceId: resolvedCredentialId, description: `Accessed OAuth credential for provider ${credential.providerId}`, metadata: { provider: credential.providerId, credentialType: 'oauth', }, request, }) captureServerEvent( oauthActorId, 'credential_used', { credential_type: 'oauth', provider_id: credential.providerId, ...(oauthWorkspaceId ? { workspace_id: oauthWorkspaceId } : {}), }, oauthWorkspaceId ? { groups: { workspace: oauthWorkspaceId } } : undefined ) } let instanceUrl: string | undefined if (credential.providerId === 'salesforce' && credential.scope) { const instanceMatch = credential.scope.match(SALESFORCE_INSTANCE_URL_REGEX) if (instanceMatch) { instanceUrl = instanceMatch[1] } } return NextResponse.json( { accessToken, idToken: credential.idToken || undefined, ...(instanceUrl && { instanceUrl }), }, { status: 200 } ) } catch (error) { logger.error(`[${requestId}] Failed to refresh access token:`, error) return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 }) } } catch (error) { logger.error(`[${requestId}] Error getting access token`, error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } }) /** * Get the access token for a specific credential */ export const GET = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() try { const parsed = await parseRequest( oauthTokenGetContract, request, {}, { validationErrorResponse: (error) => { logger.warn(`[${requestId}] Invalid query parameters`, { errors: error.issues }) return NextResponse.json( { error: getValidationErrorMessage(error, 'Validation failed') }, { status: 400 } ) }, } ) if (!parsed.success) return parsed.response const { credentialId } = parsed.data.query const authz = await authorizeCredentialUse(request, { credentialId, requireWorkflowIdForInternal: false, }) if (!authz.ok || authz.authType !== AuthType.SESSION || !authz.credentialOwnerUserId) { return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) } const resolvedCredentialId = authz.resolvedCredentialId || credentialId const credential = await getCredential( requestId, resolvedCredentialId, authz.credentialOwnerUserId ) if (!credential) { return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) } if (!credential.accessToken) { logger.warn(`[${requestId}] No access token available for credential`) return NextResponse.json({ error: 'No access token available' }, { status: 400 }) } const actorId = authz.requesterUserId const workspaceId = authz.workspaceId ?? null try { const { accessToken } = await refreshTokenIfNeeded( requestId, credential, resolvedCredentialId ) if (actorId) { recordAudit({ workspaceId, actorId, action: AuditAction.CREDENTIAL_ACCESSED, resourceType: AuditResourceType.CREDENTIAL, resourceId: resolvedCredentialId, description: `Accessed OAuth credential for provider ${credential.providerId}`, metadata: { provider: credential.providerId, credentialType: 'oauth', }, request, }) captureServerEvent( actorId, 'credential_used', { credential_type: 'oauth', provider_id: credential.providerId, ...(workspaceId ? { workspace_id: workspaceId } : {}), }, workspaceId ? { groups: { workspace: workspaceId } } : undefined ) } // For Salesforce, extract instanceUrl from the scope field let instanceUrl: string | undefined if (credential.providerId === 'salesforce' && credential.scope) { const instanceMatch = credential.scope.match(SALESFORCE_INSTANCE_URL_REGEX) if (instanceMatch) { instanceUrl = instanceMatch[1] } } return NextResponse.json( { accessToken, idToken: credential.idToken || undefined, ...(instanceUrl && { instanceUrl }), }, { status: 200 } ) } catch (_error) { return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 }) } } catch (error) { logger.error(`[${requestId}] Error fetching access token`, error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } })