mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Migrate MicrosoftAPIRefreshAccessTokenService to @azure/msal-node (#16954)
This commit is contained in:
parent
ac85e1d726
commit
a5eae50c66
4 changed files with 144 additions and 47 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
};
|
||||
21
yarn.lock
21
yarn.lock
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue