mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-26 21:19:43 -08:00
476 lines
11 KiB
TypeScript
476 lines
11 KiB
TypeScript
import type {
|
|
IDataObject,
|
|
IExecuteFunctions,
|
|
INodeExecutionData,
|
|
INodeType,
|
|
INodeTypeDescription,
|
|
} from 'n8n-workflow';
|
|
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
|
|
|
import jwt from 'jsonwebtoken';
|
|
|
|
import { formatPrivateKey } from '../../utils/utilities';
|
|
import { parseJsonParameter } from '../Set/v2/helpers/utils';
|
|
|
|
const prettifyOperation = (operation: string) => {
|
|
if (operation === 'sign') {
|
|
return 'Sign a JWT';
|
|
}
|
|
|
|
if (operation === 'decode') {
|
|
return 'Decode a JWT';
|
|
}
|
|
|
|
if (operation === 'verify') {
|
|
return 'Verify a JWT';
|
|
}
|
|
|
|
return operation;
|
|
};
|
|
|
|
const getToken = (ctx: IExecuteFunctions, itemIndex = 0) => {
|
|
const token = ctx.getNodeParameter('token', itemIndex) as string;
|
|
|
|
if (!token) {
|
|
throw new NodeOperationError(ctx.getNode(), 'The JWT token was not provided', {
|
|
itemIndex,
|
|
description: "Be sure to add a valid JWT token to the 'Token' parameter",
|
|
});
|
|
}
|
|
|
|
return token;
|
|
};
|
|
|
|
export class Jwt implements INodeType {
|
|
description: INodeTypeDescription = {
|
|
displayName: 'JWT',
|
|
name: 'jwt',
|
|
icon: 'file:jwt.svg',
|
|
group: ['transform'],
|
|
version: 1,
|
|
description: 'JWT',
|
|
subtitle: `={{(${prettifyOperation})($parameter.operation)}}`,
|
|
defaults: {
|
|
name: 'JWT',
|
|
},
|
|
inputs: [NodeConnectionType.Main],
|
|
outputs: [NodeConnectionType.Main],
|
|
credentials: [
|
|
{
|
|
// eslint-disable-next-line n8n-nodes-base/node-class-description-credentials-name-unsuffixed
|
|
name: 'jwtAuth',
|
|
required: true,
|
|
},
|
|
],
|
|
properties: [
|
|
{
|
|
displayName: 'Operation',
|
|
name: 'operation',
|
|
type: 'options',
|
|
noDataExpression: true,
|
|
options: [
|
|
{
|
|
name: 'Decode',
|
|
value: 'decode',
|
|
action: 'Decode a JWT',
|
|
},
|
|
{
|
|
name: 'Sign',
|
|
value: 'sign',
|
|
action: 'Sign a JWT',
|
|
},
|
|
{
|
|
name: 'Verify',
|
|
value: 'verify',
|
|
action: 'Verify a JWT',
|
|
},
|
|
],
|
|
default: 'sign',
|
|
},
|
|
{
|
|
displayName: 'Use JSON to Build Payload',
|
|
name: 'useJson',
|
|
type: 'boolean',
|
|
default: false,
|
|
description: 'Whether to use JSON to build the claims',
|
|
displayOptions: {
|
|
show: {
|
|
operation: ['sign'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Payload Claims',
|
|
name: 'claims',
|
|
type: 'collection',
|
|
placeholder: 'Add Claim',
|
|
default: {},
|
|
options: [
|
|
{
|
|
displayName: 'Audience',
|
|
name: 'audience',
|
|
type: 'string',
|
|
placeholder: 'e.g. https://example.com',
|
|
default: '',
|
|
description: 'Identifies the recipients that the JWT is intended for',
|
|
},
|
|
{
|
|
displayName: 'Expires In',
|
|
name: 'expiresIn',
|
|
type: 'number',
|
|
placeholder: 'e.g. 3600',
|
|
default: 3600,
|
|
description: 'The lifetime of the token in seconds',
|
|
typeOptions: {
|
|
minValue: 0,
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Issuer',
|
|
name: 'issuer',
|
|
type: 'string',
|
|
placeholder: 'e.g. https://example.com',
|
|
default: '',
|
|
description: 'Identifies the principal that issued the JWT',
|
|
},
|
|
{
|
|
displayName: 'JWT ID',
|
|
name: 'jwtid',
|
|
type: 'string',
|
|
placeholder: 'e.g. 123456',
|
|
default: '',
|
|
description: 'Unique identifier for the JWT',
|
|
},
|
|
{
|
|
displayName: 'Not Before',
|
|
name: 'notBefore',
|
|
type: 'number',
|
|
default: 0,
|
|
description: 'The time before which the JWT must not be accepted for processing',
|
|
typeOptions: {
|
|
minValue: 0,
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Subject',
|
|
name: 'subject',
|
|
type: 'string',
|
|
default: '',
|
|
description: 'Identifies the principal that is the subject of the JWT',
|
|
},
|
|
],
|
|
displayOptions: {
|
|
show: {
|
|
operation: ['sign'],
|
|
useJson: [false],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Payload Claims (JSON)',
|
|
name: 'claimsJson',
|
|
type: 'json',
|
|
description: 'Claims to add to the token in JSON format',
|
|
default: '{\n "my_field_1": "value 1",\n "my_field_2": "value 2"\n}\n',
|
|
validateType: 'object',
|
|
ignoreValidationDuringExecution: true,
|
|
typeOptions: {
|
|
rows: 5,
|
|
},
|
|
displayOptions: {
|
|
show: {
|
|
operation: ['sign'],
|
|
useJson: [true],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Token',
|
|
name: 'token',
|
|
type: 'string',
|
|
typeOptions: { password: true },
|
|
required: true,
|
|
validateType: 'jwt',
|
|
default: '',
|
|
description: 'The token to verify or decode',
|
|
displayOptions: {
|
|
show: {
|
|
operation: ['verify', 'decode'],
|
|
},
|
|
},
|
|
},
|
|
|
|
{
|
|
displayName: 'Options',
|
|
name: 'options',
|
|
type: 'collection',
|
|
placeholder: 'Add option',
|
|
default: {},
|
|
options: [
|
|
{
|
|
displayName: 'Return Additional Info',
|
|
name: 'complete',
|
|
type: 'boolean',
|
|
default: false,
|
|
description:
|
|
'Whether to return the complete decoded token with information about the header and signature or just the payload',
|
|
displayOptions: {
|
|
show: {
|
|
'/operation': ['verify', 'decode'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Ignore Expiration',
|
|
name: 'ignoreExpiration',
|
|
type: 'boolean',
|
|
default: false,
|
|
description: 'Whether to ignore the expiration of the token',
|
|
displayOptions: {
|
|
show: {
|
|
'/operation': ['verify'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Ignore Not Before Claim',
|
|
name: 'ignoreNotBefore',
|
|
type: 'boolean',
|
|
default: false,
|
|
description: 'Whether to ignore the not before claim of the token',
|
|
displayOptions: {
|
|
show: {
|
|
'/operation': ['verify'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Clock Tolerance',
|
|
name: 'clockTolerance',
|
|
type: 'number',
|
|
default: 0,
|
|
description:
|
|
'Number of seconds to tolerate when checking the nbf and exp claims, to deal with small clock differences among different servers',
|
|
typeOptions: {
|
|
minValue: 0,
|
|
},
|
|
displayOptions: {
|
|
show: {
|
|
'/operation': ['verify'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Key ID',
|
|
name: 'kid',
|
|
type: 'string',
|
|
placeholder: 'e.g. 123456',
|
|
default: '',
|
|
description:
|
|
'The kid (key ID) claim is an optional header claim, used to specify the key for validating the signature',
|
|
displayOptions: {
|
|
show: {
|
|
'/operation': ['sign'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Override Algorithm',
|
|
name: 'algorithm',
|
|
type: 'options',
|
|
options: [
|
|
{
|
|
name: 'ES256',
|
|
value: 'ES256',
|
|
},
|
|
{
|
|
name: 'ES384',
|
|
value: 'ES384',
|
|
},
|
|
{
|
|
name: 'ES512',
|
|
value: 'ES512',
|
|
},
|
|
{
|
|
name: 'HS256',
|
|
value: 'HS256',
|
|
},
|
|
{
|
|
name: 'HS384',
|
|
value: 'HS384',
|
|
},
|
|
{
|
|
name: 'HS512',
|
|
value: 'HS512',
|
|
},
|
|
{
|
|
name: 'PS256',
|
|
value: 'PS256',
|
|
},
|
|
{
|
|
name: 'PS384',
|
|
value: 'PS384',
|
|
},
|
|
{
|
|
name: 'PS512',
|
|
value: 'PS512',
|
|
},
|
|
{
|
|
name: 'RS256',
|
|
value: 'RS256',
|
|
},
|
|
{
|
|
name: 'RS384',
|
|
value: 'RS384',
|
|
},
|
|
{
|
|
name: 'RS512',
|
|
value: 'RS512',
|
|
},
|
|
],
|
|
default: 'HS256',
|
|
description:
|
|
'The algorithm to use for signing or verifying the token, overrides algorithm in credentials',
|
|
displayOptions: {
|
|
show: {
|
|
'/operation': ['sign', 'verify'],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
const items = this.getInputData();
|
|
const returnData: INodeExecutionData[] = [];
|
|
|
|
const operation = this.getNodeParameter('operation', 0);
|
|
|
|
const credentials = await this.getCredentials<{
|
|
keyType: 'passphrase' | 'pemKey';
|
|
publicKey: string;
|
|
privateKey: string;
|
|
secret: string;
|
|
algorithm: jwt.Algorithm;
|
|
}>('jwtAuth');
|
|
|
|
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
const options = this.getNodeParameter('options', itemIndex, {}) as {
|
|
algorithm?: jwt.Algorithm;
|
|
complete?: boolean;
|
|
ignoreExpiration?: boolean;
|
|
ignoreNotBefore?: boolean;
|
|
clockTolerance?: number;
|
|
kid?: string;
|
|
};
|
|
|
|
try {
|
|
if (operation === 'sign') {
|
|
const useJson = this.getNodeParameter('useJson', itemIndex) as boolean;
|
|
|
|
let payload: IDataObject = {};
|
|
|
|
if (useJson) {
|
|
payload = parseJsonParameter(
|
|
this.getNodeParameter('claimsJson', itemIndex) as IDataObject,
|
|
this.getNode(),
|
|
itemIndex,
|
|
);
|
|
} else {
|
|
payload = this.getNodeParameter('claims', itemIndex) as IDataObject;
|
|
}
|
|
|
|
let secretOrPrivateKey;
|
|
|
|
if (credentials.keyType === 'passphrase') {
|
|
secretOrPrivateKey = credentials.secret;
|
|
} else {
|
|
secretOrPrivateKey = formatPrivateKey(credentials.privateKey);
|
|
}
|
|
|
|
const signingOptions: jwt.SignOptions = {
|
|
algorithm: options.algorithm ?? credentials.algorithm,
|
|
};
|
|
if (options.kid) signingOptions.keyid = options.kid;
|
|
|
|
const token = jwt.sign(payload, secretOrPrivateKey, signingOptions);
|
|
|
|
returnData.push({
|
|
json: { token },
|
|
pairedItem: itemIndex,
|
|
});
|
|
}
|
|
|
|
if (operation === 'verify') {
|
|
const token = getToken(this, itemIndex);
|
|
|
|
let secretOrPublicKey;
|
|
|
|
if (credentials.keyType === 'passphrase') {
|
|
secretOrPublicKey = credentials.secret;
|
|
} else {
|
|
secretOrPublicKey = formatPrivateKey(credentials.publicKey, true);
|
|
}
|
|
|
|
const { ignoreExpiration, ignoreNotBefore, clockTolerance, complete } = options;
|
|
|
|
const data = jwt.verify(token, secretOrPublicKey, {
|
|
algorithms: [options.algorithm ?? credentials.algorithm],
|
|
ignoreExpiration,
|
|
ignoreNotBefore,
|
|
clockTolerance,
|
|
complete,
|
|
});
|
|
|
|
const json = options.complete && data ? (data as IDataObject) : { payload: data };
|
|
|
|
returnData.push({
|
|
json,
|
|
pairedItem: itemIndex,
|
|
});
|
|
}
|
|
|
|
if (operation === 'decode') {
|
|
const token = getToken(this, itemIndex);
|
|
|
|
const data = jwt.decode(token, { complete: options.complete });
|
|
|
|
const json = options.complete && data ? (data as IDataObject) : { payload: data };
|
|
|
|
returnData.push({
|
|
json,
|
|
pairedItem: itemIndex,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
if (error.message === 'invalid signature') {
|
|
error = new NodeOperationError(this.getNode(), "The JWT token can't be verified", {
|
|
itemIndex,
|
|
description:
|
|
'Be sure that the provided JWT token is correctly encoded and matches the selected credentials',
|
|
});
|
|
}
|
|
if (this.continueOnFail()) {
|
|
returnData.push({
|
|
json: this.getInputData(itemIndex)[0].json,
|
|
error,
|
|
pairedItem: itemIndex,
|
|
});
|
|
continue;
|
|
}
|
|
if (error.context) {
|
|
error.context.itemIndex = itemIndex;
|
|
throw error;
|
|
}
|
|
throw new NodeOperationError(this.getNode(), error, {
|
|
itemIndex,
|
|
});
|
|
}
|
|
}
|
|
|
|
return [returnData];
|
|
}
|
|
}
|