Skip to content

Commit 9625a2d

Browse files
committed
fix(microsoft): proactive refresh needed
1 parent 5987a6d commit 9625a2d

File tree

2 files changed

+102
-10
lines changed

2 files changed

+102
-10
lines changed

apps/sim/app/api/auth/oauth/utils.ts

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@ import { refreshOAuthToken } from '@/lib/oauth'
77

88
const 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+
1030
interface 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`)

apps/sim/lib/auth/auth.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,25 @@ import { SSO_TRUSTED_PROVIDERS } from './sso/constants'
6464

6565
const logger = createLogger('Auth')
6666

67+
const MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS = 90
68+
69+
const MICROSOFT_PROVIDERS = new Set([
70+
'microsoft-excel',
71+
'microsoft-planner',
72+
'microsoft-teams',
73+
'outlook',
74+
'onedrive',
75+
'sharepoint',
76+
])
77+
78+
function isMicrosoftProvider(providerId: string): boolean {
79+
return MICROSOFT_PROVIDERS.has(providerId)
80+
}
81+
82+
function getMicrosoftRefreshTokenExpiry(): Date {
83+
return new Date(Date.now() + MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000)
84+
}
85+
6786
const validStripeKey = env.STRIPE_SECRET_KEY
6887

6988
let stripeClient = null
@@ -187,6 +206,10 @@ export const auth = betterAuth({
187206
}
188207
}
189208

209+
const refreshTokenExpiresAt = isMicrosoftProvider(account.providerId)
210+
? getMicrosoftRefreshTokenExpiry()
211+
: account.refreshTokenExpiresAt
212+
190213
await db
191214
.update(schema.account)
192215
.set({
@@ -195,7 +218,7 @@ export const auth = betterAuth({
195218
refreshToken: account.refreshToken,
196219
idToken: account.idToken,
197220
accessTokenExpiresAt: account.accessTokenExpiresAt,
198-
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
221+
refreshTokenExpiresAt,
199222
scope: scopeToStore,
200223
updatedAt: new Date(),
201224
})
@@ -292,6 +315,13 @@ export const auth = betterAuth({
292315
}
293316
}
294317

318+
if (isMicrosoftProvider(account.providerId)) {
319+
await db
320+
.update(schema.account)
321+
.set({ refreshTokenExpiresAt: getMicrosoftRefreshTokenExpiry() })
322+
.where(eq(schema.account.id, account.id))
323+
}
324+
295325
// Sync webhooks for credential sets after connecting a new credential
296326
const requestId = crypto.randomUUID().slice(0, 8)
297327
const userMemberships = await db

0 commit comments

Comments
 (0)