Files
925d95de54 Merge pull request from GHSA-8cf7-32gw-wr33
* Check if node version supports asymmetricKeyDetails

* Validate algorithms for ec key type

* Rename variable

* Rename function

* Add early return for symmetric keys

* Validate algorithm for RSA key type

* Validate algorithm for RSA-PSS key type

* Check key types for EdDSA algorithm

* Rename function

* Move validateKey function to module

* Convert arrow to function notation

* Validate key in verify function

* Simplify if

* Convert if to switch..case

* Guard against empty key in validation

* Remove empty line

* Add lib to check modulus length

* Add modulus length checks

* Validate mgf1HashAlgorithm and saltLength

* Check node version before using key details API

* Use built-in modulus length getter

* Fix Node version validations

* Remove duplicate validateKey

* Add periods to error messages

* Fix validation in verify function

* Make asymmetric key validation the latest validation step

* Change key curve validation

* Remove support for ES256K

* Fix old test that was using wrong key types to sign tokens

* Enable RSA-PSS for old Node versions

* Add specific RSA-PSS validations on Node 16 LTS+

* Improve error message

* Simplify key validation code

* Fix typo

* Improve error message

* Change var to const in test

* Change const to let to avoid reassigning problem

* Improve error message

* Test incorrect private key type

* Rename invalid to unsupported

* Test verifying of jwt token with unsupported key

* Test invalid private key type

* Change order of object parameters

* Move validation test to separate file

* Move all validation tests to separate file

* Add prime256v1 ec key

* Remove modulus length check

* WIP: Add EC key validation tests

* Fix node version checks

* Fix error message check on test

* Add successful tests for EC curve check

* Remove only from describe

* Remove `only`

* Remove duplicate block of code

* Move variable to a different scope and make it const

* Convert allowed curves to object for faster lookup

* Rename variable

* Change variable assignment order

* Remove unused object properties

* Test RSA-PSS happy path and wrong length

* Add missing tests

* Pass validation if no algorithm has been provided

* Test validation of invalid salt length

* Test error when signing token with invalid key

* Change var to const/let in verify tests

* Test verifying token with invalid key

* Improve test error messages

* Add parameter to skip private key validation

* Replace DSA key with a 4096 bit long key

* Test allowInvalidPrivateKeys in key signing

* Improve test message

* Rename variable

* Add key validation flag tests

* Fix variable name in Readme

* Change private to public dsa key in verify

* Rename flag

* Run EC validation tests conditionally

* Fix tests in old node versions

* Ignore block of code from test coverage

* Separate EC validations tests into two different ones

* Add comment

* Wrap switch in if instead of having an early return

* Remove unsupported algorithms from asymmetric key validation

* Rename option to allowInvalidAsymmetricKeyTypes and improve Readme

* 9.0.0

* adding migration notes to readme

* adding changelog for version 9.0.0

Co-authored-by: julienwoll <[email protected]>
2022-12-21 13:36:01 +01:00

302 lines
11 KiB
JavaScript

