From 31003aacd15d7219fa87c919dedca7c8be09b1c2 Mon Sep 17 00:00:00 2001 From: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> Date: Fri, 2 May 2025 10:33:46 +0100 Subject: [PATCH] fix(HTTP Request Node): Add support for Bearer Auth in HttpRequest node (#15043) --- packages/@n8n/nodes-langchain/.eslintrc.js | 1 + packages/nodes-base/.eslintrc.js | 1 + .../test/v2/EmailReadImapV2.node.test.ts | 1 - .../nodes/GraphQL/test/GraphQL.node.test.ts | 1 - .../HttpRequest/V2/HttpRequestV2.node.ts | 10 ++ .../HttpRequest/V3/HttpRequestV3.node.ts | 8 + .../test/node/HttpRequestV2.test.ts | 163 ++++++++++++++++++ .../test/node/HttpRequestV3.test.ts | 7 +- .../nodes/N8nTrigger/test/trigger.test.ts | 1 - .../v2/test/RemoveDuplicates.test.ts | 1 - 10 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV2.test.ts diff --git a/packages/@n8n/nodes-langchain/.eslintrc.js b/packages/@n8n/nodes-langchain/.eslintrc.js index b08c88ce40..3db00d4dff 100644 --- a/packages/@n8n/nodes-langchain/.eslintrc.js +++ b/packages/@n8n/nodes-langchain/.eslintrc.js @@ -151,6 +151,7 @@ module.exports = { files: ['**/*.test.ts', '**/test/**/*.ts'], rules: { 'import/no-extraneous-dependencies': 'off', + 'n8n-nodes-base/node-filename-against-convention': 'off', }, }, ], diff --git a/packages/nodes-base/.eslintrc.js b/packages/nodes-base/.eslintrc.js index 2352429a5e..105ee6322a 100644 --- a/packages/nodes-base/.eslintrc.js +++ b/packages/nodes-base/.eslintrc.js @@ -153,6 +153,7 @@ module.exports = { files: ['**/*.test.ts', '**/test/**/*.ts'], rules: { 'import/no-extraneous-dependencies': 'off', + 'n8n-nodes-base/node-filename-against-convention': 'off', }, }, ], diff --git a/packages/nodes-base/nodes/EmailReadImap/test/v2/EmailReadImapV2.node.test.ts b/packages/nodes-base/nodes/EmailReadImap/test/v2/EmailReadImapV2.node.test.ts index 0236022f3b..c1651600c1 100644 --- a/packages/nodes-base/nodes/EmailReadImap/test/v2/EmailReadImapV2.node.test.ts +++ b/packages/nodes-base/nodes/EmailReadImap/test/v2/EmailReadImapV2.node.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable n8n-nodes-base/node-filename-against-convention */ import { mock } from 'jest-mock-extended'; import { type INodeTypeBaseDescription, type ITriggerFunctions } from 'n8n-workflow'; diff --git a/packages/nodes-base/nodes/GraphQL/test/GraphQL.node.test.ts b/packages/nodes-base/nodes/GraphQL/test/GraphQL.node.test.ts index bdae514921..44bbf1e18c 100644 --- a/packages/nodes-base/nodes/GraphQL/test/GraphQL.node.test.ts +++ b/packages/nodes-base/nodes/GraphQL/test/GraphQL.node.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable n8n-nodes-base/node-filename-against-convention */ import { NodeTestHarness } from '@nodes-testing/node-test-harness'; import nock from 'nock'; diff --git a/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts b/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts index 7784d42049..f1b8783169 100644 --- a/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts @@ -640,6 +640,7 @@ export class HttpRequestV2 implements INodeType { } catch {} let httpBasicAuth; + let httpBearerAuth; let httpDigestAuth; let httpHeaderAuth; let httpQueryAuth; @@ -654,6 +655,10 @@ export class HttpRequestV2 implements INodeType { try { httpBasicAuth = await this.getCredentials('httpBasicAuth'); } catch {} + } else if (genericAuthType === 'httpBearerAuth') { + try { + httpBearerAuth = await this.getCredentials('httpBearerAuth'); + } catch {} } else if (genericAuthType === 'httpDigestAuth') { try { httpDigestAuth = await this.getCredentials('httpDigestAuth'); @@ -959,6 +964,11 @@ export class HttpRequestV2 implements INodeType { }; authDataKeys.auth = ['pass']; } + if (httpBearerAuth !== undefined) { + requestOptions.headers = requestOptions.headers ?? {}; + requestOptions.headers.Authorization = `Bearer ${String(httpBearerAuth.token)}`; + authDataKeys.headers = ['Authorization']; + } if (httpHeaderAuth !== undefined) { requestOptions.headers![httpHeaderAuth.name as string] = httpHeaderAuth.value; authDataKeys.headers = [httpHeaderAuth.name as string]; diff --git a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts index e0aedc829d..9448c4add0 100644 --- a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts @@ -100,6 +100,7 @@ export class HttpRequestV3 implements INodeType { } catch {} let httpBasicAuth; + let httpBearerAuth; let httpDigestAuth; let httpHeaderAuth; let httpQueryAuth; @@ -156,6 +157,8 @@ export class HttpRequestV3 implements INodeType { if (genericCredentialType === 'httpBasicAuth') { httpBasicAuth = await this.getCredentials('httpBasicAuth', itemIndex); + } else if (genericCredentialType === 'httpBearerAuth') { + httpBearerAuth = await this.getCredentials('httpBearerAuth', itemIndex); } else if (genericCredentialType === 'httpDigestAuth') { httpDigestAuth = await this.getCredentials('httpDigestAuth', itemIndex); } else if (genericCredentialType === 'httpHeaderAuth') { @@ -496,6 +499,11 @@ export class HttpRequestV3 implements INodeType { }; authDataKeys.auth = ['pass']; } + if (httpBearerAuth !== undefined) { + requestOptions.headers = requestOptions.headers ?? {}; + requestOptions.headers.Authorization = `Bearer ${String(httpBearerAuth.token)}`; + authDataKeys.headers = ['Authorization']; + } if (httpHeaderAuth !== undefined) { requestOptions.headers![httpHeaderAuth.name as string] = httpHeaderAuth.value; authDataKeys.headers = [httpHeaderAuth.name as string]; diff --git a/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV2.test.ts b/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV2.test.ts new file mode 100644 index 0000000000..430ae6d00b --- /dev/null +++ b/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV2.test.ts @@ -0,0 +1,163 @@ +import type { IExecuteFunctions, INodeTypeBaseDescription } from 'n8n-workflow'; + +import { HttpRequestV2 } from '../../V2/HttpRequestV2.node'; + +describe('HttpRequestV2', () => { + let node: HttpRequestV2; + let executeFunctions: IExecuteFunctions; + + const baseUrl = 'http://example.com'; + const options = { + redirect: '', + batching: { batch: { batchSize: 1, batchInterval: 1 } }, + proxy: '', + timeout: '', + allowUnauthorizedCerts: '', + queryParameterArrays: '', + response: '', + lowercaseHeaders: '', + }; + + beforeEach(() => { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'HTTP Request', + name: 'httpRequest', + description: 'Makes an HTTP request and returns the response data', + group: [], + }; + node = new HttpRequestV2(baseDescription); + executeFunctions = { + getInputData: jest.fn(), + getNodeParameter: jest.fn(), + getNode: jest.fn(() => { + return { + type: 'n8n-nodes-base.httpRequest', + typeVersion: 2, + }; + }), + getCredentials: jest.fn(), + helpers: { + request: jest.fn(), + requestOAuth1: jest.fn( + async () => + await Promise.resolve({ + success: true, + }), + ), + requestOAuth2: jest.fn( + async () => + await Promise.resolve({ + success: true, + }), + ), + requestWithAuthentication: jest.fn(), + requestWithAuthenticationPaginated: jest.fn(), + assertBinaryData: jest.fn(), + getBinaryStream: jest.fn(), + getBinaryMetadata: jest.fn(), + binaryToString: jest.fn((buffer: Buffer) => { + return buffer.toString(); + }), + prepareBinaryData: jest.fn(), + }, + getContext: jest.fn(), + sendMessageToUI: jest.fn(), + continueOnFail: jest.fn(), + getMode: jest.fn(), + } as unknown as IExecuteFunctions; + }); + + describe('Authentication Handling', () => { + const authenticationTypes = [ + { + genericCredentialType: 'httpBasicAuth', + credentials: { user: 'username', password: 'password' }, + authField: 'auth', + authValue: { user: 'username', pass: 'password' }, + }, + { + genericCredentialType: 'httpBearerAuth', + credentials: { token: 'bearerToken123' }, + authField: 'headers', + authValue: { Authorization: 'Bearer bearerToken123' }, + }, + { + genericCredentialType: 'httpDigestAuth', + credentials: { user: 'username', password: 'password' }, + authField: 'auth', + authValue: { user: 'username', pass: 'password', sendImmediately: false }, + }, + { + genericCredentialType: 'httpHeaderAuth', + credentials: { name: 'Authorization', value: 'Bearer token' }, + authField: 'headers', + authValue: { Authorization: 'Bearer token' }, + }, + { + genericCredentialType: 'httpQueryAuth', + credentials: { name: 'Token', value: 'secretToken' }, + authField: 'qs', + authValue: { Token: 'secretToken' }, + }, + { + genericCredentialType: 'oAuth1Api', + credentials: { oauth_token: 'token', oauth_token_secret: 'secret' }, + authField: 'oauth', + authValue: { oauth_token: 'token', oauth_token_secret: 'secret' }, + }, + { + genericCredentialType: 'oAuth2Api', + credentials: { access_token: 'accessToken' }, + authField: 'auth', + authValue: { bearer: 'accessToken' }, + }, + ]; + + it.each(authenticationTypes)( + 'should handle $genericCredentialType authentication', + async ({ genericCredentialType, credentials, authField, authValue }) => { + (executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: {} }]); + (executeFunctions.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => { + switch (paramName) { + case 'method': + return 'GET'; + case 'url': + return baseUrl; + case 'authentication': + return 'genericCredentialType'; + case 'genericAuthType': + return genericCredentialType; + case 'options': + return options; + case 'bodyParametersUi': + case 'headerParametersUi': + case 'queryParametersUi': + return { parameter: [] }; + default: + return undefined; + } + }); + + (executeFunctions.getCredentials as jest.Mock).mockResolvedValue(credentials); + const response = { + success: true, + }; + (executeFunctions.helpers.request as jest.Mock).mockResolvedValue(response); + + const result = await node.execute.call(executeFunctions); + expect(result).toEqual([[{ json: { success: true }, pairedItem: { item: 0 } }]]); + if (genericCredentialType === 'oAuth1Api') { + expect(executeFunctions.helpers.requestOAuth1).toHaveBeenCalled(); + } else if (genericCredentialType === 'oAuth2Api') { + expect(executeFunctions.helpers.requestOAuth2).toHaveBeenCalled(); + } else { + expect(executeFunctions.helpers.request).toHaveBeenCalledWith( + expect.objectContaining({ + [authField]: expect.objectContaining(authValue), + }), + ); + } + }, + ); + }); +}); diff --git a/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV3.test.ts b/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV3.test.ts index b15da2ba2e..b7aa738380 100644 --- a/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV3.test.ts +++ b/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV3.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable n8n-nodes-base/node-filename-against-convention */ import type { IExecuteFunctions, INodeTypeBaseDescription } from 'n8n-workflow'; import { HttpRequestV3 } from '../../V3/HttpRequestV3.node'; @@ -149,6 +148,12 @@ describe('HttpRequestV3', () => { authField: 'auth', authValue: { user: 'username', pass: 'password' }, }, + { + genericCredentialType: 'httpBearerAuth', + credentials: { token: 'bearerToken123' }, + authField: 'headers', + authValue: { Authorization: 'Bearer bearerToken123' }, + }, { genericCredentialType: 'httpDigestAuth', credentials: { user: 'username', password: 'password' }, diff --git a/packages/nodes-base/nodes/N8nTrigger/test/trigger.test.ts b/packages/nodes-base/nodes/N8nTrigger/test/trigger.test.ts index 43e49554ac..3d8ab05b24 100644 --- a/packages/nodes-base/nodes/N8nTrigger/test/trigger.test.ts +++ b/packages/nodes-base/nodes/N8nTrigger/test/trigger.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable n8n-nodes-base/node-filename-against-convention */ import { N8nTrigger } from '../N8nTrigger.node'; describe('N8nTrigger', () => { diff --git a/packages/nodes-base/nodes/Transform/RemoveDuplicates/v2/test/RemoveDuplicates.test.ts b/packages/nodes-base/nodes/Transform/RemoveDuplicates/v2/test/RemoveDuplicates.test.ts index 84994b34e6..8125ed51fb 100644 --- a/packages/nodes-base/nodes/Transform/RemoveDuplicates/v2/test/RemoveDuplicates.test.ts +++ b/packages/nodes-base/nodes/Transform/RemoveDuplicates/v2/test/RemoveDuplicates.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable n8n-nodes-base/node-filename-against-convention */ import { mock } from 'jest-mock-extended'; import type { IExecuteFunctions, INodeExecutionData, INodeTypeBaseDescription } from 'n8n-workflow';