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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 144 additions and 47 deletions

View file

@ -24,6 +24,7 @@
"@aws-sdk/client-sesv2": "^3.888.0",
"@aws-sdk/client-sts": "3.825.0",
"@aws-sdk/credential-providers": "3.825.0",
"@azure/msal-node": "^3.8.4",
"@babel/preset-env": "7.26.9",
"@blocknote/server-util": "^0.31.1",
"@clickhouse/client": "^1.11.0",

View file

@ -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;
}
}

View file

@ -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,
);
};

View file

@ -2544,6 +2544,24 @@ __metadata:
languageName: node
linkType: hard
"@azure/msal-common@npm:15.13.3":
version: 15.13.3
resolution: "@azure/msal-common@npm:15.13.3"
checksum: 10c0/0d71c31ad098153985cb918c2bda9698aae0a15b659da5c0867728313ba6743854cc112332b39ba1a187b81dfc0abba22b1c22ee6f245e511c5ecec179832b03
languageName: node
linkType: hard
"@azure/msal-node@npm:^3.8.4":
version: 3.8.4
resolution: "@azure/msal-node@npm:3.8.4"
dependencies:
"@azure/msal-common": "npm:15.13.3"
jsonwebtoken: "npm:^9.0.0"
uuid: "npm:^8.3.0"
checksum: 10c0/8f61c2172c31cae156c42ada15bd7795ce7ef2b12c33e2c2e4e5802678596c53985f560cb40a93647064f1866d643f75c62699130121aa368ef9b69b87e5f071
languageName: node
linkType: hard
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.21.4, @babel/code-frame@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/code-frame@npm:7.27.1"
@ -56896,6 +56914,7 @@ __metadata:
"@aws-sdk/client-sesv2": "npm:^3.888.0"
"@aws-sdk/client-sts": "npm:3.825.0"
"@aws-sdk/credential-providers": "npm:3.825.0"
"@azure/msal-node": "npm:^3.8.4"
"@babel/preset-env": "npm:7.26.9"
"@blocknote/server-util": "npm:^0.31.1"
"@clickhouse/client": "npm:^1.11.0"
@ -58790,7 +58809,7 @@ __metadata:
languageName: node
linkType: hard
"uuid@npm:^8.3.2":
"uuid@npm:^8.3.0, uuid@npm:^8.3.2":
version: 8.3.2
resolution: "uuid@npm:8.3.2"
bin: