diff --git a/packages/@n8n/client-oauth2/src/CredentialsFlow.ts b/packages/@n8n/client-oauth2/src/CredentialsFlow.ts index eeb5550cf3..21174d0187 100644 --- a/packages/@n8n/client-oauth2/src/CredentialsFlow.ts +++ b/packages/@n8n/client-oauth2/src/CredentialsFlow.ts @@ -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) { diff --git a/packages/@n8n/nodes-langchain/credentials/AzureEntraCognitiveServicesOAuth2Api.credentials.ts b/packages/@n8n/nodes-langchain/credentials/AzureEntraCognitiveServicesOAuth2Api.credentials.ts index e5964e5714..900bdc11aa 100644 --- a/packages/@n8n/nodes-langchain/credentials/AzureEntraCognitiveServicesOAuth2Api.credentials.ts +++ b/packages/@n8n/nodes-langchain/credentials/AzureEntraCognitiveServicesOAuth2Api.credentials.ts @@ -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', diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/__tests__/N8nOAuth2TokenCredential.test.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/__tests__/N8nOAuth2TokenCredential.test.ts index c3fbe4c3d4..d1846b1b3f 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/__tests__/N8nOAuth2TokenCredential.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/__tests__/N8nOAuth2TokenCredential.test.ts @@ -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 () => { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/credentials/N8nOAuth2TokenCredential.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/credentials/N8nOAuth2TokenCredential.ts index 63a5e70d3f..9abb60c8c7 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/credentials/N8nOAuth2TokenCredential.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/credentials/N8nOAuth2TokenCredential.ts @@ -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