diff --git a/package.json b/package.json index a011971..edf5ef1 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", "scripts": { - "test": "mocha --require test/util/fakeDate && nsp check" + "test": "mocha --require test/util/fakeDate && nsp check && cost-of-modules" }, "repository": { "type": "git", @@ -19,8 +19,14 @@ "url": "https://github.com/auth0/node-jsonwebtoken/issues" }, "dependencies": { - "joi": "^6.10.1", "jws": "^3.1.4", + "lodash.includes": "^4.3.0", + "lodash.isarray": "^4.0.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.0.0", "xtend": "^4.0.1" @@ -29,6 +35,7 @@ "atob": "^1.1.2", "chai": "^1.10.0", "conventional-changelog": "~1.1.0", + "cost-of-modules": "^1.0.1", "mocha": "^2.1.0", "nsp": "^2.6.2", "sinon": "^1.15.4" diff --git a/sign.js b/sign.js index 38fa78e..370b5d7 100644 --- a/sign.js +++ b/sign.js @@ -1,29 +1,53 @@ -var Joi = require('joi'); var timespan = require('./lib/timespan'); var xtend = require('xtend'); var jws = require('jws'); +var includes = require('lodash.includes'); +var isArray = require('lodash.isarray'); +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 = Joi.object().keys({ - expiresIn: [Joi.number().integer(), Joi.string()], - notBefore: [Joi.number().integer(), Joi.string()], - audience: [Joi.string(), Joi.array()], - algorithm: Joi.string().valid('RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none'), - header: Joi.object(), - encoding: Joi.string(), - issuer: Joi.string(), - subject: Joi.string(), - jwtid: Joi.string(), - noTimestamp: Joi.boolean(), - keyid: Joi.string() -}); - -var registered_claims_schema = Joi.object().keys({ - iat: Joi.number(), - exp: Joi.number(), - nbf: Joi.number() -}).unknown(); +var sign_options_schema = { + expiresIn: { isValid: function(value) { return isInteger(value) || isString(value); }, message: '"expiresIn" should be a number of seconds or string representing a timespan' }, + notBefore: { isValid: function(value) { return isInteger(value) || isString(value); }, message: '"notBefore" should be a number of seconds or string representing a timespan' }, + audience: { isValid: function(value) { return isString(value) || 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' }, +}; + +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, unknown, object) { + if (!isPlainObject(object)) { + throw new Error('Expected object'); + } + Object.keys(object) + .forEach(function(key) { + var validator = schema[key]; + if (!validator) { + if (!unknown) { + throw new Error('"' + key + '" is not allowed'); + } + return; + } + if (!validator.isValid(object[key])) { + throw new Error(validator.message); + } + }); +} var options_to_payload = { 'audience': 'aud', @@ -73,12 +97,12 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { if (typeof payload === 'undefined') { return failure(new Error('payload is required')); } else if (isObjectPayload) { - var payload_validation_result = registered_claims_schema.validate(payload); - - if (payload_validation_result.error) { - return failure(payload_validation_result.error); + try { + validate(registered_claims_schema, true, payload); + } + catch (error) { + return failure(error); } - payload = xtend(payload); } else { var invalid_options = options_for_objects.filter(function (opt) { @@ -98,10 +122,11 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { return failure(new Error('Bad "options.notBefore" option the payload already has an "nbf" property.')); } - var validation_result = sign_options_schema.validate(options); - - if (validation_result.error) { - return failure(validation_result.error); + try { + validate(sign_options_schema, false, options); + } + catch (error) { + return failure(error); } var timestamp = payload.iat || Math.floor(Date.now() / 1000); diff --git a/test/expires_format.tests.js b/test/expires_format.tests.js index 42d18a5..7e542c4 100644 --- a/test/expires_format.tests.js +++ b/test/expires_format.tests.js @@ -33,7 +33,7 @@ describe('expires option', function() { it('should throw if expires is not an string or number', function () { expect(function () { jwt.sign({foo: 123}, '123', { expiresIn: { crazy : 213 } }); - }).to.throw(/"expiresIn" must be a number/); + }).to.throw(/"expiresIn" should be a number of seconds or string representing a timespan/); }); it('should throw an error if expiresIn and exp are provided', function () { diff --git a/test/iat.tests.js b/test/iat.tests.js index 6d2c2c0..72d7a1a 100644 --- a/test/iat.tests.js +++ b/test/iat.tests.js @@ -12,11 +12,4 @@ describe('iat', function () { expect(result.exp).to.be.closeTo(iat + expiresIn, 0.2); }); - - it('should throw if iat is not a number', function () { - expect(function () { - jwt.sign({foo: 123, iat: 'hello'}, '123'); - }).to.throw(/"iat" must be a number/); - }); - }); \ No newline at end of file diff --git a/test/schema.tests.js b/test/schema.tests.js new file mode 100644 index 0000000..1f79994 --- /dev/null +++ b/test/schema.tests.js @@ -0,0 +1,136 @@ +var jwt = require('../index'); +var expect = require('chai').expect; +var fs = require('fs'); + +describe('schema', function() { + + describe('sign options', function() { + + var cert_rsa_priv = fs.readFileSync(__dirname + '/rsa-private.pem'); + var cert_ecdsa_priv = fs.readFileSync(__dirname + '/ecdsa-private.pem'); + + function sign(options) { + var isEcdsa = options.algorithm && options.algorithm.indexOf('ES') === 0; + jwt.sign({foo: 123}, isEcdsa ? cert_ecdsa_priv : cert_rsa_priv, options); + } + + it('should validate expiresIn', function () { + expect(function () { + sign({ expiresIn: '1 monkey' }); + }).to.throw(/"expiresIn" should be a number of seconds or string representing a timespan/); + expect(function () { + sign({ expiresIn: 1.1 }); + }).to.throw(/"expiresIn" should be a number of seconds or string representing a timespan/); + sign({ expiresIn: '10s' }); + sign({ expiresIn: 10 }); + }); + + it('should validate notBefore', function () { + expect(function () { + sign({ notBefore: '1 monkey' }); + }).to.throw(/"notBefore" should be a number of seconds or string representing a timespan/); + expect(function () { + sign({ notBefore: 1.1 }); + }).to.throw(/"notBefore" should be a number of seconds or string representing a timespan/); + sign({ notBefore: '10s' }); + sign({ notBefore: 10 }); + }); + + it('should validate audience', function () { + expect(function () { + sign({ audience: 10 }); + }).to.throw(/"audience" must be a string or array/); + sign({ audience: 'urn:foo' }); + sign({ audience: ['urn:foo'] }); + }); + + it('should validate algorithm', function () { + expect(function () { + sign({ algorithm: 'foo' }); + }).to.throw(/"algorithm" must be a valid string enum value/); + sign({algorithm: 'RS256'}); + sign({algorithm: 'RS384'}); + sign({algorithm: 'RS512'}); + sign({algorithm: 'ES256'}); + sign({algorithm: 'ES384'}); + sign({algorithm: 'ES512'}); + sign({algorithm: 'HS256'}); + sign({algorithm: 'HS384'}); + sign({algorithm: 'HS512'}); + sign({algorithm: 'none'}); + }); + + it('should validate header', function () { + expect(function () { + sign({ header: 'foo' }); + }).to.throw(/"header" must be an object/); + sign({header: {}}); + }); + + it('should validate encoding', function () { + expect(function () { + sign({ encoding: 10 }); + }).to.throw(/"encoding" must be a string/); + sign({encoding: 'utf8'}); + }); + + it('should validate issuer', function () { + expect(function () { + sign({ issuer: 10 }); + }).to.throw(/"issuer" must be a string/); + sign({issuer: 'foo'}); + }); + + it('should validate subject', function () { + expect(function () { + sign({ subject: 10 }); + }).to.throw(/"subject" must be a string/); + sign({subject: 'foo'}); + }); + + it('should validate noTimestamp', function () { + expect(function () { + sign({ noTimestamp: 10 }); + }).to.throw(/"noTimestamp" must be a boolean/); + sign({noTimestamp: true}); + }); + + it('should validate keyid', function () { + expect(function () { + sign({ keyid: 10 }); + }).to.throw(/"keyid" must be a string/); + sign({keyid: 'foo'}); + }); + + }); + + describe('sign payload registered claims', function() { + + function sign(payload) { + jwt.sign(payload, 'foo123'); + } + + it('should validate iat', function () { + expect(function () { + sign({ iat: '1 monkey' }); + }).to.throw(/"iat" should be a number of seconds/); + sign({ iat: 10.1 }); + }); + + it('should validate exp', function () { + expect(function () { + sign({ exp: '1 monkey' }); + }).to.throw(/"exp" should be a number of seconds/); + sign({ exp: 10.1 }); + }); + + it('should validate nbf', function () { + expect(function () { + sign({ nbf: '1 monkey' }); + }).to.throw(/"nbf" should be a number of seconds/); + sign({ nbf: 10.1 }); + }); + + }); + +}); \ No newline at end of file