From 9625a2d3c8af236e510b42733a40e4544e43b6d8 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 21 Jan 2026 17:57:45 -0800 Subject: [PATCH 1/4] fix(microsoft): proactive refresh needed --- apps/sim/app/api/auth/oauth/utils.ts | 80 ++++++++++++++++++++++++---- apps/sim/lib/auth/auth.ts | 32 ++++++++++- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/api/auth/oauth/utils.ts b/apps/sim/app/api/auth/oauth/utils.ts index 08dd16fdff..a3441ce5e3 100644 --- a/apps/sim/app/api/auth/oauth/utils.ts +++ b/apps/sim/app/api/auth/oauth/utils.ts @@ -7,6 +7,26 @@ import { refreshOAuthToken } from '@/lib/oauth' const logger = createLogger('OAuthUtilsAPI') +const MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS = 90 +const PROACTIVE_REFRESH_THRESHOLD_DAYS = 7 + +const MICROSOFT_PROVIDERS = new Set([ + 'microsoft-excel', + 'microsoft-planner', + 'microsoft-teams', + 'outlook', + 'onedrive', + 'sharepoint', +]) + +function isMicrosoftProvider(providerId: string): boolean { + return MICROSOFT_PROVIDERS.has(providerId) +} + +function getMicrosoftRefreshTokenExpiry(): Date { + return new Date(Date.now() + MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000) +} + interface AccountInsertData { id: string userId: string @@ -205,15 +225,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, @@ -231,7 +268,7 @@ export async function refreshAccessTokenIfNeeded( } // Prepare update data - const updateData: any = { + const updateData: Record = { accessToken: refreshedToken.accessToken, accessTokenExpiresAt: new Date(Date.now() + refreshedToken.expiresIn * 1000), updatedAt: new Date(), @@ -243,6 +280,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)) @@ -277,10 +318,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) { @@ -299,7 +357,7 @@ export async function refreshTokenIfNeeded( const { accessToken: refreshedToken, expiresIn, refreshToken: newRefreshToken } = refreshResult // Prepare update data - const updateData: any = { + const updateData: Record = { accessToken: refreshedToken, accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000), // Use provider's expiry updatedAt: new Date(), @@ -311,6 +369,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`) diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 0ff169a2fd..510ab4d622 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -64,6 +64,25 @@ import { SSO_TRUSTED_PROVIDERS } from './sso/constants' const logger = createLogger('Auth') +const MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS = 90 + +const MICROSOFT_PROVIDERS = new Set([ + 'microsoft-excel', + 'microsoft-planner', + 'microsoft-teams', + 'outlook', + 'onedrive', + 'sharepoint', +]) + +function isMicrosoftProvider(providerId: string): boolean { + return MICROSOFT_PROVIDERS.has(providerId) +} + +function getMicrosoftRefreshTokenExpiry(): Date { + return new Date(Date.now() + MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000) +} + const validStripeKey = env.STRIPE_SECRET_KEY let stripeClient = null @@ -187,6 +206,10 @@ export const auth = betterAuth({ } } + const refreshTokenExpiresAt = isMicrosoftProvider(account.providerId) + ? getMicrosoftRefreshTokenExpiry() + : account.refreshTokenExpiresAt + await db .update(schema.account) .set({ @@ -195,7 +218,7 @@ export const auth = betterAuth({ refreshToken: account.refreshToken, idToken: account.idToken, accessTokenExpiresAt: account.accessTokenExpiresAt, - refreshTokenExpiresAt: account.refreshTokenExpiresAt, + refreshTokenExpiresAt, scope: scopeToStore, updatedAt: new Date(), }) @@ -292,6 +315,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 From 7ddc6191f3aefd7ea1f6cf5a0d6ebe8b6918ee16 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 21 Jan 2026 18:02:33 -0800 Subject: [PATCH 2/4] fix(x): missing token refresh flag --- apps/sim/lib/oauth/oauth.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 2f812b13f4..9ae4ac5fa5 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -835,6 +835,7 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { clientId, clientSecret, useBasicAuth: true, + supportsRefreshTokenRotation: true, } } case 'confluence': { From 7a65ab4e1fdb36818b7b8af99397c8c17f04c111 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 21 Jan 2026 18:05:51 -0800 Subject: [PATCH 3/4] notion and linear missing flag too --- apps/sim/lib/oauth/oauth.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 9ae4ac5fa5..c8cff3dde5 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -884,6 +884,7 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { clientId, clientSecret, useBasicAuth: false, + supportsRefreshTokenRotation: true, } } case 'microsoft': @@ -911,6 +912,7 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { clientId, clientSecret, useBasicAuth: true, + supportsRefreshTokenRotation: true, } } case 'dropbox': { From 054282d0edda84a29b4f9134dce8753520774821 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 21 Jan 2026 18:20:43 -0800 Subject: [PATCH 4/4] address bugbot comment --- apps/sim/app/api/auth/oauth/utils.ts | 42 +++++++++++++++------------- apps/sim/lib/auth/auth.ts | 19 +------------ apps/sim/lib/oauth/index.ts | 1 + apps/sim/lib/oauth/microsoft.ts | 19 +++++++++++++ 4 files changed, 43 insertions(+), 38 deletions(-) create mode 100644 apps/sim/lib/oauth/microsoft.ts diff --git a/apps/sim/app/api/auth/oauth/utils.ts b/apps/sim/app/api/auth/oauth/utils.ts index a3441ce5e3..9fe7d8510e 100644 --- a/apps/sim/app/api/auth/oauth/utils.ts +++ b/apps/sim/app/api/auth/oauth/utils.ts @@ -4,29 +4,14 @@ 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') -const MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS = 90 -const PROACTIVE_REFRESH_THRESHOLD_DAYS = 7 - -const MICROSOFT_PROVIDERS = new Set([ - 'microsoft-excel', - 'microsoft-planner', - 'microsoft-teams', - 'outlook', - 'onedrive', - 'sharepoint', -]) - -function isMicrosoftProvider(providerId: string): boolean { - return MICROSOFT_PROVIDERS.has(providerId) -} - -function getMicrosoftRefreshTokenExpiry(): Date { - return new Date(Date.now() + MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000) -} - interface AccountInsertData { id: string userId: string @@ -264,6 +249,10 @@ 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 } @@ -297,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) { @@ -351,6 +344,10 @@ 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') } @@ -393,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 } diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 510ab4d622..518ecf930b 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -64,24 +64,7 @@ import { SSO_TRUSTED_PROVIDERS } from './sso/constants' const logger = createLogger('Auth') -const MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS = 90 - -const MICROSOFT_PROVIDERS = new Set([ - 'microsoft-excel', - 'microsoft-planner', - 'microsoft-teams', - 'outlook', - 'onedrive', - 'sharepoint', -]) - -function isMicrosoftProvider(providerId: string): boolean { - return MICROSOFT_PROVIDERS.has(providerId) -} - -function getMicrosoftRefreshTokenExpiry(): Date { - return new Date(Date.now() + MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000) -} +import { getMicrosoftRefreshTokenExpiry, isMicrosoftProvider } from '@/lib/oauth/microsoft' const validStripeKey = env.STRIPE_SECRET_KEY diff --git a/apps/sim/lib/oauth/index.ts b/apps/sim/lib/oauth/index.ts index a8f9ce5859..f4c641730e 100644 --- a/apps/sim/lib/oauth/index.ts +++ b/apps/sim/lib/oauth/index.ts @@ -1,3 +1,4 @@ +export * from './microsoft' export * from './oauth' export * from './types' export * from './utils' diff --git a/apps/sim/lib/oauth/microsoft.ts b/apps/sim/lib/oauth/microsoft.ts new file mode 100644 index 0000000000..f512ee1e63 --- /dev/null +++ b/apps/sim/lib/oauth/microsoft.ts @@ -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) +}