const jwt = require('../index');
const jws = require('jws');
const fs = require('fs');
const path = require('path');
const sinon = require('sinon');
const JsonWebTokenError = require('../lib/JsonWebTokenError');
const assert = require('chai').assert;
const expect = require('chai').expect;
describe('verify', function() {
const pub = fs.readFileSync(path.join(__dirname, 'pub.pem'));
const priv = fs.readFileSync(path.join(__dirname, 'priv.pem'));
it('should first assume JSON claim set', function (done) {
const header = { alg: 'RS256' };
const payload = { iat: Math.floor(Date.now() / 1000 ) };
const signed = jws.sign({
header: header,
payload: payload,
secret: priv,
encoding: 'utf8'
});
jwt.verify(signed, pub, {typ: 'JWT'}, function(err, p) {
assert.isNull(err);
assert.deepEqual(p, payload);
done();
});
});
it('should not be able to verify unsigned token', function () {
const header = { alg: 'none' };
const payload = { iat: Math.floor(Date.now() / 1000 ) };
const signed = jws.sign({
header: header,
payload: payload,
secret: 'secret',
encoding: 'utf8'
});
expect(function () {
jwt.verify(signed, 'secret', {typ: 'JWT'});
}).to.throw(JsonWebTokenError, /jwt signature is required/);
});
it('should not be able to verify unsigned token', function () {
const header = { alg: 'none' };
const payload = { iat: Math.floor(Date.now() / 1000 ) };
const signed = jws.sign({
header: header,
payload: payload,
secret: 'secret',
encoding: 'utf8'
});
expect(function () {
jwt.verify(signed, undefined, {typ: 'JWT'});
}).to.throw(JsonWebTokenError, /please specify "none" in "algorithms" to verify unsigned tokens/);
});
it('should be able to verify unsigned token when none is specified', function (done) {
const header = { alg: 'none' };
const payload = { iat: Math.floor(Date.now() / 1000 ) };
const signed = jws.sign({
header: header,
payload: payload,
secret: 'secret',
encoding: 'utf8'
});
jwt.verify(signed, null, {typ: 'JWT', algorithms: ['none']}, function(err, p) {
assert.isNull(err);
assert.deepEqual(p, payload);
done();
});
});
it('should not mutate options', function (done) {
const header = { alg: 'HS256' };
const payload = { iat: Math.floor(Date.now() / 1000 ) };
const options = { typ: 'JWT' };
const signed = jws.sign({
header: header,
payload: payload,
secret: 'secret',
encoding: 'utf8'
});
jwt.verify(signed, 'secret', options, function(err) {
assert.isNull(err);
assert.deepEqual(Object.keys(options).length, 1);
done();
});
});
describe('secret or token as callback', function () {
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU';
const key = 'key';
const payload = { foo: 'bar', iat: 1437018582, exp: 1437018592 };
const options = {algorithms: ['HS256'], ignoreExpiration: true};
it('without callback', function (done) {
jwt.verify(token, key, options, function (err, p) {
assert.isNull(err);
assert.deepEqual(p, payload);
done();
});
});
it('simple callback', function (done) {
const keyFunc = function(header, callback) {
assert.deepEqual(header, { alg: 'HS256', typ: 'JWT' });
callback(undefined, key);
};
jwt.verify(token, keyFunc, options, function (err, p) {
assert.isNull(err);
assert.deepEqual(p, payload);
done();
});
});
it('should error if called synchronously', function (done) {
const keyFunc = function(header, callback) {
callback(undefined, key);
};
expect(function () {
jwt.verify(token, keyFunc, options);
}).to.throw(JsonWebTokenError, /verify must be called asynchronous if secret or public key is provided as a callback/);
done();
});
it('simple error', function (done) {
const keyFunc = function(header, callback) {
callback(new Error('key not found'));
};
jwt.verify(token, keyFunc, options, function (err, p) {
assert.equal(err.name, 'JsonWebTokenError');
assert.match(err.message, /error in secret or public key callback/);
assert.isUndefined(p);
done();
});
});
it('delayed callback', function (done) {
const keyFunc = function(header, callback) {
setTimeout(function() {
callback(undefined, key);
}, 25);
};
jwt.verify(token, keyFunc, options, function (err, p) {
assert.isNull(err);
assert.deepEqual(p, payload);
done();
});
});
it('delayed error', function (done) {
const keyFunc = function(header, callback) {
setTimeout(function() {
callback(new Error('key not found'));
}, 25);
};
jwt.verify(token, keyFunc, options, function (err, p) {
assert.equal(err.name, 'JsonWebTokenError');
assert.match(err.message, /error in secret or public key callback/);
assert.isUndefined(p);
done();
});
});
});
describe('expiration', function () {
// { foo: 'bar', iat: 1437018582, exp: 1437018592 }
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU';
const key = 'key';
let clock;
afterEach(function () {
try { clock.restore(); } catch (e) {}
});
it('should error on expired token', function (done) {
clock = sinon.useFakeTimers(1437018650000); // iat + 58s, exp + 48s
const options = {algorithms: ['HS256']};
jwt.verify(token, key, options, function (err, p) {
assert.equal(err.name, 'TokenExpiredError');
assert.equal(err.message, 'jwt expired');
assert.equal(err.expiredAt.constructor.name, 'Date');
assert.equal(Number(err.expiredAt), 1437018592000);
assert.isUndefined(p);
done();
});
});
it('should not error on expired token within clockTolerance interval', function (done) {
clock = sinon.useFakeTimers(1437018594000); // iat + 12s, exp + 2s
const options = {algorithms: ['HS256'], clockTolerance: 5 }
jwt.verify(token, key, options, function (err, p) {
assert.isNull(err);
assert.equal(p.foo, 'bar');
done();
});
});
describe('option: clockTimestamp', function () {
const clockTimestamp = 1000000000;
it('should verify unexpired token relative to user-provided clockTimestamp', function (done) {
const token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key);
jwt.verify(token, key, {clockTimestamp: clockTimestamp}, function (err) {
assert.isNull(err);
done();
});
});
it('should error on expired token relative to user-provided clockTimestamp', function (done) {
const token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key);
jwt.verify(token, key, {clockTimestamp: clockTimestamp + 1}, function (err, p) {
assert.equal(err.name, 'TokenExpiredError');
assert.equal(err.message, 'jwt expired');
assert.equal(err.expiredAt.constructor.name, 'Date');
assert.equal(Number(err.expiredAt), (clockTimestamp + 1) * 1000);
assert.isUndefined(p);
done();
});
});
it('should verify clockTimestamp is a number', function (done) {
const token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key);
jwt.verify(token, key, {clockTimestamp: 'notANumber'}, function (err, p) {
assert.equal(err.name, 'JsonWebTokenError');
assert.equal(err.message,'clockTimestamp must be a number');
assert.isUndefined(p);
done();
});
});
});
describe('option: maxAge and clockTimestamp', function () {
// { foo: 'bar', iat: 1437018582, exp: 1437018800 } exp = iat + 218s
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODgwMH0.AVOsNC7TiT-XVSpCpkwB1240izzCIJ33Lp07gjnXVpA';
it('cannot be more permissive than expiration', function (done) {
const clockTimestamp = 1437018900; // iat + 318s (exp: iat + 218s)
const options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1000y'};
jwt.verify(token, key, options, function (err, p) {
// maxAge not exceded, but still expired
assert.equal(err.name, 'TokenExpiredError');
assert.equal(err.message, 'jwt expired');
assert.equal(err.expiredAt.constructor.name, 'Date');
assert.equal(Number(err.expiredAt), 1437018800000);
assert.isUndefined(p);
done();
});
});
});
});
describe('when verifying a token with an unsupported public key type', function () {
it('should throw an error', function() {
const token = 'eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE2Njk5OTAwMDN9.YdjFWJtPg_9nccMnTfQyesWQ0UX-GsWrfCGit_HqjeIkNjoV6dkAJ8AtbnVEhA4oxwqSXx6ilMOfHEjmMlPtyyyVKkWKQHcIWYnqPbNSEv8a7Men8KhJTIWb4sf5YbhgSCpNvU_VIZjLO1Z0PzzgmEikp0vYbxZFAbCAlZCvUlcIc-kdjIRCnDJe0BBrYRxNLEJtYsf7D1yFIFIqw8-VP87yZdExA4eHsTaE84SgnL24ZK5h5UooDx-IRNd_rrMyio8kNy63grVxCWOtkXZ26iZk6v-HMsnBqxvUwR6-8wfaWrcpADkyUO1q3SNsoTdwtflbvfwgjo3uve0IvIzHMw';
const key = fs.readFileSync(path.join(__dirname, 'dsa-public.pem'));
expect(function() {
jwt.verify(token, key);
}).to.throw('Unknown key type "dsa".');
});
});
describe('when verifying a token with an incorrect public key type', function () {
it('should throw a validation error if key validation is enabled', function() {
const token = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXkiOiJsb2FkIiwiaWF0IjoxNjcwMjMwNDE2fQ.7TYP8SB_9Tw1fNIfuG60b4tvoLPpDAVBQpV1oepnuKwjUz8GOw4fRLzclo0Q2YAXisJ3zIYMEFsHpYrflfoZJQ';
const key = fs.readFileSync(path.join(__dirname, 'rsa-public.pem'));
expect(function() {
jwt.verify(token, key, { algorithms: ['ES256'] });
}).to.throw('"alg" parameter for "rsa" key type must be one of: RS256, PS256, RS384, PS384, RS512, PS512.');
});
it('should throw an unknown error if key validation is disabled', function() {
const token = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXkiOiJsb2FkIiwiaWF0IjoxNjcwMjMwNDE2fQ.7TYP8SB_9Tw1fNIfuG60b4tvoLPpDAVBQpV1oepnuKwjUz8GOw4fRLzclo0Q2YAXisJ3zIYMEFsHpYrflfoZJQ';
const key = fs.readFileSync(path.join(__dirname, 'rsa-public.pem'));
expect(function() {
jwt.verify(token, key, { algorithms: ['ES256'], allowInvalidAsymmetricKeyTypes: true });
}).to.not.throw('"alg" parameter for "rsa" key type must be one of: RS256, PS256, RS384, PS384, RS512, PS512.');
});
});
});