UNPKG

node-red-contrib-jsonwebtoken

Version:

This node allows you to sign and validate JSON Web Token (JWT)

327 lines (313 loc) 17.9 kB
module.exports = function(RED) { var jwt = require('jsonwebtoken'); var jwksClient = require('jwks-rsa'); const Ajv = require("ajv") const ajv = new Ajv({ allErrors: true, messages: true, $data: true, strictTypes: false }) require("ajv-formats")(ajv); require("ajv-errors")(ajv); function JwtVerify(config) { RED.nodes.createNode(this,config); this.name = config.name; this.algorithms = config.algorithms; this.mode = config.mode; this.secret = config.secret; this.secretType = config.secretType; this.publicKey = config.publicKey; this.publicKeyType = config.publicKeyType; this.autoDetectjwkid = config.autoDetectjwkid; this.autoDetectjwkidType = config.autoDetectjwkidType; this.jwkid = config.jwkid; this.jwkidType = config.jwkidType; this.jwkurl = config.jwkurl; this.jwkurlType = config.jwkurlType; this.ignoreExpiration = config.ignoreExpiration; this.ignoreExpirationType = config.ignoreExpirationType; this.ignoreNotBefore = config.ignoreNotBefore; this.ignoreNotBeforeType = config.ignoreNotBeforeType; this.audience = config.audience; this.audienceType = config.audienceType; this.issuer = config.issuer; this.issuerType = config.issuerType; this.token = config.token; this.maxAge = config.maxAge; this.maxAgeType = config.maxAgeType; this.constraints = config.constraints; this.output = config.output; this.outputType = config.outputType; let node = this; let currentJwkUrl = null; let jwksClientInstance = null; async function getJwksClient(jwkurl) { if (jwkurl !== currentJwkUrl) { currentJwkUrl = jwkurl; jwksClientInstance = jwksClient({ jwksUri: jwkurl }); } return jwksClientInstance; } node.on('input', async function(msg) { try { const output = node.output || 'payload' let token = "" if(node.token == 'payload' || node.token == 'token'){ token = msg[node.token] } else if(node.token == 'bearer_authorization_header' && msg.req !== undefined && msg.req.get("authorization") !== undefined){ var authz = msg.req.get("authorization").split(" ") token = authz.length == 2 && authz[0] === 'Bearer' ? authz[1]: null }else if(node.token == 'query_params' && msg.req.query.access_token !== undefined){ token = msg.req.query.access_token; } if(!token) throw new Error('JWT token not found') const ignoreExpiration = RED.util.evaluateNodeProperty(node.ignoreExpiration, node.ignoreExpirationType, node) const ignoreNotBefore = RED.util.evaluateNodeProperty(node.ignoreNotBefore, node.ignoreNotBeforeType, node) let options = { ignoreExpiration, ignoreNotBefore } const audience = await evaluateNodeProperty(node.audience, node.audienceType, node, msg); if(audience){ options.audience = audience } const issuer = await evaluateNodeProperty(node.issuer, node.issuerType, node, msg); if(issuer){ options.issuer = issuer } const maxAge = await evaluateNodeProperty(node.maxAge, node.maxAgeType, node, msg); if(maxAge){ options.maxAge = maxAge } if(node.algorithms) options.algorithms = node.algorithms.split(',') switch (node.mode) { case 'secret':{ const secret = await evaluateNodeProperty(node.secret, node.secretType, node, msg); if(!secret) throw new Error('Value not found for variable "Secret"') msg[output] = jwt.verify(token, secret , options); }break; case 'public-key':{ const publicKey = await evaluateNodeProperty(node.publicKey, node.publicKeyType, node, msg); if(!publicKey) throw new Error('Value not found for variable "Private Key"') msg[output] = jwt.verify(token, publicKey , options); }break; case 'jwtid':{ const autoDetectjwkid = await evaluateNodeProperty(node.autoDetectjwkid, node.autoDetectjwkidType, node, msg); let jwkid; if(autoDetectjwkid){ if(!options.algorithms || options.algorithms.length === 0){ throw new Error('No algorithms specified. Please specify at least one algorithm when using auto-detect JWK KID.') } const decoded = jwt.decode(token, { complete: true }); if(!decoded || !decoded.header || !decoded.header.kid) throw new Error('JWT KID not found in the token header') jwkid = decoded.header.kid; } else { jwkid = await evaluateNodeProperty(node.jwkid, node.jwkidType, node, msg); if(!jwkid) throw new Error('Value not found for variable "JWK KID"') } const jwkurl = await evaluateNodeProperty(node.jwkurl, node.jwkurlType, node, msg); if(!jwkurl) throw new Error('Value not found for variable "JWK URL"') const client = await getJwksClient(jwkurl); const key = await client.getSigningKey(jwkid); const signingKey = key.getPublicKey() || key.rsaPublicKey(); if(!autoDetectjwkid){ options.algorithms = [key.alg] } msg[output] = jwt.verify(token, signingKey, options); }break; } if(node.constraints && Array.isArray(node.constraints)){ let schema = { type: "object", properties: {}, required: [], additionalProperties: true, errorMessage: {required:{}} }; schema = generateConstraints(node.constraints, schema) const validate = ajv.compile(schema) const valid = validate(msg[output]) if(!valid){ msg.payload = validate.errors.map(x=> x.message) node.error(JSON.stringify(validate.errors), msg); return; } } node.send(msg); } catch (error) { node.error(error.message, msg); } }); } RED.nodes.registerType("jwt verify", JwtVerify); function evaluateNodeProperty(value, type, node, msg){ return new Promise((resolve, reject)=>{ RED.util.evaluateNodeProperty(value, type, node, msg, (err, result) => { if (err) { reject(error) } else { resolve(result) } }) }) } function generateConstraints(constraints, schema){ constraints.forEach(x=>{ const propertyPath = RED.util.normalisePropertyExpression(x.property); const propertyName = propertyPath.pop(); const parentSchema = prepareNestedProperty(propertyPath, schema); typeContrain(propertyName, x.validator, x.value, x.typeValue,x.error, parentSchema) }) return schema } function prepareNestedProperty(propertyPath, schema){ let currentSchema = schema; propertyPath.forEach((part) => { currentSchema.required.push(part); if (!currentSchema.properties[part]) { currentSchema.properties[part] = { type: 'object', properties: {}, required: [], additionalProperties: true, errorMessage: { required: {} } }; } currentSchema = currentSchema.properties[part]; }); return currentSchema; } function typeContrain(property, validator, value, typeValue, error, schema) { if(!schema.properties[property]){ schema.properties[property] = { errorMessage: {} } } switch (validator) { case 'required':{ schema.required.push(property) schema.errorMessage.required[property] = error || `The ${property} field is required` }break; case 'type':{ schema.properties[property].type = value schema.properties[property].errorMessage.type = error || `The ${property} field must be of type ${value}` }break; case 'email':{ schema.properties[property].type = 'string' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type string` schema.properties[property].format = 'email' schema.properties[property].errorMessage.format = error || `The ${property} field is not a valid email address.` }break; case 'equal':{ schema.properties[property].const = value schema.properties[property].errorMessage.const = error || `The ${property} field must be equal to ${value}` }break; case 'equality':{ schema.properties[property].const = { $data: `1/${value}` } schema.properties[property].errorMessage.const = error || `The value of the ${property} field must be equal to the value of the ${value} field.` }break; case 'pattern':{ schema.properties[property].pattern = value schema.properties[property].errorMessage.pattern = error || `The field ${property} does not match the regular expression ${value}` }break; case 'maxlength':{ schema.properties[property].type = 'string' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type string` schema.properties[property].maxLength = parseInt(value) schema.properties[property].errorMessage.maxLength = error || `The ${property} field must have a maximum size of ${value}` }break; case 'minlength':{ schema.properties[property].type = 'string' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type string` schema.properties[property].minLength = parseInt(value) schema.properties[property].errorMessage.minLength = error || `The ${property} field must have a minimum size of ${value}` }break; case 'url':{ schema.properties[property].type = 'string' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type string` schema.properties[property].format = 'uri' schema.properties[property].errorMessage.format = error || `The ${property} field is not a valid URL.` }break; case 'date':{ schema.properties[property].format = 'date' schema.properties[property].errorMessage.format = error || `The ${property} field is not a valid date.` }break; case 'inclusion':{ schema.properties[property].type = 'array' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type array` schema.properties[property].enum = JSON.parse(value) schema.properties[property].errorMessage.enum = error || `The value of the ${property} field is not included in the ${value} list.` }break; case 'exclusion':{ schema.properties[property].type = 'array' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type array` schema.properties[property].not = {enum:JSON.parse(value)} schema.properties[property].errorMessage.not = error || `The value of the field ${property} cannot be included in the list ${value}.` }break; case 'ipv4':{ schema.properties[property].type = 'string' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type string` schema.properties[property].format = 'ipv4' schema.properties[property].errorMessage.format = error || `The ${property} field is not a valid IPv4.` }break; case 'ipv6':{ schema.properties[property].type = 'string' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type string` schema.properties[property].format = 'ipv6' schema.properties[property].errorMessage.format = error || `The ${property} field is not a valid IPv6.` }break; case 'hostname':{ schema.properties[property].type = 'string' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type string` schema.properties[property].format = 'hostname' schema.properties[property].errorMessage.format = error || `The ${property} field is not a valid hostname.` }break; case 'json':{ schema.properties[property].format = 'json-pointer' schema.properties[property].errorMessage.format = error || `The ${property} field is not a valid JSON.` }break; case 'maximum_number':{ schema.properties[property].type = 'number' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type number` schema.properties[property].maximum = parseFloat(value) schema.properties[property].errorMessage.maximum = error || `The value of the ${property} field cannot be greater than ${value}.` }break; case 'minimum_number':{ schema.properties[property].type = 'number' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type number` schema.properties[property].minimum = parseFloat(value) schema.properties[property].errorMessage.minimum = error || `The value of the ${property} field cannot be less than ${value}.` }break; case 'maximum_items':{ schema.properties[property].type = 'array' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type array` schema.properties[property].maxItems = parseFloat(value) schema.properties[property].errorMessage.maxItems = error || `The ${property} field cannot have more than ${value} elements..` }break; case 'minimum_items':{ schema.properties[property].type = 'array' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type array` schema.properties[property].minItems = parseFloat(value) schema.properties[property].errorMessage.minItems = error || `The ${property} field cannot have less than ${value} elements.` }break; case 'uuid':{ schema.properties[property].type = 'string' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type string` schema.properties[property].format = 'uuid' schema.properties[property].errorMessage.format = error || `The field ${property} is not a valid UUID` }break; case 'any_of':{ schema.properties[property].type = 'array' schema.properties[property].errorMessage.type = error || `The ${property} field must be of type array` const list = JSON.parse(value) schema.properties[property].contains = { anyOf: list.map(x=> { return { const: x } }) } schema.properties[property].errorMessage.contains = error || `Field ${property} does not contain any value from list ${value}` }break; } } }