Skip to content

Commit

Permalink
Remove joi to shrink module size (#348)
Browse files Browse the repository at this point in the history
* Add cost-of-modules report to npm test

Results before any changes

┌─────────────┬────────────┬───────┐
│ name        │ children   │ size  │
├─────────────┼────────────┼───────┤
│ joi         │ 4          │ 3.12M │ <--!!!
├─────────────┼────────────┼───────┤
│ jws         │ 5          │ 0.18M │
├─────────────┼────────────┼───────┤
│ lodash.once │ 0          │ 0.01M │
├─────────────┼────────────┼───────┤
│ ms          │ 0          │ 0.01M │
├─────────────┼────────────┼───────┤
│ xtend       │ 0          │ 0.00M │
├─────────────┼────────────┼───────┤
│ 5 modules   │ 9 children │ 3.32M │
└─────────────┴────────────┴───────┘

* Replace joi with bespoke validator based on lodash

Dramatically reduces the module size without breaking ES5 compatability -

┌──────────────────────┬────────────┬───────┐
│ name                 │ children   │ size  │
├──────────────────────┼────────────┼───────┤
│ jws                  │ 5          │ 0.18M │
├──────────────────────┼────────────┼───────┤
│ lodash.includes      │ 0          │ 0.02M │
├──────────────────────┼────────────┼───────┤
│ lodash.once          │ 0          │ 0.01M │
├──────────────────────┼────────────┼───────┤
│ lodash.isinteger     │ 0          │ 0.01M │
├──────────────────────┼────────────┼───────┤
│ ms                   │ 0          │ 0.01M │
├──────────────────────┼────────────┼───────┤
│ lodash.isplainobject │ 0          │ 0.01M │
├──────────────────────┼────────────┼───────┤
│ xtend                │ 0          │ 0.00M │
├──────────────────────┼────────────┼───────┤
│ lodash.isstring      │ 0          │ 0.00M │
├──────────────────────┼────────────┼───────┤
│ lodash.isboolean     │ 0          │ 0.00M │
├──────────────────────┼────────────┼───────┤
│ lodash.isnumber      │ 0          │ 0.00M │
├──────────────────────┼────────────┼───────┤
│ lodash.isarray       │ 0          │ 0.00M │
├──────────────────────┼────────────┼───────┤
│ 11 modules           │ 5 children │ 0.25M │
└──────────────────────┴────────────┴───────┘

* Enhance validator error messages and add tests
  • Loading branch information
chrisprice authored and ziluvatar committed Sep 6, 2017
1 parent e54e53c commit 2e7e68d
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 39 deletions.
11 changes: 9 additions & 2 deletions package.json
Expand Up @@ -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",
Expand All @@ -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"
Expand All @@ -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"
Expand Down
83 changes: 54 additions & 29 deletions 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',
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion test/expires_format.tests.js
Expand Up @@ -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 () {
Expand Down
7 changes: 0 additions & 7 deletions test/iat.tests.js
Expand Up @@ -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/);
});

});
136 changes: 136 additions & 0 deletions 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 });
});

});

});

0 comments on commit 2e7e68d

Please sign in to comment.