Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 73 additions & 9 deletions apps/sim/app/api/auth/oauth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { createLogger } from '@sim/logger'
import { and, desc, eq, inArray } from 'drizzle-orm'
import { getSession } from '@/lib/auth'
import { refreshOAuthToken } from '@/lib/oauth'
import {
getMicrosoftRefreshTokenExpiry,
isMicrosoftProvider,
PROACTIVE_REFRESH_THRESHOLD_DAYS,
} from '@/lib/oauth/microsoft'

const logger = createLogger('OAuthUtilsAPI')

Expand Down Expand Up @@ -205,15 +210,32 @@ export async function refreshAccessTokenIfNeeded(
}

// Decide if we should refresh: token missing OR expired
const expiresAt = credential.accessTokenExpiresAt
const accessTokenExpiresAt = credential.accessTokenExpiresAt
const refreshTokenExpiresAt = credential.refreshTokenExpiresAt
const now = new Date()
const shouldRefresh =
!!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now))

// Check if access token needs refresh (missing or expired)
const accessTokenNeedsRefresh =
!!credential.refreshToken &&
(!credential.accessToken || (accessTokenExpiresAt && accessTokenExpiresAt <= now))

// Check if we should proactively refresh to prevent refresh token expiry
// This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
const proactiveRefreshThreshold = new Date(
now.getTime() + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
)
const refreshTokenNeedsProactiveRefresh =
!!credential.refreshToken &&
isMicrosoftProvider(credential.providerId) &&
refreshTokenExpiresAt &&
refreshTokenExpiresAt <= proactiveRefreshThreshold

const shouldRefresh = accessTokenNeedsRefresh || refreshTokenNeedsProactiveRefresh

const accessToken = credential.accessToken

if (shouldRefresh) {
logger.info(`[${requestId}] Token expired, attempting to refresh for credential`)
logger.info(`[${requestId}] Refreshing token for credential`)
try {
const refreshedToken = await refreshOAuthToken(
credential.providerId,
Expand All @@ -227,11 +249,15 @@ export async function refreshAccessTokenIfNeeded(
userId: credential.userId,
hasRefreshToken: !!credential.refreshToken,
})
if (!accessTokenNeedsRefresh && accessToken) {
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
return accessToken
}
return null
}

// Prepare update data
const updateData: any = {
const updateData: Record<string, unknown> = {
accessToken: refreshedToken.accessToken,
accessTokenExpiresAt: new Date(Date.now() + refreshedToken.expiresIn * 1000),
updatedAt: new Date(),
Expand All @@ -243,6 +269,10 @@ export async function refreshAccessTokenIfNeeded(
updateData.refreshToken = refreshedToken.refreshToken
}

if (isMicrosoftProvider(credential.providerId)) {
updateData.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
}

// Update the token in the database
await db.update(account).set(updateData).where(eq(account.id, credentialId))

Expand All @@ -256,6 +286,10 @@ export async function refreshAccessTokenIfNeeded(
credentialId,
userId: credential.userId,
})
if (!accessTokenNeedsRefresh && accessToken) {
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
return accessToken
}
return null
}
} else if (!accessToken) {
Expand All @@ -277,10 +311,27 @@ export async function refreshTokenIfNeeded(
credentialId: string
): Promise<{ accessToken: string; refreshed: boolean }> {
// Decide if we should refresh: token missing OR expired
const expiresAt = credential.accessTokenExpiresAt
const accessTokenExpiresAt = credential.accessTokenExpiresAt
const refreshTokenExpiresAt = credential.refreshTokenExpiresAt
const now = new Date()
const shouldRefresh =
!!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now))

// Check if access token needs refresh (missing or expired)
const accessTokenNeedsRefresh =
!!credential.refreshToken &&
(!credential.accessToken || (accessTokenExpiresAt && accessTokenExpiresAt <= now))

// Check if we should proactively refresh to prevent refresh token expiry
// This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
const proactiveRefreshThreshold = new Date(
now.getTime() + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
)
const refreshTokenNeedsProactiveRefresh =
!!credential.refreshToken &&
isMicrosoftProvider(credential.providerId) &&
refreshTokenExpiresAt &&
refreshTokenExpiresAt <= proactiveRefreshThreshold

const shouldRefresh = accessTokenNeedsRefresh || refreshTokenNeedsProactiveRefresh

