From 00ad1544d89effd367819ccbf8e5a7a058dd3b09 Mon Sep 17 00:00:00 2001 From: neo773 <62795688+neo773@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:13:00 +0530 Subject: [PATCH] Classify OAuth refresh errors by reason instead of status code (#23705) Both provider parsers treated unrecognised failures as permanent, so a single transient error marked a working account as needing reconnection and it never recovered on its own. Permanence is now decided by the provider's OAuth error code, everything else is temporary and retries. Checked against prod: 15 connected accounts currently flagged auth-failed still return a valid token when refreshed, and 12 of those were flagged in bursts across unrelated workspaces (five within 90 seconds on 2026-01-13), which points at a transient blip rather than users revoking access. --------- Co-authored-by: neo773 --- ...le-permanent-oauth-error-codes.constant.ts | 11 +++ .../utils/parse-google-oauth-error.util.ts | 58 +++------------ ...oft-permanent-auth-error-codes.constant.ts | 9 +++ .../microsoft/utils/parse-msal-error.util.ts | 70 +++---------------- 4 files changed, 40 insertions(+), 108 deletions(-) create mode 100644 packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/constants/google-permanent-oauth-error-codes.constant.ts create mode 100644 packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/constants/microsoft-permanent-auth-error-codes.constant.ts diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/constants/google-permanent-oauth-error-codes.constant.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/constants/google-permanent-oauth-error-codes.constant.ts new file mode 100644 index 0000000000..bf1b3e7b81 --- /dev/null +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/constants/google-permanent-oauth-error-codes.constant.ts @@ -0,0 +1,11 @@ +/** + * @see https://developers.google.com/identity/protocols/oauth2/web-server#authorization-errors + */ +export const GOOGLE_PERMANENT_OAUTH_ERROR_CODES = new Set([ + 'invalid_grant', + 'invalid_client', + 'unauthorized_client', + 'unsupported_grant_type', + 'invalid_scope', + 'admin_policy_enforced', +]); diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/utils/parse-google-oauth-error.util.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/utils/parse-google-oauth-error.util.ts index 5ac15a4f43..4ac8c0e901 100644 --- a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/utils/parse-google-oauth-error.util.ts +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/google/utils/parse-google-oauth-error.util.ts @@ -4,6 +4,7 @@ import { ConnectedAccountRefreshAccessTokenException, ConnectedAccountRefreshAccessTokenExceptionCode, } from 'src/engine/metadata-modules/connected-account/exceptions/connected-account-refresh-tokens.exception'; +import { GOOGLE_PERMANENT_OAUTH_ERROR_CODES } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/constants/google-permanent-oauth-error-codes.constant'; import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util'; export const parseGoogleOAuthError = ( @@ -20,63 +21,22 @@ export const parseGoogleOAuthError = ( const googleOAuthError = { code: gaxiosError.response?.status, - reason: - gaxiosError.response?.data?.error || - gaxiosError.response?.data?.error_description || - 'Unknown reason', + reason: gaxiosError.response?.data?.error || 'Unknown reason', message: gaxiosError.response?.data?.error_description || gaxiosError.message || 'Unknown error', }; - switch (googleOAuthError.code) { - case 400: - if (googleOAuthError.reason === 'invalid_grant') { - return new ConnectedAccountRefreshAccessTokenException( - googleOAuthError.message, - ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN, - ); - } - - return new ConnectedAccountRefreshAccessTokenException( - googleOAuthError.message, - ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN, - ); - - case 401: - return new ConnectedAccountRefreshAccessTokenException( - googleOAuthError.message, - ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN, - ); - - case 403: - return new ConnectedAccountRefreshAccessTokenException( - googleOAuthError.message, - ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN, - ); - - case 429: - return new ConnectedAccountRefreshAccessTokenException( - googleOAuthError.message, - ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR, - ); - - case 500: - case 502: - case 503: - case 504: - return new ConnectedAccountRefreshAccessTokenException( - `${googleOAuthError.code} - ${googleOAuthError.message}`, - ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR, - ); - - default: - break; + if (GOOGLE_PERMANENT_OAUTH_ERROR_CODES.has(googleOAuthError.reason)) { + return new ConnectedAccountRefreshAccessTokenException( + `Google auth error: ${googleOAuthError.reason} - ${googleOAuthError.message}`, + ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN, + ); } return new ConnectedAccountRefreshAccessTokenException( - `Google refresh token failed: ${googleOAuthError.message}`, - ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN, + `Google refresh token failed (${googleOAuthError.code ?? 'no status'}): ${googleOAuthError.reason} - ${googleOAuthError.message}`, + ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR, ); }; diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/constants/microsoft-permanent-auth-error-codes.constant.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/constants/microsoft-permanent-auth-error-codes.constant.ts new file mode 100644 index 0000000000..df9d04b7e7 --- /dev/null +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/constants/microsoft-permanent-auth-error-codes.constant.ts @@ -0,0 +1,9 @@ +/** + * @see https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes + */ +export const MICROSOFT_PERMANENT_AUTH_ERROR_CODES = new Set([ + 'invalid_grant', + 'invalid_client', + 'unauthorized_client', + 'invalid_request', +]); diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/utils/parse-msal-error.util.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/utils/parse-msal-error.util.ts index b1a275ff64..6afab96c20 100644 --- a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/utils/parse-msal-error.util.ts +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/utils/parse-msal-error.util.ts @@ -1,34 +1,10 @@ -import { - AuthError, - InteractionRequiredAuthError, - ServerError, -} from '@azure/msal-node'; +import { AuthError, InteractionRequiredAuthError } from '@azure/msal-node'; import { ConnectedAccountRefreshAccessTokenException, ConnectedAccountRefreshAccessTokenExceptionCode, } from 'src/engine/metadata-modules/connected-account/exceptions/connected-account-refresh-tokens.exception'; - -/** - * @see https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes - */ -const PERMANENT_AUTH_ERROR_CODES = new Set([ - 'invalid_grant', - 'invalid_client', - 'unauthorized_client', - 'invalid_request', -]); - -/** - * @see https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/src/error/ClientAuthErrorCodes.ts - */ -const TRANSIENT_AUTH_ERROR_CODES = new Set([ - 'network_error', - 'no_network_connectivity', - 'endpoints_resolution_error', - 'openid_config_error', - 'request_cannot_be_made', -]); +import { MICROSOFT_PERMANENT_AUTH_ERROR_CODES } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/constants/microsoft-permanent-auth-error-codes.constant'; export const parseMsalError = ( error: unknown, @@ -40,44 +16,20 @@ export const parseMsalError = ( ); } - if (error instanceof ServerError) { - const status = error.status; - - if (status === 429) { - return new ConnectedAccountRefreshAccessTokenException( - 'Microsoft rate limit exceeded', - ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR, - ); - } - - if (status && status >= 500 && status < 600) { - return new ConnectedAccountRefreshAccessTokenException( - `Microsoft server error (${status}): ${error.errorMessage}`, - ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR, - ); - } - } - - if (error instanceof AuthError) { - if (TRANSIENT_AUTH_ERROR_CODES.has(error.errorCode)) { - return new ConnectedAccountRefreshAccessTokenException( - `Microsoft network error: ${error.errorCode} - ${error.errorMessage}`, - ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR, - ); - } - - if (PERMANENT_AUTH_ERROR_CODES.has(error.errorCode)) { - return new ConnectedAccountRefreshAccessTokenException( - `Microsoft auth error: ${error.errorCode} - ${error.errorMessage}`, - ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN, - ); - } + if ( + error instanceof AuthError && + MICROSOFT_PERMANENT_AUTH_ERROR_CODES.has(error.errorCode) + ) { + return new ConnectedAccountRefreshAccessTokenException( + `Microsoft auth error: ${error.errorCode} - ${error.errorMessage}`, + ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN, + ); } const message = error instanceof Error ? error.message : String(error); return new ConnectedAccountRefreshAccessTokenException( `Microsoft token refresh failed: ${message}`, - ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN, + ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR, ); };