mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
fix(oauth): fix airtable oauth token refresh
This commit is contained in:
@@ -87,16 +87,26 @@ export async function POST(request: NextRequest) {
|
||||
throw new Error('Failed to refresh token')
|
||||
}
|
||||
|
||||
const { accessToken: refreshedToken, expiresIn } = refreshResult
|
||||
const {
|
||||
accessToken: refreshedToken,
|
||||
expiresIn,
|
||||
refreshToken: newRefreshToken,
|
||||
} = refreshResult
|
||||
|
||||
await db
|
||||
.update(account)
|
||||
.set({
|
||||
accessToken: refreshedToken,
|
||||
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(account.id, credentialId))
|
||||
// Prepare update data
|
||||
const updateData: any = {
|
||||
accessToken: refreshedToken,
|
||||
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
// If we received a new refresh token, update it
|
||||
if (newRefreshToken && newRefreshToken !== credential.refreshToken) {
|
||||
logger.info(`[${requestId}] Updating refresh token for credential: ${credentialId}`)
|
||||
updateData.refreshToken = newRefreshToken
|
||||
}
|
||||
|
||||
await db.update(account).set(updateData).where(eq(account.id, credentialId))
|
||||
|
||||
logger.info(`[${requestId}] Successfully refreshed access token`)
|
||||
return NextResponse.json({ accessToken: refreshedToken }, { status: 200 })
|
||||
@@ -180,18 +190,28 @@ export async function GET(request: NextRequest) {
|
||||
throw new Error('Failed to refresh token')
|
||||
}
|
||||
|
||||
const { accessToken: refreshedToken, expiresIn } = refreshResult
|
||||
const {
|
||||
accessToken: refreshedToken,
|
||||
expiresIn,
|
||||
refreshToken: newRefreshToken,
|
||||
} = refreshResult
|
||||
logger.info(`[${requestId}] Token refreshed successfully`)
|
||||
|
||||
// Prepare update data
|
||||
const updateData: any = {
|
||||
accessToken: refreshedToken,
|
||||
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
// If we received a new refresh token, update it
|
||||
if (newRefreshToken && newRefreshToken !== credential.refreshToken) {
|
||||
logger.info(`[${requestId}] Updating refresh token for credential: ${credentialId}`)
|
||||
updateData.refreshToken = newRefreshToken
|
||||
}
|
||||
|
||||
// Update the token in the database with the correct expiration time
|
||||
await db
|
||||
.update(account)
|
||||
.set({
|
||||
accessToken: refreshedToken,
|
||||
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(account.id, credentialId))
|
||||
await db.update(account).set(updateData).where(eq(account.id, credentialId))
|
||||
|
||||
accessToken = refreshedToken
|
||||
} catch (refreshError) {
|
||||
|
||||
@@ -55,17 +55,23 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
|
||||
return null
|
||||
}
|
||||
|
||||
const { accessToken, expiresIn } = refreshResult
|
||||
const { accessToken, expiresIn, refreshToken: newRefreshToken } = refreshResult
|
||||
|
||||
// Update the database with new tokens
|
||||
const updateData: any = {
|
||||
accessToken,
|
||||
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Convert seconds to milliseconds
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
// If we received a new refresh token (some providers like Airtable rotate them), save it
|
||||
if (newRefreshToken && newRefreshToken !== credential.refreshToken) {
|
||||
logger.info(`Updating refresh token for user ${userId}, provider ${providerId}`)
|
||||
updateData.refreshToken = newRefreshToken
|
||||
}
|
||||
|
||||
// Update the token in the database with the actual expiration time from the provider
|
||||
await db
|
||||
.update(account)
|
||||
.set({
|
||||
accessToken,
|
||||
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Convert seconds to milliseconds
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(account.id, credential.id))
|
||||
await db.update(account).set(updateData).where(eq(account.id, credential.id))
|
||||
|
||||
logger.info(`Successfully refreshed token for user ${userId}, provider ${providerId}`)
|
||||
return accessToken
|
||||
@@ -137,15 +143,21 @@ export async function refreshAccessTokenIfNeeded(
|
||||
return null
|
||||
}
|
||||
|
||||
// Prepare update data
|
||||
const updateData: any = {
|
||||
accessToken: refreshedToken.accessToken,
|
||||
accessTokenExpiresAt: new Date(Date.now() + refreshedToken.expiresIn * 1000),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
// If we received a new refresh token, update it
|
||||
if (refreshedToken.refreshToken && refreshedToken.refreshToken !== credential.refreshToken) {
|
||||
logger.info(`[${requestId || ''}] Updating refresh token for credential: ${credentialId}`)
|
||||
updateData.refreshToken = refreshedToken.refreshToken
|
||||
}
|
||||
|
||||
// Update the token in the database
|
||||
await db
|
||||
.update(account)
|
||||
.set({
|
||||
accessToken: refreshedToken.accessToken,
|
||||
accessTokenExpiresAt: new Date(Date.now() + refreshedToken.expiresIn * 1000), // Default 1 hour expiry
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(account.id, credentialId))
|
||||
await db.update(account).set(updateData).where(eq(account.id, credentialId))
|
||||
|
||||
logger.info(
|
||||
`[${requestId || ''}] Successfully refreshed access token for credential: ${credentialId}`
|
||||
|
||||
@@ -338,7 +338,7 @@ export function parseProvider(provider: OAuthProvider): ProviderConfig {
|
||||
export async function refreshOAuthToken(
|
||||
providerId: string,
|
||||
refreshToken: string
|
||||
): Promise<{ accessToken: string; expiresIn: number } | null> {
|
||||
): Promise<{ accessToken: string; expiresIn: number; refreshToken: string } | null> {
|
||||
try {
|
||||
// Get the provider from the providerId (e.g., 'google-drive' -> 'google')
|
||||
const provider = providerId.split('-')[0]
|
||||
@@ -397,25 +397,38 @@ export async function refreshOAuthToken(
|
||||
}),
|
||||
}
|
||||
|
||||
// For providers using Basic auth, add Authorization header
|
||||
if (useBasicAuth) {
|
||||
const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
|
||||
headers['Authorization'] = `Basic ${basicAuth}`
|
||||
}
|
||||
|
||||
// Prepare request body
|
||||
const bodyParams: Record<string, string> = {
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
}
|
||||
|
||||
// For Airtable specifically, include client_id in body even with Basic auth
|
||||
// For Airtable, check if we have both client ID and secret
|
||||
if (provider === 'airtable') {
|
||||
bodyParams.client_id = clientId
|
||||
} else if (!useBasicAuth) {
|
||||
// For other non-Basic auth providers, include both credentials
|
||||
bodyParams.client_id = clientId
|
||||
bodyParams.client_secret = clientSecret
|
||||
// Airtable requires Basic Auth with client ID and secret in the Authorization header
|
||||
// Do not include client_id or client_secret in the body when using Basic Auth
|
||||
if (clientId && clientSecret) {
|
||||
const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
|
||||
headers['Authorization'] = `Basic ${basicAuth}`
|
||||
|
||||
// Make sure to include refresh_token in body params but not client_id/client_secret
|
||||
// This ensures we're not sending credentials in both header and body
|
||||
delete bodyParams.client_id
|
||||
delete bodyParams.client_secret
|
||||
} else {
|
||||
throw new Error('Both client ID and client secret are required for Airtable OAuth')
|
||||
}
|
||||
} else {
|
||||
// For other providers, use the general approach
|
||||
if (useBasicAuth) {
|
||||
const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
|
||||
headers['Authorization'] = `Basic ${basicAuth}`
|
||||
}
|
||||
|
||||
if (!useBasicAuth) {
|
||||
bodyParams.client_id = clientId
|
||||
bodyParams.client_secret = clientSecret
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the token
|
||||
@@ -430,6 +443,12 @@ export async function refreshOAuthToken(
|
||||
logger.error('Token refresh failed:', {
|
||||
status: response.status,
|
||||
error: errorText,
|
||||
provider,
|
||||
headers: JSON.stringify(headers, null, 2).replace(
|
||||
/"Authorization":"[^"]*"/,
|
||||
'"Authorization":"[REDACTED]"'
|
||||
),
|
||||
bodyParams: JSON.stringify(bodyParams),
|
||||
})
|
||||
throw new Error(`Failed to refresh token: ${response.status} ${errorText}`)
|
||||
}
|
||||
@@ -439,6 +458,14 @@ export async function refreshOAuthToken(
|
||||
// Extract token and expiration (different providers may use different field names)
|
||||
const accessToken = data.access_token
|
||||
|
||||
// For Airtable, also capture the new refresh token if provided
|
||||
// Airtable may rotate refresh tokens
|
||||
let newRefreshToken = null
|
||||
if (provider === 'airtable' && data.refresh_token) {
|
||||
newRefreshToken = data.refresh_token
|
||||
logger.info('Received new refresh token from Airtable')
|
||||
}
|
||||
|
||||
// Get expiration time - use provider's value or default to 1 hour (3600 seconds)
|
||||
// Different providers use different names for this field
|
||||
const expiresIn = data.expires_in || data.expiresIn || 3600
|
||||
@@ -448,8 +475,17 @@ export async function refreshOAuthToken(
|
||||
return null
|
||||
}
|
||||
|
||||
logger.info('Token refreshed successfully with expiration', { expiresIn })
|
||||
return { accessToken, expiresIn }
|
||||
logger.info('Token refreshed successfully with expiration', {
|
||||
expiresIn,
|
||||
hasNewRefreshToken: !!newRefreshToken,
|
||||
provider,
|
||||
})
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
expiresIn,
|
||||
refreshToken: newRefreshToken || refreshToken, // Return new refresh token if available
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error refreshing token:', { error })
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user