Migrate MicrosoftAPIRefreshAccessTokenService to @azure/msal-node (#16954)

This commit is contained in:
neo773
2026-01-08 22:48:35 +05:30
committed by GitHub
parent ac85e1d726
commit a5eae50c66
4 changed files with 144 additions and 47 deletions
@@ -1,67 +1,61 @@
import { Injectable } from '@nestjs/common';
import axios, { AxiosError } from 'axios';
import { ConfidentialClientApplication } from '@azure/msal-node';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import {
ConnectedAccountRefreshAccessTokenException,
ConnectedAccountRefreshAccessTokenExceptionCode,
} from 'src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception';
import { type ConnectedAccountTokens } from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service';
export type MicrosoftTokens = {
accessToken: string;
refreshToken: string;
};
interface MicrosoftRefreshTokenResponse {
access_token: string;
refresh_token: string;
scope: string;
token_type: string;
expires_in: number;
id_token?: string;
}
import type { ConnectedAccountTokens } from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service';
import { parseMsalError } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/utils/parse-msal-error.util';
@Injectable()
export class MicrosoftAPIRefreshAccessTokenService {
constructor(private readonly twentyConfigService: TwentyConfigService) {}
constructor(private readonly config: TwentyConfigService) {}
async refreshTokens(refreshToken: string): Promise<ConnectedAccountTokens> {
try {
const response = await axios.post<MicrosoftRefreshTokenResponse>(
'https://login.microsoftonline.com/common/oauth2/v2.0/token',
new URLSearchParams({
client_id: this.twentyConfigService.get('AUTH_MICROSOFT_CLIENT_ID'),
client_secret: this.twentyConfigService.get(
'AUTH_MICROSOFT_CLIENT_SECRET',
),
refresh_token: refreshToken,
grant_type: 'refresh_token',
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
},
);
const responseData = response.data as MicrosoftRefreshTokenResponse;
const msalClient = new ConfidentialClientApplication({
auth: {
clientId: this.config.get('AUTH_MICROSOFT_CLIENT_ID'),
clientSecret: this.config.get('AUTH_MICROSOFT_CLIENT_SECRET'),
authority: 'https://login.microsoftonline.com/common',
},
});
return {
accessToken: responseData.access_token,
refreshToken: responseData.refresh_token,
};
} catch (error) {
if (
error instanceof AxiosError &&
error.response?.data?.error === 'invalid_grant'
) {
try {
const response = await msalClient.acquireTokenByRefreshToken({
refreshToken,
scopes: ['https://graph.microsoft.com/.default'],
forceCache: true,
});
if (!response) {
throw new ConnectedAccountRefreshAccessTokenException(
`Failed to refresh Microsoft token: ${error.response?.data?.error} - ${error.response?.data?.error_description}`,
'No response received from Microsoft token endpoint',
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
);
}
throw error;
return {
accessToken: response.accessToken,
refreshToken: this.extractRefreshTokenFromCache(msalClient),
};
} catch (error) {
if (error instanceof ConnectedAccountRefreshAccessTokenException) {
throw error;
}
throw parseMsalError(error);
}
}
private extractRefreshTokenFromCache(
msalClient: ConfidentialClientApplication,
): string {
const tokenCache = JSON.parse(msalClient.getTokenCache().serialize());
const refreshTokenKey = Object.keys(tokenCache.RefreshToken)[0];
return tokenCache.RefreshToken[refreshTokenKey].secret;
}
}
@@ -0,0 +1,83 @@
import {
AuthError,
InteractionRequiredAuthError,
ServerError,
} from '@azure/msal-node';
import {
ConnectedAccountRefreshAccessTokenException,
ConnectedAccountRefreshAccessTokenExceptionCode,
} from 'src/modules/connected-account/refresh-tokens-manager/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',
]);
export const parseMsalError = (
error: unknown,
): ConnectedAccountRefreshAccessTokenException => {
if (error instanceof InteractionRequiredAuthError) {
return new ConnectedAccountRefreshAccessTokenException(
`Microsoft token refresh requires re-authentication: ${error.errorCode}`,
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
);
}
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,
);
}
}
const message = error instanceof Error ? error.message : String(error);
return new ConnectedAccountRefreshAccessTokenException(
`Microsoft token refresh failed: ${message}`,
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
);
};