Migrate MicrosoftAPIRefreshAccessTokenService to @azure/msal-node (#16954)
This commit is contained in:
+40
-46
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+83
@@ -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,
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user