@@ -7,6 +7,26 @@ import { refreshOAuthToken } from '@/lib/oauth'
77
88const logger = createLogger ( 'OAuthUtilsAPI' )
99
10+ const MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS = 90
11+ const PROACTIVE_REFRESH_THRESHOLD_DAYS = 7
12+
13+ const MICROSOFT_PROVIDERS = new Set ( [
14+ 'microsoft-excel' ,
15+ 'microsoft-planner' ,
16+ 'microsoft-teams' ,
17+ 'outlook' ,
18+ 'onedrive' ,
19+ 'sharepoint' ,
20+ ] )
21+
22+ function isMicrosoftProvider ( providerId : string ) : boolean {
23+ return MICROSOFT_PROVIDERS . has ( providerId )
24+ }
25+
26+ function getMicrosoftRefreshTokenExpiry ( ) : Date {
27+ return new Date ( Date . now ( ) + MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000 )
28+ }
29+
1030interface AccountInsertData {
1131 id : string
1232 userId : string
@@ -205,15 +225,32 @@ export async function refreshAccessTokenIfNeeded(
205225 }
206226
207227 // Decide if we should refresh: token missing OR expired
208- const expiresAt = credential . accessTokenExpiresAt
228+ const accessTokenExpiresAt = credential . accessTokenExpiresAt
229+ const refreshTokenExpiresAt = credential . refreshTokenExpiresAt
209230 const now = new Date ( )
210- const shouldRefresh =
211- ! ! credential . refreshToken && ( ! credential . accessToken || ( expiresAt && expiresAt <= now ) )
231+
232+ // Check if access token needs refresh (missing or expired)
233+ const accessTokenNeedsRefresh =
234+ ! ! credential . refreshToken &&
235+ ( ! credential . accessToken || ( accessTokenExpiresAt && accessTokenExpiresAt <= now ) )
236+
237+ // Check if we should proactively refresh to prevent refresh token expiry
238+ // This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
239+ const proactiveRefreshThreshold = new Date (
240+ now . getTime ( ) + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
241+ )
242+ const refreshTokenNeedsProactiveRefresh =
243+ ! ! credential . refreshToken &&
244+ isMicrosoftProvider ( credential . providerId ) &&
245+ refreshTokenExpiresAt &&
246+ refreshTokenExpiresAt <= proactiveRefreshThreshold
247+
248+ const shouldRefresh = accessTokenNeedsRefresh || refreshTokenNeedsProactiveRefresh
212249
213250 const accessToken = credential . accessToken
214251
215252 if ( shouldRefresh ) {
216- logger . info ( `[${ requestId } ] Token expired, attempting to refresh for credential` )
253+ logger . info ( `[${ requestId } ] Refreshing token for credential` )
217254 try {
218255 const refreshedToken = await refreshOAuthToken (
219256 credential . providerId ,
@@ -231,7 +268,7 @@ export async function refreshAccessTokenIfNeeded(
231268 }
232269
233270 // Prepare update data
234- const updateData : any = {
271+ const updateData : Record < string , unknown > = {
235272 accessToken : refreshedToken . accessToken ,
236273 accessTokenExpiresAt : new Date ( Date . now ( ) + refreshedToken . expiresIn * 1000 ) ,
237274 updatedAt : new Date ( ) ,
@@ -243,6 +280,10 @@ export async function refreshAccessTokenIfNeeded(
243280 updateData . refreshToken = refreshedToken . refreshToken
244281 }
245282
283+ if ( isMicrosoftProvider ( credential . providerId ) ) {
284+ updateData . refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry ( )
285+ }
286+
246287 // Update the token in the database
247288 await db . update ( account ) . set ( updateData ) . where ( eq ( account . id , credentialId ) )
248289
@@ -277,10 +318,27 @@ export async function refreshTokenIfNeeded(
277318 credentialId : string
278319) : Promise < { accessToken : string ; refreshed : boolean } > {
279320 // Decide if we should refresh: token missing OR expired
280- const expiresAt = credential . accessTokenExpiresAt
321+ const accessTokenExpiresAt = credential . accessTokenExpiresAt
322+ const refreshTokenExpiresAt = credential . refreshTokenExpiresAt
281323 const now = new Date ( )
282- const shouldRefresh =
283- ! ! credential . refreshToken && ( ! credential . accessToken || ( expiresAt && expiresAt <= now ) )
324+
325+ // Check if access token needs refresh (missing or expired)
326+ const accessTokenNeedsRefresh =
327+ ! ! credential . refreshToken &&
328+ ( ! credential . accessToken || ( accessTokenExpiresAt && accessTokenExpiresAt <= now ) )
329+
330+ // Check if we should proactively refresh to prevent refresh token expiry
331+ // This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
332+ const proactiveRefreshThreshold = new Date (
333+ now . getTime ( ) + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
334+ )
335+ const refreshTokenNeedsProactiveRefresh =
336+ ! ! credential . refreshToken &&
337+ isMicrosoftProvider ( credential . providerId ) &&
338+ refreshTokenExpiresAt &&
339+ refreshTokenExpiresAt <= proactiveRefreshThreshold
340+
341+ const shouldRefresh = accessTokenNeedsRefresh || refreshTokenNeedsProactiveRefresh
284342
285343 // If token appears valid and present, return it directly
286344 if ( ! shouldRefresh ) {
@@ -299,7 +357,7 @@ export async function refreshTokenIfNeeded(
299357 const { accessToken : refreshedToken , expiresIn, refreshToken : newRefreshToken } = refreshResult
300358
301359 // Prepare update data
302- const updateData : any = {
360+ const updateData : Record < string , unknown > = {
303361 accessToken : refreshedToken ,
304362 accessTokenExpiresAt : new Date ( Date . now ( ) + expiresIn * 1000 ) , // Use provider's expiry
305363 updatedAt : new Date ( ) ,
@@ -311,6 +369,10 @@ export async function refreshTokenIfNeeded(
311369 updateData . refreshToken = newRefreshToken
312370 }
313371
372+ if ( isMicrosoftProvider ( credential . providerId ) ) {
373+ updateData . refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry ( )
374+ }
375+
314376 await db . update ( account ) . set ( updateData ) . where ( eq ( account . id , credentialId ) )
315377
316378 logger . info ( `[${ requestId } ] Successfully refreshed access token` )
0 commit comments