mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat: Add token to sendAndWait operations links to walidate in webhook (#17566)
This commit is contained in:
@@ -210,6 +210,8 @@ export class NodeTestHarness {
|
||||
hooks.addHandler('workflowExecuteAfter', (fullRunData) => waitPromise.resolve(fullRunData));
|
||||
|
||||
const additionalData = mock<IWorkflowExecuteAdditionalData>({
|
||||
executionId: '1',
|
||||
webhookWaitingBaseUrl: 'http://localhost/waiting-webhook',
|
||||
hooks,
|
||||
// Get from node.parameters
|
||||
currentNodeParameters: undefined,
|
||||
|
||||
@@ -19,3 +19,5 @@ export const CREDENTIAL_ERRORS = {
|
||||
INVALID_JSON: 'Decrypted credentials data is not valid JSON.',
|
||||
INVALID_DATA: 'Credentials data is not in a valid format.',
|
||||
};
|
||||
|
||||
export const WAITING_TOKEN_QUERY_PARAM = 'signature';
|
||||
|
||||
@@ -19,7 +19,11 @@ import { NodeExecutionContext } from '../node-execution-context';
|
||||
class TestContext extends NodeExecutionContext {}
|
||||
|
||||
describe('NodeExecutionContext', () => {
|
||||
const instanceSettings = mock<InstanceSettings>({ instanceId: 'abc123' });
|
||||
const instanceSettings = mock<InstanceSettings>({
|
||||
instanceId: 'abc123',
|
||||
encryptionKey: 'testEncryptionKey',
|
||||
hmacSignatureSecret: 'testHmacSignatureSecret',
|
||||
});
|
||||
Container.set(InstanceSettings, instanceSettings);
|
||||
|
||||
const node = mock<INode>();
|
||||
@@ -363,4 +367,41 @@ describe('NodeExecutionContext', () => {
|
||||
expect(result).toEqual([node1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSignedResumeUrl', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
testContext = new TestContext(
|
||||
workflow,
|
||||
mock<INode>({
|
||||
id: 'node456',
|
||||
}),
|
||||
mock<IWorkflowExecuteAdditionalData>({
|
||||
executionId: '123',
|
||||
webhookWaitingBaseUrl: 'http://localhost/waiting-webhook',
|
||||
}),
|
||||
mode,
|
||||
{
|
||||
validateSignature: true,
|
||||
resultData: { runData: {} },
|
||||
},
|
||||
);
|
||||
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
|
||||
});
|
||||
it('should return a signed resume URL with no query parameters', () => {
|
||||
const result = testContext.getSignedResumeUrl();
|
||||
|
||||
expect(result).toBe(
|
||||
'http://localhost/waiting-webhook/123/node456?signature=8e48dfd1107c1a736f70e7399493ffc50a2e8edd44f389c5f9c058da961682e7',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a signed resume URL with query parameters', () => {
|
||||
const result = testContext.getSignedResumeUrl({ approved: 'true' });
|
||||
|
||||
expect(result).toBe(
|
||||
'http://localhost/waiting-webhook/123/node456?approved=true&signature=11c5efc97a0d6f2ea9045dba6e397596cba29dc24adb44a9ebd3d1272c991e9b',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,12 +30,14 @@ import {
|
||||
ExpressionError,
|
||||
NodeHelpers,
|
||||
NodeOperationError,
|
||||
UnexpectedError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
HTTP_REQUEST_AS_TOOL_NODE_TYPE,
|
||||
HTTP_REQUEST_NODE_TYPE,
|
||||
HTTP_REQUEST_TOOL_NODE_TYPE,
|
||||
WAITING_TOKEN_QUERY_PARAM,
|
||||
} from '@/constants';
|
||||
import { InstanceSettings } from '@/instance-settings';
|
||||
|
||||
@@ -44,6 +46,7 @@ import { ensureType } from './utils/ensure-type';
|
||||
import { extractValue } from './utils/extract-value';
|
||||
import { getAdditionalKeys } from './utils/get-additional-keys';
|
||||
import { validateValueAgainstSchema } from './utils/validate-value-against-schema';
|
||||
import { generateUrlSignature, prepareUrlForSigning } from '../../utils/signature-helpers';
|
||||
|
||||
export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCredentials'> {
|
||||
protected readonly instanceSettings = Container.get(InstanceSettings);
|
||||
@@ -200,6 +203,32 @@ export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCr
|
||||
return this.instanceSettings.instanceId;
|
||||
}
|
||||
|
||||
setSignatureValidationRequired() {
|
||||
if (this.runExecutionData) this.runExecutionData.validateSignature = true;
|
||||
}
|
||||
|
||||
getSignedResumeUrl(parameters: Record<string, string> = {}) {
|
||||
const { webhookWaitingBaseUrl, executionId } = this.additionalData;
|
||||
|
||||
if (typeof executionId !== 'string') {
|
||||
throw new UnexpectedError('Execution id is missing');
|
||||
}
|
||||
|
||||
const baseURL = new URL(`${webhookWaitingBaseUrl}/${executionId}/${this.node.id}`);
|
||||
|
||||
for (const [key, value] of Object.entries(parameters)) {
|
||||
baseURL.searchParams.set(key, value);
|
||||
}
|
||||
|
||||
const urlForSigning = prepareUrlForSigning(baseURL);
|
||||
|
||||
const token = generateUrlSignature(urlForSigning, this.instanceSettings.hmacSignatureSecret);
|
||||
|
||||
baseURL.searchParams.set(WAITING_TOKEN_QUERY_PARAM, token);
|
||||
|
||||
return baseURL.toString();
|
||||
}
|
||||
|
||||
getTimezone() {
|
||||
return this.workflow.timezone;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import { getInputConnectionData } from './utils/get-input-connection-data';
|
||||
import { getRequestHelperFunctions } from './utils/request-helper-functions';
|
||||
import { returnJsonArray } from './utils/return-json-array';
|
||||
import { getNodeWebhookUrl } from './utils/webhook-helper-functions';
|
||||
|
||||
export class WebhookContext extends NodeExecutionContext implements IWebhookFunctions {
|
||||
readonly helpers: IWebhookFunctions['helpers'];
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ export class InstanceSettings {
|
||||
*/
|
||||
readonly instanceId: string;
|
||||
|
||||
readonly hmacSignatureSecret: string;
|
||||
|
||||
readonly instanceType: InstanceType;
|
||||
|
||||
constructor(
|
||||
@@ -64,6 +66,7 @@ export class InstanceSettings {
|
||||
this.hostId = `${this.instanceType}-${this.isDocker ? os.hostname() : nanoid()}`;
|
||||
this.settings = this.loadOrCreate();
|
||||
this.instanceId = this.generateInstanceId();
|
||||
this.hmacSignatureSecret = this.getOrGenerateHmacSignatureSecret();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -225,6 +228,14 @@ export class InstanceSettings {
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
private getOrGenerateHmacSignatureSecret() {
|
||||
const hmacSignatureSecretFromEnv = process.env.N8N_HMAC_SIGNATURE_SECRET;
|
||||
if (hmacSignatureSecretFromEnv) return hmacSignatureSecretFromEnv;
|
||||
|
||||
const { encryptionKey } = this;
|
||||
return createHash('sha256').update(`hmac-signature:${encryptionKey}`).digest('hex');
|
||||
}
|
||||
|
||||
private save(settings: Settings) {
|
||||
this.settings = settings;
|
||||
writeFileSync(this.settingsFile, JSON.stringify(this.settings, null, '\t'), {
|
||||
|
||||
26
packages/core/src/utils/__tests__/signature-helpers.test.ts
Normal file
26
packages/core/src/utils/__tests__/signature-helpers.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { generateUrlSignature } from '../signature-helpers';
|
||||
|
||||
describe('signature-helpers', () => {
|
||||
const secret = 'test-secret';
|
||||
const baseUrl = 'http://localhost:5678';
|
||||
|
||||
describe('generateUrlSignature', () => {
|
||||
it('should generate a signature token', () => {
|
||||
const url = `${baseUrl}/webhook/abc`;
|
||||
const token = generateUrlSignature(url, secret);
|
||||
expect(token).toBe('fe7f1e4c11f875b2d24681e0b28d0bfed6d66381af5b0ab9633da2202a895243');
|
||||
});
|
||||
|
||||
it('should generate a different token for a different url', () => {
|
||||
const url = `${baseUrl}/webhook/def`;
|
||||
const token = generateUrlSignature(url, secret);
|
||||
expect(token).toBe('ab8e72e7a0e47689596a6550283cbef9e2797b7370b0d6d99c89ee7c2394ea8f');
|
||||
});
|
||||
|
||||
it('should generate a different token for a different secret', () => {
|
||||
const url = `${baseUrl}/webhook/abc`;
|
||||
const token = generateUrlSignature(url, 'different-secret');
|
||||
expect(token).toBe('84a99b6950e12ffcf1fcf8e0fc0986c0c8a46df331932efd79b17e0c11801bd2');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1 +1,2 @@
|
||||
export * from './serialized-buffer';
|
||||
export * from './signature-helpers';
|
||||
|
||||
16
packages/core/src/utils/signature-helpers.ts
Normal file
16
packages/core/src/utils/signature-helpers.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* Generate signature token from url and secret
|
||||
*/
|
||||
export function generateUrlSignature(url: string, secret: string) {
|
||||
const token = crypto.createHmac('sha256', secret).update(url).digest('hex');
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare url for signing
|
||||
*/
|
||||
export function prepareUrlForSigning(url: URL) {
|
||||
return `${url.host}${url.pathname}${url.search}`;
|
||||
}
|
||||
Reference in New Issue
Block a user