diff --git a/apps/sim/app/api/auth/oauth/token/route.ts b/apps/sim/app/api/auth/oauth/token/route.ts index 788d1f7d7d..f6416ef010 100644 --- a/apps/sim/app/api/auth/oauth/token/route.ts +++ b/apps/sim/app/api/auth/oauth/token/route.ts @@ -84,14 +84,12 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) } - // Check if the access token is valid if (!credential.accessToken) { logger.warn(`[${requestId}] No access token available for credential`) return NextResponse.json({ error: 'No access token available' }, { status: 400 }) } try { - // Refresh the token if needed const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId) return NextResponse.json({ accessToken }, { status: 200 }) } catch (_error) { diff --git a/apps/sim/app/api/auth/oauth/utils.ts b/apps/sim/app/api/auth/oauth/utils.ts index b9f31c2334..666e20a094 100644 --- a/apps/sim/app/api/auth/oauth/utils.ts +++ b/apps/sim/app/api/auth/oauth/utils.ts @@ -1,4 +1,4 @@ -import { and, eq } from 'drizzle-orm' +import { and, desc, eq } from 'drizzle-orm' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console/logger' import { refreshOAuthToken } from '@/lib/oauth/oauth' @@ -70,7 +70,8 @@ export async function getOAuthToken(userId: string, providerId: string): Promise }) .from(account) .where(and(eq(account.userId, userId), eq(account.providerId, providerId))) - .orderBy(account.createdAt) + // Always use the most recently updated credential for this provider + .orderBy(desc(account.updatedAt)) .limit(1) if (connections.length === 0) { @@ -80,19 +81,13 @@ export async function getOAuthToken(userId: string, providerId: string): Promise const credential = connections[0] - // Check if we have a valid access token - if (!credential.accessToken) { - logger.warn(`Access token is null for user ${userId}, provider ${providerId}`) - return null - } - - // Check if the token is expired and needs refreshing + // Determine whether we should refresh: missing token OR expired token const now = new Date() const tokenExpiry = credential.accessTokenExpiresAt - // Only refresh if we have an expiration time AND it's expired AND we have a refresh token - const needsRefresh = tokenExpiry && tokenExpiry < now && !!credential.refreshToken + const shouldAttemptRefresh = + !!credential.refreshToken && (!credential.accessToken || (tokenExpiry && tokenExpiry < now)) - if (needsRefresh) { + if (shouldAttemptRefresh) { logger.info( `Access token expired for user ${userId}, provider ${providerId}. Attempting to refresh.` ) @@ -141,6 +136,13 @@ export async function getOAuthToken(userId: string, providerId: string): Promise } } + if (!credential.accessToken) { + logger.warn( + `Access token is null and no refresh attempted or available for user ${userId}, provider ${providerId}` + ) + return null + } + logger.info(`Found valid OAuth token for user ${userId}, provider ${providerId}`) return credential.accessToken } @@ -164,19 +166,21 @@ export async function refreshAccessTokenIfNeeded( return null } - // Check if we need to refresh the token + // Decide if we should refresh: token missing OR expired const expiresAt = credential.accessTokenExpiresAt const now = new Date() - // Only refresh if we have an expiration time AND it's expired - // If no expiration time is set (newly created credentials), assume token is valid - const needsRefresh = expiresAt && expiresAt <= now + const shouldRefresh = + !!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now)) const accessToken = credential.accessToken - if (needsRefresh && credential.refreshToken) { + if (shouldRefresh) { logger.info(`[${requestId}] Token expired, attempting to refresh for credential`) try { - const refreshedToken = await refreshOAuthToken(credential.providerId, credential.refreshToken) + const refreshedToken = await refreshOAuthToken( + credential.providerId, + credential.refreshToken! + ) if (!refreshedToken) { logger.error(`[${requestId}] Failed to refresh token for credential: ${credentialId}`, { @@ -217,6 +221,7 @@ export async function refreshAccessTokenIfNeeded( return null } } else if (!accessToken) { + // We have no access token and either no refresh token or not eligible to refresh logger.error(`[${requestId}] Missing access token for credential`) return null } @@ -233,21 +238,20 @@ export async function refreshTokenIfNeeded( credential: any, credentialId: string ): Promise<{ accessToken: string; refreshed: boolean }> { - // Check if we need to refresh the token + // Decide if we should refresh: token missing OR expired const expiresAt = credential.accessTokenExpiresAt const now = new Date() - // Only refresh if we have an expiration time AND it's expired - // If no expiration time is set (newly created credentials), assume token is valid - const needsRefresh = expiresAt && expiresAt <= now + const shouldRefresh = + !!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now)) - // If token is still valid, return it directly - if (!needsRefresh || !credential.refreshToken) { + // If token appears valid and present, return it directly + if (!shouldRefresh) { logger.info(`[${requestId}] Access token is valid`) return { accessToken: credential.accessToken, refreshed: false } } try { - const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken) + const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken!) if (!refreshResult) { logger.error(`[${requestId}] Failed to refresh token for credential`) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx index c11b63bfcf..9648f5ac57 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx @@ -237,10 +237,11 @@ export function GoogleDrivePicker({ setIsLoading(true) try { - const url = new URL('/api/auth/oauth/token', window.location.origin) - url.searchParams.set('credentialId', effectiveCredentialId) - // include workflowId if available via global registry (server adds session owner otherwise) - const response = await fetch(url.toString()) + const response = await fetch('/api/auth/oauth/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ credentialId: effectiveCredentialId, workflowId }), + }) if (!response.ok) { throw new Error(`Failed to fetch access token: ${response.status}`)