fix(Azure OpenAI Chat Model Node): Simplify Azure Entra ID Authentication and Auto-Refresh token (#15335)

This commit is contained in:
oleg
2025-05-13 10:48:51 +02:00
committed by GitHub
parent 4302c5f474
commit e750d5366e
4 changed files with 54 additions and 27 deletions

View File

@@ -29,6 +29,7 @@ export class CredentialsFlow {
const headers: Headers = { ...DEFAULT_HEADERS };
const body: CredentialsFlowBody = {
grant_type: 'client_credentials',
...(options.additionalBodyProperties ?? {}),
};
if (options.scopes !== undefined) {

View File

@@ -52,31 +52,14 @@ export class AzureEntraCognitiveServicesOAuth2Api implements ICredentialType {
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'string',
default: 'https://login.microsoftonline.com/$TENANT_ID/oauth2/authorize',
type: 'hidden',
default: '=https://login.microsoftonline.com/{{$self["tenantId"]}}/oauth2/authorize',
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'string',
default: 'https://login.microsoftonline.com/$TENANT_ID/oauth2/token',
},
{
displayName: 'Client ID',
name: 'clientId',
type: 'string',
required: true,
default: '',
description: 'Client ID obtained from the Azure AD App Registration',
},
{
displayName: 'Client Secret',
name: 'clientSecret',
type: 'string',
required: true,
typeOptions: { password: true },
default: '',
description: 'Client Secret obtained from the Azure AD App Registration',
type: 'hidden',
default: '=https://login.microsoftonline.com/{{$self["tenantId"]}}/oauth2/token',
},
{
displayName: 'Additional Body Properties',

View File

@@ -1,9 +1,28 @@
import { ClientOAuth2 } from '@n8n/client-oauth2';
import type { INode } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { N8nOAuth2TokenCredential } from '../credentials/N8nOAuth2TokenCredential';
import type { AzureEntraCognitiveServicesOAuth2ApiCredential } from '../types';
// Mock ClientOAuth2
jest.mock('@n8n/client-oauth2', () => {
return {
ClientOAuth2: jest.fn().mockImplementation(() => {
return {
credentials: {
getToken: jest.fn().mockResolvedValue({
data: {
access_token: 'fresh-test-token',
expires_on: 1234567890,
},
}),
},
};
}),
};
});
const mockNode: INode = {
id: '1',
name: 'Mock node',
@@ -21,11 +40,12 @@ describe('N8nOAuth2TokenCredential', () => {
// Create a mock credential with all required properties
mockCredential = {
authQueryParameters: '',
authentication: 'body', // Set valid authentication type
authentication: 'body',
authUrl: '',
accessTokenUrl: '', // Added missing property
grantType: 'clientCredentials', // Corrected grant type value
accessTokenUrl: '',
grantType: 'clientCredentials',
clientId: '',
clientSecret: 'secret',
customScopes: false,
apiVersion: '2023-05-15',
endpoint: 'https://test.openai.azure.com',
@@ -53,9 +73,15 @@ describe('N8nOAuth2TokenCredential', () => {
// Assert
expect(result).toEqual({
token: 'test-token',
token: 'fresh-test-token',
expiresOnTimestamp: 1234567890,
});
expect(ClientOAuth2).toHaveBeenCalledWith(
expect.objectContaining({
clientId: mockCredential.clientId,
clientSecret: mockCredential.clientSecret,
}),
);
});
it('should throw NodeOperationError when credentials do not contain token', async () => {

View File

@@ -1,4 +1,6 @@
import type { TokenCredential, AccessToken } from '@azure/identity';
import type { ClientOAuth2TokenData } from '@n8n/client-oauth2';
import { ClientOAuth2 } from '@n8n/client-oauth2';
import type { INode } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
@@ -20,10 +22,25 @@ export class N8nOAuth2TokenCredential implements TokenCredential {
if (!this.credential?.oauthTokenData?.access_token) {
throw new NodeOperationError(this.node, 'Failed to retrieve access token');
}
const oAuthClient = new ClientOAuth2({
clientId: this.credential.clientId,
clientSecret: this.credential.clientSecret,
accessTokenUri: this.credential.accessTokenUrl,
scopes: this.credential.scope?.split(' '),
authentication: this.credential.authentication,
authorizationUri: this.credential.authUrl,
additionalBodyProperties: {
resource: 'https://cognitiveservices.azure.com/',
},
});
const token = await oAuthClient.credentials.getToken();
const data = token.data as ClientOAuth2TokenData & {
expires_on: number;
};
return {
token: this.credential.oauthTokenData.access_token,
expiresOnTimestamp: this.credential.oauthTokenData.expires_on,
token: data.access_token,
expiresOnTimestamp: data.expires_on,
};
} catch (error) {
// Re-throw with better error message