Skip to content

Commit 2e7e68d

Browse files
chrispriceziluvatar
authored andcommitted
Remove joi to shrink module size (#348)
* 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
1 parent e54e53c commit 2e7e68d

File tree

5 files changed

+200
-39
lines changed

5 files changed

+200
-39
lines changed

package.json

+9-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "JSON Web Token implementation (symmetric and asymmetric)",
55
"main": "index.js",
66
"scripts": {
7-
"test": "mocha --require test/util/fakeDate && nsp check"
7+
"test": "mocha --require test/util/fakeDate && nsp check && cost-of-modules"
88
},
99
"repository": {
1010
"type": "git",
@@ -19,8 +19,14 @@
1919
"url": "https://github.com/auth0/node-jsonwebtoken/issues"
2020
},
2121
"dependencies": {
22-
"joi": "^6.10.1",
2322
"jws": "^3.1.4",
23+
"lodash.includes": "^4.3.0",
24+
"lodash.isarray": "^4.0.0",
25+
"lodash.isboolean": "^3.0.3",
26+
"lodash.isinteger": "^4.0.4",
27+
"lodash.isnumber": "^3.0.3",
28+
"lodash.isplainobject": "^4.0.6",
29+
"lodash.isstring": "^4.0.1",
2430
"lodash.once": "^4.0.0",
2531
"ms": "^2.0.0",
2632
"xtend": "^4.0.1"
@@ -29,6 +35,7 @@
2935
"atob": "^1.1.2",
3036
"chai": "^1.10.0",
3137
"conventional-changelog": "~1.1.0",
38+
"cost-of-modules": "^1.0.1",
3239
"mocha": "^2.1.0",
3340
"nsp": "^2.6.2",
3441
"sinon": "^1.15.4"

sign.js

+54-29
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,53 @@
1-
var Joi = require('joi');
21
var timespan = require('./lib/timespan');
32
var xtend = require('xtend');
43
var jws = require('jws');
4+
var includes = require('lodash.includes');
5+
var isArray = require('lodash.isarray');
6+
var isBoolean = require('lodash.isboolean');
7+
var isInteger = require('lodash.isinteger');
8+
var isNumber = require('lodash.isnumber');
9+
var isPlainObject = require('lodash.isplainobject');
10+
var isString = require('lodash.isstring');
511
var once = require('lodash.once');
612

7-
var sign_options_schema = Joi.object().keys({
8-
expiresIn: [Joi.number().integer(), Joi.string()],
9-
notBefore: [Joi.number().integer(), Joi.string()],
10-
audience: [Joi.string(), Joi.array()],
11-
algorithm: Joi.string().valid('RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none'),
12-
header: Joi.object(),
13-
encoding: Joi.string(),
14-
issuer: Joi.string(),
15-
subject: Joi.string(),
16-
jwtid: Joi.string(),
17-
noTimestamp: Joi.boolean(),
18-
keyid: Joi.string()
19-
});
20-
21-
var registered_claims_schema = Joi.object().keys({
22-
iat: Joi.number(),
23-
exp: Joi.number(),
24-
nbf: Joi.number()
25-
}).unknown();
13+
var sign_options_schema = {
14+
expiresIn: { isValid: function(value) { return isInteger(value) || isString(value); }, message: '"expiresIn" should be a number of seconds or string representing a timespan' },
15+
notBefore: { isValid: function(value) { return isInteger(value) || isString(value); }, message: '"notBefore" should be a number of seconds or string representing a timespan' },
16+
audience: { isValid: function(value) { return isString(value) || isArray(value); }, message: '"audience" must be a string or array' },
17+
algorithm: { isValid: includes.bind(null, ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none']), message: '"algorithm" must be a valid string enum value' },
18+
header: { isValid: isPlainObject, message: '"header" must be an object' },
19+
encoding: { isValid: isString, message: '"encoding" must be a string' },
20+
issuer: { isValid: isString, message: '"issuer" must be a string' },
21+
subject: { isValid: isString, message: '"subject" must be a string' },
22+
jwtid: { isValid: isString, message: '"jwtid" must be a string' },
23+
noTimestamp: { isValid: isBoolean, message: '"noTimestamp" must be a boolean' },
24+
keyid: { isValid: isString, message: '"keyid" must be a string' },
25+
};
26+
27+
var registered_claims_schema = {
28+
iat: { isValid: isNumber, message: '"iat" should be a number of seconds' },
29+
exp: { isValid: isNumber, message: '"exp" should be a number of seconds' },
30+
nbf: { isValid: isNumber, message: '"nbf" should be a number of seconds' }
31+
};
2632

33+
function validate(schema, unknown, object) {
34+
if (!isPlainObject(object)) {
35+
throw new Error('Expected object');
36+
}
37+
Object.keys(object)
38+
.forEach(function(key) {
39+
var validator = schema[key];
40+
if (!validator) {
41+
if (!unknown) {
42+
throw new Error('"' + key + '" is not allowed');
43+
}
44+
return;
45+
}
46+
if (!validator.isValid(object[key])) {
47+
throw new Error(validator.message);
48+
}
49+
});
50+
}
2751

2852
var options_to_payload = {
2953
'audience': 'aud',
@@ -73,12 +97,12 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) {
7397
if (typeof payload === 'undefined') {
7498
return failure(new Error('payload is required'));
7599
} else if (isObjectPayload) {
76-
var payload_validation_result = registered_claims_schema.validate(payload);
77-
78-
if (payload_validation_result.error) {
79-
return failure(payload_validation_result.error);
100+
try {
101+
validate(registered_claims_schema, true, payload);
102+
}
103+
catch (error) {
104+
return failure(error);
80105
}
81-
82106
payload = xtend(payload);
83107
} else {
84108
var invalid_options = options_for_objects.filter(function (opt) {
@@ -98,10 +122,11 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) {
98122
return failure(new Error('Bad "options.notBefore" option the payload already has an "nbf" property.'));
99123
}
100124

101-
var validation_result = sign_options_schema.validate(options);
102-
103-
if (validation_result.error) {
104-
return failure(validation_result.error);
125+
try {
126+
validate(sign_options_schema, false, options);
127+
}
128+
catch (error) {
129+
return failure(error);
105130
}
106131

107132
var timestamp = payload.iat || Math.floor(Date.now() / 1000);

test/expires_format.tests.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe('expires option', function() {
3333
it('should throw if expires is not an string or number', function () {
3434
expect(function () {
3535
jwt.sign({foo: 123}, '123', { expiresIn: { crazy : 213 } });
36-
}).to.throw(/"expiresIn" must be a number/);
36+
}).to.throw(/"expiresIn" should be a number of seconds or string representing a timespan/);
3737
});
3838

3939
it('should throw an error if expiresIn and exp are provided', function () {

test/iat.tests.js

-7
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,4 @@ describe('iat', function () {
1212
expect(result.exp).to.be.closeTo(iat + expiresIn, 0.2);
1313
});
1414

15-
16-
it('should throw if iat is not a number', function () {
17-
expect(function () {
18-
jwt.sign({foo: 123, iat: 'hello'}, '123');
19-
}).to.throw(/"iat" must be a number/);
20-
});
21-
2215
});

test/schema.tests.js

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
var jwt = require('../index');
2+
var expect = require('chai').expect;
3+
var fs = require('fs');
4+
5+
describe('schema', function() {
6+
7+
describe('sign options', function() {
8+
9+
var cert_rsa_priv = fs.readFileSync(__dirname + '/rsa-private.pem');
10+
var cert_ecdsa_priv = fs.readFileSync(__dirname + '/ecdsa-private.pem');
11+
12+
function sign(options) {
13+
var isEcdsa = options.algorithm && options.algorithm.indexOf('ES') === 0;
14+
jwt.sign({foo: 123}, isEcdsa ? cert_ecdsa_priv : cert_rsa_priv, options);
15+
}
16+
17+
it('should validate expiresIn', function () {
18+
expect(function () {
19+
sign({ expiresIn: '1 monkey' });
20+
}).to.throw(/"expiresIn" should be a number of seconds or string representing a timespan/);
21+
expect(function () {
22+
sign({ expiresIn: 1.1 });
23+
}).to.throw(/"expiresIn" should be a number of seconds or string representing a timespan/);
24+
sign({ expiresIn: '10s' });
25+
sign({ expiresIn: 10 });
26+
});
27+
28+
it('should validate notBefore', function () {
29+
expect(function () {
30+
sign({ notBefore: '1 monkey' });
31+
}).to.throw(/"notBefore" should be a number of seconds or string representing a timespan/);
32+
expect(function () {
33+
sign({ notBefore: 1.1 });
34+
}).to.throw(/"notBefore" should be a number of seconds or string representing a timespan/);
35+
sign({ notBefore: '10s' });
36+
sign({ notBefore: 10 });
37+
});
38+
39+
it('should validate audience', function () {
40+
expect(function () {
41+
sign({ audience: 10 });
42+
}).to.throw(/"audience" must be a string or array/);
43+
sign({ audience: 'urn:foo' });
44+
sign({ audience: ['urn:foo'] });
45+
});
46+
47+
it('should validate algorithm', function () {
48+
expect(function () {
49+
sign({ algorithm: 'foo' });
50+
}).to.throw(/"algorithm" must be a valid string enum value/);
51+
sign({algorithm: 'RS256'});
52+
sign({algorithm: 'RS384'});
53+
sign({algorithm: 'RS512'});
54+
sign({algorithm: 'ES256'});
55+
sign({algorithm: 'ES384'});
56+
sign({algorithm: 'ES512'});
57+
sign({algorithm: 'HS256'});
58+
sign({algorithm: 'HS384'});
59+
sign({algorithm: 'HS512'});
60+
sign({algorithm: 'none'});
61+
});
62+
63+
it('should validate header', function () {
64+
expect(function () {
65+
sign({ header: 'foo' });
66+
}).to.throw(/"header" must be an object/);
67+
sign({header: {}});
68+
});
69+
70+
it('should validate encoding', function () {
71+
expect(function () {
72+
sign({ encoding: 10 });
73+
}).to.throw(/"encoding" must be a string/);
74+
sign({encoding: 'utf8'});
75+
});
76+
77+
it('should validate issuer', function () {
78+
expect(function () {
79+
sign({ issuer: 10 });
80+
}).to.throw(/"issuer" must be a string/);
81+
sign({issuer: 'foo'});
82+
});
83+
84+
it('should validate subject', function () {
85+
expect(function () {
86+
sign({ subject: 10 });
87+
}).to.throw(/"subject" must be a string/);
88+
sign({subject: 'foo'});
89+
});
90+
91+
it('should validate noTimestamp', function () {
92+
expect(function () {
93+
sign({ noTimestamp: 10 });
94+
}).to.throw(/"noTimestamp" must be a boolean/);
95+
sign({noTimestamp: true});
96+
});
97+
98+
it('should validate keyid', function () {
99+
expect(function () {
100+
sign({ keyid: 10 });
101+
}).to.throw(/"keyid" must be a string/);
102+
sign({keyid: 'foo'});
103+
});
104+
105+
});
106+
107+
describe('sign payload registered claims', function() {
108+
109+
function sign(payload) {
110+
jwt.sign(payload, 'foo123');
111+
}
112+
113+
it('should validate iat', function () {
114+
expect(function () {
115+
sign({ iat: '1 monkey' });
116+
}).to.throw(/"iat" should be a number of seconds/);
117+
sign({ iat: 10.1 });
118+
});
119+
120+
it('should validate exp', function () {
121+
expect(function () {
122+
sign({ exp: '1 monkey' });
123+
}).to.throw(/"exp" should be a number of seconds/);
124+
sign({ exp: 10.1 });
125+
});
126+
127+
it('should validate nbf', function () {
128+
expect(function () {
129+
sign({ nbf: '1 monkey' });
130+
}).to.throw(/"nbf" should be a number of seconds/);
131+
sign({ nbf: 10.1 });
132+
});
133+
134+
});
135+
136+
});

0 commit comments

Comments
 (0)