Skip to content

Commit 7cfdf46

Browse files
fix(token-refresh): microsoft, notion, x, linear (#2933)
* fix(microsoft): proactive refresh needed * fix(x): missing token refresh flag * notion and linear missing flag too * address bugbot comment
1 parent d681451 commit 7cfdf46

File tree

5 files changed

+110
-10
lines changed

5 files changed

+110
-10
lines changed

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

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { createLogger } from '@sim/logger'
44
import { and, desc, eq, inArray } from 'drizzle-orm'
55
import { getSession } from '@/lib/auth'
66
import { refreshOAuthToken } from '@/lib/oauth'
7+
import {
8+
getMicrosoftRefreshTokenExpiry,
9+
isMicrosoftProvider,
10+
PROACTIVE_REFRESH_THRESHOLD_DAYS,
11+
} from '@/lib/oauth/microsoft'
712

813
const 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
}

apps/sim/lib/auth/auth.ts

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

6565
const logger = createLogger('Auth')
6666

67+
import { getMicrosoftRefreshTokenExpiry, isMicrosoftProvider } from '@/lib/oauth/microsoft'
68+
6769
const validStripeKey = env.STRIPE_SECRET_KEY
6870

6971
let stripeClient = null
@@ -187,6 +189,10 @@ export const auth = betterAuth({
187189
}
188190
}
189191

192+
const refreshTokenExpiresAt = isMicrosoftProvider(account.providerId)
193+
? getMicrosoftRefreshTokenExpiry()
194+
: account.refreshTokenExpiresAt
195+
190196
await db
191197
.update(schema.account)
192198
.set({
@@ -195,7 +201,7 @@ export const auth = betterAuth({
195201
refreshToken: account.refreshToken,
196202
idToken: account.idToken,
197203
accessTokenExpiresAt: account.accessTokenExpiresAt,
198-
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
204+
refreshTokenExpiresAt,
199205
scope: scopeToStore,
200206
updatedAt: new Date(),
201207
})
@@ -292,6 +298,13 @@ export const auth = betterAuth({
292298
}
293299
}
294300

301+
if (isMicrosoftProvider(account.providerId)) {
302+
await db
303+
.update(schema.account)
304+
.set({ refreshTokenExpiresAt: getMicrosoftRefreshTokenExpiry() })
305+
.where(eq(schema.account.id, account.id))
306+
}
307+
295308
// Sync webhooks for credential sets after connecting a new credential
296309
const requestId = crypto.randomUUID().slice(0, 8)
297310
const userMemberships = await db

apps/sim/lib/oauth/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './microsoft'
12
export * from './oauth'
23
export * from './types'
34
export * from './utils'

apps/sim/lib/oauth/microsoft.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export const MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS = 90
2+
export const PROACTIVE_REFRESH_THRESHOLD_DAYS = 7
3+
4+
export const MICROSOFT_PROVIDERS = new Set([
5+
'microsoft-excel',
6+
'microsoft-planner',
7+
'microsoft-teams',
8+
'outlook',
9+
'onedrive',
10+
'sharepoint',
11+
])
12+
13+
export function isMicrosoftProvider(providerId: string): boolean {
14+
return MICROSOFT_PROVIDERS.has(providerId)
15+
}
16+
17+
export function getMicrosoftRefreshTokenExpiry(): Date {
18+
return new Date(Date.now() + MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000)
19+
}

apps/sim/lib/oauth/oauth.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,7 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
835835
clientId,
836836
clientSecret,
837837
useBasicAuth: true,
838+
supportsRefreshTokenRotation: true,
838839
}
839840
}
840841
case 'confluence': {
@@ -883,6 +884,7 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
883884
clientId,
884885
clientSecret,
885886
useBasicAuth: false,
887+
supportsRefreshTokenRotation: true,
886888
}
887889
}
888890
case 'microsoft':
@@ -910,6 +912,7 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
910912
clientId,
911913
clientSecret,
912914
useBasicAuth: true,
915+
supportsRefreshTokenRotation: true,
913916
}
914917
}
915918
case 'dropbox': {

0 commit comments

Comments
 (0)