// If token appears valid and present, return it directly
if (!shouldRefresh) {
Expand All @@ -293,13 +344,17 @@ export async function refreshTokenIfNeeded(

if (!refreshResult) {
logger.error(`[${requestId}] Failed to refresh token for credential`)
if (!accessTokenNeedsRefresh && credential.accessToken) {
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
return { accessToken: credential.accessToken, refreshed: false }
}
throw new Error('Failed to refresh token')
}

const { accessToken: refreshedToken, expiresIn, refreshToken: newRefreshToken } = refreshResult

// Prepare update data
const updateData: any = {
const updateData: Record<string, unknown> = {
accessToken: refreshedToken,
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry
updatedAt: new Date(),
Expand All @@ -311,6 +366,10 @@ export async function refreshTokenIfNeeded(
updateData.refreshToken = newRefreshToken
}

if (isMicrosoftProvider(credential.providerId)) {
updateData.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
}

await db.update(account).set(updateData).where(eq(account.id, credentialId))

logger.info(`[${requestId}] Successfully refreshed access token`)
Expand All @@ -331,6 +390,11 @@ export async function refreshTokenIfNeeded(
}
}

if (!accessTokenNeedsRefresh && credential.accessToken) {
logger.info(`[${requestId}] Proactive refresh failed but access token still valid`)
return { accessToken: credential.accessToken, refreshed: false }
}

logger.error(`[${requestId}] Refresh failed and no valid token found in DB`, error)
throw error
}
Expand Down
15 changes: 14 additions & 1 deletion apps/sim/lib/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ import { SSO_TRUSTED_PROVIDERS } from './sso/constants'

const logger = createLogger('Auth')

import { getMicrosoftRefreshTokenExpiry, isMicrosoftProvider } from '@/lib/oauth/microsoft'

const validStripeKey = env.STRIPE_SECRET_KEY

let stripeClient = null
Expand Down Expand Up @@ -187,6 +189,10 @@ export const auth = betterAuth({
}
}

const refreshTokenExpiresAt = isMicrosoftProvider(account.providerId)
? getMicrosoftRefreshTokenExpiry()
: account.refreshTokenExpiresAt

await db
.update(schema.account)
.set({
Expand All @@ -195,7 +201,7 @@ export const auth = betterAuth({
refreshToken: account.refreshToken,
idToken: account.idToken,
accessTokenExpiresAt: account.accessTokenExpiresAt,
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
refreshTokenExpiresAt,
scope: scopeToStore,
updatedAt: new Date(),
})
Expand Down Expand Up @@ -292,6 +298,13 @@ export const auth = betterAuth({
}
}

if (isMicrosoftProvider(account.providerId)) {
await db
.update(schema.account)
.set({ refreshTokenExpiresAt: getMicrosoftRefreshTokenExpiry() })
.where(eq(schema.account.id, account.id))
}

// Sync webhooks for credential sets after connecting a new credential
const requestId = crypto.randomUUID().slice(0, 8)
const userMemberships = await db
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/oauth/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './microsoft'
export * from './oauth'
export * from './types'
export * from './utils'
19 changes: 19 additions & 0 deletions apps/sim/lib/oauth/microsoft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS = 90
export const PROACTIVE_REFRESH_THRESHOLD_DAYS = 7

export const MICROSOFT_PROVIDERS = new Set([
'microsoft-excel',
'microsoft-planner',
'microsoft-teams',
'outlook',
'onedrive',
'sharepoint',
])

export function isMicrosoftProvider(providerId: string): boolean {
return MICROSOFT_PROVIDERS.has(providerId)
}

export function getMicrosoftRefreshTokenExpiry(): Date {
return new Date(Date.now() + MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000)
}
3 changes: 3 additions & 0 deletions apps/sim/lib/oauth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,7 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
clientId,
clientSecret,
useBasicAuth: true,
supportsRefreshTokenRotation: true,
}
}
case 'confluence': {
Expand Down Expand Up @@ -883,6 +884,7 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
clientId,
clientSecret,
useBasicAuth: false,
supportsRefreshTokenRotation: true,
}
}
case 'microsoft':
Expand Down Expand Up @@ -910,6 +912,7 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
clientId,
clientSecret,
useBasicAuth: true,
supportsRefreshTokenRotation: true,
}
}
case 'dropbox': {
Expand Down