mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(Azure OpenAI Chat Model Node): Simplify Azure Entra ID Authentication and Auto-Refresh token (#15335)
This commit is contained in:
@@ -29,6 +29,7 @@ export class CredentialsFlow {
|
|||||||
const headers: Headers = { ...DEFAULT_HEADERS };
|
const headers: Headers = { ...DEFAULT_HEADERS };
|
||||||
const body: CredentialsFlowBody = {
|
const body: CredentialsFlowBody = {
|
||||||
grant_type: 'client_credentials',
|
grant_type: 'client_credentials',
|
||||||
|
...(options.additionalBodyProperties ?? {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options.scopes !== undefined) {
|
if (options.scopes !== undefined) {
|
||||||
|
|||||||
@@ -52,31 +52,14 @@ export class AzureEntraCognitiveServicesOAuth2Api implements ICredentialType {
|
|||||||
{
|
{
|
||||||
displayName: 'Authorization URL',
|
displayName: 'Authorization URL',
|
||||||
name: 'authUrl',
|
name: 'authUrl',
|
||||||
type: 'string',
|
type: 'hidden',
|
||||||
default: 'https://login.microsoftonline.com/$TENANT_ID/oauth2/authorize',
|
default: '=https://login.microsoftonline.com/{{$self["tenantId"]}}/oauth2/authorize',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Access Token URL',
|
displayName: 'Access Token URL',
|
||||||
name: 'accessTokenUrl',
|
name: 'accessTokenUrl',
|
||||||
type: 'string',
|
type: 'hidden',
|
||||||
default: 'https://login.microsoftonline.com/$TENANT_ID/oauth2/token',
|
default: '=https://login.microsoftonline.com/{{$self["tenantId"]}}/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',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Additional Body Properties',
|
displayName: 'Additional Body Properties',
|
||||||
|
|||||||
@@ -1,9 +1,28 @@
|
|||||||
|
import { ClientOAuth2 } from '@n8n/client-oauth2';
|
||||||
import type { INode } from 'n8n-workflow';
|
import type { INode } from 'n8n-workflow';
|
||||||
import { NodeOperationError } from 'n8n-workflow';
|
import { NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { N8nOAuth2TokenCredential } from '../credentials/N8nOAuth2TokenCredential';
|
import { N8nOAuth2TokenCredential } from '../credentials/N8nOAuth2TokenCredential';
|
||||||
import type { AzureEntraCognitiveServicesOAuth2ApiCredential } from '../types';
|
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 = {
|
const mockNode: INode = {
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'Mock node',
|
name: 'Mock node',
|
||||||
@@ -21,11 +40,12 @@ describe('N8nOAuth2TokenCredential', () => {
|
|||||||
// Create a mock credential with all required properties
|
// Create a mock credential with all required properties
|
||||||
mockCredential = {
|
mockCredential = {
|
||||||
authQueryParameters: '',
|
authQueryParameters: '',
|
||||||
authentication: 'body', // Set valid authentication type
|
authentication: 'body',
|
||||||
authUrl: '',
|
authUrl: '',
|
||||||
accessTokenUrl: '', // Added missing property
|
accessTokenUrl: '',
|
||||||
grantType: 'clientCredentials', // Corrected grant type value
|
grantType: 'clientCredentials',
|
||||||
clientId: '',
|
clientId: '',
|
||||||
|
clientSecret: 'secret',
|
||||||
customScopes: false,
|
customScopes: false,
|
||||||
apiVersion: '2023-05-15',
|
apiVersion: '2023-05-15',
|
||||||
endpoint: 'https://test.openai.azure.com',
|
endpoint: 'https://test.openai.azure.com',
|
||||||
@@ -53,9 +73,15 @@ describe('N8nOAuth2TokenCredential', () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
token: 'test-token',
|
token: 'fresh-test-token',
|
||||||
expiresOnTimestamp: 1234567890,
|
expiresOnTimestamp: 1234567890,
|
||||||
});
|
});
|
||||||
|
expect(ClientOAuth2).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
clientId: mockCredential.clientId,
|
||||||
|
clientSecret: mockCredential.clientSecret,
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw NodeOperationError when credentials do not contain token', async () => {
|
it('should throw NodeOperationError when credentials do not contain token', async () => {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { TokenCredential, AccessToken } from '@azure/identity';
|
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 type { INode } from 'n8n-workflow';
|
||||||
import { NodeOperationError } from 'n8n-workflow';
|
import { NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
@@ -20,10 +22,25 @@ export class N8nOAuth2TokenCredential implements TokenCredential {
|
|||||||
if (!this.credential?.oauthTokenData?.access_token) {
|
if (!this.credential?.oauthTokenData?.access_token) {
|
||||||
throw new NodeOperationError(this.node, 'Failed to retrieve 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 {
|
return {
|
||||||
token: this.credential.oauthTokenData.access_token,
|
token: data.access_token,
|
||||||
expiresOnTimestamp: this.credential.oauthTokenData.expires_on,
|
expiresOnTimestamp: data.expires_on,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Re-throw with better error message
|
// Re-throw with better error message
|
||||||
|
|||||||
Reference in New Issue
Block a user