Skip to content

fix(HTTP Request Node): Add support for Bearer Auth in HttpRequest node #15043

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
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/@n8n/nodes-langchain/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
],
Expand Down
1 change: 1 addition & 0 deletions packages/nodes-base/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
10 changes: 10 additions & 0 deletions packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@ export class HttpRequestV2 implements INodeType {
} catch {}

let httpBasicAuth;
let httpBearerAuth;
let httpDigestAuth;
let httpHeaderAuth;
let httpQueryAuth;
Expand All @@ -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');
Expand Down Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export class HttpRequestV3 implements INodeType {
} catch {}

let httpBasicAuth;
let httpBearerAuth;
let httpDigestAuth;
let httpHeaderAuth;
let httpQueryAuth;
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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];
Expand Down
163 changes: 163 additions & 0 deletions packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV2.test.ts
Original file line number Diff line number Diff line change
@@ -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),
}),
);
}
},
);
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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' },
Expand Down
1 change: 0 additions & 1 deletion packages/nodes-base/nodes/N8nTrigger/test/trigger.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import { N8nTrigger } from '../N8nTrigger.node';

describe('N8nTrigger', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
Loading