import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import type { IWebhookFunctions, INodeExecutionData, IDataObject, ICredentialDataDecryptedObject, } from 'n8n-workflow'; import basicAuth from 'basic-auth'; import jwt from 'jsonwebtoken'; import { formatPrivateKey } from '../../utils/utilities'; import { WebhookAuthorizationError } from './error'; export type WebhookParameters = { httpMethod: string | string[]; responseMode: string; responseData: string; responseCode?: number; //typeVersion <= 1.1 options?: { responseData?: string; responseCode?: { values?: { responseCode: number; customCode?: number; }; }; noResponseBody?: boolean; }; }; export const getResponseCode = (parameters: WebhookParameters) => { if (parameters.responseCode) { return parameters.responseCode; } const responseCodeOptions = parameters.options; if (responseCodeOptions?.responseCode?.values) { const { responseCode, customCode } = responseCodeOptions.responseCode.values; if (customCode) { return customCode; } return responseCode; } return 200; }; export const getResponseData = (parameters: WebhookParameters) => { const { responseData, responseMode, options } = parameters; if (responseData) return responseData; if (responseMode === 'onReceived') { const data = options?.responseData; if (data) return data; } if (options?.noResponseBody) return 'noData'; return undefined; }; export const configuredOutputs = (parameters: WebhookParameters) => { const httpMethod = parameters.httpMethod as string | string[]; if (!Array.isArray(httpMethod)) return [ { type: `${NodeConnectionType.Main}`, displayName: httpMethod, }, ]; const outputs = httpMethod.map((method) => { return { type: `${NodeConnectionType.Main}`, displayName: method, }; }); return outputs; }; export const setupOutputConnection = ( ctx: IWebhookFunctions, method: string, additionalData: { jwtPayload?: IDataObject; }, ) => { const httpMethod = ctx.getNodeParameter('httpMethod', []) as string[] | string; let webhookUrl = ctx.getNodeWebhookUrl('default') as string; const executionMode = ctx.getMode() === 'manual' ? 'test' : 'production'; if (executionMode === 'test') { webhookUrl = webhookUrl.replace('/webhook/', '/webhook-test/'); } // multi methods could be set in settings of node, so we need to check if it's an array if (!Array.isArray(httpMethod)) { return (outputData: INodeExecutionData): INodeExecutionData[][] => { outputData.json.webhookUrl = webhookUrl; outputData.json.executionMode = executionMode; if (additionalData?.jwtPayload) { outputData.json.jwtPayload = additionalData.jwtPayload; } return [[outputData]]; }; } const outputIndex = httpMethod.indexOf(method.toUpperCase()); const outputs: INodeExecutionData[][] = httpMethod.map(() => []); return (outputData: INodeExecutionData): INodeExecutionData[][] => { outputData.json.webhookUrl = webhookUrl; outputData.json.executionMode = executionMode; if (additionalData?.jwtPayload) { outputData.json.jwtPayload = additionalData.jwtPayload; } outputs[outputIndex] = [outputData]; return outputs; }; }; export const isIpWhitelisted = ( whitelist: string | string[] | undefined, ips: string[], ip?: string, ) => { if (whitelist === undefined || whitelist === '') { return true; } if (!Array.isArray(whitelist)) { whitelist = whitelist.split(',').map((entry) => entry.trim()); } for (const address of whitelist) { if (ip && ip.includes(address)) { return true; } if (ips.some((entry) => entry.includes(address))) { return true; } } return false; }; export const checkResponseModeConfiguration = (context: IWebhookFunctions) => { const responseMode = context.getNodeParameter('responseMode', 'onReceived') as string; const connectedNodes = context.getChildNodes(context.getNode().name); const isRespondToWebhookConnected = connectedNodes.some( (node) => node.type === 'n8n-nodes-base.respondToWebhook', ); if (!isRespondToWebhookConnected && responseMode === 'responseNode') { throw new NodeOperationError( context.getNode(), new Error('No Respond to Webhook node found in the workflow'), { description: 'Insert a Respond to Webhook node to your workflow to respond to the webhook or choose another option for the “Respond” parameter', }, ); } if (isRespondToWebhookConnected && responseMode !== 'responseNode') { throw new NodeOperationError( context.getNode(), new Error('Webhook node not correctly configured'), { description: 'Set the “Respond” parameter to “Using Respond to Webhook Node” or remove the Respond to Webhook node', }, ); } }; export async function validateWebhookAuthentication( ctx: IWebhookFunctions, authPropertyName: string, ) { const authentication = ctx.getNodeParameter(authPropertyName) as string; if (authentication === 'none') return; const req = ctx.getRequestObject(); const headers = ctx.getHeaderData(); if (authentication === 'basicAuth') { // Basic authorization is needed to call webhook let expectedAuth: ICredentialDataDecryptedObject | undefined; try { expectedAuth = await ctx.getCredentials('httpBasicAuth'); } catch {} if (expectedAuth === undefined || !expectedAuth.user || !expectedAuth.password) { // Data is not defined on node so can not authenticate throw new WebhookAuthorizationError(500, 'No authentication data defined on node!'); } const providedAuth = basicAuth(req); // Authorization data is missing if (!providedAuth) throw new WebhookAuthorizationError(401); if (providedAuth.name !== expectedAuth.user || providedAuth.pass !== expectedAuth.password) { // Provided authentication data is wrong throw new WebhookAuthorizationError(403); } } else if (authentication === 'headerAuth') { // Special header with value is needed to call webhook let expectedAuth: ICredentialDataDecryptedObject | undefined; try { expectedAuth = await ctx.getCredentials('httpHeaderAuth'); } catch {} if (expectedAuth === undefined || !expectedAuth.name || !expectedAuth.value) { // Data is not defined on node so can not authenticate throw new WebhookAuthorizationError(500, 'No authentication data defined on node!'); } const headerName = (expectedAuth.name as string).toLowerCase(); const expectedValue = expectedAuth.value as string; if ( !headers.hasOwnProperty(headerName) || (headers as IDataObject)[headerName] !== expectedValue ) { // Provided authentication data is wrong throw new WebhookAuthorizationError(403); } } else if (authentication === 'jwtAuth') { let expectedAuth; try { expectedAuth = await ctx.getCredentials<{ keyType: 'passphrase' | 'pemKey'; publicKey: string; secret: string; algorithm: jwt.Algorithm; }>('jwtAuth'); } catch {} if (expectedAuth === undefined) { // Data is not defined on node so can not authenticate throw new WebhookAuthorizationError(500, 'No authentication data defined on node!'); } const authHeader = req.headers.authorization; const token = authHeader?.split(' ')[1]; if (!token) { throw new WebhookAuthorizationError(401, 'No token provided'); } let secretOrPublicKey; if (expectedAuth.keyType === 'passphrase') { secretOrPublicKey = expectedAuth.secret; } else { secretOrPublicKey = formatPrivateKey(expectedAuth.publicKey, true); } try { return jwt.verify(token, secretOrPublicKey, { algorithms: [expectedAuth.algorithm], }) as IDataObject; } catch (error) { throw new WebhookAuthorizationError(403, error.message); } } }