From 0a9f6b3de8f5548700e736b7d5f1d31c229595f5 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:16:48 +0300 Subject: [PATCH] feat(JWT Node): New node (#9005) Co-authored-by: Giulio Andreini --- .../credentials/jwtAuth.credentials.ts | 3 +- packages/nodes-base/nodes/Jwt/Jwt.node.json | 19 + packages/nodes-base/nodes/Jwt/Jwt.node.ts | 457 ++++++++++++++++++ packages/nodes-base/nodes/Jwt/jwt.svg | 112 +++++ .../nodes/Jwt/test/Jwt.node.test.ts | 4 + .../nodes/Jwt/test/jwt.workflow.json | 412 ++++++++++++++++ .../nodes-base/nodes/Webhook/Webhook.node.ts | 2 +- packages/nodes-base/package.json | 1 + .../test/nodes/FakeCredentialsMap.ts | 50 ++ packages/nodes-base/utils/utilities.ts | 7 +- packages/workflow/src/Interfaces.ts | 3 +- packages/workflow/src/TypeValidation.ts | 24 + 12 files changed, 1088 insertions(+), 6 deletions(-) create mode 100644 packages/nodes-base/nodes/Jwt/Jwt.node.json create mode 100644 packages/nodes-base/nodes/Jwt/Jwt.node.ts create mode 100644 packages/nodes-base/nodes/Jwt/jwt.svg create mode 100644 packages/nodes-base/nodes/Jwt/test/Jwt.node.test.ts create mode 100644 packages/nodes-base/nodes/Jwt/test/jwt.workflow.json diff --git a/packages/nodes-base/credentials/jwtAuth.credentials.ts b/packages/nodes-base/credentials/jwtAuth.credentials.ts index d5c772fe5f..82fc9850dd 100644 --- a/packages/nodes-base/credentials/jwtAuth.credentials.ts +++ b/packages/nodes-base/credentials/jwtAuth.credentials.ts @@ -71,8 +71,7 @@ export class jwtAuth implements ICredentialType { displayName: 'Key Type', name: 'keyType', type: 'options', - description: - 'Choose either the secret passphrase for PEM encoded public keys for RSA and ECDSA', + description: 'Choose either the secret passphrase or PEM encoded public keys', options: [ { name: 'Passphrase', diff --git a/packages/nodes-base/nodes/Jwt/Jwt.node.json b/packages/nodes-base/nodes/Jwt/Jwt.node.json new file mode 100644 index 0000000000..83d0cad2b5 --- /dev/null +++ b/packages/nodes-base/nodes/Jwt/Jwt.node.json @@ -0,0 +1,19 @@ +{ + "node": "n8n-nodes-base.jwt", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Development"], + "alias": ["Token", "Key", "JSON", "Payload", "Sign", "Verify", "Decode"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/jwt" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.jwt/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/Jwt/Jwt.node.ts b/packages/nodes-base/nodes/Jwt/Jwt.node.ts new file mode 100644 index 0000000000..816a48da13 --- /dev/null +++ b/packages/nodes-base/nodes/Jwt/Jwt.node.ts @@ -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 { + 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]; + } +} diff --git a/packages/nodes-base/nodes/Jwt/jwt.svg b/packages/nodes-base/nodes/Jwt/jwt.svg new file mode 100644 index 0000000000..f29c89d2bd --- /dev/null +++ b/packages/nodes-base/nodes/Jwt/jwt.svg @@ -0,0 +1,112 @@ + + + + Group + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Group + + + + diff --git a/packages/nodes-base/nodes/Jwt/test/Jwt.node.test.ts b/packages/nodes-base/nodes/Jwt/test/Jwt.node.test.ts new file mode 100644 index 0000000000..5757cd2a2b --- /dev/null +++ b/packages/nodes-base/nodes/Jwt/test/Jwt.node.test.ts @@ -0,0 +1,4 @@ +import { testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers'; +const workflows = getWorkflowFilenames(__dirname); + +describe('Test Jwt Node', () => testWorkflows(workflows)); diff --git a/packages/nodes-base/nodes/Jwt/test/jwt.workflow.json b/packages/nodes-base/nodes/Jwt/test/jwt.workflow.json new file mode 100644 index 0000000000..12cba9fefc --- /dev/null +++ b/packages/nodes-base/nodes/Jwt/test/jwt.workflow.json @@ -0,0 +1,412 @@ +{ + "name": "My workflow 31", + "nodes": [ + { + "parameters": {}, + "id": "fcc3e9dc-90c9-4b26-9b44-e661e0ebf658", + "name": "When clicking \"Test workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 780, + 380 + ] + }, + { + "parameters": { + "claims": { + "audience": "test", + "issuer": "test", + "jwtid": "123", + "subject": "test" + }, + "options": {} + }, + "id": "6b1ba38f-60d8-482a-a7d4-4fee7054334b", + "name": "JWT", + "type": "n8n-nodes-base.jwt", + "typeVersion": 1, + "position": [ + 1000, + 380 + ], + "credentials": { + "jwtAuth": { + "id": "AosB7WdhmVA3be8t", + "name": "JWT Auth test" + } + } + }, + { + "parameters": { + "operation": "decode", + "token": "={{ $json.token }}", + "options": {} + }, + "id": "d07b9335-29a0-41bc-a6c4-0232d94a0559", + "name": "JWT1", + "type": "n8n-nodes-base.jwt", + "typeVersion": 1, + "position": [ + 1260, + 220 + ], + "credentials": { + "jwtAuth": { + "id": "AosB7WdhmVA3be8t", + "name": "JWT Auth test" + } + } + }, + { + "parameters": { + "operation": "verify", + "token": "={{ $json.token }}", + "options": { + "complete": true, + "ignoreExpiration": true, + "ignoreNotBefore": true + } + }, + "id": "b6228805-f0c1-479d-bbec-cefcde7298e3", + "name": "JWT2", + "type": "n8n-nodes-base.jwt", + "typeVersion": 1, + "position": [ + 1280, + 440 + ], + "credentials": { + "jwtAuth": { + "id": "AosB7WdhmVA3be8t", + "name": "JWT Auth test" + } + } + }, + { + "parameters": { + "useJson": true, + "options": {} + }, + "id": "56ede43f-6771-4341-8c17-ba4b9185711a", + "name": "JWT3", + "type": "n8n-nodes-base.jwt", + "typeVersion": 1, + "position": [ + 1000, + 640 + ], + "credentials": { + "jwtAuth": { + "id": "G45TOKX5kBEraTr1", + "name": "JWT Auth test PEM" + } + } + }, + { + "parameters": { + "operation": "decode", + "token": "={{ $json.token }}", + "options": {} + }, + "id": "1b57af10-710f-4b86-9020-e1fc43a222cd", + "name": "JWT4", + "type": "n8n-nodes-base.jwt", + "typeVersion": 1, + "position": [ + 1280, + 640 + ], + "credentials": { + "jwtAuth": { + "id": "G45TOKX5kBEraTr1", + "name": "JWT Auth test PEM" + } + } + }, + { + "parameters": { + "operation": "verify", + "token": "={{ $json.token }}", + "options": {} + }, + "id": "9a658330-ecc3-4007-9524-534ad15a3a40", + "name": "JWT5", + "type": "n8n-nodes-base.jwt", + "typeVersion": 1, + "position": [ + 1280, + 860 + ], + "credentials": { + "jwtAuth": { + "id": "G45TOKX5kBEraTr1", + "name": "JWT Auth test PEM" + } + } + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "1289f607-d46f-45f5-953a-1492f3b50bbd", + "name": "payload.audience", + "value": "={{ $json.payload.audience }}", + "type": "string" + }, + { + "id": "e32ae71b-62ca-45e5-8351-2fd9ab7451ef", + "name": "payload.jwtid", + "value": "={{ $json.payload.jwtid }}", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "51a6c4b9-ebae-4ffa-a870-9915c3304cd5", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.3, + "position": [ + 1500, + 220 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "3ff845d5-9c2c-4744-bc33-a7318b4741fc", + "name": "payload.audience", + "value": "={{ $json.payload.audience }}", + "type": "string" + }, + { + "id": "d1206579-634e-472a-9190-a176cf2477a1", + "name": "payload.jwtid", + "value": "={{ $json.payload.jwtid }}", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "9e97ea34-ec32-4935-b116-e40815567e1c", + "name": "Edit Fields1", + "type": "n8n-nodes-base.set", + "typeVersion": 3.3, + "position": [ + 1520, + 440 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "46ed5c03-d41a-4387-988e-2ed3821f32d4", + "name": "payload.my_field_1", + "value": "={{ $json.payload.my_field_1 }}", + "type": "string" + }, + { + "id": "0007786e-3f93-4146-8e10-32ab8e088b81", + "name": "payload.my_field_2", + "value": "={{ $json.payload.my_field_2 }}", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "f205e447-2a98-48ef-bba5-12e9aecab90e", + "name": "Edit Fields2", + "type": "n8n-nodes-base.set", + "typeVersion": 3.3, + "position": [ + 1520, + 860 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "fe90f817-1b89-409c-992c-e42070fc67bf", + "name": "payload.my_field_1", + "value": "={{ $json.payload.my_field_1 }}", + "type": "string" + }, + { + "id": "1b80e8b3-3004-4959-8c6c-0d36176579ea", + "name": "payload.my_field_2", + "value": "={{ $json.payload.my_field_2 }}", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "0788f03e-0c1c-4e21-a408-9441930cd82f", + "name": "Edit Fields3", + "type": "n8n-nodes-base.set", + "typeVersion": 3.3, + "position": [ + 1520, + 640 + ] + } + ], + "pinData": { + "Edit Fields": [ + { + "json": { + "payload": { + "audience": "test", + "jwtid": "123" + } + } + } + ], + "Edit Fields1": [ + { + "json": { + "payload": { + "audience": "test", + "jwtid": "123" + } + } + } + ], + "Edit Fields3": [ + { + "json": { + "payload": { + "my_field_1": "value 1", + "my_field_2": "value 2" + } + } + } + ], + "Edit Fields2": [ + { + "json": { + "payload": { + "my_field_1": "value 1", + "my_field_2": "value 2" + } + } + } + ] + }, + "connections": { + "When clicking \"Test workflow\"": { + "main": [ + [ + { + "node": "JWT", + "type": "main", + "index": 0 + }, + { + "node": "JWT3", + "type": "main", + "index": 0 + } + ] + ] + }, + "JWT": { + "main": [ + [ + { + "node": "JWT1", + "type": "main", + "index": 0 + }, + { + "node": "JWT2", + "type": "main", + "index": 0 + } + ] + ] + }, + "JWT3": { + "main": [ + [ + { + "node": "JWT4", + "type": "main", + "index": 0 + }, + { + "node": "JWT5", + "type": "main", + "index": 0 + } + ] + ] + }, + "JWT1": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + }, + "JWT2": { + "main": [ + [ + { + "node": "Edit Fields1", + "type": "main", + "index": 0 + } + ] + ] + }, + "JWT4": { + "main": [ + [ + { + "node": "Edit Fields3", + "type": "main", + "index": 0 + } + ] + ] + }, + "JWT5": { + "main": [ + [ + { + "node": "Edit Fields2", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "f70ffef1-3c5d-4991-a3aa-b4141b93e4cb", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd" + }, + "id": "H0sZEXDuE7VIP5vz", + "tags": [] +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Webhook/Webhook.node.ts b/packages/nodes-base/nodes/Webhook/Webhook.node.ts index 90d832ec5b..31a877941a 100644 --- a/packages/nodes-base/nodes/Webhook/Webhook.node.ts +++ b/packages/nodes-base/nodes/Webhook/Webhook.node.ts @@ -287,7 +287,7 @@ export class Webhook extends Node { if (expectedAuth.keyType === 'passphrase') { secretOrPublicKey = expectedAuth.secret; } else { - secretOrPublicKey = formatPrivateKey(expectedAuth.publicKey); + secretOrPublicKey = formatPrivateKey(expectedAuth.publicKey, true); } try { diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 7d00f63d48..6b98c95b92 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -574,6 +574,7 @@ "dist/nodes/Jira/Jira.node.js", "dist/nodes/Jira/JiraTrigger.node.js", "dist/nodes/JotForm/JotFormTrigger.node.js", + "dist/nodes/Jwt/Jwt.node.js", "dist/nodes/Kafka/Kafka.node.js", "dist/nodes/Kafka/KafkaTrigger.node.js", "dist/nodes/Keap/Keap.node.js", diff --git a/packages/nodes-base/test/nodes/FakeCredentialsMap.ts b/packages/nodes-base/test/nodes/FakeCredentialsMap.ts index 3b13075233..fbf6d00b71 100644 --- a/packages/nodes-base/test/nodes/FakeCredentialsMap.ts +++ b/packages/nodes-base/test/nodes/FakeCredentialsMap.ts @@ -6,6 +6,51 @@ export const FAKE_CREDENTIALS_DATA = { [JSON.stringify({ id: '20', name: 'Airtable account' })]: { apiKey: 'key456', }, + [JSON.stringify({ id: 'G45TOKX5kBEraTr1', name: 'JWT Auth test PEM' })]: { + keyType: 'pemKey', + privateKey: ` +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCfw0m1K+M1/6Tw +CvLYDv0gmxa+reEdKBfT0/hfkkjFWqbMRo0f4CQ3PwrOavS+80PDy6nVL21BmGev +w1bF7KXmqOzr+yKOUJ8A4u6vXUQKzVSFBqb2YZmZL1s7va9aaO6pVANTbmYHpTjh +SBnBrXgidVOXNX1c+IG+OZZMiTFWg4lJTE9rvMbLh4o5FPwdZlA1rLAux4KXVNNr +mE7T5/tsuikR06KMJS6V6YR4PZmTsy/3D2clADXDCtbUdEe0eBYoUmRLMhRL6umq +h98Dsm5ZG+YB2dn0ThR/g7DPVwvmnrK2S5e4hpqFYxQ8V8pGx7dQLsc/utbvsn32 +dctGivkFAgMBAAECggEABDB0QdZtRlC2S/8VgBnghFbcwVJA6WlQOqM/y43D77zh +S9D7yV6wxGwYRfJjCZDKcZtpECiGtmYfLeoy38zFSueaEtQKg23UxYqt1JZe/uOE +eFqEzUgg5XXq8AWY0AeZXoJP9gOalE++TpX76uq4EDtAXmIuL95qVIkhCk+8pfaR +avLcejnyYGSJAG1J9pXHNChXXDVPd7PrIa20A44osvusifVMlcIYM3qkv167ULzX +4nu2hZwlNxGKtpVPldFY/qu5S7SdLo/2BQinrMSSKRSFihA4Uuod8GK0+UwjE4gO +TD15bjqIcadlAYV6bn34sHnMU9hjhPB5NyXiINYdsQKBgQDNu0XFjYyAgwORaZYs +jVTJg+fQ9wM7xnlfxXCVb9eoOGF0blW9VjIEz8lLjmPlPFFVS+EPN0andHHqH4t5 +SQZVZxgNMcodWs8BJTVZUkXa+IljHXT1Vkb2zvtH96ADzs3c43+tNpmKhjG3XK1U +rL/v8feU31nwQb7imOmYmzbHCQKBgQDGzJ/pRLByB24W6FRfHIItP81vVg5ckCXi +sIhzHUdUmTdVbdAxeS6IW2oAc/IuksvmiZMLYsm+sIFFePJQrBsoD41R5VsFcJqE +o5x0DUzbOzqaV4HPOHDniibudyryZKnBvkXlCjyCv4iPKaFhH4T1aB+wdK4pJPo2 +fyABs2lFHQKBgQDHz6IFK+QREK3PdhA/twhpK65jWvUOAkbxyE3/JX/7xr6IGX02 +hdfQqoqj0midRMbto+OzJol9q+/TZs3MfysimR1X+0qE1iSExUGaPfjQomC1He/x +M9l6bi7Jh+wmpp10cpQXhBb93jW9E8rYmWtVPNmsAn1UhlZBuCfwapd6GQKBgATM +f7ezzsaR41huN0ssdv/8oErluucFG8UDGegddtFV+X34bqQjFrp36nEkW15AcOeZ +vpDxy4js3dH9f2vvG6C172VgsffJphE5mdc7UvWf0mRTZHDKHf+Y2CO9gK3lPCvP +GgTTYG6PjQ5XpOuhRSZfYxRxXJrlp5yVKQKhgBMJAoGBAMc6ktd0iqHAYCW3d9QP +e618RiMlVIYZIUlLWAUQWQSf3linqMjo1rCbbI/lSxE216XwI/VBX50gg/Oy3aUl +CibHHk2aKGlxVxe0Huv5gcjbZgVh1EMi4oxh4600IrWRH1Uz5AleXnheNiappKnA +lOMhy99LXMlAOL7qOBnZHgrm +-----END PRIVATE KEY----- + `, + publicKey: ` +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn8NJtSvjNf+k8Ary2A79 +IJsWvq3hHSgX09P4X5JIxVqmzEaNH+AkNz8Kzmr0vvNDw8up1S9tQZhnr8NWxeyl +5qjs6/sijlCfAOLur11ECs1UhQam9mGZmS9bO72vWmjuqVQDU25mB6U44UgZwa14 +InVTlzV9XPiBvjmWTIkxVoOJSUxPa7zGy4eKORT8HWZQNaywLseCl1TTa5hO0+f7 +bLopEdOijCUulemEeD2Zk7Mv9w9nJQA1wwrW1HRHtHgWKFJkSzIUS+rpqoffA7Ju +WRvmAdnZ9E4Uf4Owz1cL5p6ytkuXuIaahWMUPFfKRse3UC7HP7rW77J99nXLRor5 +BQIDAQAB +-----END PUBLIC KEY----- + `, + algorithm: 'RS256', + }, airtableApi: { apiKey: 'key123', }, @@ -45,4 +90,9 @@ export const FAKE_CREDENTIALS_DATA = { refresh_token: 'REFRESHTOKEN', }, }, + jwtAuth: { + keyType: 'passphrase', + secret: 'baz', + algorithm: 'HS256', + }, } as const; diff --git a/packages/nodes-base/utils/utilities.ts b/packages/nodes-base/utils/utilities.ts index deed6b9c84..6783a5d8b6 100644 --- a/packages/nodes-base/utils/utilities.ts +++ b/packages/nodes-base/utils/utilities.ts @@ -223,14 +223,17 @@ export const keysToLowercase = (headers: T) => { * @param privateKey - The private key to format. * @returns The formatted private key. */ -export function formatPrivateKey(privateKey: string): string { +export function formatPrivateKey(privateKey: string, keyIsPublic = false): string { + let regex = /(PRIVATE KEY|CERTIFICATE)/; + if (keyIsPublic) { + regex = /(PUBLIC KEY)/; + } if (!privateKey || /\n/.test(privateKey)) { return privateKey; } let formattedPrivateKey = ''; const parts = privateKey.split('-----').filter((item) => item !== ''); parts.forEach((part) => { - const regex = /(PRIVATE KEY|CERTIFICATE)/; if (regex.test(part)) { formattedPrivateKey += `-----${part}-----`; } else { diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 2c6f3741f5..84ccddbd12 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2341,7 +2341,8 @@ export type FieldType = | 'array' | 'object' | 'options' - | 'url'; + | 'url' + | 'jwt'; export type ValidationResult = { valid: boolean; diff --git a/packages/workflow/src/TypeValidation.ts b/packages/workflow/src/TypeValidation.ts index 273d4bb0e6..01a2fdb35e 100644 --- a/packages/workflow/src/TypeValidation.ts +++ b/packages/workflow/src/TypeValidation.ts @@ -158,6 +158,20 @@ export const tryToParseUrl = (value: unknown): string => { return String(value); }; +export const tryToParseJwt = (value: unknown): string => { + const error = new ApplicationError(`The value "${String(value)}" is not a valid JWT token.`, { + extra: { value }, + }); + + if (!value) throw error; + + const jwtPattern = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_.+/=]*$/; + + if (!jwtPattern.test(String(value))) throw error; + + return String(value); +}; + type ValidateFieldTypeOptions = Partial<{ valueOptions: INodePropertyOptions[]; strict: boolean; @@ -279,6 +293,16 @@ export const validateFieldType = ( return { valid: false, errorMessage: defaultErrorMessage }; } } + case 'jwt': { + try { + return { valid: true, newValue: tryToParseJwt(value) }; + } catch (e) { + return { + valid: false, + errorMessage: 'Value is not a valid JWT token', + }; + } + } default: { return { valid: true, newValue: value }; }