var timespan = require('./lib/timespan'); var jws = require('jws'); var includes = require('lodash.includes'); var isBoolean = require('lodash.isboolean'); var isInteger = require('lodash.isinteger'); var isNumber = require('lodash.isnumber'); var isPlainObject = require('lodash.isplainobject'); var isString = require('lodash.isstring'); var once = require('lodash.once'); var sign_options_schema = { expiresIn: { isValid: function(value) { return isInteger(value) || (isString(value) && value); }, message: '"expiresIn" should be a number of seconds or string representing a timespan' }, notBefore: { isValid: function(value) { return isInteger(value) || (isString(value) && value); }, message: '"notBefore" should be a number of seconds or string representing a timespan' }, audience: { isValid: function(value) { return isString(value) || Array.isArray(value); }, message: '"audience" must be a string or array' }, algorithm: { isValid: includes.bind(null, ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none']), message: '"algorithm" must be a valid string enum value' }, header: { isValid: isPlainObject, message: '"header" must be an object' }, encoding: { isValid: isString, message: '"encoding" must be a string' }, issuer: { isValid: isString, message: '"issuer" must be a string' }, subject: { isValid: isString, message: '"subject" must be a string' }, jwtid: { isValid: isString, message: '"jwtid" must be a string' }, noTimestamp: { isValid: isBoolean, message: '"noTimestamp" must be a boolean' }, keyid: { isValid: isString, message: '"keyid" must be a string' }, mutatePayload: { isValid: isBoolean, message: '"mutatePayload" must be a boolean' } }; var registered_claims_schema = { iat: { isValid: isNumber, message: '"iat" should be a number of seconds' }, exp: { isValid: isNumber, message: '"exp" should be a number of seconds' }, nbf: { isValid: isNumber, message: '"nbf" should be a number of seconds' } }; function validate(schema, allowUnknown, object, parameterName) { if (!isPlainObject(object)) { throw new Error('Expected "' + parameterName + '" to be a plain object.'); } Object.keys(object) .forEach(function(key) { var validator = schema[key]; if (!validator) { if (!allowUnknown) { throw new Error('"' + key + '" is not allowed in "' + parameterName + '"'); } return; } if (!validator.isValid(object[key])) { throw new Error(validator.message); } }); } function validateOptions(options) { return validate(sign_options_schema, false, options, 'options'); } function validatePayload(payload) { return validate(registered_claims_schema, true, payload, 'payload'); } var options_to_payload = { 'audience': 'aud', 'issuer': 'iss', 'subject': 'sub', 'jwtid': 'jti' }; var options_for_objects = [ 'expiresIn', 'notBefore', 'noTimestamp', 'audience', 'issuer', 'subject', 'jwtid', ]; module.exports = function (payload, secretOrPrivateKey, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } else { options = options || {}; } var isObjectPayload = typeof payload === 'object' && !Buffer.isBuffer(payload); var header = Object.assign({ alg: options.algorithm || 'HS256', typ: isObjectPayload ? 'JWT' : undefined, kid: options.keyid }, options.header); function failure(err) { if (callback) { return callback(err); } throw err; } if (!secretOrPrivateKey && options.algorithm !== 'none') { return failure(new Error('secretOrPrivateKey must have a value')); } if (typeof payload === 'undefined') { return failure(new Error('payload is required')); } else if (isObjectPayload) { try { validatePayload(payload); } catch (error) { return failure(error); } if (!options.mutatePayload) { payload = Object.assign({},payload); } } else { var invalid_options = options_for_objects.filter(function (opt) { return typeof options[opt] !== 'undefined'; }); if (invalid_options.length > 0) { return failure(new Error('invalid ' + invalid_options.join(',') + ' option for ' + (typeof payload ) + ' payload')); } } if (typeof payload.exp !== 'undefined' && typeof options.expiresIn !== 'undefined') { return failure(new Error('Bad "options.expiresIn" option the payload already has an "exp" property.')); } if (typeof payload.nbf !== 'undefined' && typeof options.notBefore !== 'undefined') { return failure(new Error('Bad "options.notBefore" option the payload already has an "nbf" property.')); } try { validateOptions(options); } catch (error) { return failure(error); } var timestamp = payload.iat || Math.floor(Date.now() / 1000); if (!options.noTimestamp) { payload.iat = timestamp; } else { delete payload.iat; } if (typeof options.notBefore !== 'undefined') { try { payload.nbf = timespan(options.notBefore, timestamp); } catch (err) { return failure(err); } if (typeof payload.nbf === 'undefined') { return failure(new Error('"notBefore" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60')); } } if (typeof options.expiresIn !== 'undefined' && typeof payload === 'object') { try { payload.exp = timespan(options.expiresIn, timestamp); } catch (err) { return failure(err); } if (typeof payload.exp === 'undefined') { return failure(new Error('"expiresIn" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60')); } } Object.keys(options_to_payload).forEach(function (key) { var claim = options_to_payload[key]; if (typeof options[key] !== 'undefined') { if (typeof payload[claim] !== 'undefined') { return failure(new Error('Bad "options.' + key + '" option. The payload already has an "' + claim + '" property.')); } payload[claim] = options[key]; } }); var encoding = options.encoding || 'utf8'; if (typeof callback === 'function') { callback = callback && once(callback); jws.createSign({ header: header, privateKey: secretOrPrivateKey, payload: payload, encoding: encoding }).once('error', callback) .once('done', function (signature) { callback(null, signature); }); } else { return jws.sign({header: header, payload: payload, secret: secretOrPrivateKey, encoding: encoding}); } };