Skip to content

Commit 0c0b6bf

Browse files
committed
fix(oauth): gdrive picker race condition, token route cleanup
1 parent b39bdfd commit 0c0b6bf

3 files changed

Lines changed: 38 additions & 36 deletions

File tree

apps/sim/app/api/auth/oauth/token/route.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,12 @@ export async function GET(request: NextRequest) {
8484
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
8585
}
8686

87-
// Check if the access token is valid
88-
if (!credential.accessToken) {
89-
logger.warn(`[${requestId}] No access token available for credential`)
90-
return NextResponse.json({ error: 'No access token available' }, { status: 400 })
91-
}
92-
9387
try {
94-
// Refresh the token if needed
9588
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
89+
if (!accessToken) {
90+
logger.warn(`[${requestId}] No access token could be obtained for credential`)
91+
return NextResponse.json({ error: 'No access token available' }, { status: 400 })
92+
}
9693
return NextResponse.json({ accessToken }, { status: 200 })
9794
} catch (_error) {
9895
return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 })

apps/sim/app/api/auth/oauth/utils.ts

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { and, eq } from 'drizzle-orm'
1+
import { and, desc, eq } from 'drizzle-orm'
22
import { getSession } from '@/lib/auth'
33
import { createLogger } from '@/lib/logs/console/logger'
44
import { refreshOAuthToken } from '@/lib/oauth/oauth'
@@ -70,7 +70,8 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
7070
})
7171
.from(account)
7272
.where(and(eq(account.userId, userId), eq(account.providerId, providerId)))
73-
.orderBy(account.createdAt)
73+
// Always use the most recently updated credential for this provider
74+
.orderBy(desc(account.updatedAt))
7475
.limit(1)
7576

7677
if (connections.length === 0) {
@@ -80,19 +81,13 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
8081

8182
const credential = connections[0]
8283

83-
// Check if we have a valid access token
84-
if (!credential.accessToken) {
85-
logger.warn(`Access token is null for user ${userId}, provider ${providerId}`)
86-
return null
87-
}
88-
89-
// Check if the token is expired and needs refreshing
84+
// Determine whether we should refresh: missing token OR expired token
9085
const now = new Date()
9186
const tokenExpiry = credential.accessTokenExpiresAt
92-
// Only refresh if we have an expiration time AND it's expired AND we have a refresh token
93-
const needsRefresh = tokenExpiry && tokenExpiry < now && !!credential.refreshToken
87+
const shouldAttemptRefresh =
88+
!!credential.refreshToken && (!credential.accessToken || (tokenExpiry && tokenExpiry < now))
9489

95-
if (needsRefresh) {
90+
if (shouldAttemptRefresh) {
9691
logger.info(
9792
`Access token expired for user ${userId}, provider ${providerId}. Attempting to refresh.`
9893
)
@@ -141,6 +136,13 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
141136
}
142137
}
143138

139+
if (!credential.accessToken) {
140+
logger.warn(
141+
`Access token is null and no refresh attempted or available for user ${userId}, provider ${providerId}`
142+
)
143+
return null
144+
}
145+
144146
logger.info(`Found valid OAuth token for user ${userId}, provider ${providerId}`)
145147
return credential.accessToken
146148
}
@@ -164,19 +166,21 @@ export async function refreshAccessTokenIfNeeded(
164166
return null
165167
}
166168

167-
// Check if we need to refresh the token
169+
// Decide if we should refresh: token missing OR expired
168170
const expiresAt = credential.accessTokenExpiresAt
169171
const now = new Date()
170-
// Only refresh if we have an expiration time AND it's expired
171-
// If no expiration time is set (newly created credentials), assume token is valid
172-
const needsRefresh = expiresAt && expiresAt <= now
172+
const shouldRefresh =
173+
!!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now))
173174

174175
const accessToken = credential.accessToken
175176

176-
if (needsRefresh && credential.refreshToken) {
177+
if (shouldRefresh) {
177178
logger.info(`[${requestId}] Token expired, attempting to refresh for credential`)
178179
try {
179-
const refreshedToken = await refreshOAuthToken(credential.providerId, credential.refreshToken)
180+
const refreshedToken = await refreshOAuthToken(
181+
credential.providerId,
182+
credential.refreshToken!
183+
)
180184

181185
if (!refreshedToken) {
182186
logger.error(`[${requestId}] Failed to refresh token for credential: ${credentialId}`, {
@@ -217,6 +221,7 @@ export async function refreshAccessTokenIfNeeded(
217221
return null
218222
}
219223
} else if (!accessToken) {
224+
// We have no access token and either no refresh token or not eligible to refresh
220225
logger.error(`[${requestId}] Missing access token for credential`)
221226
return null
222227
}
@@ -233,21 +238,20 @@ export async function refreshTokenIfNeeded(
233238
credential: any,
234239
credentialId: string
235240
): Promise<{ accessToken: string; refreshed: boolean }> {
236-
// Check if we need to refresh the token
241+
// Decide if we should refresh: token missing OR expired
237242
const expiresAt = credential.accessTokenExpiresAt
238243
const now = new Date()
239-
// Only refresh if we have an expiration time AND it's expired
240-
// If no expiration time is set (newly created credentials), assume token is valid
241-
const needsRefresh = expiresAt && expiresAt <= now
244+
const shouldRefresh =
245+
!!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now))
242246

243-
// If token is still valid, return it directly
244-
if (!needsRefresh || !credential.refreshToken) {
247+
// If token appears valid and present, return it directly
248+
if (!shouldRefresh) {
245249
logger.info(`[${requestId}] Access token is valid`)
246250
return { accessToken: credential.accessToken, refreshed: false }
247251
}
248252

249253
try {
250-
const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken)
254+
const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken!)
251255

252256
if (!refreshResult) {
253257
logger.error(`[${requestId}] Failed to refresh token for credential`)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,11 @@ export function GoogleDrivePicker({
237237

238238
setIsLoading(true)
239239
try {
240-
const url = new URL('/api/auth/oauth/token', window.location.origin)
241-
url.searchParams.set('credentialId', effectiveCredentialId)
242-
// include workflowId if available via global registry (server adds session owner otherwise)
243-
const response = await fetch(url.toString())
240+
const response = await fetch('/api/auth/oauth/token', {
241+
method: 'POST',
242+
headers: { 'Content-Type': 'application/json' },
243+
body: JSON.stringify({ credentialId: effectiveCredentialId, workflowId }),
244+
})
244245

245246
if (!response.ok) {
246247
throw new Error(`Failed to fetch access token: ${response.status}`)

0 commit comments

Comments
 (0)