@@ -4,6 +4,11 @@ import { createLogger } from '@sim/logger'
44import { and , desc , eq , inArray } from 'drizzle-orm'
55import { getSession } from '@/lib/auth'
66import { refreshOAuthToken } from '@/lib/oauth'
7+ import {
8+ getMicrosoftRefreshTokenExpiry ,
9+ isMicrosoftProvider ,
10+ PROACTIVE_REFRESH_THRESHOLD_DAYS ,
11+ } from '@/lib/oauth/microsoft'
712
813const logger = createLogger ( 'OAuthUtilsAPI' )
914
@@ -205,15 +210,32 @@ export async function refreshAccessTokenIfNeeded(
205210 }
206211
207212 // Decide if we should refresh: token missing OR expired
208- const expiresAt = credential . accessTokenExpiresAt
213+ const accessTokenExpiresAt = credential . accessTokenExpiresAt
214+ const refreshTokenExpiresAt = credential . refreshTokenExpiresAt
209215 const now = new Date ( )
210- const shouldRefresh =
211- ! ! credential . refreshToken && ( ! credential . accessToken || ( expiresAt && expiresAt <= now ) )
216+
217+ // Check if access token needs refresh (missing or expired)
218+ const accessTokenNeedsRefresh =
219+ ! ! credential . refreshToken &&
220+ ( ! credential . accessToken || ( accessTokenExpiresAt && accessTokenExpiresAt <= now ) )
221+
222+ // Check if we should proactively refresh to prevent refresh token expiry
223+ // This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
224+ const proactiveRefreshThreshold = new Date (
225+ now . getTime ( ) + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
226+ )
227+ const refreshTokenNeedsProactiveRefresh =
228+ ! ! credential . refreshToken &&
229+ isMicrosoftProvider ( credential . providerId ) &&
230+ refreshTokenExpiresAt &&
231+ refreshTokenExpiresAt <= proactiveRefreshThreshold
232+
233+ const shouldRefresh = accessTokenNeedsRefresh || refreshTokenNeedsProactiveRefresh
212234
213235 const accessToken = credential . accessToken
214236
215237 if ( shouldRefresh ) {
216- logger . info ( `[${ requestId } ] Token expired, attempting to refresh for credential` )
238+ logger . info ( `[${ requestId } ] Refreshing token for credential` )
217239 try {
218240 const refreshedToken = await refreshOAuthToken (
219241 credential . providerId ,
@@ -227,11 +249,15 @@ export async function refreshAccessTokenIfNeeded(
227249 userId : credential . userId ,
228250 hasRefreshToken : ! ! credential . refreshToken ,
229251 } )
252+ if ( ! accessTokenNeedsRefresh && accessToken ) {
253+ logger . info ( `[${ requestId } ] Proactive refresh failed but access token still valid` )
254+ return accessToken
255+ }
230256 return null
231257 }
232258
233259 // Prepare update data
234- const updateData : any = {
260+ const updateData : Record < string , unknown > = {
235261 accessToken : refreshedToken . accessToken ,
236262 accessTokenExpiresAt : new Date ( Date . now ( ) + refreshedToken . expiresIn * 1000 ) ,
237263 updatedAt : new Date ( ) ,
@@ -243,6 +269,10 @@ export async function refreshAccessTokenIfNeeded(
243269 updateData . refreshToken = refreshedToken . refreshToken
244270 }
245271
272+ if ( isMicrosoftProvider ( credential . providerId ) ) {
273+ updateData . refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry ( )
274+ }
275+
246276 // Update the token in the database
247277 await db . update ( account ) . set ( updateData ) . where ( eq ( account . id , credentialId ) )
248278
@@ -256,6 +286,10 @@ export async function refreshAccessTokenIfNeeded(
256286 credentialId,
257287 userId : credential . userId ,
258288 } )
289+ if ( ! accessTokenNeedsRefresh && accessToken ) {
290+ logger . info ( `[${ requestId } ] Proactive refresh failed but access token still valid` )
291+ return accessToken
292+ }
259293 return null
260294 }
261295 } else if ( ! accessToken ) {
@@ -277,10 +311,27 @@ export async function refreshTokenIfNeeded(
277311 credentialId : string
278312) : Promise < { accessToken : string ; refreshed : boolean } > {
279313 // Decide if we should refresh: token missing OR expired
280- const expiresAt = credential . accessTokenExpiresAt
314+ const accessTokenExpiresAt = credential . accessTokenExpiresAt
315+ const refreshTokenExpiresAt = credential . refreshTokenExpiresAt
281316 const now = new Date ( )
282- const shouldRefresh =
283- ! ! credential . refreshToken && ( ! credential . accessToken || ( expiresAt && expiresAt <= now ) )
317+
318+ // Check if access token needs refresh (missing or expired)
319+ const accessTokenNeedsRefresh =
320+ ! ! credential . refreshToken &&
321+ ( ! credential . accessToken || ( accessTokenExpiresAt && accessTokenExpiresAt <= now ) )
322+
323+ // Check if we should proactively refresh to prevent refresh token expiry
324+ // This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
325+ const proactiveRefreshThreshold = new Date (
326+ now . getTime ( ) + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
327+ )
328+ const refreshTokenNeedsProactiveRefresh =
329+ ! ! credential . refreshToken &&
330+ isMicrosoftProvider ( credential . providerId ) &&
331+ refreshTokenExpiresAt &&
332+ refreshTokenExpiresAt <= proactiveRefreshThreshold
333+
334+ const shouldRefresh = accessTokenNeedsRefresh || refreshTokenNeedsProactiveRefresh
284335
285336 // If token appears valid and present, return it directly
286337 if ( ! shouldRefresh ) {
@@ -293,13 +344,17 @@ export async function refreshTokenIfNeeded(
293344
294345 if ( ! refreshResult ) {
295346 logger . error ( `[${ requestId } ] Failed to refresh token for credential` )
347+ if ( ! accessTokenNeedsRefresh && credential . accessToken ) {
348+ logger . info ( `[${ requestId } ] Proactive refresh failed but access token still valid` )
349+ return { accessToken : credential . accessToken , refreshed : false }
350+ }
296351 throw new Error ( 'Failed to refresh token' )
297352 }
298353
299354 const { accessToken : refreshedToken , expiresIn, refreshToken : newRefreshToken } = refreshResult
300355
301356 // Prepare update data
302- const updateData : any = {
357+ const updateData : Record < string , unknown > = {
303358 accessToken : refreshedToken ,
304359 accessTokenExpiresAt : new Date ( Date . now ( ) + expiresIn * 1000 ) , // Use provider's expiry
305360 updatedAt : new Date ( ) ,
@@ -311,6 +366,10 @@ export async function refreshTokenIfNeeded(
311366 updateData . refreshToken = newRefreshToken
312367 }
313368
369+ if ( isMicrosoftProvider ( credential . providerId ) ) {
370+ updateData . refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry ( )
371+ }
372+
314373 await db . update ( account ) . set ( updateData ) . where ( eq ( account . id , credentialId ) )
315374
316375 logger . info ( `[${ requestId } ] Successfully refreshed access token` )
@@ -331,6 +390,11 @@ export async function refreshTokenIfNeeded(
331390 }
332391 }
333392
393+ if ( ! accessTokenNeedsRefresh && credential . accessToken ) {
394+ logger . info ( `[${ requestId } ] Proactive refresh failed but access token still valid` )
395+ return { accessToken : credential . accessToken , refreshed : false }
396+ }
397+
334398 logger . error ( `[${ requestId } ] Refresh failed and no valid token found in DB` , error )
335399 throw error
336400 }
0 commit comments