mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat(JWT Node): New node (#9005)
Co-authored-by: Giulio Andreini <andreini@netseven.it>
This commit is contained in:
457
packages/nodes-base/nodes/Jwt/Jwt.node.ts
Normal file
457
packages/nodes-base/nodes/Jwt/Jwt.node.ts
Normal file
@@ -0,0 +1,457 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { 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: ['main'],
|
||||
outputs: ['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: '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('jwtAuth')) as {
|
||||
keyType: 'passphrase' | 'pemKey';
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
secret: string;
|
||||
algorithm: jwt.Algorithm;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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 token = jwt.sign(payload, secretOrPrivateKey, {
|
||||
algorithm: options.algorithm ?? credentials.algorithm,
|
||||
});
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user