Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/twenty-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,67 +1,53 @@
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 type { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Using import type for TwentyConfigService may break NestJS dependency injection. Type-only imports are erased at compile time, which can prevent TypeScript from emitting the design-time type metadata that NestJS needs for constructor injection. Use a regular import to match the pattern in the Google service.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service.ts, line 5:

<comment>Using `import type` for `TwentyConfigService` may break NestJS dependency injection. Type-only imports are erased at compile time, which can prevent TypeScript from emitting the design-time type metadata that NestJS needs for constructor injection. Use a regular import to match the pattern in the Google service.</comment>

<file context>
@@ -1,67 +1,53 @@
+import { ConfidentialClientApplication } from &#39;@azure/msal-node&#39;;
 
-import { TwentyConfigService } from &#39;src/engine/core-modules/twenty-config/twenty-config.service&#39;;
+import type { TwentyConfigService } from &#39;src/engine/core-modules/twenty-config/twenty-config.service&#39;;
 import {
   ConnectedAccountRefreshAccessTokenException,
</file context>
Suggested change
import type { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';

✅ Addressed in d4ecb69

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) {}
private msalClient: ConfidentialClientApplication;

constructor(private readonly config: TwentyConfigService) {
this.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',
},
});
}

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 response = await this.msalClient.acquireTokenByRefreshToken({
refreshToken,
scopes: ['https://graph.microsoft.com/.default'],
});

return {
accessToken: responseData.access_token,
refreshToken: responseData.refresh_token,
};
} catch (error) {
if (
error instanceof AxiosError &&
error.response?.data?.error === 'invalid_grant'
) {
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,
};
} catch (error) {
if (error instanceof ConnectedAccountRefreshAccessTokenException) {
throw error;
}

throw parseMsalError(error);
}
}
}
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -56926,6 +56944,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"
Expand Down Expand Up @@ -58821,7 +58840,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:
Expand Down
Loading