From 7eded6c4c2923550ed343b17495c5d958065da56 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 11 May 2026 09:44:37 -0700 Subject: [PATCH 1/2] fix(oauth): persist rotated Microsoft refresh tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Microsoft Entra rotates refresh tokens on every refresh and expects clients to replace the stored token with the new one. The Microsoft provider config was missing supportsRefreshTokenRotation, so the rotated refresh_token returned by Azure AD was silently discarded and the original token from initial OAuth connect was reused indefinitely — causing periodic 'Failed to refresh access token' errors for Excel, Teams, Outlook, OneDrive, SharePoint, Planner, AD, and Dataverse integrations. --- apps/sim/lib/oauth/oauth.test.ts | 30 ++++++++++++++++++++++++++++++ apps/sim/lib/oauth/oauth.ts | 1 + 2 files changed, 31 insertions(+) diff --git a/apps/sim/lib/oauth/oauth.test.ts b/apps/sim/lib/oauth/oauth.test.ts index 93ad2a9b9ab..caa90730eb4 100644 --- a/apps/sim/lib/oauth/oauth.test.ts +++ b/apps/sim/lib/oauth/oauth.test.ts @@ -389,6 +389,36 @@ describe('OAuth Token Refresh', () => { }) }) + it.concurrent( + 'should rotate refresh token for Microsoft providers (microsoft, outlook, onedrive, sharepoint)', + async () => { + const microsoftProviders = ['microsoft', 'outlook', 'onedrive', 'sharepoint'] + const oldRefreshToken = 'old_microsoft_refresh_token' + const rotatedRefreshToken = 'rotated_microsoft_refresh_token' + + for (const providerId of microsoftProviders) { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + access_token: 'new_access_token', + expires_in: 3600, + refresh_token: rotatedRefreshToken, + }), + }) + + const result = await withMockFetch(mockFetch, () => + refreshOAuthToken(providerId, oldRefreshToken) + ) + + expect(result).toEqual({ + accessToken: 'new_access_token', + expiresIn: 3600, + refreshToken: rotatedRefreshToken, + }) + } + } + ) + it.concurrent('should use original refresh token when new one is not provided', async () => { const refreshToken = 'original_refresh_token' diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 4e26dc688c5..8702399024a 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -1163,6 +1163,7 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { clientId, clientSecret, useBasicAuth: false, + supportsRefreshTokenRotation: true, } } case 'linear': { From fd885f267a5e12003eb884b97823606e23d542ee Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 11 May 2026 09:50:41 -0700 Subject: [PATCH 2/2] test(oauth): cover hyphenated Microsoft service IDs in rotation test --- apps/sim/lib/oauth/oauth.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/oauth/oauth.test.ts b/apps/sim/lib/oauth/oauth.test.ts index caa90730eb4..c677c7b2c84 100644 --- a/apps/sim/lib/oauth/oauth.test.ts +++ b/apps/sim/lib/oauth/oauth.test.ts @@ -392,7 +392,17 @@ describe('OAuth Token Refresh', () => { it.concurrent( 'should rotate refresh token for Microsoft providers (microsoft, outlook, onedrive, sharepoint)', async () => { - const microsoftProviders = ['microsoft', 'outlook', 'onedrive', 'sharepoint'] + const microsoftProviders = [ + 'microsoft', + 'outlook', + 'onedrive', + 'sharepoint', + 'microsoft-excel', + 'microsoft-teams', + 'microsoft-planner', + 'microsoft-ad', + 'microsoft-dataverse', + ] const oldRefreshToken = 'old_microsoft_refresh_token' const rotatedRefreshToken = 'rotated_microsoft_refresh_token'