From dddcb73ac05de11b81feeb629f6cf78dd03d2047 Mon Sep 17 00:00:00 2001 From: Harkirat Singh Date: Fri, 17 Nov 2017 21:50:04 +0530 Subject: [PATCH 01/77] Minor typo (#424) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dbb91c1d..b1a613fe 100644 --- a/README.md +++ b/README.md @@ -291,7 +291,7 @@ none | No digital signature or MAC value included First of all, we recommend to think carefully if auto-refreshing a JWT will not introduce any vulnerability in your system. -We are not comfortable including this as part of the library, however, you can take a look to [this example](https://gist.github.com/ziluvatar/a3feb505c4c0ec37059054537b38fc48) to show how this could be accomplish. +We are not comfortable including this as part of the library, however, you can take a look to [this example](https://gist.github.com/ziluvatar/a3feb505c4c0ec37059054537b38fc48) to show how this could be accomplished. Apart from that example there are [an issue](https://github.com/auth0/node-jsonwebtoken/issues/122) and [a pull request](https://github.com/auth0/node-jsonwebtoken/pull/172) to get more knowledge about this topic. # TODO From 83f3eee44e122da06f812d7da4ace1fa26c24d9d Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Wed, 10 Jan 2018 22:38:47 +0100 Subject: [PATCH 02/77] add newer node versions to build matrix (#428) * add newer node versions to build matrix * Dont test for node 9 Its not stable and seems to fail on travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index fc8c8e32..c64ac69c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: node_js sudo: false before_install: npm i -g npm node_js: + - "8" - "7" - "6" - "5" From 25e0e624545eaef76f3c324a134bf103bc394724 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Fri, 19 Jan 2018 09:50:57 -0330 Subject: [PATCH 03/77] Bump ms version to add support for negative numbers (#438) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fc50041d..c91f8574 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", - "ms": "^2.0.0", + "ms": "^2.1.1", "xtend": "^4.0.1" }, "devDependencies": { From 2764a64908d97c043d62eba0bf6c600674f9a6d6 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Fri, 19 Jan 2018 13:58:49 -0330 Subject: [PATCH 04/77] Not Before (nbf) calculated based on iat/timestamp (#437) fix #435 --- sign.js | 2 +- test/iat.tests.js | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/sign.js b/sign.js index d6f87556..d71189be 100644 --- a/sign.js +++ b/sign.js @@ -145,7 +145,7 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { } if (typeof options.notBefore !== 'undefined') { - payload.nbf = timespan(options.notBefore); + payload.nbf = timespan(options.notBefore, timestamp); 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')); } diff --git a/test/iat.tests.js b/test/iat.tests.js index 72d7a1ad..00647f15 100644 --- a/test/iat.tests.js +++ b/test/iat.tests.js @@ -12,4 +12,13 @@ describe('iat', function () { expect(result.exp).to.be.closeTo(iat + expiresIn, 0.2); }); -}); \ No newline at end of file + it('should work with a nbf calculated based on numeric iat', function () { + var dateNow = Math.floor(Date.now() / 1000); + var iat = dateNow - 30; + var notBefore = -50; + var token = jwt.sign({foo: 123, iat: iat}, '123', {notBefore: notBefore}); + var result = jwt.verify(token, '123'); + expect(result.nbf).to.equal(iat + notBefore); + }); + +}); From d265cf1355498d48287beaa39d8ccc00e5e780b1 Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Mon, 22 Jan 2018 20:25:43 +0100 Subject: [PATCH 05/77] update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 328d345d..c5516e14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file starting from version **v4.0.0**. This project adheres to [Semantic Versioning](http://semver.org/). +## 8.1.1 - 2018-01-22 + + - ci: add newer node versions to build matrix (#428) ([83f3eee44e122da06f812d7da4ace1fa26c24d9d](https://github.com/auth0/node-jsonwebtoken/commit/83f3eee44e122da06f812d7da4ace1fa26c24d9d)) + - deps: Bump ms version to add support for negative numbers (#438) ([25e0e624545eaef76f3c324a134bf103bc394724](https://github.com/auth0/node-jsonwebtoken/commit/25e0e624545eaef76f3c324a134bf103bc394724)) + - docs: Minor typo (#424) ([dddcb73ac05de11b81feeb629f6cf78dd03d2047](https://github.com/auth0/node-jsonwebtoken/commit/dddcb73ac05de11b81feeb629f6cf78dd03d2047)) + - bug fix: Not Before (nbf) calculated based on iat/timestamp (#437) ([2764a64908d97c043d62eba0bf6c600674f9a6d6](https://github.com/auth0/node-jsonwebtoken/commit/2764a64908d97c043d62eba0bf6c600674f9a6d6)), closes [#435](https://github.com/auth0/node-jsonwebtoken/issues/435) + + ## 8.1.0 - 2017-10-09 - #402: Don't fail if captureStackTrace is not a function (#410) ([77ee965d9081faaf21650f266399f203f69533c5](https://github.com/auth0/node-jsonwebtoken/commit/77ee965d9081faaf21650f266399f203f69533c5)) From 7b0a010e8a8ede7b2d31a702cf4fbab1cfc774b7 Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Mon, 22 Jan 2018 20:23:47 +0100 Subject: [PATCH 06/77] 8.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c91f8574..86bba924 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonwebtoken", - "version": "8.1.0", + "version": "8.1.1", "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", "scripts": { From d6d7c5e5103f05a92d3633ac190d3025a0455be0 Mon Sep 17 00:00:00 2001 From: Jonathan Gros-Dubois Date: Fri, 2 Mar 2018 13:48:25 +0100 Subject: [PATCH 07/77] Add a new mutatePayload option (#446) This option allows you to keep a reference to the raw token payload after claims have been applied to it but before it has been encoded. --- README.md | 1 + sign.js | 7 +++++-- test/async_sign.tests.js | 24 ++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b1a613fe..99a5cc8b 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ encoded private key for RSA and ECDSA. In case of a private key with passphrase * `noTimestamp` * `header` * `keyid` +* `mutatePayload`: if true, the sign function will modify the payload object directly. This is useful if you need a raw reference to the payload after claims have been applied to it but before it has been encoded into a token. If `payload` is not a buffer or a string, it will be coerced into a string using `JSON.stringify`. diff --git a/sign.js b/sign.js index d71189be..ea3bb36e 100644 --- a/sign.js +++ b/sign.js @@ -20,7 +20,8 @@ var sign_options_schema = { 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' } + keyid: { isValid: isString, message: '"keyid" must be a string' }, + mutatePayload: { isValid: isBoolean, message: '"mutatePayload" must be a boolean' } }; var registered_claims_schema = { @@ -110,7 +111,9 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { catch (error) { return failure(error); } - payload = xtend(payload); + if (!options.mutatePayload) { + payload = xtend(payload); + } } else { var invalid_options = options_for_objects.filter(function (opt) { return typeof options[opt] !== 'undefined'; diff --git a/test/async_sign.tests.js b/test/async_sign.tests.js index b6cc0526..d22f9740 100644 --- a/test/async_sign.tests.js +++ b/test/async_sign.tests.js @@ -82,6 +82,30 @@ describe('signing a token asynchronously', function() { }); }); + describe('when mutatePayload is not set', function() { + it('should not apply claims to the original payload object (mutatePayload defaults to false)', function(done) { + var originalPayload = { foo: 'bar' }; + jwt.sign(originalPayload, 'secret', { notBefore: 60, expiresIn: 600 }, function (err) { + if (err) { return done(err); } + expect(originalPayload).to.not.have.property('nbf'); + expect(originalPayload).to.not.have.property('exp'); + done(); + }); + }); + }); + + describe('when mutatePayload is set to true', function() { + it('should apply claims directly to the original payload object', function(done) { + var originalPayload = { foo: 'bar' }; + jwt.sign(originalPayload, 'secret', { notBefore: 60, expiresIn: 600, mutatePayload: true }, function (err) { + if (err) { return done(err); } + expect(originalPayload).to.have.property('nbf').that.is.a('number'); + expect(originalPayload).to.have.property('exp').that.is.a('number'); + done(); + }); + }); + }); + describe('secret must have a value', function(){ [undefined, '', 0].forEach(function(secret){ it('should return an error if the secret is falsy and algorithm is not set to none: ' + (typeof secret === 'string' ? '(empty string)' : secret), function(done) { From c86a093cd1357e5bd49923454c91ab22b061d7cd Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Fri, 2 Mar 2018 13:56:17 +0100 Subject: [PATCH 08/77] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5516e14..9eca9199 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file starting from version **v4.0.0**. This project adheres to [Semantic Versioning](http://semver.org/). +## 8.2.0 - 2018-03-02 + + - Add a new mutatePayload option (#446) ([d6d7c5e5103f05a92d3633ac190d3025a0455be0](https://github.com/auth0/node-jsonwebtoken/commit/d6d7c5e5103f05a92d3633ac190d3025a0455be0)) + + ## 8.1.1 - 2018-01-22 - ci: add newer node versions to build matrix (#428) ([83f3eee44e122da06f812d7da4ace1fa26c24d9d](https://github.com/auth0/node-jsonwebtoken/commit/83f3eee44e122da06f812d7da4ace1fa26c24d9d)) From dee583a04c6808bec0b17a2121043d2b8c98711d Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Fri, 2 Mar 2018 13:56:32 +0100 Subject: [PATCH 09/77] 8.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 86bba924..afd8f274 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonwebtoken", - "version": "8.1.1", + "version": "8.2.0", "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", "scripts": { From e8ac1be7565a3fd986d40cb5e31a9f6c4d9aed1b Mon Sep 17 00:00:00 2001 From: David Beitey Date: Fri, 2 Mar 2018 13:08:24 +0000 Subject: [PATCH 10/77] Clarify that buffer/string payloads must be JSON (#442) It does kinda go without saying that JWTs (_JSON_ Web Tokens) need to contain JSON but it's worth mentioning that signing doesn't check your payload. In some unit tests I was writing where the payload was a dummy (non-JSON parsable) string, JWTs were being signed okay but they're not valid according to the spec. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 99a5cc8b..ce198d91 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ $ npm install jsonwebtoken (Synchronous) Returns the JsonWebToken as string -`payload` could be an object literal, buffer or string. *Please note that* `exp` is only set if the payload is an object literal. +`payload` could be an object literal, buffer or string representing valid JSON. *Please note that* `exp` is only set if the payload is an object literal. Buffer or string payloads are not checked for JSON validity. `secretOrPrivateKey` is a string, buffer, or object containing either the secret for HMAC algorithms or the PEM encoded private key for RSA and ECDSA. In case of a private key with passphrase an object `{ key, passphrase }` can be used (based on [crypto documentation](https://nodejs.org/api/crypto.html#crypto_sign_sign_private_key_output_format)), in this case be sure you pass the `algorithm` option. From 1232ae9352ce5fd1ca6c593291ce6ad0834a1ff5 Mon Sep 17 00:00:00 2001 From: Nicolas Cognaux Date: Thu, 5 Apr 2018 13:28:41 +0200 Subject: [PATCH 11/77] Check payload is not null when decoded. (#444) * Check payload is not null when decoded. Fixed "Cannot read property 'nbf' of null" * Condition on obj !== null for clarity * Added test for decoding null token --- decode.js | 2 +- test/decoding.tests.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 test/decoding.tests.js diff --git a/decode.js b/decode.js index 783e6e71..8fe1adcd 100644 --- a/decode.js +++ b/decode.js @@ -10,7 +10,7 @@ module.exports = function (jwt, options) { if(typeof payload === 'string') { try { var obj = JSON.parse(payload); - if(typeof obj === 'object') { + if(obj !== null && typeof obj === 'object') { payload = obj; } } catch (e) { } diff --git a/test/decoding.tests.js b/test/decoding.tests.js new file mode 100644 index 00000000..7c901488 --- /dev/null +++ b/test/decoding.tests.js @@ -0,0 +1,12 @@ +var jwt = require('../index'); +var expect = require('chai').expect; +var atob = require('atob'); + +describe('decoding', function() { + + it('should not crash when decoding a null token', function () { + var decoded = jwt.decode("null"); + expect(decoded).to.equal(null); + }); + +}); From 7a9954a153525e5eeb3b109b2fa934b983c91e93 Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Thu, 5 Apr 2018 13:31:41 +0200 Subject: [PATCH 12/77] update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eca9199..9d424639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file starting from version **v4.0.0**. This project adheres to [Semantic Versioning](http://semver.org/). +## 8.2.1 - 2018-04-05 + + - bug fix: Check payload is not null when decoded. (#444) ([1232ae9352ce5fd1ca6c593291ce6ad0834a1ff5](https://github.com/auth0/node-jsonwebtoken/commit/1232ae9352ce5fd1ca6c593291ce6ad0834a1ff5)) + - docs: Clarify that buffer/string payloads must be JSON (#442) ([e8ac1be7565a3fd986d40cb5e31a9f6c4d9aed1b](https://github.com/auth0/node-jsonwebtoken/commit/e8ac1be7565a3fd986d40cb5e31a9f6c4d9aed1b)) + + ## 8.2.0 - 2018-03-02 - Add a new mutatePayload option (#446) ([d6d7c5e5103f05a92d3633ac190d3025a0455be0](https://github.com/auth0/node-jsonwebtoken/commit/d6d7c5e5103f05a92d3633ac190d3025a0455be0)) From 092d55a02419f3e39fbc10d1abd2e5b89c84fb82 Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Thu, 5 Apr 2018 13:38:11 +0200 Subject: [PATCH 13/77] 8.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index afd8f274..d17947d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonwebtoken", - "version": "8.2.0", + "version": "8.2.1", "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", "scripts": { From f0e0954505f274da95a8d9603598e455b4d2c894 Mon Sep 17 00:00:00 2001 From: Josh Unger Date: Thu, 26 Apr 2018 04:27:49 -0600 Subject: [PATCH 14/77] Update README.md (#461) --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ce198d91..27f17a1c 100644 --- a/README.md +++ b/README.md @@ -55,25 +55,32 @@ The header can be customized via the `options.header` object. Generated jwts will include an `iat` (issued at) claim by default unless `noTimestamp` is specified. If `iat` is inserted in the payload, it will be used instead of the real timestamp for calculating other things like `exp` given a timespan in `options.expiresIn`. -Example +Sign with default (HMAC SHA256) ```js -// sign with default (HMAC SHA256) var jwt = require('jsonwebtoken'); var token = jwt.sign({ foo: 'bar' }, 'shhhhh'); -//backdate a jwt 30 seconds -var older_token = jwt.sign({ foo: 'bar', iat: Math.floor(Date.now() / 1000) - 30 }, 'shhhhh'); +``` +Sign with RSA SHA256 +```js // sign with RSA SHA256 -var cert = fs.readFileSync('private.key'); // get private key +var cert = fs.readFileSync('private.key'); var token = jwt.sign({ foo: 'bar' }, cert, { algorithm: 'RS256'}); +``` -// sign asynchronously +Sign asynchronously +```js jwt.sign({ foo: 'bar' }, cert, { algorithm: 'RS256' }, function(err, token) { console.log(token); }); ``` +Backdate a jwt 30 seconds +```js +var older_token = jwt.sign({ foo: 'bar', iat: Math.floor(Date.now() / 1000) - 30 }, 'shhhhh'); +``` + #### Token Expiration (exp claim) The standard for JWT defines an `exp` claim for expiration. The expiration is represented as a **NumericDate**: From c8ff7b2c3ffcd954a64a0273c20a7d1b22339aa5 Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Tue, 22 May 2018 19:15:22 +0200 Subject: [PATCH 15/77] fix ci execution, remove not needed script (#472) --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c64ac69c..7f2f9aa3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: node_js sudo: false -before_install: npm i -g npm node_js: - "8" - "7" From cd33cc81f06068b9df6c224d300dc6f70d8904ab Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Wed, 23 May 2018 18:27:06 +0200 Subject: [PATCH 16/77] add some clarifications (#473) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 27f17a1c..8ffded0d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ $ npm install jsonwebtoken (Synchronous) Returns the JsonWebToken as string -`payload` could be an object literal, buffer or string representing valid JSON. *Please note that* `exp` is only set if the payload is an object literal. Buffer or string payloads are not checked for JSON validity. +`payload` could be an object literal, buffer or string representing valid JSON. *Please note that* `exp` or any other claim is only set if the payload is an object literal. Buffer or string payloads are not checked for JSON validity. `secretOrPrivateKey` is a string, buffer, or object containing either the secret for HMAC algorithms or the PEM encoded private key for RSA and ECDSA. In case of a private key with passphrase an object `{ key, passphrase }` can be used (based on [crypto documentation](https://nodejs.org/api/crypto.html#crypto_sign_sign_private_key_output_format)), in this case be sure you pass the `algorithm` option. @@ -33,8 +33,8 @@ encoded private key for RSA and ECDSA. In case of a private key with passphrase `options`: * `algorithm` (default: `HS256`) -* `expiresIn`: expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). Eg: `60`, `"2 days"`, `"10h"`, `"7d"` -* `notBefore`: expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). Eg: `60`, `"2 days"`, `"10h"`, `"7d"` +* `expiresIn`: expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). Eg: `60`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). +* `notBefore`: expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). Eg: `60`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). * `audience` * `issuer` * `jwtid` @@ -134,7 +134,7 @@ As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues * `ignoreNotBefore`... * `subject`: if you want to check subject (`sub`), provide a value here * `clockTolerance`: number of seconds to tolerate when checking the `nbf` and `exp` claims, to deal with small clock differences among different servers -* `maxAge`: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). Eg: `1000`, `"2 days"`, `"10h"`, `"7d"`. +* `maxAge`: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). Eg: `1000`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). * `clockTimestamp`: the time in seconds that should be used as the current time for all necessary comparisons. From ebde9b7cc75cb7ab5176de7ebc4a1d6a8f05bd51 Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Wed, 30 May 2018 16:42:16 +0200 Subject: [PATCH 17/77] deps: jws@3.1.5 (#477) Update 'jws' dependency to latest to fix a security issue --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d17947d2..cc385826 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "url": "https://github.com/auth0/node-jsonwebtoken/issues" }, "dependencies": { - "jws": "^3.1.4", + "jws": "^3.1.5", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", From 5e3e3968e8ab0dec7d720451e9c74c3bb3fd9cba Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Wed, 30 May 2018 17:01:35 +0200 Subject: [PATCH 18/77] update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d424639..81d3ecab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file starting from version **v4.0.0**. This project adheres to [Semantic Versioning](http://semver.org/). +## 8.2.2 - 2018-05-30 + + - security: deps: jws@3.1.5 (#477) ([ebde9b7cc75cb7ab5176de7ebc4a1d6a8f05bd51](https://github.com/auth0/node-jsonwebtoken/commit/ebde9b7cc75cb7ab5176de7ebc4a1d6a8f05bd51)), closes [#465](https://github.com/auth0/node-jsonwebtoken/issues/465) + - docs: add some clarifications (#473) ([cd33cc81f06068b9df6c224d300dc6f70d8904ab](https://github.com/auth0/node-jsonwebtoken/commit/cd33cc81f06068b9df6c224d300dc6f70d8904ab)), closes [#473](https://github.com/auth0/node-jsonwebtoken/issues/473) + - ci: fix ci execution, remove not needed script (#472) ([c8ff7b2c3ffcd954a64a0273c20a7d1b22339aa5](https://github.com/auth0/node-jsonwebtoken/commit/c8ff7b2c3ffcd954a64a0273c20a7d1b22339aa5)), closes [#472](https://github.com/auth0/node-jsonwebtoken/issues/472) + - docs: Update README.md (#461) ([f0e0954505f274da95a8d9603598e455b4d2c894](https://github.com/auth0/node-jsonwebtoken/commit/f0e0954505f274da95a8d9603598e455b4d2c894)), closes [#461](https://github.com/auth0/node-jsonwebtoken/issues/461) + + ## 8.2.1 - 2018-04-05 - bug fix: Check payload is not null when decoded. (#444) ([1232ae9352ce5fd1ca6c593291ce6ad0834a1ff5](https://github.com/auth0/node-jsonwebtoken/commit/1232ae9352ce5fd1ca6c593291ce6ad0834a1ff5)) From 73c4a5ac45d557ef0dc1c43fcae6b7fc9bc3f19e Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Wed, 30 May 2018 17:03:24 +0200 Subject: [PATCH 19/77] 8.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc385826..efac52f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonwebtoken", - "version": "8.2.1", + "version": "8.2.2", "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", "scripts": { From d01cc7bcbdeb606d997a580f967b3169fcc622ba Mon Sep 17 00:00:00 2001 From: Jaco Koster Date: Mon, 11 Jun 2018 17:32:29 +0200 Subject: [PATCH 20/77] Secret callback revisited (#480) * Introduction of the secret callback Without the more contentious 'none'-changes * Removed some spaces... I should really add a editor.config and eslint to this project ;-) * Removed xtend as a dependency, as the native Object.Assign can do this as well * Removed xtend as a dependency, as the native Object.Assign can do this as well * Resolve feedback from review * Added extra test and fixed the associated bug * The return of the header * Forgot to change this one as well... Sorry bout that * Updated the readme and made the if-statements consistent * Space; The final frontier --- .editorconfig | 3 + .eslintrc | 8 ++ README.md | 18 ++++ package.json | 3 +- sign.js | 5 +- test/keyid.tests.js | 8 +- test/verify.tests.js | 92 +++++++++++++++++++- verify.js | 201 +++++++++++++++++++++++-------------------- 8 files changed, 234 insertions(+), 104 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintrc diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..76a93c06 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*] +indent_style = space +indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..c49f04f7 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,8 @@ +{ + "env": { + "es6": true + }, + "rules": { + "indent": [2,2] + } +} \ No newline at end of file diff --git a/README.md b/README.md index 8ffded0d..f8a2d2c2 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ jwt.sign({ `secretOrPublicKey` is a string or buffer containing either the secret for HMAC algorithms, or the PEM encoded public key for RSA and ECDSA. +If `jwt.verify` is called asynchronous, `secretOrPublicKey` can be a function that should fetch the secret or public key. See below for a detailed example As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues/208#issuecomment-231861138), there are other libraries that expect base64 encoded secrets (random bytes encoded using base64), if that is your case you can pass `Buffer.from(secret, 'base64')`, by doing this the secret will be decoded using base64 and the token verification will use the original random bytes. @@ -197,6 +198,23 @@ jwt.verify(token, cert, { algorithms: ['RS256'] }, function (err, payload) { // if token alg != RS256, err == invalid signature }); +// Verify using getKey callback +// Example uses https://github.com/auth0/node-jwks-rsa as a way to fetch the keys. +var jwksClient = require('jwks-rsa'); +var client = jwksClient({ + jwksUri: 'https://sandrino.auth0.com/.well-known/jwks.json' +}); +function getKey(header, callback){ + client.getSigningKey(header.kid, function(err, key) { + var signingKey = key.publicKey || key.rsaPublicKey; + callback(null, signingKey); + }); +} + +jwt.verify(token, getKey, options, function(err, decoded) { + console.log(decoded.foo) // bar +}); + ``` ### jwt.decode(token [, options]) diff --git a/package.json b/package.json index efac52f9..e1d9f49c 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,7 @@ "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "xtend": "^4.0.1" + "ms": "^2.1.1" }, "devDependencies": { "atob": "^1.1.2", diff --git a/sign.js b/sign.js index ea3bb36e..6041596c 100644 --- a/sign.js +++ b/sign.js @@ -1,5 +1,4 @@ var timespan = require('./lib/timespan'); -var xtend = require('xtend'); var jws = require('jws'); var includes = require('lodash.includes'); var isBoolean = require('lodash.isboolean'); @@ -85,7 +84,7 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { var isObjectPayload = typeof payload === 'object' && !Buffer.isBuffer(payload); - var header = xtend({ + var header = Object.assign({ alg: options.algorithm || 'HS256', typ: isObjectPayload ? 'JWT' : undefined, kid: options.keyid @@ -112,7 +111,7 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { return failure(error); } if (!options.mutatePayload) { - payload = xtend(payload); + payload = Object.assign({},payload); } } else { var invalid_options = options_for_objects.filter(function (opt) { diff --git a/test/keyid.tests.js b/test/keyid.tests.js index f428ccb6..84a231c1 100644 --- a/test/keyid.tests.js +++ b/test/keyid.tests.js @@ -2,8 +2,8 @@ var jwt = require('../index'); var claims = {"name": "doron", "age": 46}; jwt.sign(claims, 'secret', {"keyid": "1234"}, function(err, good) { - console.log(jwt.decode(good, {"complete": true}).header.kid); - jwt.verify(good, 'secret', function(err, result) { - console.log(result); - }) + console.log(jwt.decode(good, {"complete": true}).header.kid); + jwt.verify(good, 'secret', function(err, result) { + console.log(result); + }) }); diff --git a/test/verify.tests.js b/test/verify.tests.js index 96855255..51d107e3 100644 --- a/test/verify.tests.js +++ b/test/verify.tests.js @@ -3,8 +3,10 @@ var jws = require('jws'); var fs = require('fs'); var path = require('path'); var sinon = require('sinon'); +var JsonWebTokenError = require('../lib/JsonWebTokenError'); var assert = require('chai').assert; +var expect = require('chai').expect; describe('verify', function() { var pub = fs.readFileSync(path.join(__dirname, 'pub.pem')); @@ -16,9 +18,9 @@ describe('verify', function() { var signed = jws.sign({ header: header, - payload: payload, - secret: priv, - encoding: 'utf8' + payload: payload, + secret: priv, + encoding: 'utf8' }); jwt.verify(signed, pub, {typ: 'JWT'}, function(err, p) { @@ -67,6 +69,90 @@ describe('verify', function() { }); }); + describe('secret or token as callback', function () { + var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU'; + var key = 'key'; + + var payload = { foo: 'bar', iat: 1437018582, exp: 1437018592 }; + var 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) { + var 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) { + var 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) { + var 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) { + var 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) { + var 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 } var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU'; diff --git a/verify.js b/verify.js index 4f8326be..e730e2aa 100644 --- a/verify.js +++ b/verify.js @@ -4,7 +4,6 @@ var TokenExpiredError = require('./lib/TokenExpiredError'); var decode = require('./decode'); var timespan = require('./lib/timespan'); var jws = require('jws'); -var xtend = require('xtend'); module.exports = function (jwtString, secretOrPublicKey, options, callback) { if ((typeof options === 'function') && !callback) { @@ -17,7 +16,8 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { } //clone this object since we are going to mutate it. - options = xtend(options); + options = Object.assign({}, options); + var done; if (callback) { @@ -49,33 +49,10 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError('jwt malformed')); } - var hasSignature = parts[2].trim() !== ''; - - if (!hasSignature && secretOrPublicKey){ - return done(new JsonWebTokenError('jwt signature is required')); - } - - if (hasSignature && !secretOrPublicKey) { - return done(new JsonWebTokenError('secret or public key must be provided')); - } - - if (!hasSignature && !options.algorithms) { - options.algorithms = ['none']; - } - - if (!options.algorithms) { - options.algorithms = ~secretOrPublicKey.toString().indexOf('BEGIN CERTIFICATE') || - ~secretOrPublicKey.toString().indexOf('BEGIN PUBLIC KEY') ? - [ 'RS256','RS384','RS512','ES256','ES384','ES512' ] : - ~secretOrPublicKey.toString().indexOf('BEGIN RSA PUBLIC KEY') ? - [ 'RS256','RS384','RS512' ] : - [ 'HS256','HS384','HS512' ]; - - } - var decodedToken; + try { - decodedToken = jws.decode(jwtString); + decodedToken = decode(jwtString, { complete: true }); } catch(err) { return done(err); } @@ -85,97 +62,137 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { } var header = decodedToken.header; + var getSecret; - if (!~options.algorithms.indexOf(header.alg)) { - return done(new JsonWebTokenError('invalid algorithm')); - } - - var valid; + if(typeof secretOrPublicKey === 'function') { + if(!callback) { + return done(new JsonWebTokenError('verify must be called asynchronous if secret or public key is provided as a callback')); + } - try { - valid = jws.verify(jwtString, header.alg, secretOrPublicKey); - } catch (e) { - return done(e); + getSecret = secretOrPublicKey; + } + else { + getSecret = function(header, secretCallback) { + return secretCallback(null, secretOrPublicKey); + }; } - if (!valid) - return done(new JsonWebTokenError('invalid signature')); + return getSecret(header, function(err, secretOrPublicKey) { + if(err) { + return done(new JsonWebTokenError('error in secret or public key callback: ' + err.message)); + } - var payload; + var hasSignature = parts[2].trim() !== ''; - try { - payload = decode(jwtString); - } catch(err) { - return done(err); - } + if (!hasSignature && secretOrPublicKey){ + return done(new JsonWebTokenError('jwt signature is required')); + } - if (typeof payload.nbf !== 'undefined' && !options.ignoreNotBefore) { - if (typeof payload.nbf !== 'number') { - return done(new JsonWebTokenError('invalid nbf value')); + if (hasSignature && !secretOrPublicKey) { + return done(new JsonWebTokenError('secret or public key must be provided')); } - if (payload.nbf > clockTimestamp + (options.clockTolerance || 0)) { - return done(new NotBeforeError('jwt not active', new Date(payload.nbf * 1000))); + + if (!hasSignature && !options.algorithms) { + options.algorithms = ['none']; } - } - if (typeof payload.exp !== 'undefined' && !options.ignoreExpiration) { - if (typeof payload.exp !== 'number') { - return done(new JsonWebTokenError('invalid exp value')); + if (!options.algorithms) { + options.algorithms = ~secretOrPublicKey.toString().indexOf('BEGIN CERTIFICATE') || + ~secretOrPublicKey.toString().indexOf('BEGIN PUBLIC KEY') ? + ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'] : + ~secretOrPublicKey.toString().indexOf('BEGIN RSA PUBLIC KEY') ? + ['RS256', 'RS384', 'RS512'] : + ['HS256', 'HS384', 'HS512']; + } - if (clockTimestamp >= payload.exp + (options.clockTolerance || 0)) { - return done(new TokenExpiredError('jwt expired', new Date(payload.exp * 1000))); + + if (!~options.algorithms.indexOf(decodedToken.header.alg)) { + return done(new JsonWebTokenError('invalid algorithm')); } - } - if (options.audience) { - var audiences = Array.isArray(options.audience)? options.audience : [options.audience]; - var target = Array.isArray(payload.aud) ? payload.aud : [payload.aud]; + var valid; - var match = target.some(function(targetAudience) { - return audiences.some(function(audience) { - return audience instanceof RegExp ? audience.test(targetAudience) : audience === targetAudience; - }); - }); + try { + valid = jws.verify(jwtString, decodedToken.header.alg, secretOrPublicKey); + } catch (e) { + return done(e); + } - if (!match) - return done(new JsonWebTokenError('jwt audience invalid. expected: ' + audiences.join(' or '))); - } + if (!valid) { + return done(new JsonWebTokenError('invalid signature')); + } - if (options.issuer) { - var invalid_issuer = - (typeof options.issuer === 'string' && payload.iss !== options.issuer) || - (Array.isArray(options.issuer) && options.issuer.indexOf(payload.iss) === -1); + var payload = decodedToken.payload; - if (invalid_issuer) { - return done(new JsonWebTokenError('jwt issuer invalid. expected: ' + options.issuer)); + if (typeof payload.nbf !== 'undefined' && !options.ignoreNotBefore) { + if (typeof payload.nbf !== 'number') { + return done(new JsonWebTokenError('invalid nbf value')); + } + if (payload.nbf > clockTimestamp + (options.clockTolerance || 0)) { + return done(new NotBeforeError('jwt not active', new Date(payload.nbf * 1000))); + } } - } - if (options.subject) { - if (payload.sub !== options.subject) { - return done(new JsonWebTokenError('jwt subject invalid. expected: ' + options.subject)); + if (typeof payload.exp !== 'undefined' && !options.ignoreExpiration) { + if (typeof payload.exp !== 'number') { + return done(new JsonWebTokenError('invalid exp value')); + } + if (clockTimestamp >= payload.exp + (options.clockTolerance || 0)) { + return done(new TokenExpiredError('jwt expired', new Date(payload.exp * 1000))); + } } - } - if (options.jwtid) { - if (payload.jti !== options.jwtid) { - return done(new JsonWebTokenError('jwt jwtid invalid. expected: ' + options.jwtid)); + if (options.audience) { + var audiences = Array.isArray(options.audience) ? options.audience : [options.audience]; + var target = Array.isArray(payload.aud) ? payload.aud : [payload.aud]; + + var match = target.some(function (targetAudience) { + return audiences.some(function (audience) { + return audience instanceof RegExp ? audience.test(targetAudience) : audience === targetAudience; + }); + }); + + if (!match) { + return done(new JsonWebTokenError('jwt audience invalid. expected: ' + audiences.join(' or '))); + } } - } - if (options.maxAge) { - if (typeof payload.iat !== 'number') { - return done(new JsonWebTokenError('iat required when maxAge is specified')); + if (options.issuer) { + var invalid_issuer = + (typeof options.issuer === 'string' && payload.iss !== options.issuer) || + (Array.isArray(options.issuer) && options.issuer.indexOf(payload.iss) === -1); + + if (invalid_issuer) { + return done(new JsonWebTokenError('jwt issuer invalid. expected: ' + options.issuer)); + } } - var maxAgeTimestamp = timespan(options.maxAge, payload.iat); - if (typeof maxAgeTimestamp === 'undefined') { - return done(new JsonWebTokenError('"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60')); + if (options.subject) { + if (payload.sub !== options.subject) { + return done(new JsonWebTokenError('jwt subject invalid. expected: ' + options.subject)); + } } - if (clockTimestamp >= maxAgeTimestamp + (options.clockTolerance || 0)) { - return done(new TokenExpiredError('maxAge exceeded', new Date(maxAgeTimestamp * 1000))); + + if (options.jwtid) { + if (payload.jti !== options.jwtid) { + return done(new JsonWebTokenError('jwt jwtid invalid. expected: ' + options.jwtid)); + } + } + + if (options.maxAge) { + if (typeof payload.iat !== 'number') { + return done(new JsonWebTokenError('iat required when maxAge is specified')); + } + + var maxAgeTimestamp = timespan(options.maxAge, payload.iat); + if (typeof maxAgeTimestamp === 'undefined') { + return done(new JsonWebTokenError('"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60')); + } + if (clockTimestamp >= maxAgeTimestamp + (options.clockTolerance || 0)) { + return done(new TokenExpiredError('maxAge exceeded', new Date(maxAgeTimestamp * 1000))); + } } - } - return done(null, payload); + return done(null, payload); + }); }; From 969813f1df3aa597545dca45af03c29a956fd1e5 Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Mon, 11 Jun 2018 17:35:47 +0200 Subject: [PATCH 21/77] update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81d3ecab..d7a32498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file starting from version **v4.0.0**. This project adheres to [Semantic Versioning](http://semver.org/). +## 8.3.0 - 2018-06-11 + + - docs: add some clarifications (#473) ([cd33cc81f06068b9df6c224d300dc6f70d8904ab](https://github.com/auth0/node-jsonwebtoken/commit/cd33cc81f06068b9df6c224d300dc6f70d8904ab)), closes [#473](https://github.com/auth0/node-jsonwebtoken/issues/473) + - ci: fix ci execution, remove not needed script (#472) ([c8ff7b2c3ffcd954a64a0273c20a7d1b22339aa5](https://github.com/auth0/node-jsonwebtoken/commit/c8ff7b2c3ffcd954a64a0273c20a7d1b22339aa5)), closes [#472](https://github.com/auth0/node-jsonwebtoken/issues/472) + - new feature: Secret callback revisited (#480) ([d01cc7bcbdeb606d997a580f967b3169fcc622ba](https://github.com/auth0/node-jsonwebtoken/commit/d01cc7bcbdeb606d997a580f967b3169fcc622ba)), closes [#480](https://github.com/auth0/node-jsonwebtoken/issues/480) + - docs:Update README.md (#461) ([f0e0954505f274da95a8d9603598e455b4d2c894](https://github.com/auth0/node-jsonwebtoken/commit/f0e0954505f274da95a8d9603598e455b4d2c894)), closes [#461](https://github.com/auth0/node-jsonwebtoken/issues/461) + + ## 8.2.2 - 2018-05-30 - security: deps: jws@3.1.5 (#477) ([ebde9b7cc75cb7ab5176de7ebc4a1d6a8f05bd51](https://github.com/auth0/node-jsonwebtoken/commit/ebde9b7cc75cb7ab5176de7ebc4a1d6a8f05bd51)), closes [#465](https://github.com/auth0/node-jsonwebtoken/issues/465) From ad983587159cd89d4ef44aef1f1373b6a34f4662 Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Mon, 11 Jun 2018 17:36:46 +0200 Subject: [PATCH 22/77] 8.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e1d9f49c..753e6fac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonwebtoken", - "version": "8.2.2", + "version": "8.3.0", "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", "scripts": { From 9676a8306428a045e34c3987bd0680fb952b44e3 Mon Sep 17 00:00:00 2001 From: Jaco Koster Date: Sat, 16 Jun 2018 12:44:54 +0200 Subject: [PATCH 23/77] Added Istanbul to check test-coverage (#468) * Added Istanbul to check test-coverage * node_modules_bak is generated and automatically removed by cost-of-modules and isn't needed in the ignore. --- .gitignore | 4 +++- package.json | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 28f1ba75..88861393 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules -.DS_Store \ No newline at end of file +.DS_Store +.nyc_output +coverage diff --git a/package.json b/package.json index 753e6fac..3d7b5e71 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 && cost-of-modules" + "test": "nyc --reporter=html --reporter=text mocha --require test/util/fakeDate && nsp check && cost-of-modules" }, "repository": { "type": "git", @@ -36,6 +36,7 @@ "cost-of-modules": "^1.0.1", "mocha": "^2.1.0", "nsp": "^2.6.2", + "nyc": "^11.8.0", "sinon": "^1.15.4" }, "engines": { From 677ead6d64482f2067b11437dda07309abe73cfa Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Sat, 16 Jun 2018 14:06:05 -0230 Subject: [PATCH 24/77] Use lolex for faking date in tests (#491) Sinon.JS provides a project called lolex to handle faking dates. This change replaces the fakeDate utility with the equivalent Sinon.JS code. --- package.json | 4 ++-- test/jwt.asymmetric_signing.tests.js | 5 +++-- test/util/fakeDate.js | 32 ---------------------------- 3 files changed, 5 insertions(+), 36 deletions(-) delete mode 100644 test/util/fakeDate.js diff --git a/package.json b/package.json index 3d7b5e71..d84013cb 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", "scripts": { - "test": "nyc --reporter=html --reporter=text mocha --require test/util/fakeDate && nsp check && cost-of-modules" + "test": "nyc --reporter=html --reporter=text mocha && nsp check && cost-of-modules" }, "repository": { "type": "git", @@ -37,7 +37,7 @@ "mocha": "^2.1.0", "nsp": "^2.6.2", "nyc": "^11.8.0", - "sinon": "^1.15.4" + "sinon": "^6.0.0" }, "engines": { "npm": ">=1.4.28", diff --git a/test/jwt.asymmetric_signing.tests.js b/test/jwt.asymmetric_signing.tests.js index 55b8fac2..287a233e 100644 --- a/test/jwt.asymmetric_signing.tests.js +++ b/test/jwt.asymmetric_signing.tests.js @@ -5,6 +5,7 @@ var path = require('path'); var expect = require('chai').expect; var assert = require('chai').assert; var ms = require('ms'); +var sinon = require('sinon'); function loadKey(filename) { return fs.readFileSync(path.join(__dirname, filename)); @@ -140,14 +141,14 @@ describe('Asymmetric Algorithms', function(){ it('should valid when date are equals', function (done) { - Date.fix(1451908031); + var fakeClock = sinon.useFakeTimers({now: 1451908031}); token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, notBefore: 0 }); jwt.verify(token, pub, function (err, decoded) { + fakeClock.uninstall(); assert.isNull(err); assert.isNotNull(decoded); - Date.unfix(); done(); }); }); diff --git a/test/util/fakeDate.js b/test/util/fakeDate.js deleted file mode 100644 index d889c826..00000000 --- a/test/util/fakeDate.js +++ /dev/null @@ -1,32 +0,0 @@ -var oldDate = global.Date; - -/* - * fix new Date() to a fixed unix timestamp. - */ -global.Date.fix = function (timestamp) { - var time = timestamp * 1000; - - if (global.Date.unfake) { - global.Date.unfake(); - } - - global.Date = function (ts) { - return new oldDate(ts || time); - }; - - global.Date.prototype = Object.create(oldDate.prototype); - global.Date.prototype.constructor = global.Date; - - global.Date.prototype.now = function () { - return time; - }; - - global.Date.now = function () { - return time; - }; - - global.Date.unfix = function () { - global.Date = oldDate; - }; - -}; \ No newline at end of file From cb1d2e1e40547f7ecf29fa6635041df6cbba7f40 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Mon, 25 Jun 2018 15:24:04 -0230 Subject: [PATCH 25/77] Complete ESLint conversion and cleanup (#490) * Add extension to ESLint config file The .eslintrc file without an extension was deprecated a few years ago, so this change renames the file to add the required extension. See: https://github.com/eslint/eslint/commit/c9a8883d450d63a8d044a9e66d275f5b1973a3ba * Add ESLint to package.json This change adds ESLint as a dev-dependency and adds a lint script that will run ESLint. * Complete switch from JSHint to ESLint Convert all the JSHint rules to the ESLint equivalents where possible. The no-undef rule in ESLint caught a few cases of undefined usages in the tests, so they were also fixed. * Add a .eslintignore file The HTML coverage report is currently being linted, which causes a lot if invalid linting errors. This change adds a ignore file to ensure these files are properly skipped during linting. --- .eslintignore | 2 ++ .eslintrc | 8 -------- .eslintrc.json | 23 +++++++++++++++++++++++ .jshintrc | 22 ---------------------- package.json | 4 +++- test/.eslintrc.json | 5 +++++ test/async_sign.tests.js | 4 ++-- test/decoding.tests.js | 1 - test/invalid_exp.tests.js | 3 +-- test/issue_304.tests.js | 10 +++++----- test/jwt.hs.tests.js | 2 +- test/undefined_secretOrPublickey.tests.js | 1 - test/verify.tests.js | 4 ++-- 13 files changed, 44 insertions(+), 45 deletions(-) create mode 100644 .eslintignore delete mode 100644 .eslintrc create mode 100644 .eslintrc.json delete mode 100644 .jshintrc create mode 100644 test/.eslintrc.json diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..c1cb757a --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +.nyc_output/ +coverage/ diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index c49f04f7..00000000 --- a/.eslintrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "env": { - "es6": true - }, - "rules": { - "indent": [2,2] - } -} \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..572b76fd --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "root": true, + "parserOptions": { + "ecmaVersion": 6 + }, + "env": { + "es6": true, + "node": true + }, + "rules": { + "comma-style": "error", + "dot-notation": "error", + "indent": ["error", 2], + "no-control-regex": "error", + "no-div-regex": "error", + "no-eval": "error", + "no-implied-eval": "error", + "no-invalid-regexp": "error", + "no-trailing-spaces": "error", + "no-undef": "error", + "no-unused-vars": "error" + } +} diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 2c03a0a2..00000000 --- a/.jshintrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - "evil": true, - "regexdash": true, - "browser": true, - "wsh": true, - "trailing": true, - "sub": true, - "unused": true, - "undef": true, - "laxcomma": true, - "node": true, - "browser": false, - "esnext": true, - "globals": { - "describe": true, - "it": true, - "require": true, - "atob": false, - "escape": true, - "before": true - } -} \ No newline at end of file diff --git a/package.json b/package.json index d84013cb..d7e09f71 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", "scripts": { - "test": "nyc --reporter=html --reporter=text mocha && nsp check && cost-of-modules" + "lint": "eslint .", + "test": "npm run lint && nyc --reporter=html --reporter=text mocha && nsp check && cost-of-modules" }, "repository": { "type": "git", @@ -34,6 +35,7 @@ "chai": "^1.10.0", "conventional-changelog": "~1.1.0", "cost-of-modules": "^1.0.1", + "eslint": "^4.19.1", "mocha": "^2.1.0", "nsp": "^2.6.2", "nyc": "^11.8.0", diff --git a/test/.eslintrc.json b/test/.eslintrc.json new file mode 100644 index 00000000..7eeefc33 --- /dev/null +++ b/test/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "env": { + "mocha": true + } +} diff --git a/test/async_sign.tests.js b/test/async_sign.tests.js index d22f9740..6a4f1530 100644 --- a/test/async_sign.tests.js +++ b/test/async_sign.tests.js @@ -19,14 +19,14 @@ describe('signing a token asynchronously', function() { }); it('should work with empty options', function (done) { - jwt.sign({abc: 1}, "secret", {}, function (err, res) { + jwt.sign({abc: 1}, "secret", {}, function (err) { expect(err).to.be.null(); done(); }); }); it('should work without options object at all', function (done) { - jwt.sign({abc: 1}, "secret", function (err, res) { + jwt.sign({abc: 1}, "secret", function (err) { expect(err).to.be.null(); done(); }); diff --git a/test/decoding.tests.js b/test/decoding.tests.js index 7c901488..3bd8c130 100644 --- a/test/decoding.tests.js +++ b/test/decoding.tests.js @@ -1,6 +1,5 @@ var jwt = require('../index'); var expect = require('chai').expect; -var atob = require('atob'); describe('decoding', function() { diff --git a/test/invalid_exp.tests.js b/test/invalid_exp.tests.js index 397b62fe..dfb89b4a 100644 --- a/test/invalid_exp.tests.js +++ b/test/invalid_exp.tests.js @@ -1,13 +1,12 @@ var jwt = require('../index'); var expect = require('chai').expect; -var assert = require('chai').assert; describe('invalid expiration', function() { it('should fail with string', function (done) { var broken_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxMjMiLCJmb28iOiJhZGFzIn0.cDa81le-pnwJMcJi3o3PBwB7cTJMiXCkizIhxbXAKRg'; - jwt.verify(broken_token, '123', function (err, decoded) { + jwt.verify(broken_token, '123', function (err) { expect(err.name).to.equal('JsonWebTokenError'); done(); }); diff --git a/test/issue_304.tests.js b/test/issue_304.tests.js index db4a1e39..c1ed8af0 100644 --- a/test/issue_304.tests.js +++ b/test/issue_304.tests.js @@ -4,35 +4,35 @@ var expect = require('chai').expect; describe('issue 304 - verifying values other than strings', function() { it('should fail with numbers', function (done) { - jwt.verify(123, 'foo', function (err, decoded) { + jwt.verify(123, 'foo', function (err) { expect(err.name).to.equal('JsonWebTokenError'); done(); }); }); it('should fail with objects', function (done) { - jwt.verify({ foo: 'bar' }, 'biz', function (err, decoded) { + jwt.verify({ foo: 'bar' }, 'biz', function (err) { expect(err.name).to.equal('JsonWebTokenError'); done(); }); }); it('should fail with arrays', function (done) { - jwt.verify(['foo'], 'bar', function (err, decoded) { + jwt.verify(['foo'], 'bar', function (err) { expect(err.name).to.equal('JsonWebTokenError'); done(); }); }); it('should fail with functions', function (done) { - jwt.verify(function() {}, 'foo', function (err, decoded) { + jwt.verify(function() {}, 'foo', function (err) { expect(err.name).to.equal('JsonWebTokenError'); done(); }); }); it('should fail with booleans', function (done) { - jwt.verify(true, 'foo', function (err, decoded) { + jwt.verify(true, 'foo', function (err) { expect(err.name).to.equal('JsonWebTokenError'); done(); }); diff --git a/test/jwt.hs.tests.js b/test/jwt.hs.tests.js index af977dad..5c12a734 100644 --- a/test/jwt.hs.tests.js +++ b/test/jwt.hs.tests.js @@ -102,7 +102,7 @@ describe('HS256', function() { it('should return the "invalid token" error', function(done) { var malformedToken = token + ' '; // corrupt the token by adding a space - jwt.verify(malformedToken, secret, { algorithm: 'HS256', ignoreExpiration: true }, function(err, decoded) { + jwt.verify(malformedToken, secret, { algorithm: 'HS256', ignoreExpiration: true }, function(err) { assert.isNotNull(err); assert.equal('JsonWebTokenError', err.name); assert.equal('invalid token', err.message); diff --git a/test/undefined_secretOrPublickey.tests.js b/test/undefined_secretOrPublickey.tests.js index 01132ad9..39d4f137 100644 --- a/test/undefined_secretOrPublickey.tests.js +++ b/test/undefined_secretOrPublickey.tests.js @@ -1,4 +1,3 @@ -var fs = require('fs'); var jwt = require('../index'); var JsonWebTokenError = require('../lib/JsonWebTokenError'); var expect = require('chai').expect; diff --git a/test/verify.tests.js b/test/verify.tests.js index 51d107e3..f0c26fec 100644 --- a/test/verify.tests.js +++ b/test/verify.tests.js @@ -308,7 +308,7 @@ describe('verify', function() { var clockTimestamp = 1000000000; it('should verify unexpired token relative to user-provided clockTimestamp', function (done) { var token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); - jwt.verify(token, key, {clockTimestamp: clockTimestamp}, function (err, p) { + jwt.verify(token, key, {clockTimestamp: clockTimestamp}, function (err) { assert.isNull(err); done(); }); @@ -340,7 +340,7 @@ describe('verify', function() { nbf: clockTimestamp + 1, exp: clockTimestamp + 2 }, key); - jwt.verify(token, key, {clockTimestamp: clockTimestamp + 1}, function (err, p) { + jwt.verify(token, key, {clockTimestamp: clockTimestamp + 1}, function (err) { assert.isNull(err); done(); }); From fb0084a78535bfea8d0087c0870e7e3614a2cbe5 Mon Sep 17 00:00:00 2001 From: Jaco Koster Date: Mon, 25 Jun 2018 22:49:15 +0200 Subject: [PATCH 26/77] Make code-coverage mandatory when running tests (#495) * Made code-coverage mandatory when running the tests. * Missed the trailing-comma... --- package.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d7e09f71..84488f11 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,24 @@ "version": "8.3.0", "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", + "nyc": { + "check-coverage": true, + "lines": 95, + "statements": 95, + "functions": 100, + "branches": 95, + "exclude": [ + "./test/**" + ], + "reporter": [ + "json", + "lcov", + "text-summary" + ] + }, "scripts": { "lint": "eslint .", - "test": "npm run lint && nyc --reporter=html --reporter=text mocha && nsp check && cost-of-modules" + "test": "npm run lint && nyc mocha && nsp check && cost-of-modules" }, "repository": { "type": "git", From 39adf87a6faef3df984140f88e6724ddd709fd89 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Wed, 27 Jun 2018 12:22:32 -0230 Subject: [PATCH 27/77] Refactor tests related to notBefore and nbf (#497) This change extracts all tests in the current files related to notBefore and nbf into a single test file. It also adds several missing related tests. --- package.json | 3 +- test/iat.tests.js | 9 - test/jwt.asymmetric_signing.tests.js | 52 ----- test/nbf.test.js | 281 +++++++++++++++++++++++++++ test/schema.tests.js | 18 -- test/verify.tests.js | 27 --- 6 files changed, 283 insertions(+), 107 deletions(-) create mode 100644 test/nbf.test.js diff --git a/package.json b/package.json index 84488f11..5130eea9 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ }, "scripts": { "lint": "eslint .", - "test": "npm run lint && nyc mocha && nsp check && cost-of-modules" + "coverage": "nyc mocha", + "test": "npm run lint && npm run coverage && nsp check && cost-of-modules" }, "repository": { "type": "git", diff --git a/test/iat.tests.js b/test/iat.tests.js index 00647f15..953bdb31 100644 --- a/test/iat.tests.js +++ b/test/iat.tests.js @@ -12,13 +12,4 @@ describe('iat', function () { expect(result.exp).to.be.closeTo(iat + expiresIn, 0.2); }); - it('should work with a nbf calculated based on numeric iat', function () { - var dateNow = Math.floor(Date.now() / 1000); - var iat = dateNow - 30; - var notBefore = -50; - var token = jwt.sign({foo: 123, iat: iat}, '123', {notBefore: notBefore}); - var result = jwt.verify(token, '123'); - expect(result.nbf).to.equal(iat + notBefore); - }); - }); diff --git a/test/jwt.asymmetric_signing.tests.js b/test/jwt.asymmetric_signing.tests.js index 287a233e..866dd361 100644 --- a/test/jwt.asymmetric_signing.tests.js +++ b/test/jwt.asymmetric_signing.tests.js @@ -5,7 +5,6 @@ var path = require('path'); var expect = require('chai').expect; var assert = require('chai').assert; var ms = require('ms'); -var sinon = require('sinon'); function loadKey(filename) { return fs.readFileSync(path.join(__dirname, filename)); @@ -114,57 +113,6 @@ describe('Asymmetric Algorithms', function(){ }); }); - describe('when signing a token with not before', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, notBefore: -10 * 3600 }); - - it('should be valid expiration', function (done) { - jwt.verify(token, pub, function (err, decoded) { - assert.isNotNull(decoded); - assert.isNull(err); - done(); - }); - }); - - it('should be invalid', function (done) { - // not active token - token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, notBefore: '10m' }); - - jwt.verify(token, pub, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'NotBeforeError'); - assert.instanceOf(err.date, Date); - assert.instanceOf(err, jwt.NotBeforeError); - done(); - }); - }); - - - it('should valid when date are equals', function (done) { - var fakeClock = sinon.useFakeTimers({now: 1451908031}); - - token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, notBefore: 0 }); - - jwt.verify(token, pub, function (err, decoded) { - fakeClock.uninstall(); - assert.isNull(err); - assert.isNotNull(decoded); - done(); - }); - }); - - it('should NOT be invalid', function (done) { - // not active token - token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, notBefore: '10m' }); - - jwt.verify(token, pub, { ignoreNotBefore: true }, function (err, decoded) { - assert.ok(decoded.foo); - assert.equal('bar', decoded.foo); - done(); - }); - }); - }); - describe('when signing a token with audience', function () { var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, audience: 'urn:foo' }); diff --git a/test/nbf.test.js b/test/nbf.test.js new file mode 100644 index 00000000..51c05214 --- /dev/null +++ b/test/nbf.test.js @@ -0,0 +1,281 @@ +'use strict'; + +const jwt = require('../'); +const expect = require('chai').expect; +const sinon = require('sinon'); +const util = require('util'); + +function base64UrlEncode(str) { + return Buffer.from(str).toString('base64') + .replace(/\=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_") + ; +} + +function signWithNoBefore(payload, notBefore) { + const options = {algorithm: 'none'}; + if (notBefore !== undefined) { + options.notBefore = notBefore; + } + return jwt.sign(payload, undefined, options); +} + +const noneAlgorithmHeader = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0'; + +describe('not before', function() { + describe('`jwt.sign` "notBefore" option validation', function () { + [ + true, + false, + null, + -1.1, + 1.1, + -Infinity, + Infinity, + NaN, + ' ', + 'invalid', + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((notBefore) => { + it(`should error with with value ${util.inspect(notBefore)}`, function () { + expect(() => signWithNoBefore({}, notBefore)).to.throw( + '"notBefore" should be a number of seconds or string representing a timespan' + ); + }); + }); + + // TODO this should throw the same error as other invalid inputs + it(`should error with with value ''`, function () { + expect(() => signWithNoBefore({}, '')).to.throw( + 'val is not a non-empty string or a valid number. val=""' + ); + }); + + // undefined needs special treatment because {} is not the same as {notBefore: undefined} + it('should error with with value undefined', function () { + expect(() => jwt.sign({}, undefined, {notBefore: undefined, algorithm: 'none'})).to.throw( + '"notBefore" should be a number of seconds or string representing a timespan' + ); + }); + + it('should error when "nbf" is in payload', function () { + expect(() => signWithNoBefore({nbf: 100}, 100)).to.throw( + 'Bad "options.notBefore" option the payload already has an "nbf" property.' + ); + }); + + it('should error with a string payload', function () { + expect(() => signWithNoBefore('a string payload', 100)).to.throw( + 'invalid notBefore option for string payload' + ); + }); + + it('should error with a Buffer payload', function () { + expect(() => signWithNoBefore(new Buffer('a Buffer payload'), 100)).to.throw( + 'invalid notBefore option for object payload' + ); + }); + }); + + describe('`jwt.sign` "nbf" claim validation', function () { + [ + true, + false, + null, + undefined, + '', + ' ', + 'invalid', + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((nbf) => { + it(`should error with with value ${util.inspect(nbf)}`, function () { + expect(() => signWithNoBefore({nbf})).to.throw( + '"nbf" should be a number of seconds' + ); + }); + }); + }); + + describe('"nbf" in payload validation', function () { + [ + true, + false, + null, + -Infinity, + Infinity, + NaN, + '', + ' ', + 'invalid', + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((nbf) => { + it(`should error with with value ${util.inspect(nbf)}`, function () { + const encodedPayload = base64UrlEncode(JSON.stringify({nbf})); + const token = `${noneAlgorithmHeader}.${encodedPayload}.`; + expect(() => jwt.verify(token, undefined)).to.throw( + jwt.JsonWebTokenError, + 'invalid nbf value' + ); + }); + }) + }); + + describe('when signing and verifying a token with "notBefore" option', function () { + let fakeClock; + beforeEach(function () { + fakeClock = sinon.useFakeTimers({now: 60000}); + }); + + afterEach(function () { + fakeClock.uninstall(); + }); + + it('should set correct "nbf" with negative number of seconds', function () { + const token = signWithNoBefore({}, -10); + const decoded = jwt.decode(token); + + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.nbf).to.equal(50); + }); + + it('should set correct "nbf" with positive number of seconds', function () { + const token = signWithNoBefore({}, 10); + + fakeClock.tick(10000); + const decoded = jwt.decode(token); + + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.nbf).to.equal(70); + }); + + it('should set correct "nbf" with zero seconds', function () { + const token = signWithNoBefore({}, 0); + + const decoded = jwt.decode(token); + + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.nbf).to.equal(60); + }); + + it('should set correct "nbf" with negative string timespan', function () { + const token = signWithNoBefore({}, '-10 s'); + + const decoded = jwt.decode(token); + + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.nbf).to.equal(50); + }); + + + it('should set correct "nbf" with positive string timespan', function () { + const token = signWithNoBefore({}, '10 s'); + + fakeClock.tick(10000); + const decoded = jwt.decode(token); + + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.nbf).to.equal(70); + }); + + it('should set correct "nbf" with zero string timespan', function () { + const token = signWithNoBefore({}, '0 s'); + + const decoded = jwt.decode(token); + + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.nbf).to.equal(60); + }); + + // TODO an nbf of -Infinity should fail validation + it('should set null "nbf" when given -Infinity', function () { + const token = signWithNoBefore({nbf: -Infinity}); + + const decoded = jwt.decode(token); + expect(decoded.nbf).to.be.null; + }); + + // TODO an nbf of Infinity should fail validation + it('should set null "nbf" when given value Infinity', function () { + const token = signWithNoBefore({nbf: Infinity}); + + const decoded = jwt.decode(token); + expect(decoded.nbf).to.be.null; + }); + + // TODO an nbf of NaN should fail validation + it('should set null "nbf" when given value NaN', function () { + const token = signWithNoBefore({nbf: NaN}); + + const decoded = jwt.decode(token); + expect(decoded.nbf).to.be.null; + }); + + it('should set correct "nbf" when "iat" is passed', function () { + const token = signWithNoBefore({iat: 40}, -10); + + const decoded = jwt.decode(token); + + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.nbf).to.equal(30); + }); + + it('should verify "nbf" using "clockTimestamp"', function () { + const token = signWithNoBefore({}, 10); + + const verified = jwt.verify(token, undefined, {clockTimestamp: 70}); + expect(verified.iat).to.equal(60); + expect(verified.nbf).to.equal(70); + }); + + it('should verify "nbf" using "clockTolerance"', function () { + const token = signWithNoBefore({}, 5); + + const verified = jwt.verify(token, undefined, {clockTolerance: 6}); + expect(verified.iat).to.equal(60); + expect(verified.nbf).to.equal(65); + }); + + it('should ignore a not active token when "ignoreNotBefore" is true', function () { + const token = signWithNoBefore({}, '10 s'); + + const verified = jwt.verify(token, undefined, {ignoreNotBefore: true}); + expect(verified.iat).to.equal(60); + expect(verified.nbf).to.equal(70); + }); + + it('should error on verify if "nbf" is after current time', function () { + const token = signWithNoBefore({nbf: 61}); + + expect(() => jwt.verify(token, undefined)).to.throw( + jwt.NotBeforeError, + 'jwt not active' + ); + }); + + it('should error on verify if "nbf" is after current time using clockTolerance', function () { + const token = signWithNoBefore({}, 5); + + expect(() => jwt.verify(token, undefined, {clockTolerance: 4})).to.throw( + jwt.NotBeforeError, + 'jwt not active' + ); + }); + }); +}); \ No newline at end of file diff --git a/test/schema.tests.js b/test/schema.tests.js index 1f799943..a6a9128f 100644 --- a/test/schema.tests.js +++ b/test/schema.tests.js @@ -25,17 +25,6 @@ describe('schema', function() { 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 }); @@ -124,13 +113,6 @@ describe('schema', function() { 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 diff --git a/test/verify.tests.js b/test/verify.tests.js index f0c26fec..ac3f1d39 100644 --- a/test/verify.tests.js +++ b/test/verify.tests.js @@ -333,33 +333,6 @@ describe('verify', function() { done(); }); }); - it('should verify valid token with nbf', function (done) { - var token = jwt.sign({ - foo: 'bar', - iat: clockTimestamp, - nbf: clockTimestamp + 1, - exp: clockTimestamp + 2 - }, key); - jwt.verify(token, key, {clockTimestamp: clockTimestamp + 1}, function (err) { - assert.isNull(err); - done(); - }); - }); - it('should error on token used before nbf', function (done) { - var token = jwt.sign({ - foo: 'bar', - iat: clockTimestamp, - nbf: clockTimestamp + 1, - exp: clockTimestamp + 2 - }, key); - jwt.verify(token, key, {clockTimestamp: clockTimestamp}, function (err, p) { - assert.equal(err.name, 'NotBeforeError'); - assert.equal(err.date.constructor.name, 'Date'); - assert.equal(Number(err.date), (clockTimestamp + 1) * 1000); - assert.isUndefined(p); - done(); - }); - }); }); describe('option: maxAge and clockTimestamp', function () { From 72f0d9e5b11a99082250665d1200c58182903fa6 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Fri, 6 Jul 2018 19:10:49 -0230 Subject: [PATCH 28/77] Refactor tests related to expiresIn and exp (#501) This change extracts all tests in the current test files related to expiresIn and exp into a single test file. It also adds several missing tests. --- test/exp.test.js | 277 ++++++++++++++++++++++++++++++++ test/expires_format.tests.js | 41 ----- test/iat.tests.js | 15 -- test/nbf.test.js | 12 +- test/non_object_values.tests.js | 7 - test/schema.tests.js | 11 -- test/test-utils.js | 13 ++ 7 files changed, 293 insertions(+), 83 deletions(-) create mode 100644 test/exp.test.js delete mode 100644 test/iat.tests.js create mode 100644 test/test-utils.js diff --git a/test/exp.test.js b/test/exp.test.js new file mode 100644 index 00000000..dec044ac --- /dev/null +++ b/test/exp.test.js @@ -0,0 +1,277 @@ +'use strict'; + +const jwt = require('../'); +const expect = require('chai').expect; +const sinon = require('sinon'); +const util = require('util'); +const testUtils = require('./test-utils'); + +const base64UrlEncode = testUtils.base64UrlEncode; +const noneAlgorithmHeader = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0'; + +function signWithExpiresIn(payload, expiresIn) { + const options = {algorithm: 'none'}; + if (expiresIn !== undefined) { + options.expiresIn = expiresIn; + } + return jwt.sign(payload, undefined, options); +} + +describe('expires', function() { + describe('`jwt.sign` "expiresIn" option validation', function () { + [ + true, + false, + null, + -1.1, + 1.1, + -Infinity, + Infinity, + NaN, + ' ', + 'invalid', + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((expiresIn) => { + it(`should error with with value ${util.inspect(expiresIn)}`, function () { + expect(() => signWithExpiresIn({}, expiresIn)).to.throw( + '"expiresIn" should be a number of seconds or string representing a timespan' + ); + }); + }); + + // TODO this should throw the same error as other invalid inputs + it(`should error with with value ''`, function () { + expect(() => signWithExpiresIn({}, '')).to.throw( + 'val is not a non-empty string or a valid number. val=""' + ); + }); + + // undefined needs special treatment because {} is not the same as {expiresIn: undefined} + it('should error with with value undefined', function () { + expect(() =>jwt.sign({}, undefined, {expiresIn: undefined, algorithm: 'none'})).to.throw( + '"expiresIn" should be a number of seconds or string representing a timespan' + ); + }); + + it ('should error when "exp" is in payload', function() { + expect(() => signWithExpiresIn({exp: 100}, 100)).to.throw( + 'Bad "options.expiresIn" option the payload already has an "exp" property.' + ); + }); + + it('should error with a string payload', function() { + expect(() => signWithExpiresIn('a string payload', 100)).to.throw( + 'invalid expiresIn option for string payload' + ); + }); + + it('should error with a Buffer payload', function() { + expect(() => signWithExpiresIn(Buffer.from('a Buffer payload'), 100)).to.throw( + 'invalid expiresIn option for object payload' + ); + }); + }); + + describe('`jwt.sign` "exp" claim validation', function () { + [ + true, + false, + null, + undefined, + '', + ' ', + 'invalid', + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((exp) => { + it(`should error with with value ${util.inspect(exp)}`, function () { + expect(() => signWithExpiresIn({exp})).to.throw( + '"exp" should be a number of seconds' + ); + }); + }); + }); + + describe('"exp" in payload validation', function () { + [ + true, + false, + null, + -Infinity, + Infinity, + NaN, + '', + ' ', + 'invalid', + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((exp) => { + it(`should error with with value ${util.inspect(exp)}`, function () { + const encodedPayload = base64UrlEncode(JSON.stringify({exp})); + const token = `${noneAlgorithmHeader}.${encodedPayload}.`; + expect(() => jwt.verify(token, undefined)).to.throw( + jwt.JsonWebTokenError, + 'invalid exp value' + ); + }); + }) + }); + + describe('when signing and verifying a token with expires option', function () { + let fakeClock; + beforeEach(function() { + fakeClock = sinon.useFakeTimers({now: 60000}); + }); + + afterEach(function() { + fakeClock.uninstall(); + }); + + it('should set correct "exp" with negative number of seconds', function() { + const token = signWithExpiresIn({}, -10); + fakeClock.tick(-10001); + + const decoded = jwt.decode(token); + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.exp).to.equal(50); + }); + + it('should set correct "exp" with positive number of seconds', function() { + const token = signWithExpiresIn({}, 10); + + const decoded = jwt.decode(token); + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.exp).to.equal(70); + }); + + it('should set correct "exp" with zero seconds', function() { + const token = signWithExpiresIn({}, 0); + + fakeClock.tick(-1); + + const decoded = jwt.decode(token); + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.exp).to.equal(60); + }); + + it('should set correct "exp" with negative string timespan', function() { + const token = signWithExpiresIn({}, '-10 s'); + + fakeClock.tick(-10001); + + const decoded = jwt.decode(token); + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.exp).to.equal(50); + }); + + it('should set correct "exp" with positive string timespan', function() { + const token = signWithExpiresIn({}, '10 s'); + + fakeClock.tick(-10001); + const decoded = jwt.decode(token); + + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.exp).to.equal(70); + }); + + it('should set correct "exp" with zero string timespan', function() { + const token = signWithExpiresIn({}, '0 s'); + + fakeClock.tick(-1); + const decoded = jwt.decode(token); + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.exp).to.equal(60); + }); + + // TODO an exp of -Infinity should fail validation + it('should set null "exp" when given -Infinity', function () { + const token = signWithExpiresIn({exp: -Infinity}); + + const decoded = jwt.decode(token); + expect(decoded.exp).to.be.null; + }); + + // TODO an exp of Infinity should fail validation + it('should set null "exp" when given value Infinity', function () { + const token = signWithExpiresIn({exp: Infinity}); + + const decoded = jwt.decode(token); + expect(decoded.exp).to.be.null; + }); + + // TODO an exp of NaN should fail validation + it('should set null "exp" when given value NaN', function () { + const token = signWithExpiresIn({exp: NaN}); + + const decoded = jwt.decode(token); + expect(decoded.exp).to.be.null; + }); + + it('should set correct "exp" when "iat" is passed', function () { + const token = signWithExpiresIn({iat: 80}, -10); + + const decoded = jwt.decode(token); + + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.exp).to.equal(70); + }); + + it('should verify "exp" using "clockTimestamp"', function () { + const token = signWithExpiresIn({}, 10); + + const verified = jwt.verify(token, undefined, {clockTimestamp: 69}); + expect(verified.iat).to.equal(60); + expect(verified.exp).to.equal(70); + }); + + it('should verify "exp" using "clockTolerance"', function () { + const token = signWithExpiresIn({}, 5); + + fakeClock.tick(10000); + + const verified = jwt.verify(token, undefined, {clockTolerance: 6}); + expect(verified.iat).to.equal(60); + expect(verified.exp).to.equal(65); + }); + + it('should ignore a expired token when "ignoreExpiration" is true', function () { + const token = signWithExpiresIn({}, '-10 s'); + + const verified = jwt.verify(token, undefined, {ignoreExpiration: true}); + expect(verified.iat).to.equal(60); + expect(verified.exp).to.equal(50); + }); + + it('should error on verify if "exp" is at current time', function() { + const token = signWithExpiresIn({exp: 60}); + + expect(() => jwt.verify(token, undefined)).to.throw( + jwt.TokenExpiredError, + 'jwt expired' + ); + }); + + it('should error on verify if "exp" is before current time using clockTolerance', function () { + const token = signWithExpiresIn({}, -5); + + expect(() => jwt.verify(token, undefined, {clockTolerance: 5})).to.throw( + jwt.TokenExpiredError, + 'jwt expired' + ); + }); + }); +}); diff --git a/test/expires_format.tests.js b/test/expires_format.tests.js index 7e542c49..6c2e1002 100644 --- a/test/expires_format.tests.js +++ b/test/expires_format.tests.js @@ -3,51 +3,10 @@ var expect = require('chai').expect; describe('expires option', function() { - it('should work with a number of seconds', function () { - var token = jwt.sign({foo: 123}, '123', { expiresIn: 10 }); - var result = jwt.verify(token, '123'); - expect(result.exp).to.be.closeTo(Math.floor(Date.now() / 1000) + 10, 0.2); - }); - - it('should work with a string', function () { - var token = jwt.sign({foo: 123}, '123', { expiresIn: '2d' }); - var result = jwt.verify(token, '123'); - var two_days_in_secs = 2 * 24 * 60 * 60; - expect(result.exp).to.be.closeTo(Math.floor(Date.now() / 1000) + two_days_in_secs, 0.2); - }); - - it('should work with a string second example', function () { - var token = jwt.sign({foo: 123}, '123', { expiresIn: '36h' }); - var result = jwt.verify(token, '123'); - var day_and_a_half_in_secs = 1.5 * 24 * 60 * 60; - expect(result.exp).to.be.closeTo(Math.floor(Date.now() / 1000) + day_and_a_half_in_secs, 0.2); - }); - - - it('should throw if expires has a bad string format', function () { - expect(function () { - jwt.sign({foo: 123}, '123', { expiresIn: '1 monkey' }); - }).to.throw(/"expiresIn" should be a number of seconds or string representing a timespan/); - }); - - 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" should be a number of seconds or string representing a timespan/); - }); - - it('should throw an error if expiresIn and exp are provided', function () { - expect(function () { - jwt.sign({ foo: 123, exp: 839218392183 }, '123', { expiresIn: '5h' }); - }).to.throw(/Bad "options.expiresIn" option the payload already has an "exp" property./); - }); - - it('should throw on deprecated expiresInSeconds option', function () { expect(function () { jwt.sign({foo: 123}, '123', { expiresInSeconds: 5 }); }).to.throw('"expiresInSeconds" is not allowed'); }); - }); diff --git a/test/iat.tests.js b/test/iat.tests.js deleted file mode 100644 index 953bdb31..00000000 --- a/test/iat.tests.js +++ /dev/null @@ -1,15 +0,0 @@ -var jwt = require('../index'); -var expect = require('chai').expect; - -describe('iat', function () { - - it('should work with a exp calculated based on numeric iat', function () { - var dateNow = Math.floor(Date.now() / 1000); - var iat = dateNow - 30; - var expiresIn = 50; - var token = jwt.sign({foo: 123, iat: iat}, '123', {expiresIn: expiresIn}); - var result = jwt.verify(token, '123'); - expect(result.exp).to.be.closeTo(iat + expiresIn, 0.2); - }); - -}); diff --git a/test/nbf.test.js b/test/nbf.test.js index 51c05214..9a96907f 100644 --- a/test/nbf.test.js +++ b/test/nbf.test.js @@ -4,14 +4,10 @@ const jwt = require('../'); const expect = require('chai').expect; const sinon = require('sinon'); const util = require('util'); +const testUtils = require('./test-utils'); -function base64UrlEncode(str) { - return Buffer.from(str).toString('base64') - .replace(/\=/g, "") - .replace(/\+/g, "-") - .replace(/\//g, "_") - ; -} +const base64UrlEncode = testUtils.base64UrlEncode; +const noneAlgorithmHeader = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0'; function signWithNoBefore(payload, notBefore) { const options = {algorithm: 'none'}; @@ -21,8 +17,6 @@ function signWithNoBefore(payload, notBefore) { return jwt.sign(payload, undefined, options); } -const noneAlgorithmHeader = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0'; - describe('not before', function() { describe('`jwt.sign` "notBefore" option validation', function () { [ diff --git a/test/non_object_values.tests.js b/test/non_object_values.tests.js index e7477f33..3d489a77 100644 --- a/test/non_object_values.tests.js +++ b/test/non_object_values.tests.js @@ -10,13 +10,6 @@ describe('non_object_values values', function() { expect(result).to.equal('hello'); }); - //v6 version will throw in this case: - it('should throw with expiresIn', function () { - expect(function () { - jwt.sign('hello', '123', { expiresIn: '12h' }); - }).to.throw(/invalid expiresIn option for string payload/); - }); - it('should fail to validate audience when the payload is string', function () { var token = jwt.sign('hello', '123'); expect(function () { diff --git a/test/schema.tests.js b/test/schema.tests.js index a6a9128f..c3fc38fc 100644 --- a/test/schema.tests.js +++ b/test/schema.tests.js @@ -14,17 +14,6 @@ describe('schema', function() { 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 audience', function () { expect(function () { sign({ audience: 10 }); diff --git a/test/test-utils.js b/test/test-utils.js new file mode 100644 index 00000000..f49889e2 --- /dev/null +++ b/test/test-utils.js @@ -0,0 +1,13 @@ +'use strict'; + +function base64UrlEncode(str) { + return Buffer.from(str).toString('base64') + .replace(/\=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_") + ; +} + +module.exports = { + base64UrlEncode, +}; From 53d405e0223cce7c83cb51ecf290ca6bec1e9679 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Thu, 12 Jul 2018 10:06:35 -0230 Subject: [PATCH 29/77] Refactor tests related to audience and aud (#503) This change extracts all tests in the existing test files related to audience and aud into a single test file. Several other tests are also added that were missing from the existing files. --- test/aud.test.js | 323 +++++++++++++++++++++++++++ test/jwt.asymmetric_signing.tests.js | 187 ---------------- test/non_object_values.tests.js | 8 - test/schema.tests.js | 8 - 4 files changed, 323 insertions(+), 203 deletions(-) create mode 100644 test/aud.test.js diff --git a/test/aud.test.js b/test/aud.test.js new file mode 100644 index 00000000..1763f134 --- /dev/null +++ b/test/aud.test.js @@ -0,0 +1,323 @@ +'use strict'; + +const jwt = require('../'); +const expect = require('chai').expect; +const util = require('util'); + +function signWithAudience(payload, audience) { + const options = {algorithm: 'none'}; + if (audience !== undefined) { + options.audience = audience; + } + return jwt.sign(payload, undefined, options); +} + +describe('audience', function() { + describe('`jwt.sign` "audience" option validation', function () { + [ + true, + false, + null, + -1, + 1, + 0, + -1.1, + 1.1, + -Infinity, + Infinity, + NaN, + {}, + {foo: 'bar'}, + ].forEach((audience) => { + it(`should error with with value ${util.inspect(audience)}`, function () { + expect(() => signWithAudience({}, audience)).to.throw('"audience" must be a string or array'); + }); + }); + + // undefined needs special treatment because {} is not the same as {aud: undefined} + it('should error with with value undefined', function () { + expect(() => jwt.sign({}, undefined, {audience: undefined, algorithm: 'none'})).to.throw( + '"audience" must be a string or array' + ); + }); + + it('should error when "aud" is in payload', function () { + expect(() => signWithAudience({aud: ''}, 'my_aud')).to.throw( + 'Bad "options.audience" option. The payload already has an "aud" property.' + ); + }); + + it('should error with a string payload', function () { + expect(() => signWithAudience('a string payload', 'my_aud')).to.throw( + 'invalid audience option for string payload' + ); + }); + + it('should error with a Buffer payload', function () { + expect(() => signWithAudience(new Buffer('a Buffer payload'), 'my_aud')).to.throw( + 'invalid audience option for object payload' + ); + }); + }); + + describe('when signing and verifying a token with "audience" option', function () { + describe('with a string for "aud" value in payload', function () { + let token; + + beforeEach(function () { + token = signWithAudience({}, 'urn:foo'); + }); + + it('should verify and decode without verify "audience" option', function () { + const decoded = jwt.decode(token); + const verified = jwt.verify(token, undefined); + + expect(decoded).to.deep.equal(verified); + expect(decoded.aud).to.equal('urn:foo'); + }); + + it('should verify with a string "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: 'urn:foo' + })).to.not.throw; + }); + + it('should verify with an array of strings "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: ['urn:no_match', 'urn:foo'] + })).to.not.throw; + }); + + it('should verify with a Regex "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: /^urn:f[o]{2}$/ + })).to.not.throw; + }); + + it('should verify with an array of Regex "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: [/^urn:no_match$/, /^urn:f[o]{2}$/] + })).to.not.throw; + }); + + it('should verify with an array containing a string and a Regex "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: ['urn:no_match', /^urn:f[o]{2}$/] + })).to.not.throw; + }); + + it('should verify with an array containing a Regex and a string "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: [/^urn:no_match$/, 'urn:foo'] + })).to.not.throw; + }); + + it('should error on no match with a string "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: 'urn:no-match' + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: urn:no-match'); + }); + + it('should error on no match with an array of string "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: ['urn:no-match-1', 'urn:no-match-2'] + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: urn:no-match-1 or urn:no-match-2'); + }); + + it('should error on no match with a Regex "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: /^urn:no-match$/ + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match$/'); + }); + + it('should error on no match with an array of Regex "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: [/^urn:no-match-1$/, /^urn:no-match-2$/] + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match-1$/ or /^urn:no-match-2$/'); + }); + + it('should error on no match with an array of a Regex and a string in "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: [/^urn:no-match$/, 'urn:no-match'] + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match$/ or urn:no-match'); + }); + }); + + describe('with an array for "aud" value in payload', function () { + let token; + + beforeEach(function () { + token = signWithAudience({}, ['urn:foo', 'urn:bar']); + }); + + it('should verify and decode without verify "audience" option', function () { + const decoded = jwt.decode(token); + const verified = jwt.verify(token, undefined); + + expect(decoded).to.deep.equal(verified); + expect(decoded.aud).to.deep.equal(['urn:foo', 'urn:bar']); + }); + + it('should error on no match with a string "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: 'urn:no-match' + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: urn:no-match'); + }); + + it('should error on no match with an array of string "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: ['urn:no-match-1', 'urn:no-match-2'] + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: urn:no-match-1 or urn:no-match-2'); + }); + + it('should error on no match with a Regex "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: /^urn:no-match$/ + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match$/'); + }); + + it('should error on no match with an array of Regex "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: [/^urn:no-match-1$/, /^urn:no-match-2$/] + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match-1$/ or /^urn:no-match-2$/'); + }); + + it('should error on no match with an array of a Regex and a string in "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: [/^urn:no-match$/, 'urn:no-match'] + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match$/ or urn:no-match'); + }); + + describe('when checking matching for both "urn:foo" and "urn:bar"', function() { + + it('should verify with an array of stings "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: ['urn:foo', 'urn:bar'] + })).to.not.throw; + }); + + it('should verify with a Regex "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: /^urn:[a-z]{3}$/ + })).to.not.throw; + }); + + it('should verify with an array of Regex "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: [/^urn:f[o]{2}$/, /^urn:b[ar]{2}$/] + })).to.not.throw; + }); + }); + + describe('when checking for a matching for "urn:foo"', function() { + it('should verify with a string "verify.audience"', function () { + expect(jwt.verify(token, undefined, { + audience: 'urn:foo' + })).to.not.throw; + }); + + it('should verify with a Regex "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: /^urn:f[o]{2}$/ + })).to.not.throw; + }); + + it('should verify with an array of Regex "verify.audience"', function () { + expect(jwt.verify(token, undefined, { + audience: [/^urn:no-match$/, /^urn:f[o]{2}$/] + })).to.not.throw; + }); + + it('should verify with an array containing a string and a Regex "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: ['urn:no_match', /^urn:f[o]{2}$/] + })).to.not.throw; + }); + + it('should verify with an array containing a Regex and a string "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: [/^urn:no-match$/, 'urn:foo'] + })).to.not.throw; + }); + }); + + describe('when checking matching for "urn:bar"', function() { + it('should verify with a string "verify.audience"', function () { + expect(jwt.verify(token, undefined, { + audience: 'urn:bar' + })).to.not.throw; + }); + + it('should verify with a Regex "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: /^urn:b[ar]{2}$/ + })).to.not.throw; + }); + + it('should verify with an array of Regex "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: [/^urn:no-match$/, /^urn:b[ar]{2}$/] + })).to.not.throw; + }); + + it('should verify with an array containing a string and a Regex "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: ['urn:no_match', /^urn:b[ar]{2}$/] + })).to.not.throw; + }); + + it('should verify with an array containing a Regex and a string "verify.audience" option', function () { + expect(jwt.verify(token, undefined, { + audience: [/^urn:no-match$/, 'urn:bar'] + })).to.not.throw; + }); + }); + }); + + describe('without a "aud" value in payload', function () { + let token; + + beforeEach(function () { + token = signWithAudience({}); + }); + + it('should verify and decode without verify "audience" option', function () { + const decoded = jwt.decode(token); + const verified = jwt.verify(token, undefined); + + expect(decoded).to.deep.equal(verified); + expect(decoded).to.not.have.property('aud'); + }); + + it('should error on no match with a string "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: 'urn:no-match' + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: urn:no-match'); + }); + + it('should error on no match with an array of string "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: ['urn:no-match-1', 'urn:no-match-2'] + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: urn:no-match-1 or urn:no-match-2'); + }); + + it('should error on no match with a Regex "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: /^urn:no-match$/ + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match$/'); + }); + + it('should error on no match with an array of Regex "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: [/^urn:no-match-1$/, /^urn:no-match-2$/] + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match-1$/ or /^urn:no-match-2$/'); + }); + + it('should error on no match with an array of a Regex and a string in "verify.audience" option', function () { + expect(() => jwt.verify(token, undefined, { + audience: [/^urn:no-match$/, 'urn:no-match'] + })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match$/ or urn:no-match'); + }); + }); + }); +}); diff --git a/test/jwt.asymmetric_signing.tests.js b/test/jwt.asymmetric_signing.tests.js index 866dd361..27a52123 100644 --- a/test/jwt.asymmetric_signing.tests.js +++ b/test/jwt.asymmetric_signing.tests.js @@ -113,193 +113,6 @@ describe('Asymmetric Algorithms', function(){ }); }); - describe('when signing a token with audience', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, audience: 'urn:foo' }); - - it('should check audience', function (done) { - jwt.verify(token, pub, { audience: 'urn:foo' }, function (err, decoded) { - assert.isNotNull(decoded); - assert.isNull(err); - done(); - }); - }); - - it('should check audience using RegExp', function (done) { - jwt.verify(token, pub, { audience: /urn:f[o]{2}/ }, function (err, decoded) { - assert.isNotNull(decoded); - assert.isNull(err); - done(); - }); - }); - - it('should check audience in array', function (done) { - jwt.verify(token, pub, { audience: ['urn:foo', 'urn:other'] }, function (err, decoded) { - assert.isNotNull(decoded); - assert.isNull(err); - done(); - }); - }); - - it('should check audience in array using RegExp', function (done) { - jwt.verify(token, pub, { audience: ['urn:bar', /urn:f[o]{2}/, 'urn:other'] }, function (err, decoded) { - assert.isNotNull(decoded); - assert.isNull(err); - done(); - }); - }); - - it('should throw when invalid audience', function (done) { - jwt.verify(token, pub, { audience: 'urn:wrong' }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - - it('should throw when invalid audience using RegExp', function (done) { - jwt.verify(token, pub, { audience: /urn:bar/ }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - - it('should throw when invalid audience in array', function (done) { - jwt.verify(token, pub, { audience: ['urn:wrong', 'urn:morewrong', /urn:bar/] }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - - }); - - describe('when signing a token with array audience', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, audience: ['urn:foo', 'urn:bar'] }); - - it('should check audience', function (done) { - jwt.verify(token, pub, { audience: 'urn:foo' }, function (err, decoded) { - assert.isNotNull(decoded); - assert.isNull(err); - done(); - }); - }); - - it('should check other audience', function (done) { - jwt.verify(token, pub, { audience: 'urn:bar' }, function (err, decoded) { - assert.isNotNull(decoded); - assert.isNull(err); - done(); - }); - }); - - it('should check audience using RegExp', function (done) { - jwt.verify(token, pub, { audience: /urn:f[o]{2}/ }, function (err, decoded) { - assert.isNotNull(decoded); - assert.isNull(err); - done(); - }); - }); - - it('should check audience in array', function (done) { - jwt.verify(token, pub, { audience: ['urn:foo', 'urn:other'] }, function (err, decoded) { - assert.isNotNull(decoded); - assert.isNull(err); - done(); - }); - }); - - it('should check audience in array using RegExp', function (done) { - jwt.verify(token, pub, { audience: ['urn:one', 'urn:other', /urn:f[o]{2}/] }, function (err, decoded) { - assert.isNotNull(decoded); - assert.isNull(err); - done(); - }); - }); - - it('should throw when invalid audience', function (done) { - jwt.verify(token, pub, { audience: 'urn:wrong' }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - - it('should throw when invalid audience using RegExp', function (done) { - jwt.verify(token, pub, { audience: /urn:wrong/ }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - - it('should throw when invalid audience in array', function (done) { - jwt.verify(token, pub, { audience: ['urn:wrong', 'urn:morewrong'] }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - - it('should throw when invalid audience in array', function (done) { - jwt.verify(token, pub, { audience: ['urn:wrong', 'urn:morewrong', /urn:alsowrong/] }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - - }); - - describe('when signing a token without audience', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm }); - - it('should check audience', function (done) { - jwt.verify(token, pub, { audience: 'urn:wrong' }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - - it('should check audience using RegExp', function (done) { - jwt.verify(token, pub, { audience: /urn:wrong/ }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - - it('should check audience in array', function (done) { - jwt.verify(token, pub, { audience: ['urn:wrong', 'urn:morewrong', /urn:alsowrong/] }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - - }); - describe('when signing a token with issuer', function () { var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, issuer: 'urn:foo' }); diff --git a/test/non_object_values.tests.js b/test/non_object_values.tests.js index 3d489a77..a3de4ea6 100644 --- a/test/non_object_values.tests.js +++ b/test/non_object_values.tests.js @@ -1,6 +1,5 @@ var jwt = require('../index'); var expect = require('chai').expect; -var JsonWebTokenError = require('../lib/JsonWebTokenError'); describe('non_object_values values', function() { @@ -10,13 +9,6 @@ describe('non_object_values values', function() { expect(result).to.equal('hello'); }); - it('should fail to validate audience when the payload is string', function () { - var token = jwt.sign('hello', '123'); - expect(function () { - jwt.verify(token, '123', { audience: 'foo' }); - }).to.throw(JsonWebTokenError); - }); - it('should work with number', function () { var token = jwt.sign(123, '123'); var result = jwt.verify(token, '123'); diff --git a/test/schema.tests.js b/test/schema.tests.js index c3fc38fc..30c817a1 100644 --- a/test/schema.tests.js +++ b/test/schema.tests.js @@ -14,14 +14,6 @@ describe('schema', function() { jwt.sign({foo: 123}, isEcdsa ? cert_ecdsa_priv : cert_rsa_priv, options); } - 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' }); From e2860a9d2a412627d79741a95bc7159971b923b9 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Fri, 20 Jul 2018 13:44:02 -0230 Subject: [PATCH 30/77] Minor test refactoring for recently added tests (#504) * Prefix claim- to claim related test files * Fix typo of "signWithNoBfore" in notBefore tests --- test/{aud.test.js => claim-aud.test.js} | 0 test/{exp.test.js => claim-exp.test.js} | 0 test/{nbf.test.js => claim-nbf.test.js} | 44 ++++++++++++------------- 3 files changed, 22 insertions(+), 22 deletions(-) rename test/{aud.test.js => claim-aud.test.js} (100%) rename test/{exp.test.js => claim-exp.test.js} (100%) rename test/{nbf.test.js => claim-nbf.test.js} (85%) diff --git a/test/aud.test.js b/test/claim-aud.test.js similarity index 100% rename from test/aud.test.js rename to test/claim-aud.test.js diff --git a/test/exp.test.js b/test/claim-exp.test.js similarity index 100% rename from test/exp.test.js rename to test/claim-exp.test.js diff --git a/test/nbf.test.js b/test/claim-nbf.test.js similarity index 85% rename from test/nbf.test.js rename to test/claim-nbf.test.js index 9a96907f..5eb68001 100644 --- a/test/nbf.test.js +++ b/test/claim-nbf.test.js @@ -9,7 +9,7 @@ const testUtils = require('./test-utils'); const base64UrlEncode = testUtils.base64UrlEncode; const noneAlgorithmHeader = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0'; -function signWithNoBefore(payload, notBefore) { +function signWithNotBefore(payload, notBefore) { const options = {algorithm: 'none'}; if (notBefore !== undefined) { options.notBefore = notBefore; @@ -36,7 +36,7 @@ describe('not before', function() { {foo: 'bar'}, ].forEach((notBefore) => { it(`should error with with value ${util.inspect(notBefore)}`, function () { - expect(() => signWithNoBefore({}, notBefore)).to.throw( + expect(() => signWithNotBefore({}, notBefore)).to.throw( '"notBefore" should be a number of seconds or string representing a timespan' ); }); @@ -44,7 +44,7 @@ describe('not before', function() { // TODO this should throw the same error as other invalid inputs it(`should error with with value ''`, function () { - expect(() => signWithNoBefore({}, '')).to.throw( + expect(() => signWithNotBefore({}, '')).to.throw( 'val is not a non-empty string or a valid number. val=""' ); }); @@ -57,19 +57,19 @@ describe('not before', function() { }); it('should error when "nbf" is in payload', function () { - expect(() => signWithNoBefore({nbf: 100}, 100)).to.throw( + expect(() => signWithNotBefore({nbf: 100}, 100)).to.throw( 'Bad "options.notBefore" option the payload already has an "nbf" property.' ); }); it('should error with a string payload', function () { - expect(() => signWithNoBefore('a string payload', 100)).to.throw( + expect(() => signWithNotBefore('a string payload', 100)).to.throw( 'invalid notBefore option for string payload' ); }); it('should error with a Buffer payload', function () { - expect(() => signWithNoBefore(new Buffer('a Buffer payload'), 100)).to.throw( + expect(() => signWithNotBefore(new Buffer('a Buffer payload'), 100)).to.throw( 'invalid notBefore option for object payload' ); }); @@ -90,7 +90,7 @@ describe('not before', function() { {foo: 'bar'}, ].forEach((nbf) => { it(`should error with with value ${util.inspect(nbf)}`, function () { - expect(() => signWithNoBefore({nbf})).to.throw( + expect(() => signWithNotBefore({nbf})).to.throw( '"nbf" should be a number of seconds' ); }); @@ -135,7 +135,7 @@ describe('not before', function() { }); it('should set correct "nbf" with negative number of seconds', function () { - const token = signWithNoBefore({}, -10); + const token = signWithNotBefore({}, -10); const decoded = jwt.decode(token); const verified = jwt.verify(token, undefined); @@ -144,7 +144,7 @@ describe('not before', function() { }); it('should set correct "nbf" with positive number of seconds', function () { - const token = signWithNoBefore({}, 10); + const token = signWithNotBefore({}, 10); fakeClock.tick(10000); const decoded = jwt.decode(token); @@ -155,7 +155,7 @@ describe('not before', function() { }); it('should set correct "nbf" with zero seconds', function () { - const token = signWithNoBefore({}, 0); + const token = signWithNotBefore({}, 0); const decoded = jwt.decode(token); @@ -165,7 +165,7 @@ describe('not before', function() { }); it('should set correct "nbf" with negative string timespan', function () { - const token = signWithNoBefore({}, '-10 s'); + const token = signWithNotBefore({}, '-10 s'); const decoded = jwt.decode(token); @@ -176,7 +176,7 @@ describe('not before', function() { it('should set correct "nbf" with positive string timespan', function () { - const token = signWithNoBefore({}, '10 s'); + const token = signWithNotBefore({}, '10 s'); fakeClock.tick(10000); const decoded = jwt.decode(token); @@ -187,7 +187,7 @@ describe('not before', function() { }); it('should set correct "nbf" with zero string timespan', function () { - const token = signWithNoBefore({}, '0 s'); + const token = signWithNotBefore({}, '0 s'); const decoded = jwt.decode(token); @@ -198,7 +198,7 @@ describe('not before', function() { // TODO an nbf of -Infinity should fail validation it('should set null "nbf" when given -Infinity', function () { - const token = signWithNoBefore({nbf: -Infinity}); + const token = signWithNotBefore({nbf: -Infinity}); const decoded = jwt.decode(token); expect(decoded.nbf).to.be.null; @@ -206,7 +206,7 @@ describe('not before', function() { // TODO an nbf of Infinity should fail validation it('should set null "nbf" when given value Infinity', function () { - const token = signWithNoBefore({nbf: Infinity}); + const token = signWithNotBefore({nbf: Infinity}); const decoded = jwt.decode(token); expect(decoded.nbf).to.be.null; @@ -214,14 +214,14 @@ describe('not before', function() { // TODO an nbf of NaN should fail validation it('should set null "nbf" when given value NaN', function () { - const token = signWithNoBefore({nbf: NaN}); + const token = signWithNotBefore({nbf: NaN}); const decoded = jwt.decode(token); expect(decoded.nbf).to.be.null; }); it('should set correct "nbf" when "iat" is passed', function () { - const token = signWithNoBefore({iat: 40}, -10); + const token = signWithNotBefore({iat: 40}, -10); const decoded = jwt.decode(token); @@ -231,7 +231,7 @@ describe('not before', function() { }); it('should verify "nbf" using "clockTimestamp"', function () { - const token = signWithNoBefore({}, 10); + const token = signWithNotBefore({}, 10); const verified = jwt.verify(token, undefined, {clockTimestamp: 70}); expect(verified.iat).to.equal(60); @@ -239,7 +239,7 @@ describe('not before', function() { }); it('should verify "nbf" using "clockTolerance"', function () { - const token = signWithNoBefore({}, 5); + const token = signWithNotBefore({}, 5); const verified = jwt.verify(token, undefined, {clockTolerance: 6}); expect(verified.iat).to.equal(60); @@ -247,7 +247,7 @@ describe('not before', function() { }); it('should ignore a not active token when "ignoreNotBefore" is true', function () { - const token = signWithNoBefore({}, '10 s'); + const token = signWithNotBefore({}, '10 s'); const verified = jwt.verify(token, undefined, {ignoreNotBefore: true}); expect(verified.iat).to.equal(60); @@ -255,7 +255,7 @@ describe('not before', function() { }); it('should error on verify if "nbf" is after current time', function () { - const token = signWithNoBefore({nbf: 61}); + const token = signWithNotBefore({nbf: 61}); expect(() => jwt.verify(token, undefined)).to.throw( jwt.NotBeforeError, @@ -264,7 +264,7 @@ describe('not before', function() { }); it('should error on verify if "nbf" is after current time using clockTolerance', function () { - const token = signWithNoBefore({}, 5); + const token = signWithNotBefore({}, 5); expect(() => jwt.verify(token, undefined, {clockTolerance: 4})).to.throw( jwt.NotBeforeError, From 5a7fa23c0b4ac6c25304dab8767ef840b43a0eca Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Fri, 20 Jul 2018 13:52:10 -0230 Subject: [PATCH 31/77] Refactor tests related to subject and sub (#505) This change extracts all tests in the existing files related to the subject option and sub claim into a single test file. Several other tests are also added that were missing from the existing files. --- test/claim-sub.tests.js | 107 +++++++++++++++++++++++++++ test/jwt.asymmetric_signing.tests.js | 36 --------- test/schema.tests.js | 7 -- 3 files changed, 107 insertions(+), 43 deletions(-) create mode 100644 test/claim-sub.tests.js diff --git a/test/claim-sub.tests.js b/test/claim-sub.tests.js new file mode 100644 index 00000000..cd7ffac8 --- /dev/null +++ b/test/claim-sub.tests.js @@ -0,0 +1,107 @@ +'use strict'; + +const jwt = require('../'); +const expect = require('chai').expect; +const util = require('util'); + +function signWithSubject(payload, subject) { + const options = {algorithm: 'none'}; + if (subject !== undefined) { + options.subject = subject; + } + return jwt.sign(payload, undefined, options); +} + +describe('subject', function() { + describe('`jwt.sign` "subject" option validation', function () { + [ + true, + false, + null, + -1, + 0, + 1, + -1.1, + 1.1, + -Infinity, + Infinity, + NaN, + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((subject) => { + it(`should error with with value ${util.inspect(subject)}`, function () { + expect(() => signWithSubject({}, subject)).to.throw('"subject" must be a string'); + }); + }); + + // undefined needs special treatment because {} is not the same as {subject: undefined} + it('should error with with value undefined', function () { + expect(() => jwt.sign({}, undefined, {subject: undefined, algorithm: 'none'})).to.throw( + '"subject" must be a string' + ); + }); + + it('should error when "sub" is in payload', function () { + expect(() => signWithSubject({sub: 'bar'}, 'foo')).to.throw( + 'Bad "options.subject" option. The payload already has an "sub" property.' + ); + }); + + + it('should error with a string payload', function () { + expect(() => signWithSubject('a string payload', 'foo')).to.throw( + 'invalid subject option for string payload' + ); + }); + + it('should error with a Buffer payload', function () { + expect(() => signWithSubject(new Buffer('a Buffer payload'), 'foo')).to.throw( + 'invalid subject option for object payload' + ); + }); + }); + + describe('when signing and verifying a token with "subject" option', function () { + it('should verify with a string "subject"', function () { + const token = signWithSubject({}, 'foo'); + const decoded = jwt.decode(token); + const verified = jwt.verify(token, undefined, {subject: 'foo'}); + expect(decoded).to.deep.equal(verified); + expect(decoded.sub).to.equal('foo'); + }); + + it('should verify with a string "sub"', function () { + const token = signWithSubject({sub: 'foo'}); + const decoded = jwt.decode(token); + const verified = jwt.verify(token, undefined, {subject: 'foo'}); + expect(decoded).to.deep.equal(verified); + expect(decoded.sub).to.equal('foo'); + }); + + it('should not verify "sub" if "verify.subject" option not provided', function() { + const token = signWithSubject({sub: 'foo'}); + const decoded = jwt.decode(token); + const verified = jwt.verify(token, undefined); + expect(decoded).to.deep.equal(verified); + expect(decoded.sub).to.equal('foo'); + }); + + it('should error if "sub" does not match "verify.subject" option', function() { + const token = signWithSubject({sub: 'foo'}); + expect(() => jwt.verify(token, undefined, {subject: 'bar'})).to.throw( + jwt.JsonWebTokenError, + 'jwt subject invalid. expected: bar' + ); + }); + + it('should error without "sub" and with "verify.subject" option', function() { + const token = signWithSubject({}); + expect(() => jwt.verify(token, undefined, {subject: 'foo'})).to.throw( + jwt.JsonWebTokenError, + 'jwt subject invalid. expected: foo' + ); + }); + }); +}); diff --git a/test/jwt.asymmetric_signing.tests.js b/test/jwt.asymmetric_signing.tests.js index 27a52123..6951b95a 100644 --- a/test/jwt.asymmetric_signing.tests.js +++ b/test/jwt.asymmetric_signing.tests.js @@ -157,42 +157,6 @@ describe('Asymmetric Algorithms', function(){ }); }); - describe('when signing a token with subject', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, subject: 'subject' }); - - it('should check subject', function (done) { - jwt.verify(token, pub, { subject: 'subject' }, function (err, decoded) { - assert.isNotNull(decoded); - assert.isNull(err); - done(); - }); - }); - - it('should throw when invalid subject', function (done) { - jwt.verify(token, pub, { subject: 'wrongSubject' }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - }); - - describe('when signing a token without subject', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm }); - - it('should check subject', function (done) { - jwt.verify(token, pub, { subject: 'subject' }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - }); - describe('when signing a token with jwt id', function () { var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, jwtid: 'jwtid' }); diff --git a/test/schema.tests.js b/test/schema.tests.js index 30c817a1..77592d65 100644 --- a/test/schema.tests.js +++ b/test/schema.tests.js @@ -51,13 +51,6 @@ describe('schema', function() { 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 }); From 877bd57ab2aca9b7d230805b21f921baed3da169 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Sun, 26 Aug 2018 08:22:15 -0230 Subject: [PATCH 32/77] Refactor tests related to iat and maxAge (#507) This change extracts all tests related to the iat claim and the maxAge verify option into two test files. Several additional tests are added that were missing from the existing tests. --- test/claim-iat.test.js | 273 +++++++++++++++++++++++++++++++++++++ test/option-maxAge.test.js | 70 ++++++++++ test/schema.tests.js | 7 - test/verify.tests.js | 179 ------------------------ 4 files changed, 343 insertions(+), 186 deletions(-) create mode 100644 test/claim-iat.test.js create mode 100644 test/option-maxAge.test.js diff --git a/test/claim-iat.test.js b/test/claim-iat.test.js new file mode 100644 index 00000000..01358e07 --- /dev/null +++ b/test/claim-iat.test.js @@ -0,0 +1,273 @@ +'use strict'; + +const jwt = require('../'); +const expect = require('chai').expect; +const sinon = require('sinon'); +const util = require('util'); +const testUtils = require('./test-utils'); + +const base64UrlEncode = testUtils.base64UrlEncode; +const noneAlgorithmHeader = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0'; + +function signWithIssueAtSync(issueAt, options) { + const payload = {}; + if (issueAt !== undefined) { + payload.iat = issueAt; + } + const opts = Object.assign({algorithm: 'none'}, options); + return jwt.sign(payload, undefined, opts); +} + +function signWithIssueAtAsync(issueAt, options, cb) { + const payload = {}; + if (issueAt !== undefined) { + payload.iat = issueAt; + } + const opts = Object.assign({algorithm: 'none'}, options); + // async calls require a truthy secret + // see: https://github.com/brianloveswords/node-jws/issues/62 + return jwt.sign(payload, 'secret', opts, cb); +} + +function verifyWithIssueAtSync(token, maxAge, options) { + const opts = Object.assign({maxAge}, options); + return jwt.verify(token, undefined, opts) +} + +function verifyWithIssueAtAsync(token, maxAge, options, cb) { + const opts = Object.assign({maxAge}, options); + return jwt.verify(token, undefined, opts, cb) +} + +describe('issue at', function() { + describe('`jwt.sign` "iat" claim validation', function () { + [ + true, + false, + null, + '', + 'invalid', + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((iat) => { + it(`should error with iat of ${util.inspect(iat)}`, function (done) { + expect(() => signWithIssueAtSync(iat, {})).to.throw('"iat" should be a number of seconds'); + signWithIssueAtAsync(iat, {}, (err) => { + expect(err.message).to.equal('"iat" should be a number of seconds'); + done(); + }); + }); + }); + + // undefined needs special treatment because {} is not the same as {iat: undefined} + it('should error with iat of undefined', function (done) { + expect(() => jwt.sign({iat: undefined}, undefined, {algorithm: 'none'})).to.throw( + '"iat" should be a number of seconds' + ); + jwt.sign({iat: undefined}, undefined, {algorithm: 'none'}, (err) => { + expect(err.message).to.equal('"iat" should be a number of seconds'); + done(); + }); + }); + }); + + describe('"iat" in payload with "maxAge" option validation', function () { + [ + true, + false, + null, + undefined, + -Infinity, + Infinity, + NaN, + '', + 'invalid', + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((iat) => { + it(`should error with iat of ${util.inspect(iat)}`, function (done) { + const encodedPayload = base64UrlEncode(JSON.stringify({iat})); + const token = `${noneAlgorithmHeader}.${encodedPayload}.`; + expect(() => verifyWithIssueAtSync(token, '1 min', {})).to.throw( + jwt.JsonWebTokenError, 'iat required when maxAge is specified' + ); + + verifyWithIssueAtAsync(token, '1 min', {}, (err) => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err.message).to.equal('iat required when maxAge is specified'); + done(); + }); + }); + }) + }); + + describe('when signing a token', function () { + let fakeClock; + beforeEach(function () { + fakeClock = sinon.useFakeTimers({now: 60000}); + }); + + afterEach(function () { + fakeClock.uninstall(); + }); + + [ + { + description: 'should default to current time for "iat"', + iat: undefined, + expectedIssueAt: 60, + options: {} + }, + { + description: 'should sign with provided time for "iat"', + iat: 100, + expectedIssueAt: 100, + options: {} + }, + // TODO an iat of -Infinity should fail validation + { + description: 'should set null "iat" when given -Infinity', + iat: -Infinity, + expectedIssueAt: null, + options: {} + }, + // TODO an iat of Infinity should fail validation + { + description: 'should set null "iat" when given Infinity', + iat: Infinity, + expectedIssueAt: null, + options: {} + }, + // TODO an iat of NaN should fail validation + { + description: 'should set to current time for "iat" when given value NaN', + iat: NaN, + expectedIssueAt: 60, + options: {} + }, + { + description: 'should remove default "iat" with "noTimestamp" option', + iat: undefined, + expectedIssueAt: undefined, + options: {noTimestamp: true} + }, + { + description: 'should remove provided "iat" with "noTimestamp" option', + iat: 10, + expectedIssueAt: undefined, + options: {noTimestamp: true} + }, + ].forEach((testCase) => { + it(testCase.description, function (done) { + const token = signWithIssueAtSync(testCase.iat, testCase.options); + expect(jwt.decode(token).iat).to.equal(testCase.expectedIssueAt); + signWithIssueAtAsync(testCase.iat, testCase.options, (err, token) => { + // node-jsw catches the error from expect, so we have to wrap it in try/catch and use done(error) + try { + expect(err).to.be.null; + expect(jwt.decode(token).iat).to.equal(testCase.expectedIssueAt); + done(); + } + catch (e) { + done(e); + } + }); + }); + }); + }); + + describe('when verifying a token', function() { + let token; + let fakeClock; + + beforeEach(function() { + fakeClock = sinon.useFakeTimers({now: 60000}); + }); + + afterEach(function () { + fakeClock.uninstall(); + }); + + [ + { + description: 'should verify using "iat" before the "maxAge"', + clockAdvance: 10000, + maxAge: 11, + options: {}, + }, + { + description: 'should verify using "iat" before the "maxAge" with a provided "clockTimestamp', + clockAdvance: 60000, + maxAge: 11, + options: {clockTimestamp: 70}, + }, + { + description: 'should verify using "iat" after the "maxAge" but within "clockTolerance"', + clockAdvance: 10000, + maxAge: 9, + options: {clockTimestamp: 2}, + }, + ].forEach((testCase) => { + it(testCase.description, function (done) { + const token = signWithIssueAtSync(undefined, {}); + fakeClock.tick(testCase.clockAdvance); + expect(verifyWithIssueAtSync(token, testCase.maxAge, testCase.options)).to.not.throw; + verifyWithIssueAtAsync(token, testCase.maxAge, testCase.options, done) + }); + }); + + [ + { + description: 'should throw using "iat" equal to the "maxAge"', + clockAdvance: 10000, + maxAge: 10, + options: {}, + expectedError: 'maxAge exceeded', + expectedExpiresAt: 70000, + }, + { + description: 'should throw using "iat" after the "maxAge"', + clockAdvance: 10000, + maxAge: 9, + options: {}, + expectedError: 'maxAge exceeded', + expectedExpiresAt: 69000, + }, + { + description: 'should throw using "iat" after the "maxAge" with a provided "clockTimestamp', + clockAdvance: 60000, + maxAge: 10, + options: {clockTimestamp: 70}, + expectedError: 'maxAge exceeded', + expectedExpiresAt: 70000, + }, + { + description: 'should throw using "iat" after the "maxAge" and "clockTolerance', + clockAdvance: 10000, + maxAge: 8, + options: {clockTolerance: 2}, + expectedError: 'maxAge exceeded', + expectedExpiresAt: 68000, + }, + ].forEach((testCase) => { + it(testCase.description, function(done) { + const expectedExpiresAtDate = new Date(testCase.expectedExpiresAt); + token = signWithIssueAtSync(undefined, {}); + fakeClock.tick(testCase.clockAdvance); + expect(() => verifyWithIssueAtSync(token, testCase.maxAge, {})) + .to.throw(jwt.TokenExpiredError, testCase.expectedError) + .to.have.property('expiredAt').that.deep.equals(expectedExpiresAtDate); + verifyWithIssueAtAsync(token, testCase.maxAge, {}, (err) => { + expect(err).to.be.instanceOf(jwt.TokenExpiredError); + expect(err.message).to.equal(testCase.expectedError); + expect(err.expiredAt).to.deep.equal(expectedExpiresAtDate); + done(); + }); + }); + }); + }); +}); diff --git a/test/option-maxAge.test.js b/test/option-maxAge.test.js new file mode 100644 index 00000000..c76676fc --- /dev/null +++ b/test/option-maxAge.test.js @@ -0,0 +1,70 @@ +'use strict'; + +const jwt = require('../'); +const expect = require('chai').expect; +const sinon = require('sinon'); +const util = require('util'); + +describe('maxAge option', function() { + let token; + + let fakeClock; + beforeEach(function() { + fakeClock = sinon.useFakeTimers({now: 60000}); + token = jwt.sign({iat: 70}, undefined, {algorithm: 'none'}); + }); + + afterEach(function() { + fakeClock.uninstall(); + }); + + [ + { + description: 'should work with a positive string value', + maxAge: '3s', + }, + { + description: 'should work with a negative string value', + maxAge: '-3s', + }, + { + description: 'should work with a positive numeric value', + maxAge: 3, + }, + { + description: 'should work with a negative numeric value', + maxAge: -3, + }, + ].forEach((testCase) => { + it(testCase.description, function (done) { + expect(jwt.verify(token, undefined, {maxAge: '3s'})).to.not.throw; + jwt.verify(token, undefined, {maxAge: testCase.maxAge}, (err) => { + expect(err).to.be.null; + done(); + }) + }); + }); + + [ + true, + 'invalid', + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((maxAge) => { + it(`should error with value ${util.inspect(maxAge)}`, function (done) { + expect(() => jwt.verify(token, undefined, {maxAge})).to.throw( + jwt.JsonWebTokenError, + '"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60' + ); + jwt.verify(token, undefined, {maxAge}, (err) => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err.message).to.equal( + '"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60' + ); + done(); + }) + }); + }); +}); diff --git a/test/schema.tests.js b/test/schema.tests.js index 77592d65..924bf700 100644 --- a/test/schema.tests.js +++ b/test/schema.tests.js @@ -73,13 +73,6 @@ describe('schema', function() { 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' }); diff --git a/test/verify.tests.js b/test/verify.tests.js index ac3f1d39..29bbe104 100644 --- a/test/verify.tests.js +++ b/test/verify.tests.js @@ -188,122 +188,6 @@ describe('verify', function() { }); }); - it('should not error if within maxAge timespan', function (done) { - clock = sinon.useFakeTimers(1437018587500); // iat + 5.5s, exp - 4.5s - var options = {algorithms: ['HS256'], maxAge: '6s'}; - - jwt.verify(token, key, options, function (err, p) { - assert.isNull(err); - assert.equal(p.foo, 'bar'); - done(); - }); - }); - - describe('option: maxAge', function () { - - [String('3s'), '3s', 3].forEach(function(maxAge) { - it(`should error for claims issued before a certain timespan (${typeof maxAge} type)`, function (done) { - clock = sinon.useFakeTimers(1437018587000); // iat + 5s, exp - 5s - var options = {algorithms: ['HS256'], maxAge: maxAge}; - - jwt.verify(token, key, options, function (err, p) { - assert.equal(err.name, 'TokenExpiredError'); - assert.equal(err.message, 'maxAge exceeded'); - assert.equal(err.expiredAt.constructor.name, 'Date'); - assert.equal(Number(err.expiredAt), 1437018585000); - assert.isUndefined(p); - done(); - }); - }); - }); - - [String('5s'), '5s', 5].forEach(function (maxAge) { - it(`should not error for claims issued before a certain timespan but still inside clockTolerance timespan (${typeof maxAge} type)`, function (done) { - clock = sinon.useFakeTimers(1437018587500); // iat + 5.5s, exp - 4.5s - var options = {algorithms: ['HS256'], maxAge: maxAge, clockTolerance: 1 }; - - jwt.verify(token, key, options, function (err, p) { - assert.isNull(err); - assert.equal(p.foo, 'bar'); - done(); - }); - }); - }); - - [String('6s'), '6s', 6].forEach(function (maxAge) { - it(`should not error if within maxAge timespan (${typeof maxAge} type)`, function (done) { - clock = sinon.useFakeTimers(1437018587500);// iat + 5.5s, exp - 4.5s - var options = {algorithms: ['HS256'], maxAge: maxAge}; - - jwt.verify(token, key, options, function (err, p) { - assert.isNull(err); - assert.equal(p.foo, 'bar'); - done(); - }); - }); - }); - - [String('8s'), '8s', 8].forEach(function (maxAge) { - it(`can be more restrictive than expiration (${typeof maxAge} type)`, function (done) { - clock = sinon.useFakeTimers(1437018591900); // iat + 9.9s, exp - 0.1s - var options = {algorithms: ['HS256'], maxAge: maxAge }; - - jwt.verify(token, key, options, function (err, p) { - assert.equal(err.name, 'TokenExpiredError'); - assert.equal(err.message, 'maxAge exceeded'); - assert.equal(err.expiredAt.constructor.name, 'Date'); - assert.equal(Number(err.expiredAt), 1437018590000); - assert.isUndefined(p); - done(); - }); - }); - }); - - [String('12s'), '12s', 12].forEach(function (maxAge) { - it(`cannot be more permissive than expiration (${typeof maxAge} type)`, function (done) { - clock = sinon.useFakeTimers(1437018593000); // iat + 11s, exp + 1s - var options = {algorithms: ['HS256'], maxAge: '12s'}; - - 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), 1437018592000); - assert.isUndefined(p); - done(); - }); - }); - }); - - [new String('1s'), 'no-timespan-string'].forEach(function (maxAge){ - it(`should error if maxAge is specified with a wrong string format/type (value: ${maxAge}, type: ${typeof maxAge})`, function (done) { - clock = sinon.useFakeTimers(1437018587000); // iat + 5s, exp - 5s - var options = { algorithms: ['HS256'], maxAge: maxAge }; - - jwt.verify(token, key, options, function (err, p) { - assert.equal(err.name, 'JsonWebTokenError'); - assert.equal(err.message, '"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'); - assert.isUndefined(p); - done(); - }); - }); - }); - - it('should error if maxAge is specified but there is no iat claim', function (done) { - var token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIifQ.0MBPd4Bru9-fK_HY3xmuDAc6N_embknmNuhdb9bKL_U'; - var options = {algorithms: ['HS256'], maxAge: '1s'}; - - jwt.verify(token, key, options, function (err, p) { - assert.equal(err.name, 'JsonWebTokenError'); - assert.equal(err.message, 'iat required when maxAge is specified'); - assert.isUndefined(p); - done(); - }); - }); - - }); - describe('option: clockTimestamp', function () { var clockTimestamp = 1000000000; it('should verify unexpired token relative to user-provided clockTimestamp', function (done) { @@ -338,57 +222,6 @@ describe('verify', function() { describe('option: maxAge and clockTimestamp', function () { // { foo: 'bar', iat: 1437018582, exp: 1437018800 } exp = iat + 218s var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODgwMH0.AVOsNC7TiT-XVSpCpkwB1240izzCIJ33Lp07gjnXVpA'; - it('should error for claims issued before a certain timespan', function (done) { - var clockTimestamp = 1437018682; - var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1m'}; - - jwt.verify(token, key, options, function (err, p) { - assert.equal(err.name, 'TokenExpiredError'); - assert.equal(err.message, 'maxAge exceeded'); - assert.equal(err.expiredAt.constructor.name, 'Date'); - assert.equal(Number(err.expiredAt), 1437018642000); - assert.isUndefined(p); - done(); - }); - }); - it('should not error for claims issued before a certain timespan but still inside clockTolerance timespan', function (done) { - var clockTimestamp = 1437018592; // iat + 10s - var options = { - algorithms: ['HS256'], - clockTimestamp: clockTimestamp, - maxAge: '3s', - clockTolerance: 10 - }; - - jwt.verify(token, key, options, function (err, p) { - assert.isNull(err); - assert.equal(p.foo, 'bar'); - done(); - }); - }); - it('should not error if within maxAge timespan', function (done) { - var clockTimestamp = 1437018587; // iat + 5s - var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '6s'}; - - jwt.verify(token, key, options, function (err, p) { - assert.isNull(err); - assert.equal(p.foo, 'bar'); - done(); - }); - }); - it('can be more restrictive than expiration', function (done) { - var clockTimestamp = 1437018588; // iat + 6s - var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '5s'}; - - jwt.verify(token, key, options, function (err, p) { - assert.equal(err.name, 'TokenExpiredError'); - assert.equal(err.message, 'maxAge exceeded'); - assert.equal(err.expiredAt.constructor.name, 'Date'); - assert.equal(Number(err.expiredAt), 1437018587000); - assert.isUndefined(p); - done(); - }); - }); it('cannot be more permissive than expiration', function (done) { var clockTimestamp = 1437018900; // iat + 318s (exp: iat + 218s) var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1000y'}; @@ -403,18 +236,6 @@ describe('verify', function() { done(); }); }); - it('should error if maxAge is specified but there is no iat claim', function (done) { - var clockTimestamp = 1437018582; - var token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIifQ.0MBPd4Bru9-fK_HY3xmuDAc6N_embknmNuhdb9bKL_U'; - var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1s'}; - - jwt.verify(token, key, options, function (err, p) { - assert.equal(err.name, 'JsonWebTokenError'); - assert.equal(err.message, 'iat required when maxAge is specified'); - assert.isUndefined(p); - done(); - }); - }); }); }); }); From 5498bdc4865ffb2ba2fd44d889fad7e83873bb33 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Mon, 10 Sep 2018 07:18:41 -0230 Subject: [PATCH 33/77] Update dependencies used for running tests (#518) Update chai and mocha to the latest versions and update nyc to the latest version that supports Node 4. --- package.json | 6 +++--- test/async_sign.tests.js | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 5130eea9..c81e9882 100644 --- a/package.json +++ b/package.json @@ -48,13 +48,13 @@ }, "devDependencies": { "atob": "^1.1.2", - "chai": "^1.10.0", + "chai": "^4.1.2", "conventional-changelog": "~1.1.0", "cost-of-modules": "^1.0.1", "eslint": "^4.19.1", - "mocha": "^2.1.0", + "mocha": "^5.2.0", "nsp": "^2.6.2", - "nyc": "^11.8.0", + "nyc": "^11.9.0", "sinon": "^6.0.0" }, "engines": { diff --git a/test/async_sign.tests.js b/test/async_sign.tests.js index 6a4f1530..b0948d3b 100644 --- a/test/async_sign.tests.js +++ b/test/async_sign.tests.js @@ -20,14 +20,14 @@ describe('signing a token asynchronously', function() { it('should work with empty options', function (done) { jwt.sign({abc: 1}, "secret", {}, function (err) { - expect(err).to.be.null(); + expect(err).to.be.null; done(); }); }); it('should work without options object at all', function (done) { jwt.sign({abc: 1}, "secret", function (err) { - expect(err).to.be.null(); + expect(err).to.be.null; done(); }); }); @@ -53,7 +53,7 @@ describe('signing a token asynchronously', function() { it('should return error when secret is not a cert for RS256', function(done) { //this throw an error because the secret is not a cert and RS256 requires a cert. jwt.sign({ foo: 'bar' }, secret, { algorithm: 'RS256' }, function (err) { - expect(err).to.be.ok(); + expect(err).to.be.ok; done(); }); }); @@ -61,14 +61,14 @@ describe('signing a token asynchronously', function() { it('should return error on wrong arguments', function(done) { //this throw an error because the secret is not a cert and RS256 requires a cert. jwt.sign({ foo: 'bar' }, secret, { notBefore: {} }, function (err) { - expect(err).to.be.ok(); + expect(err).to.be.ok; done(); }); }); it('should return error on wrong arguments (2)', function(done) { jwt.sign('string', 'secret', {noTimestamp: true}, function (err) { - expect(err).to.be.ok(); + expect(err).to.be.ok; expect(err).to.be.instanceof(Error); done(); }); @@ -111,7 +111,7 @@ describe('signing a token asynchronously', function() { it('should return an error if the secret is falsy and algorithm is not set to none: ' + (typeof secret === 'string' ? '(empty string)' : secret), function(done) { // This is needed since jws will not answer for falsy secrets jwt.sign('string', secret, {}, function(err, token) { - expect(err).to.be.exist(); + expect(err).to.exist; expect(err.message).to.equal('secretOrPrivateKey must have a value'); expect(token).to.not.exist; done(); From b76f2a80f5229ee5cde321dd2ff14aa5df16d283 Mon Sep 17 00:00:00 2001 From: Kundan <33788217+Kundan28@users.noreply.github.com> Date: Thu, 11 Oct 2018 00:03:53 +0530 Subject: [PATCH 34/77] Update README.md (#527) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f8a2d2c2..eb7861b4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # jsonwebtoken -[![Build Status](https://secure.travis-ci.org/auth0/node-jsonwebtoken.svg?branch=master)](http://travis-ci.org/auth0/node-jsonwebtoken)[![Dependency Status](https://david-dm.org/auth0/node-jsonwebtoken.svg)](https://david-dm.org/auth0/node-jsonwebtoken) +| **Build** | **Dependency** | +|-----------|---------------| +| [![Build Status](https://secure.travis-ci.org/auth0/node-jsonwebtoken.svg?branch=master)](http://travis-ci.org/auth0/node-jsonwebtoken) | [![Dependency Status](https://david-dm.org/auth0/node-jsonwebtoken.svg)](https://david-dm.org/auth0/node-jsonwebtoken) | An implementation of [JSON Web Tokens](https://tools.ietf.org/html/rfc7519). From 683d8a9b31ad6327948f84268bd2c8e4350779d1 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Wed, 10 Oct 2018 16:07:34 -0230 Subject: [PATCH 35/77] Create and implement async/sync test helpers (#523) It is difficult to write tests that ensure that both the asynchronous and synchronous calls to the sign and verify functions had the same result. These helpers ensure that the calls are the same and return the common result. As a proof of concept, the iat claim tests have been updated to use the new helpers. --- test/claim-iat.test.js | 96 +++++++++++++--------------------- test/test-utils.js | 114 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 150 insertions(+), 60 deletions(-) diff --git a/test/claim-iat.test.js b/test/claim-iat.test.js index 01358e07..a1c63ba6 100644 --- a/test/claim-iat.test.js +++ b/test/claim-iat.test.js @@ -9,16 +9,7 @@ const testUtils = require('./test-utils'); const base64UrlEncode = testUtils.base64UrlEncode; const noneAlgorithmHeader = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0'; -function signWithIssueAtSync(issueAt, options) { - const payload = {}; - if (issueAt !== undefined) { - payload.iat = issueAt; - } - const opts = Object.assign({algorithm: 'none'}, options); - return jwt.sign(payload, undefined, opts); -} - -function signWithIssueAtAsync(issueAt, options, cb) { +function signWithIssueAt(issueAt, options, callback) { const payload = {}; if (issueAt !== undefined) { payload.iat = issueAt; @@ -26,17 +17,12 @@ function signWithIssueAtAsync(issueAt, options, cb) { const opts = Object.assign({algorithm: 'none'}, options); // async calls require a truthy secret // see: https://github.com/brianloveswords/node-jws/issues/62 - return jwt.sign(payload, 'secret', opts, cb); + testUtils.signJWTHelper(payload, 'secret', opts, callback); } -function verifyWithIssueAtSync(token, maxAge, options) { +function verifyWithIssueAt(token, maxAge, options, callback) { const opts = Object.assign({maxAge}, options); - return jwt.verify(token, undefined, opts) -} - -function verifyWithIssueAtAsync(token, maxAge, options, cb) { - const opts = Object.assign({maxAge}, options); - return jwt.verify(token, undefined, opts, cb) + testUtils.verifyJWTHelper(token, undefined, opts, callback); } describe('issue at', function() { @@ -53,22 +39,22 @@ describe('issue at', function() { {foo: 'bar'}, ].forEach((iat) => { it(`should error with iat of ${util.inspect(iat)}`, function (done) { - expect(() => signWithIssueAtSync(iat, {})).to.throw('"iat" should be a number of seconds'); - signWithIssueAtAsync(iat, {}, (err) => { - expect(err.message).to.equal('"iat" should be a number of seconds'); - done(); + signWithIssueAt(iat, {}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err.message).to.equal('"iat" should be a number of seconds'); + }); }); }); }); // undefined needs special treatment because {} is not the same as {iat: undefined} it('should error with iat of undefined', function (done) { - expect(() => jwt.sign({iat: undefined}, undefined, {algorithm: 'none'})).to.throw( - '"iat" should be a number of seconds' - ); - jwt.sign({iat: undefined}, undefined, {algorithm: 'none'}, (err) => { - expect(err.message).to.equal('"iat" should be a number of seconds'); - done(); + testUtils.signJWTHelper({iat: undefined}, 'secret', {algorithm: 'none'}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err.message).to.equal('"iat" should be a number of seconds'); + }); }); }); }); @@ -92,14 +78,11 @@ describe('issue at', function() { it(`should error with iat of ${util.inspect(iat)}`, function (done) { const encodedPayload = base64UrlEncode(JSON.stringify({iat})); const token = `${noneAlgorithmHeader}.${encodedPayload}.`; - expect(() => verifyWithIssueAtSync(token, '1 min', {})).to.throw( - jwt.JsonWebTokenError, 'iat required when maxAge is specified' - ); - - verifyWithIssueAtAsync(token, '1 min', {}, (err) => { - expect(err).to.be.instanceOf(jwt.JsonWebTokenError); - expect(err.message).to.equal('iat required when maxAge is specified'); - done(); + verifyWithIssueAt(token, '1 min', {}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err.message).to.equal('iat required when maxAge is specified'); + }); }); }); }) @@ -163,25 +146,17 @@ describe('issue at', function() { }, ].forEach((testCase) => { it(testCase.description, function (done) { - const token = signWithIssueAtSync(testCase.iat, testCase.options); - expect(jwt.decode(token).iat).to.equal(testCase.expectedIssueAt); - signWithIssueAtAsync(testCase.iat, testCase.options, (err, token) => { - // node-jsw catches the error from expect, so we have to wrap it in try/catch and use done(error) - try { + signWithIssueAt(testCase.iat, testCase.options, (err, token) => { + testUtils.asyncCheck(done, () => { expect(err).to.be.null; expect(jwt.decode(token).iat).to.equal(testCase.expectedIssueAt); - done(); - } - catch (e) { - done(e); - } + }); }); }); }); }); describe('when verifying a token', function() { - let token; let fakeClock; beforeEach(function() { @@ -213,10 +188,14 @@ describe('issue at', function() { }, ].forEach((testCase) => { it(testCase.description, function (done) { - const token = signWithIssueAtSync(undefined, {}); + const token = jwt.sign({}, 'secret', {algorithm: 'none'}); fakeClock.tick(testCase.clockAdvance); - expect(verifyWithIssueAtSync(token, testCase.maxAge, testCase.options)).to.not.throw; - verifyWithIssueAtAsync(token, testCase.maxAge, testCase.options, done) + verifyWithIssueAt(token, testCase.maxAge, testCase.options, (err, token) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(token).to.be.a('object'); + }); + }); }); }); @@ -256,16 +235,15 @@ describe('issue at', function() { ].forEach((testCase) => { it(testCase.description, function(done) { const expectedExpiresAtDate = new Date(testCase.expectedExpiresAt); - token = signWithIssueAtSync(undefined, {}); + const token = jwt.sign({}, 'secret', {algorithm: 'none'}); fakeClock.tick(testCase.clockAdvance); - expect(() => verifyWithIssueAtSync(token, testCase.maxAge, {})) - .to.throw(jwt.TokenExpiredError, testCase.expectedError) - .to.have.property('expiredAt').that.deep.equals(expectedExpiresAtDate); - verifyWithIssueAtAsync(token, testCase.maxAge, {}, (err) => { - expect(err).to.be.instanceOf(jwt.TokenExpiredError); - expect(err.message).to.equal(testCase.expectedError); - expect(err.expiredAt).to.deep.equal(expectedExpiresAtDate); - done(); + + verifyWithIssueAt(token, testCase.maxAge, testCase.options, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err.message).to.equal(testCase.expectedError); + expect(err.expiredAt).to.deep.equal(expectedExpiresAtDate); + }); }); }); }); diff --git a/test/test-utils.js b/test/test-utils.js index f49889e2..aa115dae 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -1,13 +1,125 @@ 'use strict'; +const jwt = require('../'); +const expect = require('chai').expect; +const sinon = require('sinon'); + +/** + * Correctly report errors that occur in an asynchronous callback + * @param {function(err): void} done The mocha callback + * @param {function(): void} testFunction The assertions function + */ +function asyncCheck(done, testFunction) { + try { + testFunction(); + done(); + } + catch(err) { + done(err); + } +} + +/** + * Assert that two errors are equal + * @param e1 {Error} The first error + * @param e2 {Error} The second error + */ +// chai does not do deep equality on errors: https://github.com/chaijs/chai/issues/1009 +function expectEqualError(e1, e2) { + // message and name are not always enumerable, so manually reference them + expect(e1.message, 'Async/Sync Error equality: message').to.equal(e2.message); + expect(e1.name, 'Async/Sync Error equality: name').to.equal(e2.name); + + // compare other enumerable error properties + for(const propertyName in e1) { + expect(e1[propertyName], `Async/Sync Error equality: ${propertyName}`).to.deep.equal(e2[propertyName]); + } +} + +/** + * Base64-url encode a string + * @param str {string} The string to encode + * @returns {string} The encoded string + */ function base64UrlEncode(str) { return Buffer.from(str).toString('base64') - .replace(/\=/g, "") + .replace(/[=]/g, "") .replace(/\+/g, "-") .replace(/\//g, "_") ; } +/** + * Verify a JWT, ensuring that the asynchronous and synchronous calls to `verify` have the same result + * @param {string} jwtString The JWT as a string + * @param {string} secretOrPrivateKey The shared secret or private key + * @param {object} options Verify options + * @param {function(err, token):void} callback + */ +function verifyJWTHelper(jwtString, secretOrPrivateKey, options, callback) { + // freeze the time to ensure the clock remains stable across the async and sync calls + const fakeClock = sinon.useFakeTimers({now: Date.now()}); + let error; + let syncVerified; + try { + syncVerified = jwt.verify(jwtString, secretOrPrivateKey, options); + } + catch (err) { + error = err; + } + jwt.verify(jwtString, secretOrPrivateKey, options, (err, asyncVerifiedToken) => { + try { + if (error) { + expectEqualError(err, error); + callback(err); + } + else { + expect(syncVerified, 'Async/Sync token equality').to.deep.equal(asyncVerifiedToken); + callback(null, syncVerified); + } + } + finally { + if (fakeClock) { + fakeClock.restore(); + } + } + }); +} + +/** + * Sign a payload to create a JWT, ensuring that the asynchronous and synchronous calls to `sign` have the same result + * @param {object} payload The JWT payload + * @param {string} secretOrPrivateKey The shared secret or private key + * @param {object} options Sign options + * @param {function(err, token):void} callback + */ +function signJWTHelper(payload, secretOrPrivateKey, options, callback) { + // freeze the time to ensure the clock remains stable across the async and sync calls + const fakeClock = sinon.useFakeTimers({now: Date.now()}); + let error; + let syncSigned; + try { + syncSigned = jwt.sign(payload, secretOrPrivateKey, options); + } + catch (err) { + error = err; + } + jwt.sign(payload, secretOrPrivateKey, options, (err, asyncSigned) => { + fakeClock.restore(); + if (error) { + expectEqualError(err, error); + callback(err); + } + else { + expect(syncSigned, 'Async/Sync token equality').to.equal(asyncSigned); + callback(null, syncSigned); + } + }); +} + module.exports = { + asyncCheck, base64UrlEncode, + signJWTHelper, + verifyJWTHelper, }; From 29cd654b956529e939ae8f8c30b9da7063aad501 Mon Sep 17 00:00:00 2001 From: Brian Ploetz Date: Wed, 10 Oct 2018 14:41:57 -0400 Subject: [PATCH 36/77] Document NotBeforeError (#529) --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index eb7861b4..8343f9ea 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,30 @@ jwt.verify(token, 'shhhhh', function(err, decoded) { }); ``` +### NotBeforeError +Thrown if current time is before the nbf claim. + +Error object: + +* name: 'NotBeforeError' +* message: 'jwt not active' +* date: 2018-10-04T16:10:44.000Z + +```js +jwt.verify(token, 'shhhhh', function(err, decoded) { + if (err) { + /* + err = { + name: 'NotBeforeError', + message: 'jwt not active', + date: 2018-10-04T16:10:44.000Z + } + */ + } +}); +``` + + ## Algorithms supported Array of supported algorithms. The following algorithms are currently supported. From 342b07bb105a35739eb91265ba5b9dd33c300fc6 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Tue, 16 Oct 2018 14:15:42 -0230 Subject: [PATCH 37/77] Implement async/sync tests for sub claim (#534) Refactor existing tests for the sub claim to use the async/sync test helpers. --- test/claim-sub.tests.js | 148 ++++++++++++++++++++++++++-------------- 1 file changed, 97 insertions(+), 51 deletions(-) diff --git a/test/claim-sub.tests.js b/test/claim-sub.tests.js index cd7ffac8..5cd33790 100644 --- a/test/claim-sub.tests.js +++ b/test/claim-sub.tests.js @@ -3,13 +3,14 @@ const jwt = require('../'); const expect = require('chai').expect; const util = require('util'); +const testUtils = require('./test-utils'); -function signWithSubject(payload, subject) { +function signWithSubject(subject, payload, callback) { const options = {algorithm: 'none'}; if (subject !== undefined) { options.subject = subject; } - return jwt.sign(payload, undefined, options); + testUtils.signJWTHelper(payload, 'secret', options, callback); } describe('subject', function() { @@ -31,77 +32,122 @@ describe('subject', function() { {}, {foo: 'bar'}, ].forEach((subject) => { - it(`should error with with value ${util.inspect(subject)}`, function () { - expect(() => signWithSubject({}, subject)).to.throw('"subject" must be a string'); + it(`should error with with value ${util.inspect(subject)}`, function (done) { + signWithSubject(subject, {}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', '"subject" must be a string'); + }); + }); }); }); // undefined needs special treatment because {} is not the same as {subject: undefined} - it('should error with with value undefined', function () { - expect(() => jwt.sign({}, undefined, {subject: undefined, algorithm: 'none'})).to.throw( - '"subject" must be a string' - ); + it('should error with with value undefined', function (done) { + testUtils.signJWTHelper({}, undefined, {subject: undefined, algorithm: 'none'}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', '"subject" must be a string'); + }); + }); }); - it('should error when "sub" is in payload', function () { - expect(() => signWithSubject({sub: 'bar'}, 'foo')).to.throw( - 'Bad "options.subject" option. The payload already has an "sub" property.' - ); + it('should error when "sub" is in payload', function (done) { + signWithSubject('foo', {sub: 'bar'}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property( + 'message', + 'Bad "options.subject" option. The payload already has an "sub" property.' + ); + }); + }); }); - - it('should error with a string payload', function () { - expect(() => signWithSubject('a string payload', 'foo')).to.throw( - 'invalid subject option for string payload' - ); + it('should error with a string payload', function (done) { + signWithSubject('foo', 'a string payload', (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property( + 'message', + 'invalid subject option for string payload' + ); + }); + }); }); - it('should error with a Buffer payload', function () { - expect(() => signWithSubject(new Buffer('a Buffer payload'), 'foo')).to.throw( - 'invalid subject option for object payload' - ); + it('should error with a Buffer payload', function (done) { + signWithSubject('foo', new Buffer('a Buffer payload'), (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property( + 'message', + 'invalid subject option for object payload' + ); + }); + }); }); }); describe('when signing and verifying a token with "subject" option', function () { - it('should verify with a string "subject"', function () { - const token = signWithSubject({}, 'foo'); - const decoded = jwt.decode(token); - const verified = jwt.verify(token, undefined, {subject: 'foo'}); - expect(decoded).to.deep.equal(verified); - expect(decoded.sub).to.equal('foo'); + it('should verify with a string "subject"', function (done) { + signWithSubject('foo', {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {subject: 'foo'}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('sub', 'foo'); + }); + }) + }); }); - it('should verify with a string "sub"', function () { - const token = signWithSubject({sub: 'foo'}); - const decoded = jwt.decode(token); - const verified = jwt.verify(token, undefined, {subject: 'foo'}); - expect(decoded).to.deep.equal(verified); - expect(decoded.sub).to.equal('foo'); + it('should verify with a string "sub"', function (done) { + signWithSubject(undefined, {sub: 'foo'}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {subject: 'foo'}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('sub', 'foo'); + }); + }) + }); }); - it('should not verify "sub" if "verify.subject" option not provided', function() { - const token = signWithSubject({sub: 'foo'}); - const decoded = jwt.decode(token); - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.sub).to.equal('foo'); + it('should not verify "sub" if verify "subject" option not provided', function(done) { + signWithSubject(undefined, {sub: 'foo'}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('sub', 'foo'); + }); + }) + }); }); - it('should error if "sub" does not match "verify.subject" option', function() { - const token = signWithSubject({sub: 'foo'}); - expect(() => jwt.verify(token, undefined, {subject: 'bar'})).to.throw( - jwt.JsonWebTokenError, - 'jwt subject invalid. expected: bar' - ); + it('should error if "sub" does not match verify "subject" option', function(done) { + signWithSubject(undefined, {sub: 'foo'}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {subject: 'bar'}, (e2) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); + expect(e2).to.have.property('message', 'jwt subject invalid. expected: bar'); + }); + }) + }); }); - it('should error without "sub" and with "verify.subject" option', function() { - const token = signWithSubject({}); - expect(() => jwt.verify(token, undefined, {subject: 'foo'})).to.throw( - jwt.JsonWebTokenError, - 'jwt subject invalid. expected: foo' - ); + it('should error without "sub" and with verify "subject" option', function(done) { + signWithSubject(undefined, {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {subject: 'foo'}, (e2) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); + expect(e2).to.have.property('message', 'jwt subject invalid. expected: foo'); + }); + }) + }); }); }); }); From 9ae3f207ac64b7450ea0a3434418f5ca58d8125e Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Tue, 16 Oct 2018 14:27:20 -0230 Subject: [PATCH 38/77] Implement async/sync tests for exp claim (#536) Refactor existing tests for the exp claim to use the async/sync test helpers. This required fixing a case where an error was improperly handled in the async case. --- sign.js | 7 +- test/claim-exp.test.js | 360 +++++++++++++++++++++++++---------------- 2 files changed, 224 insertions(+), 143 deletions(-) diff --git a/sign.js b/sign.js index 6041596c..64ce548e 100644 --- a/sign.js +++ b/sign.js @@ -154,7 +154,12 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { } if (typeof options.expiresIn !== 'undefined' && typeof payload === 'object') { - payload.exp = timespan(options.expiresIn, timestamp); + 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')); } diff --git a/test/claim-exp.test.js b/test/claim-exp.test.js index dec044ac..816d12e7 100644 --- a/test/claim-exp.test.js +++ b/test/claim-exp.test.js @@ -9,12 +9,12 @@ const testUtils = require('./test-utils'); const base64UrlEncode = testUtils.base64UrlEncode; const noneAlgorithmHeader = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0'; -function signWithExpiresIn(payload, expiresIn) { +function signWithExpiresIn(expiresIn, payload, callback) { const options = {algorithm: 'none'}; if (expiresIn !== undefined) { options.expiresIn = expiresIn; } - return jwt.sign(payload, undefined, options); + testUtils.signJWTHelper(payload, 'secret', options, callback); } describe('expires', function() { @@ -35,43 +35,68 @@ describe('expires', function() { {}, {foo: 'bar'}, ].forEach((expiresIn) => { - it(`should error with with value ${util.inspect(expiresIn)}`, function () { - expect(() => signWithExpiresIn({}, expiresIn)).to.throw( - '"expiresIn" should be a number of seconds or string representing a timespan' - ); + it(`should error with with value ${util.inspect(expiresIn)}`, function (done) { + signWithExpiresIn(expiresIn, {}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message') + .match(/"expiresIn" should be a number of seconds or string representing a timespan/); + }); + }); }); }); // TODO this should throw the same error as other invalid inputs - it(`should error with with value ''`, function () { - expect(() => signWithExpiresIn({}, '')).to.throw( - 'val is not a non-empty string or a valid number. val=""' - ); + it(`should error with with value ''`, function (done) { + signWithExpiresIn('', {}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', 'val is not a non-empty string or a valid number. val=""'); + }); + }); }); // undefined needs special treatment because {} is not the same as {expiresIn: undefined} - it('should error with with value undefined', function () { - expect(() =>jwt.sign({}, undefined, {expiresIn: undefined, algorithm: 'none'})).to.throw( - '"expiresIn" should be a number of seconds or string representing a timespan' - ); + it('should error with with value undefined', function (done) { + testUtils.signJWTHelper({}, undefined, {expiresIn: undefined, algorithm: 'none'}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property( + 'message', + '"expiresIn" should be a number of seconds or string representing a timespan' + ); + }); + }); }); - it ('should error when "exp" is in payload', function() { - expect(() => signWithExpiresIn({exp: 100}, 100)).to.throw( - 'Bad "options.expiresIn" option the payload already has an "exp" property.' - ); + it ('should error when "exp" is in payload', function(done) { + signWithExpiresIn(100, {exp: 100}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property( + 'message', + 'Bad "options.expiresIn" option the payload already has an "exp" property.' + ); + }); + }); }); - it('should error with a string payload', function() { - expect(() => signWithExpiresIn('a string payload', 100)).to.throw( - 'invalid expiresIn option for string payload' - ); + it('should error with a string payload', function(done) { + signWithExpiresIn(100, 'a string payload', (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', 'invalid expiresIn option for string payload'); + }); + }); }); - it('should error with a Buffer payload', function() { - expect(() => signWithExpiresIn(Buffer.from('a Buffer payload'), 100)).to.throw( - 'invalid expiresIn option for object payload' - ); + it('should error with a Buffer payload', function(done) { + signWithExpiresIn(100, Buffer.from('a Buffer payload'), (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', 'invalid expiresIn option for object payload'); + }); + }); }); }); @@ -89,10 +114,13 @@ describe('expires', function() { {}, {foo: 'bar'}, ].forEach((exp) => { - it(`should error with with value ${util.inspect(exp)}`, function () { - expect(() => signWithExpiresIn({exp})).to.throw( - '"exp" should be a number of seconds' - ); + it(`should error with with value ${util.inspect(exp)}`, function (done) { + signWithExpiresIn(undefined, {exp}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', '"exp" should be a number of seconds'); + }); + }); }); }); }); @@ -113,13 +141,15 @@ describe('expires', function() { {}, {foo: 'bar'}, ].forEach((exp) => { - it(`should error with with value ${util.inspect(exp)}`, function () { + it(`should error with with value ${util.inspect(exp)}`, function (done) { const encodedPayload = base64UrlEncode(JSON.stringify({exp})); const token = `${noneAlgorithmHeader}.${encodedPayload}.`; - expect(() => jwt.verify(token, undefined)).to.throw( - jwt.JsonWebTokenError, - 'invalid exp value' - ); + testUtils.verifyJWTHelper(token, undefined, {exp}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', 'invalid exp value'); + }); + }); }); }) }); @@ -134,144 +164,190 @@ describe('expires', function() { fakeClock.uninstall(); }); - it('should set correct "exp" with negative number of seconds', function() { - const token = signWithExpiresIn({}, -10); - fakeClock.tick(-10001); - - const decoded = jwt.decode(token); - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.exp).to.equal(50); + it('should set correct "exp" with negative number of seconds', function(done) { + signWithExpiresIn(-10, {}, (e1, token) => { + fakeClock.tick(-10001); + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('exp', 50); + }); + }) + }); }); - it('should set correct "exp" with positive number of seconds', function() { - const token = signWithExpiresIn({}, 10); - - const decoded = jwt.decode(token); - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.exp).to.equal(70); + it('should set correct "exp" with positive number of seconds', function(done) { + signWithExpiresIn(10, {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('exp', 70); + }); + }) + }); }); - it('should set correct "exp" with zero seconds', function() { - const token = signWithExpiresIn({}, 0); - - fakeClock.tick(-1); - - const decoded = jwt.decode(token); - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.exp).to.equal(60); + it('should set correct "exp" with zero seconds', function(done) { + signWithExpiresIn(0, {}, (e1, token) => { + fakeClock.tick(-1); + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('exp', 60); + }); + }) + }); }); - it('should set correct "exp" with negative string timespan', function() { - const token = signWithExpiresIn({}, '-10 s'); - - fakeClock.tick(-10001); - - const decoded = jwt.decode(token); - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.exp).to.equal(50); + it('should set correct "exp" with negative string timespan', function(done) { + signWithExpiresIn('-10 s', {}, (e1, token) => { + fakeClock.tick(-10001); + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('exp', 50); + }); + }) + }); }); - it('should set correct "exp" with positive string timespan', function() { - const token = signWithExpiresIn({}, '10 s'); - - fakeClock.tick(-10001); - const decoded = jwt.decode(token); - - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.exp).to.equal(70); + it('should set correct "exp" with positive string timespan', function(done) { + signWithExpiresIn('10 s', {}, (e1, token) => { + fakeClock.tick(-10001); + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('exp', 70); + }); + }) + }); }); - it('should set correct "exp" with zero string timespan', function() { - const token = signWithExpiresIn({}, '0 s'); - - fakeClock.tick(-1); - const decoded = jwt.decode(token); - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.exp).to.equal(60); + it('should set correct "exp" with zero string timespan', function(done) { + signWithExpiresIn('0 s', {}, (e1, token) => { + fakeClock.tick(-1); + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('exp', 60); + }); + }) + }); }); // TODO an exp of -Infinity should fail validation - it('should set null "exp" when given -Infinity', function () { - const token = signWithExpiresIn({exp: -Infinity}); - - const decoded = jwt.decode(token); - expect(decoded.exp).to.be.null; + it('should set null "exp" when given -Infinity', function (done) { + signWithExpiresIn(undefined, {exp: -Infinity}, (err, token) => { + const decoded = jwt.decode(token); + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('exp', null); + }); + }); }); // TODO an exp of Infinity should fail validation - it('should set null "exp" when given value Infinity', function () { - const token = signWithExpiresIn({exp: Infinity}); - - const decoded = jwt.decode(token); - expect(decoded.exp).to.be.null; + it('should set null "exp" when given value Infinity', function (done) { + signWithExpiresIn(undefined, {exp: Infinity}, (err, token) => { + const decoded = jwt.decode(token); + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('exp', null); + }); + }); }); // TODO an exp of NaN should fail validation - it('should set null "exp" when given value NaN', function () { - const token = signWithExpiresIn({exp: NaN}); - - const decoded = jwt.decode(token); - expect(decoded.exp).to.be.null; + it('should set null "exp" when given value NaN', function (done) { + signWithExpiresIn(undefined, {exp: NaN}, (err, token) => { + const decoded = jwt.decode(token); + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('exp', null); + }); + }); }); - it('should set correct "exp" when "iat" is passed', function () { - const token = signWithExpiresIn({iat: 80}, -10); - - const decoded = jwt.decode(token); - - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.exp).to.equal(70); + it('should set correct "exp" when "iat" is passed', function (done) { + signWithExpiresIn(-10, {iat: 80}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('exp', 70); + }); + }) + }); }); - it('should verify "exp" using "clockTimestamp"', function () { - const token = signWithExpiresIn({}, 10); - - const verified = jwt.verify(token, undefined, {clockTimestamp: 69}); - expect(verified.iat).to.equal(60); - expect(verified.exp).to.equal(70); + it('should verify "exp" using "clockTimestamp"', function (done) { + signWithExpiresIn(10, {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {clockTimestamp: 69}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('iat', 60); + expect(decoded).to.have.property('exp', 70); + }); + }) + }); }); - it('should verify "exp" using "clockTolerance"', function () { - const token = signWithExpiresIn({}, 5); - - fakeClock.tick(10000); - - const verified = jwt.verify(token, undefined, {clockTolerance: 6}); - expect(verified.iat).to.equal(60); - expect(verified.exp).to.equal(65); + it('should verify "exp" using "clockTolerance"', function (done) { + signWithExpiresIn(5, {}, (e1, token) => { + fakeClock.tick(10000); + testUtils.verifyJWTHelper(token, undefined, {clockTimestamp: 6}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('iat', 60); + expect(decoded).to.have.property('exp', 65); + }); + }) + }); }); - it('should ignore a expired token when "ignoreExpiration" is true', function () { - const token = signWithExpiresIn({}, '-10 s'); - - const verified = jwt.verify(token, undefined, {ignoreExpiration: true}); - expect(verified.iat).to.equal(60); - expect(verified.exp).to.equal(50); + it('should ignore a expired token when "ignoreExpiration" is true', function (done) { + signWithExpiresIn('-10 s', {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {ignoreExpiration: true}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('iat', 60); + expect(decoded).to.have.property('exp', 50); + }); + }) + }); }); - it('should error on verify if "exp" is at current time', function() { - const token = signWithExpiresIn({exp: 60}); - - expect(() => jwt.verify(token, undefined)).to.throw( - jwt.TokenExpiredError, - 'jwt expired' - ); + it('should error on verify if "exp" is at current time', function(done) { + signWithExpiresIn(undefined, {exp: 60}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.instanceOf(jwt.TokenExpiredError); + expect(e2).to.have.property('message', 'jwt expired'); + }); + }); + }); }); - it('should error on verify if "exp" is before current time using clockTolerance', function () { - const token = signWithExpiresIn({}, -5); - - expect(() => jwt.verify(token, undefined, {clockTolerance: 5})).to.throw( - jwt.TokenExpiredError, - 'jwt expired' - ); + it('should error on verify if "exp" is before current time using clockTolerance', function (done) { + signWithExpiresIn(-5, {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {clockTolerance: 5}, (e2) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.instanceOf(jwt.TokenExpiredError); + expect(e2).to.have.property('message', 'jwt expired'); + }); + }); + }); }); }); }); From 88bc965061ed65299a395f42a100fb8f8c3c683e Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Tue, 16 Oct 2018 15:16:21 -0230 Subject: [PATCH 39/77] Implement async/sync tests for nbf claim (#537) Refactor the existing tests for the nbf claim to use the async/sync test helpers. This required fixing a case where an error was improperly handled for an async call. --- sign.js | 7 +- test/claim-nbf.test.js | 354 +++++++++++++++++++++++++---------------- 2 files changed, 220 insertions(+), 141 deletions(-) diff --git a/sign.js b/sign.js index 64ce548e..c608f4d4 100644 --- a/sign.js +++ b/sign.js @@ -147,7 +147,12 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { } if (typeof options.notBefore !== 'undefined') { - payload.nbf = timespan(options.notBefore, timestamp); + 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')); } diff --git a/test/claim-nbf.test.js b/test/claim-nbf.test.js index 5eb68001..f36396c3 100644 --- a/test/claim-nbf.test.js +++ b/test/claim-nbf.test.js @@ -9,12 +9,12 @@ const testUtils = require('./test-utils'); const base64UrlEncode = testUtils.base64UrlEncode; const noneAlgorithmHeader = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0'; -function signWithNotBefore(payload, notBefore) { +function signWithNotBefore(notBefore, payload, callback) { const options = {algorithm: 'none'}; if (notBefore !== undefined) { options.notBefore = notBefore; } - return jwt.sign(payload, undefined, options); + testUtils.signJWTHelper(payload, 'secret', options, callback); } describe('not before', function() { @@ -35,43 +35,68 @@ describe('not before', function() { {}, {foo: 'bar'}, ].forEach((notBefore) => { - it(`should error with with value ${util.inspect(notBefore)}`, function () { - expect(() => signWithNotBefore({}, notBefore)).to.throw( - '"notBefore" should be a number of seconds or string representing a timespan' - ); + it(`should error with with value ${util.inspect(notBefore)}`, function (done) { + signWithNotBefore(notBefore, {}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message') + .match(/"notBefore" should be a number of seconds or string representing a timespan/); + }); + }); }); }); // TODO this should throw the same error as other invalid inputs - it(`should error with with value ''`, function () { - expect(() => signWithNotBefore({}, '')).to.throw( - 'val is not a non-empty string or a valid number. val=""' - ); + it(`should error with with value ''`, function (done) { + signWithNotBefore('', {}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', 'val is not a non-empty string or a valid number. val=""'); + }); + }); }); // undefined needs special treatment because {} is not the same as {notBefore: undefined} - it('should error with with value undefined', function () { - expect(() => jwt.sign({}, undefined, {notBefore: undefined, algorithm: 'none'})).to.throw( - '"notBefore" should be a number of seconds or string representing a timespan' - ); + it('should error with with value undefined', function (done) { + testUtils.signJWTHelper({}, undefined, {notBefore: undefined, algorithm: 'none'}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property( + 'message', + '"notBefore" should be a number of seconds or string representing a timespan' + ); + }); + }); }); - it('should error when "nbf" is in payload', function () { - expect(() => signWithNotBefore({nbf: 100}, 100)).to.throw( - 'Bad "options.notBefore" option the payload already has an "nbf" property.' - ); + it('should error when "nbf" is in payload', function (done) { + signWithNotBefore(100, {nbf: 100}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property( + 'message', + 'Bad "options.notBefore" option the payload already has an "nbf" property.' + ); + }); + }); }); - it('should error with a string payload', function () { - expect(() => signWithNotBefore('a string payload', 100)).to.throw( - 'invalid notBefore option for string payload' - ); + it('should error with a string payload', function (done) { + signWithNotBefore(100, 'a string payload', (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', 'invalid notBefore option for string payload'); + }); + }); }); - it('should error with a Buffer payload', function () { - expect(() => signWithNotBefore(new Buffer('a Buffer payload'), 100)).to.throw( - 'invalid notBefore option for object payload' - ); + it('should error with a Buffer payload', function (done) { + signWithNotBefore(100, new Buffer('a Buffer payload'), (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', 'invalid notBefore option for object payload'); + }); + }); }); }); @@ -89,10 +114,13 @@ describe('not before', function() { {}, {foo: 'bar'}, ].forEach((nbf) => { - it(`should error with with value ${util.inspect(nbf)}`, function () { - expect(() => signWithNotBefore({nbf})).to.throw( - '"nbf" should be a number of seconds' - ); + it(`should error with with value ${util.inspect(nbf)}`, function (done) { + signWithNotBefore(undefined, {nbf}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', '"nbf" should be a number of seconds'); + }); + }); }); }); }); @@ -113,13 +141,15 @@ describe('not before', function() { {}, {foo: 'bar'}, ].forEach((nbf) => { - it(`should error with with value ${util.inspect(nbf)}`, function () { + it(`should error with with value ${util.inspect(nbf)}`, function (done) { const encodedPayload = base64UrlEncode(JSON.stringify({nbf})); const token = `${noneAlgorithmHeader}.${encodedPayload}.`; - expect(() => jwt.verify(token, undefined)).to.throw( - jwt.JsonWebTokenError, - 'invalid nbf value' - ); + testUtils.verifyJWTHelper(token, undefined, {nbf}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', 'invalid nbf value'); + }); + }); }); }) }); @@ -134,142 +164,186 @@ describe('not before', function() { fakeClock.uninstall(); }); - it('should set correct "nbf" with negative number of seconds', function () { - const token = signWithNotBefore({}, -10); - const decoded = jwt.decode(token); - - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.nbf).to.equal(50); + it('should set correct "nbf" with negative number of seconds', function (done) { + signWithNotBefore(-10, {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('nbf', 50); + }); + }) + }); }); - it('should set correct "nbf" with positive number of seconds', function () { - const token = signWithNotBefore({}, 10); - - fakeClock.tick(10000); - const decoded = jwt.decode(token); - - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.nbf).to.equal(70); + it('should set correct "nbf" with positive number of seconds', function (done) { + signWithNotBefore(10, {}, (e1, token) => { + fakeClock.tick(10000); + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('nbf', 70); + }); + }) + }); }); - it('should set correct "nbf" with zero seconds', function () { - const token = signWithNotBefore({}, 0); - - const decoded = jwt.decode(token); - - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.nbf).to.equal(60); + it('should set correct "nbf" with zero seconds', function (done) { + signWithNotBefore(0, {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('nbf', 60); + }); + }) + }); }); - it('should set correct "nbf" with negative string timespan', function () { - const token = signWithNotBefore({}, '-10 s'); - - const decoded = jwt.decode(token); - - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.nbf).to.equal(50); + it('should set correct "nbf" with negative string timespan', function (done) { + signWithNotBefore('-10 s', {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('nbf', 50); + }); + }) + }); }); - - it('should set correct "nbf" with positive string timespan', function () { - const token = signWithNotBefore({}, '10 s'); - - fakeClock.tick(10000); - const decoded = jwt.decode(token); - - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.nbf).to.equal(70); + it('should set correct "nbf" with positive string timespan', function (done) { + signWithNotBefore('10 s', {}, (e1, token) => { + fakeClock.tick(10000); + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('nbf', 70); + }); + }) + }); }); - it('should set correct "nbf" with zero string timespan', function () { - const token = signWithNotBefore({}, '0 s'); - - const decoded = jwt.decode(token); - - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.nbf).to.equal(60); + it('should set correct "nbf" with zero string timespan', function (done) { + signWithNotBefore('0 s', {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('nbf', 60); + }); + }) + }); }); // TODO an nbf of -Infinity should fail validation - it('should set null "nbf" when given -Infinity', function () { - const token = signWithNotBefore({nbf: -Infinity}); - - const decoded = jwt.decode(token); - expect(decoded.nbf).to.be.null; + it('should set null "nbf" when given -Infinity', function (done) { + signWithNotBefore(undefined, {nbf: -Infinity}, (err, token) => { + const decoded = jwt.decode(token); + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('nbf', null); + }); + }); }); // TODO an nbf of Infinity should fail validation - it('should set null "nbf" when given value Infinity', function () { - const token = signWithNotBefore({nbf: Infinity}); - - const decoded = jwt.decode(token); - expect(decoded.nbf).to.be.null; + it('should set null "nbf" when given value Infinity', function (done) { + signWithNotBefore(undefined, {nbf: Infinity}, (err, token) => { + const decoded = jwt.decode(token); + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('nbf', null); + }); + }); }); // TODO an nbf of NaN should fail validation - it('should set null "nbf" when given value NaN', function () { - const token = signWithNotBefore({nbf: NaN}); - - const decoded = jwt.decode(token); - expect(decoded.nbf).to.be.null; + it('should set null "nbf" when given value NaN', function (done) { + signWithNotBefore(undefined, {nbf: NaN}, (err, token) => { + const decoded = jwt.decode(token); + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('nbf', null); + }); + }); }); - it('should set correct "nbf" when "iat" is passed', function () { - const token = signWithNotBefore({iat: 40}, -10); - - const decoded = jwt.decode(token); - - const verified = jwt.verify(token, undefined); - expect(decoded).to.deep.equal(verified); - expect(decoded.nbf).to.equal(30); + it('should set correct "nbf" when "iat" is passed', function (done) { + signWithNotBefore(-10, {iat: 40}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('nbf', 30); + }); + }) + }); }); - it('should verify "nbf" using "clockTimestamp"', function () { - const token = signWithNotBefore({}, 10); - - const verified = jwt.verify(token, undefined, {clockTimestamp: 70}); - expect(verified.iat).to.equal(60); - expect(verified.nbf).to.equal(70); + it('should verify "nbf" using "clockTimestamp"', function (done) { + signWithNotBefore(10, {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {clockTimestamp: 70}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('iat', 60); + expect(decoded).to.have.property('nbf', 70); + }); + }) + }); }); - it('should verify "nbf" using "clockTolerance"', function () { - const token = signWithNotBefore({}, 5); - - const verified = jwt.verify(token, undefined, {clockTolerance: 6}); - expect(verified.iat).to.equal(60); - expect(verified.nbf).to.equal(65); + it('should verify "nbf" using "clockTolerance"', function (done) { + signWithNotBefore(5, {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {clockTolerance: 6}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('iat', 60); + expect(decoded).to.have.property('nbf', 65); + }); + }) + }); }); - it('should ignore a not active token when "ignoreNotBefore" is true', function () { - const token = signWithNotBefore({}, '10 s'); - - const verified = jwt.verify(token, undefined, {ignoreNotBefore: true}); - expect(verified.iat).to.equal(60); - expect(verified.nbf).to.equal(70); + it('should ignore a not active token when "ignoreNotBefore" is true', function (done) { + signWithNotBefore('10 s', {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {ignoreNotBefore: true}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('iat', 60); + expect(decoded).to.have.property('nbf', 70); + }); + }) + }); }); - it('should error on verify if "nbf" is after current time', function () { - const token = signWithNotBefore({nbf: 61}); - - expect(() => jwt.verify(token, undefined)).to.throw( - jwt.NotBeforeError, - 'jwt not active' - ); + it('should error on verify if "nbf" is after current time', function (done) { + signWithNotBefore(undefined, {nbf: 61}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.instanceOf(jwt.NotBeforeError); + expect(e2).to.have.property('message', 'jwt not active'); + }); + }) + }); }); - it('should error on verify if "nbf" is after current time using clockTolerance', function () { - const token = signWithNotBefore({}, 5); - - expect(() => jwt.verify(token, undefined, {clockTolerance: 4})).to.throw( - jwt.NotBeforeError, - 'jwt not active' - ); + it('should error on verify if "nbf" is after current time using clockTolerance', function (done) { + signWithNotBefore(5, {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {clockTolerance: 4}, (e2) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.instanceOf(jwt.NotBeforeError); + expect(e2).to.have.property('message', 'jwt not active'); + }); + }) + }); }); }); }); \ No newline at end of file From 1c8ff5a68e6da73af2809c9d87faaf78602c99bb Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Wed, 17 Oct 2018 07:07:29 -0230 Subject: [PATCH 40/77] Implement async/sync tests for the aud claim (#535) Refactor existing tests for the aud claim to use the async/sync test helpers. --- test/claim-aud.test.js | 505 +++++++++++++++++++++++++---------------- 1 file changed, 309 insertions(+), 196 deletions(-) diff --git a/test/claim-aud.test.js b/test/claim-aud.test.js index 1763f134..448da5c8 100644 --- a/test/claim-aud.test.js +++ b/test/claim-aud.test.js @@ -3,13 +3,19 @@ const jwt = require('../'); const expect = require('chai').expect; const util = require('util'); +const testUtils = require('./test-utils'); -function signWithAudience(payload, audience) { +function signWithAudience(audience, payload, callback) { const options = {algorithm: 'none'}; if (audience !== undefined) { options.audience = audience; } - return jwt.sign(payload, undefined, options); + + testUtils.signJWTHelper(payload, 'secret', options, callback); +} + +function verifyWithAudience(token, audience, callback) { + testUtils.verifyJWTHelper(token, undefined, {audience}, callback); } describe('audience', function() { @@ -29,247 +35,335 @@ describe('audience', function() { {}, {foo: 'bar'}, ].forEach((audience) => { - it(`should error with with value ${util.inspect(audience)}`, function () { - expect(() => signWithAudience({}, audience)).to.throw('"audience" must be a string or array'); + it(`should error with with value ${util.inspect(audience)}`, function (done) { + signWithAudience(audience, {}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', '"audience" must be a string or array'); + }); + }); }); }); // undefined needs special treatment because {} is not the same as {aud: undefined} - it('should error with with value undefined', function () { - expect(() => jwt.sign({}, undefined, {audience: undefined, algorithm: 'none'})).to.throw( - '"audience" must be a string or array' - ); + it('should error with with value undefined', function (done) { + testUtils.signJWTHelper({}, 'secret', {audience: undefined, algorithm: 'none'}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', '"audience" must be a string or array'); + }); + }); }); - it('should error when "aud" is in payload', function () { - expect(() => signWithAudience({aud: ''}, 'my_aud')).to.throw( - 'Bad "options.audience" option. The payload already has an "aud" property.' - ); + it('should error when "aud" is in payload', function (done) { + signWithAudience('my_aud', {aud: ''}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property( + 'message', + 'Bad "options.audience" option. The payload already has an "aud" property.' + ); + }); + }); }); - it('should error with a string payload', function () { - expect(() => signWithAudience('a string payload', 'my_aud')).to.throw( - 'invalid audience option for string payload' - ); + it('should error with a string payload', function (done) { + signWithAudience('my_aud', 'a string payload', (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', 'invalid audience option for string payload'); + }); + }); }); - it('should error with a Buffer payload', function () { - expect(() => signWithAudience(new Buffer('a Buffer payload'), 'my_aud')).to.throw( - 'invalid audience option for object payload' - ); + it('should error with a Buffer payload', function (done) { + signWithAudience('my_aud', new Buffer('a Buffer payload'), (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', 'invalid audience option for object payload'); + }); + }); }); }); describe('when signing and verifying a token with "audience" option', function () { - describe('with a string for "aud" value in payload', function () { + describe('with a "aud" of "urn:foo" in payload', function () { let token; - beforeEach(function () { - token = signWithAudience({}, 'urn:foo'); - }); - - it('should verify and decode without verify "audience" option', function () { - const decoded = jwt.decode(token); - const verified = jwt.verify(token, undefined); - - expect(decoded).to.deep.equal(verified); - expect(decoded.aud).to.equal('urn:foo'); - }); - - it('should verify with a string "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: 'urn:foo' - })).to.not.throw; - }); - - it('should verify with an array of strings "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: ['urn:no_match', 'urn:foo'] - })).to.not.throw; - }); - - it('should verify with a Regex "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: /^urn:f[o]{2}$/ - })).to.not.throw; - }); - - it('should verify with an array of Regex "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: [/^urn:no_match$/, /^urn:f[o]{2}$/] - })).to.not.throw; - }); - - it('should verify with an array containing a string and a Regex "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: ['urn:no_match', /^urn:f[o]{2}$/] - })).to.not.throw; + beforeEach(function (done) { + signWithAudience('urn:foo', {}, (err, t) => { + token = t; + done(err); + }); }); - it('should verify with an array containing a Regex and a string "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: [/^urn:no_match$/, 'urn:foo'] - })).to.not.throw; + [ + undefined, + 'urn:foo', + /^urn:f[o]{2}$/, + ['urn:no_match', 'urn:foo'], + ['urn:no_match', /^urn:f[o]{2}$/], + [/^urn:no_match$/, /^urn:f[o]{2}$/], + [/^urn:no_match$/, 'urn:foo'] + ].forEach((audience) =>{ + it(`should verify and decode with verify "audience" option of ${util.inspect(audience)}`, function (done) { + verifyWithAudience(token, audience, (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud', 'urn:foo'); + }); + }); + }); }); - it('should error on no match with a string "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: 'urn:no-match' - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: urn:no-match'); + it(`should error on no match with a string verify "audience" option`, function (done) { + verifyWithAudience(token, 'urn:no-match', (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', `jwt audience invalid. expected: urn:no-match`); + }); + }); }); - it('should error on no match with an array of string "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: ['urn:no-match-1', 'urn:no-match-2'] - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: urn:no-match-1 or urn:no-match-2'); + it('should error on no match with an array of string verify "audience" option', function (done) { + verifyWithAudience(token, ['urn:no-match-1', 'urn:no-match-2'], (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', `jwt audience invalid. expected: urn:no-match-1 or urn:no-match-2`); + }); + }); }); - it('should error on no match with a Regex "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: /^urn:no-match$/ - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match$/'); + it('should error on no match with a Regex verify "audience" option', function (done) { + verifyWithAudience(token, /^urn:no-match$/, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', `jwt audience invalid. expected: /^urn:no-match$/`); + }); + }); }); - it('should error on no match with an array of Regex "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: [/^urn:no-match-1$/, /^urn:no-match-2$/] - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match-1$/ or /^urn:no-match-2$/'); + it('should error on no match with an array of Regex verify "audience" option', function (done) { + verifyWithAudience(token, [/^urn:no-match-1$/, /^urn:no-match-2$/], (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property( + 'message', `jwt audience invalid. expected: /^urn:no-match-1$/ or /^urn:no-match-2$/` + ); + }); + }); }); - it('should error on no match with an array of a Regex and a string in "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: [/^urn:no-match$/, 'urn:no-match'] - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match$/ or urn:no-match'); + it('should error on no match with an array of a Regex and a string in verify "audience" option', function (done) { + verifyWithAudience(token, [/^urn:no-match$/, 'urn:no-match'], (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property( + 'message', `jwt audience invalid. expected: /^urn:no-match$/ or urn:no-match` + ); + }); + }); }); }); - describe('with an array for "aud" value in payload', function () { + describe('with an array of ["urn:foo", "urn:bar"] for "aud" value in payload', function () { let token; - beforeEach(function () { - token = signWithAudience({}, ['urn:foo', 'urn:bar']); + beforeEach(function (done) { + signWithAudience(['urn:foo', 'urn:bar'], {}, (err, t) => { + token = t; + done(err); + }); }); - it('should verify and decode without verify "audience" option', function () { - const decoded = jwt.decode(token); - const verified = jwt.verify(token, undefined); - - expect(decoded).to.deep.equal(verified); - expect(decoded.aud).to.deep.equal(['urn:foo', 'urn:bar']); + [ + undefined, + 'urn:foo', + /^urn:f[o]{2}$/, + ['urn:no_match', 'urn:foo'], + ['urn:no_match', /^urn:f[o]{2}$/], + [/^urn:no_match$/, /^urn:f[o]{2}$/], + [/^urn:no_match$/, 'urn:foo'] + ].forEach((audience) =>{ + it(`should verify and decode with verify "audience" option of ${util.inspect(audience)}`, function (done) { + verifyWithAudience(token, audience, (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); + }); + }); + }); }); - it('should error on no match with a string "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: 'urn:no-match' - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: urn:no-match'); + it(`should error on no match with a string verify "audience" option`, function (done) { + verifyWithAudience(token, 'urn:no-match', (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', `jwt audience invalid. expected: urn:no-match`); + }); + }); }); - it('should error on no match with an array of string "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: ['urn:no-match-1', 'urn:no-match-2'] - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: urn:no-match-1 or urn:no-match-2'); + it('should error on no match with an array of string verify "audience" option', function (done) { + verifyWithAudience(token, ['urn:no-match-1', 'urn:no-match-2'], (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', `jwt audience invalid. expected: urn:no-match-1 or urn:no-match-2`); + }); + }); }); - it('should error on no match with a Regex "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: /^urn:no-match$/ - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match$/'); + it('should error on no match with a Regex verify "audience" option', function (done) { + verifyWithAudience(token, /^urn:no-match$/, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', `jwt audience invalid. expected: /^urn:no-match$/`); + }); + }); }); - it('should error on no match with an array of Regex "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: [/^urn:no-match-1$/, /^urn:no-match-2$/] - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match-1$/ or /^urn:no-match-2$/'); + it('should error on no match with an array of Regex verify "audience" option', function (done) { + verifyWithAudience(token, [/^urn:no-match-1$/, /^urn:no-match-2$/], (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property( + 'message', `jwt audience invalid. expected: /^urn:no-match-1$/ or /^urn:no-match-2$/` + ); + }); + }); }); - it('should error on no match with an array of a Regex and a string in "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: [/^urn:no-match$/, 'urn:no-match'] - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match$/ or urn:no-match'); + it('should error on no match with an array of a Regex and a string in verify "audience" option', function (done) { + verifyWithAudience(token, [/^urn:no-match$/, 'urn:no-match'], (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property( + 'message', `jwt audience invalid. expected: /^urn:no-match$/ or urn:no-match` + ); + }); + }); }); - describe('when checking matching for both "urn:foo" and "urn:bar"', function() { - - it('should verify with an array of stings "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: ['urn:foo', 'urn:bar'] - })).to.not.throw; + describe('when checking for a matching on both "urn:foo" and "urn:bar"', function() { + it('should verify with an array of stings verify "audience" option', function (done) { + verifyWithAudience(token, ['urn:foo', 'urn:bar'], (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); + }); + }); }); - it('should verify with a Regex "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: /^urn:[a-z]{3}$/ - })).to.not.throw; + it('should verify with a Regex verify "audience" option', function (done) { + verifyWithAudience(token, /^urn:[a-z]{3}$/, (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); + }); + }); }); - it('should verify with an array of Regex "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: [/^urn:f[o]{2}$/, /^urn:b[ar]{2}$/] - })).to.not.throw; + it('should verify with an array of Regex verify "audience" option', function (done) { + verifyWithAudience(token, [/^urn:f[o]{2}$/, /^urn:b[ar]{2}$/], (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); + }); + }); }); }); describe('when checking for a matching for "urn:foo"', function() { - it('should verify with a string "verify.audience"', function () { - expect(jwt.verify(token, undefined, { - audience: 'urn:foo' - })).to.not.throw; + it('should verify with a string verify "audience"', function (done) { + verifyWithAudience(token, 'urn:foo', (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); + }); + }); }); - it('should verify with a Regex "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: /^urn:f[o]{2}$/ - })).to.not.throw; + it('should verify with a Regex verify "audience" option', function (done) { + verifyWithAudience(token, /^urn:f[o]{2}$/, (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); + }); + }); }); - it('should verify with an array of Regex "verify.audience"', function () { - expect(jwt.verify(token, undefined, { - audience: [/^urn:no-match$/, /^urn:f[o]{2}$/] - })).to.not.throw; + it('should verify with an array of Regex verify "audience"', function (done) { + verifyWithAudience(token, [/^urn:no-match$/, /^urn:f[o]{2}$/], (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); + }); + }); }); - it('should verify with an array containing a string and a Regex "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: ['urn:no_match', /^urn:f[o]{2}$/] - })).to.not.throw; + it('should verify with an array containing a string and a Regex verify "audience" option', function (done) { + verifyWithAudience(token, ['urn:no_match', /^urn:f[o]{2}$/], (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); + }); + }); }); - it('should verify with an array containing a Regex and a string "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: [/^urn:no-match$/, 'urn:foo'] - })).to.not.throw; + it('should verify with an array containing a Regex and a string verify "audience" option', function (done) { + verifyWithAudience(token, [/^urn:no-match$/, 'urn:foo'], (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); + }); + }); }); }); describe('when checking matching for "urn:bar"', function() { - it('should verify with a string "verify.audience"', function () { - expect(jwt.verify(token, undefined, { - audience: 'urn:bar' - })).to.not.throw; + it('should verify with a string verify "audience"', function (done) { + verifyWithAudience(token, 'urn:bar', (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); + }); + }); }); - it('should verify with a Regex "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: /^urn:b[ar]{2}$/ - })).to.not.throw; + it('should verify with a Regex verify "audience" option', function (done) { + verifyWithAudience(token, /^urn:b[ar]{2}$/, (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); + }); + }); }); - it('should verify with an array of Regex "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: [/^urn:no-match$/, /^urn:b[ar]{2}$/] - })).to.not.throw; + it('should verify with an array of Regex verify "audience" option', function (done) { + verifyWithAudience(token, [/^urn:no-match$/, /^urn:b[ar]{2}$/], (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); + }); + }); }); - it('should verify with an array containing a string and a Regex "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: ['urn:no_match', /^urn:b[ar]{2}$/] - })).to.not.throw; + it('should verify with an array containing a string and a Regex verify "audience" option', function (done) { + verifyWithAudience(token, ['urn:no_match', /^urn:b[ar]{2}$/], (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); + }); + }); }); - it('should verify with an array containing a Regex and a string "verify.audience" option', function () { - expect(jwt.verify(token, undefined, { - audience: [/^urn:no-match$/, 'urn:bar'] - })).to.not.throw; + it('should verify with an array containing a Regex and a string verify "audience" option', function (done) { + verifyWithAudience(token, [/^urn:no-match$/, 'urn:bar'], (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('aud').deep.equals(['urn:foo', 'urn:bar']); + }); + }); }); }); }); @@ -277,46 +371,65 @@ describe('audience', function() { describe('without a "aud" value in payload', function () { let token; - beforeEach(function () { - token = signWithAudience({}); + beforeEach(function (done) { + signWithAudience(undefined, {}, (err, t) => { + token = t; + done(err); + }); }); - it('should verify and decode without verify "audience" option', function () { - const decoded = jwt.decode(token); - const verified = jwt.verify(token, undefined); - - expect(decoded).to.deep.equal(verified); - expect(decoded).to.not.have.property('aud'); + it('should verify and decode without verify "audience" option', function (done) { + verifyWithAudience(token, undefined, (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.not.have.property('aud'); + }); + }); }); - it('should error on no match with a string "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: 'urn:no-match' - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: urn:no-match'); + it('should error on no match with a string verify "audience" option', function (done) { + verifyWithAudience(token, 'urn:no-match', (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', 'jwt audience invalid. expected: urn:no-match'); + }); + }); }); - it('should error on no match with an array of string "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: ['urn:no-match-1', 'urn:no-match-2'] - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: urn:no-match-1 or urn:no-match-2'); + it('should error on no match with an array of string verify "audience" option', function (done) { + verifyWithAudience(token, ['urn:no-match-1', 'urn:no-match-2'], (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', 'jwt audience invalid. expected: urn:no-match-1 or urn:no-match-2'); + }); + }); }); - it('should error on no match with a Regex "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: /^urn:no-match$/ - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match$/'); + it('should error on no match with a Regex verify "audience" option', function (done) { + verifyWithAudience(token, /^urn:no-match$/, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', 'jwt audience invalid. expected: /^urn:no-match$/'); + }); + }); }); - it('should error on no match with an array of Regex "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: [/^urn:no-match-1$/, /^urn:no-match-2$/] - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match-1$/ or /^urn:no-match-2$/'); + it('should error on no match with an array of Regex verify "audience" option', function (done) { + verifyWithAudience(token, [/^urn:no-match-1$/, /^urn:no-match-2$/], (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', 'jwt audience invalid. expected: /^urn:no-match-1$/ or /^urn:no-match-2$/'); + }); + }); }); - it('should error on no match with an array of a Regex and a string in "verify.audience" option', function () { - expect(() => jwt.verify(token, undefined, { - audience: [/^urn:no-match$/, 'urn:no-match'] - })).to.throw(jwt.JsonWebTokenError, 'jwt audience invalid. expected: /^urn:no-match$/ or urn:no-match'); + it('should error on no match with an array of a Regex and a string in verify "audience" option', function (done) { + verifyWithAudience(token, [/^urn:no-match$/, 'urn:no-match'], (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', 'jwt audience invalid. expected: /^urn:no-match$/ or urn:no-match'); + }); + }); }); }); }); From cfd1079305170a897dee6a5f55039783e6ee2711 Mon Sep 17 00:00:00 2001 From: Aakash Agarwal Date: Tue, 23 Oct 2018 00:00:43 +0530 Subject: [PATCH 41/77] Updating Node version in Engines spec in package.json (#528) * Updating Node version in package.json Updated Node version from 0.12 to 1.4 in package.json engines * Updated node version Updated Node version from 0.12 to 4 in package.json Engines Spec Fixes Issue #509 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c81e9882..4e3f2ae5 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ }, "engines": { "npm": ">=1.4.28", - "node": ">=0.12" + "node": ">=4" }, "files": [ "lib", From 7f9604ac98d4d0ff8d873c3d2b2ea64bd285cb76 Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Mon, 22 Oct 2018 20:40:25 +0200 Subject: [PATCH 42/77] Fixed error message when empty string passed as expiresIn or notBefore option (#531) * Fixed error message when empty string passed as expiresIn or notBefore option * Moved tests to option validation block --- sign.js | 4 ++-- test/claim-exp.test.js | 11 +---------- test/claim-nbf.test.js | 11 +---------- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/sign.js b/sign.js index c608f4d4..de80b024 100644 --- a/sign.js +++ b/sign.js @@ -9,8 +9,8 @@ var isString = require('lodash.isstring'); var once = require('lodash.once'); 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' }, + 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' }, diff --git a/test/claim-exp.test.js b/test/claim-exp.test.js index 816d12e7..94360f6b 100644 --- a/test/claim-exp.test.js +++ b/test/claim-exp.test.js @@ -29,6 +29,7 @@ describe('expires', function() { Infinity, NaN, ' ', + '', 'invalid', [], ['foo'], @@ -46,16 +47,6 @@ describe('expires', function() { }); }); - // TODO this should throw the same error as other invalid inputs - it(`should error with with value ''`, function (done) { - signWithExpiresIn('', {}, (err) => { - testUtils.asyncCheck(done, () => { - expect(err).to.be.instanceOf(Error); - expect(err).to.have.property('message', 'val is not a non-empty string or a valid number. val=""'); - }); - }); - }); - // undefined needs special treatment because {} is not the same as {expiresIn: undefined} it('should error with with value undefined', function (done) { testUtils.signJWTHelper({}, undefined, {expiresIn: undefined, algorithm: 'none'}, (err) => { diff --git a/test/claim-nbf.test.js b/test/claim-nbf.test.js index f36396c3..1aa5cda7 100644 --- a/test/claim-nbf.test.js +++ b/test/claim-nbf.test.js @@ -28,6 +28,7 @@ describe('not before', function() { -Infinity, Infinity, NaN, + '', ' ', 'invalid', [], @@ -46,16 +47,6 @@ describe('not before', function() { }); }); - // TODO this should throw the same error as other invalid inputs - it(`should error with with value ''`, function (done) { - signWithNotBefore('', {}, (err) => { - testUtils.asyncCheck(done, () => { - expect(err).to.be.instanceOf(Error); - expect(err).to.have.property('message', 'val is not a non-empty string or a valid number. val=""'); - }); - }); - }); - // undefined needs special treatment because {} is not the same as {notBefore: undefined} it('should error with with value undefined', function (done) { testUtils.signJWTHelper({}, undefined, {notBefore: undefined, algorithm: 'none'}, (err) => { From 1956c4006472fd285b8a85074257cbdbe9131cbf Mon Sep 17 00:00:00 2001 From: Yash Gaurkar Date: Thu, 1 Nov 2018 16:11:54 +0530 Subject: [PATCH 43/77] Update README.md (#538) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8343f9ea..9b1998a1 100644 --- a/README.md +++ b/README.md @@ -343,7 +343,7 @@ none | No digital signature or MAC value included First of all, we recommend to think carefully if auto-refreshing a JWT will not introduce any vulnerability in your system. -We are not comfortable including this as part of the library, however, you can take a look to [this example](https://gist.github.com/ziluvatar/a3feb505c4c0ec37059054537b38fc48) to show how this could be accomplished. +We are not comfortable including this as part of the library, however, you can take a look at [this example](https://gist.github.com/ziluvatar/a3feb505c4c0ec37059054537b38fc48) to show how this could be accomplished. Apart from that example there are [an issue](https://github.com/auth0/node-jsonwebtoken/issues/122) and [a pull request](https://github.com/auth0/node-jsonwebtoken/pull/172) to get more knowledge about this topic. # TODO From 0906a3fa80f52f959ac1b6343d3024ce5c7e9dea Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Fri, 2 Nov 2018 07:32:16 -0230 Subject: [PATCH 44/77] Refactor tests related to iss and issuer (#543) This change extracts all tests related to the iss claim and the issuer option into a single test file. Additional tests were added that were missing. --- test/claim-iss.test.js | 205 +++++++++++++++++++++++++++ test/issue_196.tests.js | 15 -- test/jwt.asymmetric_signing.tests.js | 44 ------ test/schema.tests.js | 7 - 4 files changed, 205 insertions(+), 66 deletions(-) create mode 100644 test/claim-iss.test.js delete mode 100644 test/issue_196.tests.js diff --git a/test/claim-iss.test.js b/test/claim-iss.test.js new file mode 100644 index 00000000..ec82102a --- /dev/null +++ b/test/claim-iss.test.js @@ -0,0 +1,205 @@ +'use strict'; + +const jwt = require('../'); +const expect = require('chai').expect; +const util = require('util'); +const testUtils = require('./test-utils'); + +function signWithIssuer(issuer, payload, callback) { + const options = {algorithm: 'none'}; + if (issuer !== undefined) { + options.issuer = issuer; + } + testUtils.signJWTHelper(payload, 'secret', options, callback); +} + +describe('issuer', function() { + describe('`jwt.sign` "issuer" option validation', function () { + [ + true, + false, + null, + -1, + 0, + 1, + -1.1, + 1.1, + -Infinity, + Infinity, + NaN, + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((issuer) => { + it(`should error with with value ${util.inspect(issuer)}`, function (done) { + signWithIssuer(issuer, {}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', '"issuer" must be a string'); + }); + }); + }); + }); + + // undefined needs special treatment because {} is not the same as {issuer: undefined} + it('should error with with value undefined', function (done) { + testUtils.signJWTHelper({}, undefined, {issuer: undefined, algorithm: 'none'}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', '"issuer" must be a string'); + }); + }); + }); + + it('should error when "iss" is in payload', function (done) { + signWithIssuer('foo', {iss: 'bar'}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property( + 'message', + 'Bad "options.issuer" option. The payload already has an "iss" property.' + ); + }); + }); + }); + + it('should error with a string payload', function (done) { + signWithIssuer('foo', 'a string payload', (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property( + 'message', + 'invalid issuer option for string payload' + ); + }); + }); + }); + + it('should error with a Buffer payload', function (done) { + signWithIssuer('foo', new Buffer('a Buffer payload'), (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property( + 'message', + 'invalid issuer option for object payload' + ); + }); + }); + }); + }); + + describe('when signing and verifying a token', function () { + it('should not verify "iss" if verify "issuer" option not provided', function(done) { + signWithIssuer(undefined, {iss: 'foo'}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('iss', 'foo'); + }); + }) + }); + }); + + describe('with string "issuer" option', function () { + it('should verify with a string "issuer"', function (done) { + signWithIssuer('foo', {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {issuer: 'foo'}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('iss', 'foo'); + }); + }) + }); + }); + + it('should verify with a string "iss"', function (done) { + signWithIssuer(undefined, {iss: 'foo'}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {issuer: 'foo'}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('iss', 'foo'); + }); + }) + }); + }); + + it('should error if "iss" does not match verify "issuer" option', function(done) { + signWithIssuer(undefined, {iss: 'foobar'}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {issuer: 'foo'}, (e2) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); + expect(e2).to.have.property('message', 'jwt issuer invalid. expected: foo'); + }); + }) + }); + }); + + it('should error without "iss" and with verify "issuer" option', function(done) { + signWithIssuer(undefined, {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {issuer: 'foo'}, (e2) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); + expect(e2).to.have.property('message', 'jwt issuer invalid. expected: foo'); + }); + }) + }); + }); + }); + + describe('with array "issuer" option', function () { + it('should verify with a string "issuer"', function (done) { + signWithIssuer('bar', {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {issuer: ['foo', 'bar']}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('iss', 'bar'); + }); + }) + }); + }); + + it('should verify with a string "iss"', function (done) { + signWithIssuer(undefined, {iss: 'foo'}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {issuer: ['foo', 'bar']}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('iss', 'foo'); + }); + }) + }); + }); + + it('should error if "iss" does not match verify "issuer" option', function(done) { + signWithIssuer(undefined, {iss: 'foobar'}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {issuer: ['foo', 'bar']}, (e2) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); + expect(e2).to.have.property('message', 'jwt issuer invalid. expected: foo,bar'); + }); + }) + }); + }); + + it('should error without "iss" and with verify "issuer" option', function(done) { + signWithIssuer(undefined, {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {issuer: ['foo', 'bar']}, (e2) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); + expect(e2).to.have.property('message', 'jwt issuer invalid. expected: foo,bar'); + }); + }) + }); + }); + }); + }); +}); diff --git a/test/issue_196.tests.js b/test/issue_196.tests.js deleted file mode 100644 index 7bd9a94e..00000000 --- a/test/issue_196.tests.js +++ /dev/null @@ -1,15 +0,0 @@ -var expect = require('chai').expect; -var jwt = require('./..'); -var atob = require('atob'); - -describe('issue 196', function () { - function b64_to_utf8 (str) { - return decodeURIComponent(escape(atob( str ))); - } - - it('should use issuer provided in payload.iss', function () { - var token = jwt.sign({ iss: 'foo' }, 'shhhhh'); - var decoded_issuer = JSON.parse(b64_to_utf8(token.split('.')[1])).iss; - expect(decoded_issuer).to.equal('foo'); - }); -}); diff --git a/test/jwt.asymmetric_signing.tests.js b/test/jwt.asymmetric_signing.tests.js index 6951b95a..dd6f27ea 100644 --- a/test/jwt.asymmetric_signing.tests.js +++ b/test/jwt.asymmetric_signing.tests.js @@ -113,50 +113,6 @@ describe('Asymmetric Algorithms', function(){ }); }); - describe('when signing a token with issuer', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, issuer: 'urn:foo' }); - - it('should check issuer', function (done) { - jwt.verify(token, pub, { issuer: 'urn:foo' }, function (err, decoded) { - assert.isNotNull(decoded); - assert.isNull(err); - done(); - }); - }); - - it('should check the issuer when providing a list of valid issuers', function (done) { - jwt.verify(token, pub, { issuer: ['urn:foo', 'urn:bar'] }, function (err, decoded) { - assert.isNotNull(decoded); - assert.isNull(err); - done(); - }); - }); - - it('should throw when invalid issuer', function (done) { - jwt.verify(token, pub, { issuer: 'urn:wrong' }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - }); - - describe('when signing a token without issuer', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm }); - - it('should check issuer', function (done) { - jwt.verify(token, pub, { issuer: 'urn:foo' }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - }); - describe('when signing a token with jwt id', function () { var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, jwtid: 'jwtid' }); diff --git a/test/schema.tests.js b/test/schema.tests.js index 924bf700..26f085eb 100644 --- a/test/schema.tests.js +++ b/test/schema.tests.js @@ -44,13 +44,6 @@ describe('schema', function() { 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 noTimestamp', function () { expect(function () { sign({ noTimestamp: 10 }); From 88645427a0adb420bd3e149199a2a6bf1e17277e Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Fri, 2 Nov 2018 07:35:03 -0230 Subject: [PATCH 45/77] Refactor tests related to kid and keyid (#545) Thie change extracts the tests related to the kid header and the keyid option into a single file. Additonal tests were added that were missing. --- test/header-kid.test.js | 97 +++++++++++++++++++++++++++++++++++++++++ test/keyid.tests.js | 9 ---- test/schema.tests.js | 8 ---- 3 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 test/header-kid.test.js delete mode 100644 test/keyid.tests.js diff --git a/test/header-kid.test.js b/test/header-kid.test.js new file mode 100644 index 00000000..42633ade --- /dev/null +++ b/test/header-kid.test.js @@ -0,0 +1,97 @@ +'use strict'; + +const jwt = require('../'); +const expect = require('chai').expect; +const util = require('util'); +const testUtils = require('./test-utils'); + +function signWithKeyId(keyid, payload, callback) { + const options = {algorithm: 'none'}; + if (keyid !== undefined) { + options.keyid = keyid; + } + testUtils.signJWTHelper(payload, 'secret', options, callback); +} + +describe('keyid', function() { + describe('`jwt.sign` "keyid" option validation', function () { + [ + true, + false, + null, + -1, + 0, + 1, + -1.1, + 1.1, + -Infinity, + Infinity, + NaN, + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((keyid) => { + it(`should error with with value ${util.inspect(keyid)}`, function (done) { + signWithKeyId(keyid, {}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', '"keyid" must be a string'); + }); + }); + }); + }); + + // undefined needs special treatment because {} is not the same as {keyid: undefined} + it('should error with with value undefined', function (done) { + testUtils.signJWTHelper({}, undefined, {keyid: undefined, algorithm: 'none'}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', '"keyid" must be a string'); + }); + }); + }); + }); + + describe('when signing a token', function () { + it('should not add "kid" header when "keyid" option not provided', function(done) { + signWithKeyId(undefined, {}, (err, token) => { + testUtils.asyncCheck(done, () => { + const decoded = jwt.decode(token, {complete: true}); + expect(err).to.be.null; + expect(decoded.header).to.not.have.property('kid'); + }); + }); + }); + + it('should add "kid" header when "keyid" option is provided and an object payload', function(done) { + signWithKeyId('foo', {}, (err, token) => { + testUtils.asyncCheck(done, () => { + const decoded = jwt.decode(token, {complete: true}); + expect(err).to.be.null; + expect(decoded.header).to.have.property('kid', 'foo'); + }); + }); + }); + + it('should add "kid" header when "keyid" option is provided and a Buffer payload', function(done) { + signWithKeyId('foo', new Buffer('a Buffer payload'), (err, token) => { + testUtils.asyncCheck(done, () => { + const decoded = jwt.decode(token, {complete: true}); + expect(err).to.be.null; + expect(decoded.header).to.have.property('kid', 'foo'); + }); + }); + }); + + it('should add "kid" header when "keyid" option is provided and a string payload', function(done) { + signWithKeyId('foo', 'a string payload', (err, token) => { + testUtils.asyncCheck(done, () => { + const decoded = jwt.decode(token, {complete: true}); + expect(err).to.be.null; + expect(decoded.header).to.have.property('kid', 'foo'); + }); + }); + }); + }); +}); diff --git a/test/keyid.tests.js b/test/keyid.tests.js deleted file mode 100644 index 84a231c1..00000000 --- a/test/keyid.tests.js +++ /dev/null @@ -1,9 +0,0 @@ -var jwt = require('../index'); - -var claims = {"name": "doron", "age": 46}; -jwt.sign(claims, 'secret', {"keyid": "1234"}, function(err, good) { - console.log(jwt.decode(good, {"complete": true}).header.kid); - jwt.verify(good, 'secret', function(err, result) { - console.log(result); - }) -}); diff --git a/test/schema.tests.js b/test/schema.tests.js index 26f085eb..887b59f3 100644 --- a/test/schema.tests.js +++ b/test/schema.tests.js @@ -50,14 +50,6 @@ describe('schema', function() { }).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() { From dc89a641293d42f72ecfc623ce2eabc33954cb9d Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 2 Nov 2018 03:25:04 -0700 Subject: [PATCH 46/77] Edited the README.md to make certain parts of the document for the api easier to read, emphasizing the examples. (#548) --- README.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9b1998a1..d37bfeff 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,10 @@ $ npm install jsonwebtoken (Synchronous) Returns the JsonWebToken as string -`payload` could be an object literal, buffer or string representing valid JSON. *Please note that* `exp` or any other claim is only set if the payload is an object literal. Buffer or string payloads are not checked for JSON validity. +`payload` could be an object literal, buffer or string representing valid JSON. +> **Please _note_ that** `exp` or any other claim is only set if the payload is an object literal. Buffer or string payloads are not checked for JSON validity. + +> If `payload` is not a buffer or a string, it will be coerced into a string using `JSON.stringify`. `secretOrPrivateKey` is a string, buffer, or object containing either the secret for HMAC algorithms or the PEM encoded private key for RSA and ECDSA. In case of a private key with passphrase an object `{ key, passphrase }` can be used (based on [crypto documentation](https://nodejs.org/api/crypto.html#crypto_sign_sign_private_key_output_format)), in this case be sure you pass the `algorithm` option. @@ -35,8 +38,10 @@ encoded private key for RSA and ECDSA. In case of a private key with passphrase `options`: * `algorithm` (default: `HS256`) -* `expiresIn`: expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). Eg: `60`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). -* `notBefore`: expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). Eg: `60`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). +* `expiresIn`: expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). + > Eg: `60`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). +* `notBefore`: expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). + > Eg: `60`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). * `audience` * `issuer` * `jwtid` @@ -46,9 +51,9 @@ encoded private key for RSA and ECDSA. In case of a private key with passphrase * `keyid` * `mutatePayload`: if true, the sign function will modify the payload object directly. This is useful if you need a raw reference to the payload after claims have been applied to it but before it has been encoded into a token. -If `payload` is not a buffer or a string, it will be coerced into a string using `JSON.stringify`. -There are no default values for `expiresIn`, `notBefore`, `audience`, `subject`, `issuer`. These claims can also be provided in the payload directly with `exp`, `nbf`, `aud`, `sub` and `iss` respectively, but you can't include in both places. + +> There are no default values for `expiresIn`, `notBefore`, `audience`, `subject`, `issuer`. These claims can also be provided in the payload directly with `exp`, `nbf`, `aud`, `sub` and `iss` respectively, but you **_can't_** include in both places. Remember that `exp`, `nbf` and `iat` are **NumericDate**, see related [Token Expiration (exp claim)](#token-expiration-exp-claim) @@ -57,14 +62,14 @@ The header can be customized via the `options.header` object. Generated jwts will include an `iat` (issued at) claim by default unless `noTimestamp` is specified. If `iat` is inserted in the payload, it will be used instead of the real timestamp for calculating other things like `exp` given a timespan in `options.expiresIn`. -Sign with default (HMAC SHA256) +Synchronous Sign with default (HMAC SHA256) ```js var jwt = require('jsonwebtoken'); var token = jwt.sign({ foo: 'bar' }, 'shhhhh'); ``` -Sign with RSA SHA256 +Synchronous Sign with RSA SHA256 ```js // sign with RSA SHA256 var cert = fs.readFileSync('private.key'); @@ -131,13 +136,15 @@ As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues `options` * `algorithms`: List of strings with the names of the allowed algorithms. For instance, `["HS256", "HS384"]`. -* `audience`: if you want to check audience (`aud`), provide a value here. The audience can be checked against a string, a regular expression or a list of strings and/or regular expressions. Eg: `"urn:foo"`, `/urn:f[o]{2}/`, `[/urn:f[o]{2}/, "urn:bar"]` +* `audience`: if you want to check audience (`aud`), provide a value here. The audience can be checked against a string, a regular expression or a list of strings and/or regular expressions. + > Eg: `"urn:foo"`, `/urn:f[o]{2}/`, `[/urn:f[o]{2}/, "urn:bar"]` * `issuer` (optional): string or array of strings of valid values for the `iss` field. * `ignoreExpiration`: if `true` do not validate the expiration of the token. * `ignoreNotBefore`... * `subject`: if you want to check subject (`sub`), provide a value here * `clockTolerance`: number of seconds to tolerate when checking the `nbf` and `exp` claims, to deal with small clock differences among different servers -* `maxAge`: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). Eg: `1000`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). +* `maxAge`: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). + > Eg: `1000`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). * `clockTimestamp`: the time in seconds that should be used as the current time for all necessary comparisons. @@ -223,7 +230,7 @@ jwt.verify(token, getKey, options, function(err, decoded) { (Synchronous) Returns the decoded payload without verifying if the signature is valid. -__Warning:__ This will __not__ verify whether the signature is valid. You should __not__ use this for untrusted messages. You most likely want to use `jwt.verify` instead. +> __Warning:__ This will __not__ verify whether the signature is valid. You should __not__ use this for untrusted messages. You most likely want to use `jwt.verify` instead. `token` is the JsonWebToken string @@ -341,7 +348,7 @@ none | No digital signature or MAC value included ## Refreshing JWTs -First of all, we recommend to think carefully if auto-refreshing a JWT will not introduce any vulnerability in your system. +First of all, we recommend you to think carefully if auto-refreshing a JWT will not introduce any vulnerability in your system. We are not comfortable including this as part of the library, however, you can take a look at [this example](https://gist.github.com/ziluvatar/a3feb505c4c0ec37059054537b38fc48) to show how this could be accomplished. Apart from that example there are [an issue](https://github.com/auth0/node-jsonwebtoken/issues/122) and [a pull request](https://github.com/auth0/node-jsonwebtoken/pull/172) to get more knowledge about this topic. From 02688132c2b4824add50a4cdb5c63fcf5a70d36a Mon Sep 17 00:00:00 2001 From: gkwang Date: Fri, 2 Nov 2018 04:00:08 -0700 Subject: [PATCH 47/77] devDeps: atob@2.1.2 (#539) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4e3f2ae5..22bc5bfa 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "ms": "^2.1.1" }, "devDependencies": { - "atob": "^1.1.2", + "atob": "^2.1.2", "chai": "^4.1.2", "conventional-changelog": "~1.1.0", "cost-of-modules": "^1.0.1", From e7938f06fdf2ed3aa88745b72b8ae4ee66c2d0d0 Mon Sep 17 00:00:00 2001 From: kazuki229 Date: Wed, 14 Nov 2018 19:02:59 +0900 Subject: [PATCH 48/77] Add verify option for nonce validation (#540) * Add verify option for nonce validation * Update README.md Co-Authored-By: kazuki229 * Refactor option-nonce test * Add nonce option validation --- README.md | 1 + test/option-nonce.test.js | 57 +++++++++++++++++++++++++++++++++++++++ verify.js | 10 +++++++ 3 files changed, 68 insertions(+) create mode 100644 test/option-nonce.test.js diff --git a/README.md b/README.md index d37bfeff..6e653d3c 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues * `maxAge`: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). > Eg: `1000`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). * `clockTimestamp`: the time in seconds that should be used as the current time for all necessary comparisons. +* `nonce`: if you want to check `nonce` claim, provide a string value here. It is used on Open ID for the ID Tokens. ([Open ID implementation notes](https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes)) ```js diff --git a/test/option-nonce.test.js b/test/option-nonce.test.js new file mode 100644 index 00000000..841bdc29 --- /dev/null +++ b/test/option-nonce.test.js @@ -0,0 +1,57 @@ +'use strict'; + +const jwt = require('../'); +const expect = require('chai').expect; +const util = require('util'); +const testUtils = require('./test-utils') + +describe('nonce option', function () { + let token; + + beforeEach(function () { + token = jwt.sign({ nonce: 'abcde' }, undefined, { algorithm: 'none' }); + }); + [ + { + description: 'should work with a string', + nonce: 'abcde', + }, + ].forEach((testCase) => { + it(testCase.description, function (done) { + testUtils.verifyJWTHelper(token, undefined, { nonce: testCase.nonce }, (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('nonce', 'abcde'); + }); + }); + }); + }); + [ + true, + false, + null, + -1, + 0, + 1, + -1.1, + 1.1, + -Infinity, + Infinity, + NaN, + '', + ' ', + [], + ['foo'], + {}, + { foo: 'bar' }, + ].forEach((nonce) => { + it(`should error with value ${util.inspect(nonce)}`, function (done) { + testUtils.verifyJWTHelper(token, undefined, { nonce }, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', 'nonce must be a non-empty string') + }); + }); + }); + }); +}); diff --git a/verify.js b/verify.js index e730e2aa..fa1339ae 100644 --- a/verify.js +++ b/verify.js @@ -33,6 +33,10 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError('clockTimestamp must be a number')); } + if (options.nonce !== undefined && (typeof options.nonce !== 'string' || options.nonce.trim() === '')) { + return done(new JsonWebTokenError('nonce must be a non-empty string')); + } + var clockTimestamp = options.clockTimestamp || Math.floor(Date.now() / 1000); if (!jwtString){ @@ -179,6 +183,12 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { } } + if (options.nonce) { + if (payload.nonce !== options.nonce) { + return done(new JsonWebTokenError('jwt nonce invalid. expected: ' + options.nonce)); + } + } + if (options.maxAge) { if (typeof payload.iat !== 'number') { return done(new JsonWebTokenError('iat required when maxAge is specified')); From 86334aa4d8c8034e7d8b52760b9a84f25769513c Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Wed, 14 Nov 2018 11:25:12 +0100 Subject: [PATCH 49/77] 8.4.0 --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7a32498..3196aaa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,49 @@ All notable changes to this project will be documented in this file starting from version **v4.0.0**. This project adheres to [Semantic Versioning](http://semver.org/). +## 8.4.0 - 2018-11-14 + +### New Functionality + + - Add verify option for nonce validation (#540) ([e7938f06fdf2ed3aa88745b72b8ae4ee66c2d0d0](https://github.com/auth0/node-jsonwebtoken/commit/e7938f06fdf2ed3aa88745b72b8ae4ee66c2d0d0)), closes [#540](https://github.com/auth0/node-jsonwebtoken/issues/540) + +### Bug Fixes + + - Updating Node version in Engines spec in package.json (#528) ([cfd1079305170a897dee6a5f55039783e6ee2711](https://github.com/auth0/node-jsonwebtoken/commit/cfd1079305170a897dee6a5f55039783e6ee2711)), closes [#528](https://github.com/auth0/node-jsonwebtoken/issues/528) [#509](https://github.com/auth0/node-jsonwebtoken/issues/509) + - Fixed error message when empty string passed as expiresIn or notBefore option (#531) ([7f9604ac98d4d0ff8d873c3d2b2ea64bd285cb76](https://github.com/auth0/node-jsonwebtoken/commit/7f9604ac98d4d0ff8d873c3d2b2ea64bd285cb76)), closes [#531](https://github.com/auth0/node-jsonwebtoken/issues/531) + +### Docs + + - Update README.md (#527) ([b76f2a80f5229ee5cde321dd2ff14aa5df16d283](https://github.com/auth0/node-jsonwebtoken/commit/b76f2a80f5229ee5cde321dd2ff14aa5df16d283)), closes [#527](https://github.com/auth0/node-jsonwebtoken/issues/527) + - Update README.md (#538) ([1956c4006472fd285b8a85074257cbdbe9131cbf](https://github.com/auth0/node-jsonwebtoken/commit/1956c4006472fd285b8a85074257cbdbe9131cbf)), closes [#538](https://github.com/auth0/node-jsonwebtoken/issues/538) + - Edited the README.md to make certain parts of the document for the api easier to read, emphasizing the examples. (#548) ([dc89a641293d42f72ecfc623ce2eabc33954cb9d](https://github.com/auth0/node-jsonwebtoken/commit/dc89a641293d42f72ecfc623ce2eabc33954cb9d)), closes [#548](https://github.com/auth0/node-jsonwebtoken/issues/548) + - Document NotBeforeError (#529) ([29cd654b956529e939ae8f8c30b9da7063aad501](https://github.com/auth0/node-jsonwebtoken/commit/29cd654b956529e939ae8f8c30b9da7063aad501)), closes [#529](https://github.com/auth0/node-jsonwebtoken/issues/529) + +### Test Improvements + + - Use lolex for faking date in tests (#491) ([677ead6d64482f2067b11437dda07309abe73cfa](https://github.com/auth0/node-jsonwebtoken/commit/677ead6d64482f2067b11437dda07309abe73cfa)), closes [#491](https://github.com/auth0/node-jsonwebtoken/issues/491) + - Update dependencies used for running tests (#518) ([5498bdc4865ffb2ba2fd44d889fad7e83873bb33](https://github.com/auth0/node-jsonwebtoken/commit/5498bdc4865ffb2ba2fd44d889fad7e83873bb33)), closes [#518](https://github.com/auth0/node-jsonwebtoken/issues/518) + - Minor test refactoring for recently added tests (#504) ([e2860a9d2a412627d79741a95bc7159971b923b9](https://github.com/auth0/node-jsonwebtoken/commit/e2860a9d2a412627d79741a95bc7159971b923b9)), closes [#504](https://github.com/auth0/node-jsonwebtoken/issues/504) + - Create and implement async/sync test helpers (#523) ([683d8a9b31ad6327948f84268bd2c8e4350779d1](https://github.com/auth0/node-jsonwebtoken/commit/683d8a9b31ad6327948f84268bd2c8e4350779d1)), closes [#523](https://github.com/auth0/node-jsonwebtoken/issues/523) + - Refactor tests related to audience and aud (#503) ([53d405e0223cce7c83cb51ecf290ca6bec1e9679](https://github.com/auth0/node-jsonwebtoken/commit/53d405e0223cce7c83cb51ecf290ca6bec1e9679)), closes [#503](https://github.com/auth0/node-jsonwebtoken/issues/503) + - Refactor tests related to expiresIn and exp (#501) ([72f0d9e5b11a99082250665d1200c58182903fa6](https://github.com/auth0/node-jsonwebtoken/commit/72f0d9e5b11a99082250665d1200c58182903fa6)), closes [#501](https://github.com/auth0/node-jsonwebtoken/issues/501) + - Refactor tests related to iat and maxAge (#507) ([877bd57ab2aca9b7d230805b21f921baed3da169](https://github.com/auth0/node-jsonwebtoken/commit/877bd57ab2aca9b7d230805b21f921baed3da169)), closes [#507](https://github.com/auth0/node-jsonwebtoken/issues/507) + - Refactor tests related to iss and issuer (#543) ([0906a3fa80f52f959ac1b6343d3024ce5c7e9dea](https://github.com/auth0/node-jsonwebtoken/commit/0906a3fa80f52f959ac1b6343d3024ce5c7e9dea)), closes [#543](https://github.com/auth0/node-jsonwebtoken/issues/543) + - Refactor tests related to kid and keyid (#545) ([88645427a0adb420bd3e149199a2a6bf1e17277e](https://github.com/auth0/node-jsonwebtoken/commit/88645427a0adb420bd3e149199a2a6bf1e17277e)), closes [#545](https://github.com/auth0/node-jsonwebtoken/issues/545) + - Refactor tests related to notBefore and nbf (#497) ([39adf87a6faef3df984140f88e6724ddd709fd89](https://github.com/auth0/node-jsonwebtoken/commit/39adf87a6faef3df984140f88e6724ddd709fd89)), closes [#497](https://github.com/auth0/node-jsonwebtoken/issues/497) + - Refactor tests related to subject and sub (#505) ([5a7fa23c0b4ac6c25304dab8767ef840b43a0eca](https://github.com/auth0/node-jsonwebtoken/commit/5a7fa23c0b4ac6c25304dab8767ef840b43a0eca)), closes [#505](https://github.com/auth0/node-jsonwebtoken/issues/505) + - Implement async/sync tests for exp claim (#536) ([9ae3f207ac64b7450ea0a3434418f5ca58d8125e](https://github.com/auth0/node-jsonwebtoken/commit/9ae3f207ac64b7450ea0a3434418f5ca58d8125e)), closes [#536](https://github.com/auth0/node-jsonwebtoken/issues/536) + - Implement async/sync tests for nbf claim (#537) ([88bc965061ed65299a395f42a100fb8f8c3c683e](https://github.com/auth0/node-jsonwebtoken/commit/88bc965061ed65299a395f42a100fb8f8c3c683e)), closes [#537](https://github.com/auth0/node-jsonwebtoken/issues/537) + - Implement async/sync tests for sub claim (#534) ([342b07bb105a35739eb91265ba5b9dd33c300fc6](https://github.com/auth0/node-jsonwebtoken/commit/342b07bb105a35739eb91265ba5b9dd33c300fc6)), closes [#534](https://github.com/auth0/node-jsonwebtoken/issues/534) + - Implement async/sync tests for the aud claim (#535) ([1c8ff5a68e6da73af2809c9d87faaf78602c99bb](https://github.com/auth0/node-jsonwebtoken/commit/1c8ff5a68e6da73af2809c9d87faaf78602c99bb)), closes [#535](https://github.com/auth0/node-jsonwebtoken/issues/535) + +### CI + + - Added Istanbul to check test-coverage (#468) ([9676a8306428a045e34c3987bd0680fb952b44e3](https://github.com/auth0/node-jsonwebtoken/commit/9676a8306428a045e34c3987bd0680fb952b44e3)), closes [#468](https://github.com/auth0/node-jsonwebtoken/issues/468) + - Complete ESLint conversion and cleanup (#490) ([cb1d2e1e40547f7ecf29fa6635041df6cbba7f40](https://github.com/auth0/node-jsonwebtoken/commit/cb1d2e1e40547f7ecf29fa6635041df6cbba7f40)), closes [#490](https://github.com/auth0/node-jsonwebtoken/issues/490) + - Make code-coverage mandatory when running tests (#495) ([fb0084a78535bfea8d0087c0870e7e3614a2cbe5](https://github.com/auth0/node-jsonwebtoken/commit/fb0084a78535bfea8d0087c0870e7e3614a2cbe5)), closes [#495](https://github.com/auth0/node-jsonwebtoken/issues/495) + + ## 8.3.0 - 2018-06-11 - docs: add some clarifications (#473) ([cd33cc81f06068b9df6c224d300dc6f70d8904ab](https://github.com/auth0/node-jsonwebtoken/commit/cd33cc81f06068b9df6c224d300dc6f70d8904ab)), closes [#473](https://github.com/auth0/node-jsonwebtoken/issues/473) diff --git a/package.json b/package.json index 22bc5bfa..2520dd8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonwebtoken", - "version": "8.3.0", + "version": "8.4.0", "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", "nyc": { From 7eebbc75ab89e01af5dacf2aae90fe05a13a1454 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Fri, 23 Nov 2018 07:42:59 -0330 Subject: [PATCH 50/77] Refactor tests related to jti and jwtid (#544) This change extracts all tests related to the jti claim and the jwtid option into a single test file. Additional tests were added that were missing. --- test/claim-jti.test.js | 155 +++++++++++++++++++++++++++ test/jwt.asymmetric_signing.tests.js | 36 ------- 2 files changed, 155 insertions(+), 36 deletions(-) create mode 100644 test/claim-jti.test.js diff --git a/test/claim-jti.test.js b/test/claim-jti.test.js new file mode 100644 index 00000000..a10a9b59 --- /dev/null +++ b/test/claim-jti.test.js @@ -0,0 +1,155 @@ +'use strict'; + +const jwt = require('../'); +const expect = require('chai').expect; +const util = require('util'); +const testUtils = require('./test-utils'); + +function signWithJWTId(jwtid, payload, callback) { + const options = {algorithm: 'none'}; + if (jwtid !== undefined) { + options.jwtid = jwtid; + } + testUtils.signJWTHelper(payload, 'secret', options, callback); +} + +describe('jwtid', function() { + describe('`jwt.sign` "jwtid" option validation', function () { + [ + true, + false, + null, + -1, + 0, + 1, + -1.1, + 1.1, + -Infinity, + Infinity, + NaN, + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((jwtid) => { + it(`should error with with value ${util.inspect(jwtid)}`, function (done) { + signWithJWTId(jwtid, {}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', '"jwtid" must be a string'); + }); + }); + }); + }); + + // undefined needs special treatment because {} is not the same as {jwtid: undefined} + it('should error with with value undefined', function (done) { + testUtils.signJWTHelper({}, undefined, {jwtid: undefined, algorithm: 'none'}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property('message', '"jwtid" must be a string'); + }); + }); + }); + + it('should error when "jti" is in payload', function (done) { + signWithJWTId('foo', {jti: 'bar'}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property( + 'message', + 'Bad "options.jwtid" option. The payload already has an "jti" property.' + ); + }); + }); + }); + + it('should error with a string payload', function (done) { + signWithJWTId('foo', 'a string payload', (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property( + 'message', + 'invalid jwtid option for string payload' + ); + }); + }); + }); + + it('should error with a Buffer payload', function (done) { + signWithJWTId('foo', new Buffer('a Buffer payload'), (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(Error); + expect(err).to.have.property( + 'message', + 'invalid jwtid option for object payload' + ); + }); + }); + }); + }); + + describe('when signing and verifying a token', function () { + it('should not verify "jti" if verify "jwtid" option not provided', function(done) { + signWithJWTId(undefined, {jti: 'foo'}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('jti', 'foo'); + }); + }) + }); + }); + + describe('with "jwtid" option', function () { + it('should verify with "jwtid" option', function (done) { + signWithJWTId('foo', {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {jwtid: 'foo'}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('jti', 'foo'); + }); + }) + }); + }); + + it('should verify with "jti" in payload', function (done) { + signWithJWTId(undefined, {jti: 'foo'}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {jetid: 'foo'}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('jti', 'foo'); + }); + }) + }); + }); + + it('should error if "jti" does not match verify "jwtid" option', function(done) { + signWithJWTId(undefined, {jti: 'bar'}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {jwtid: 'foo'}, (e2) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); + expect(e2).to.have.property('message', 'jwt jwtid invalid. expected: foo'); + }); + }) + }); + }); + + it('should error without "jti" and with verify "jwtid" option', function(done) { + signWithJWTId(undefined, {}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {jwtid: 'foo'}, (e2) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); + expect(e2).to.have.property('message', 'jwt jwtid invalid. expected: foo'); + }); + }) + }); + }); + }); + }); +}); diff --git a/test/jwt.asymmetric_signing.tests.js b/test/jwt.asymmetric_signing.tests.js index dd6f27ea..49b9ed00 100644 --- a/test/jwt.asymmetric_signing.tests.js +++ b/test/jwt.asymmetric_signing.tests.js @@ -113,42 +113,6 @@ describe('Asymmetric Algorithms', function(){ }); }); - describe('when signing a token with jwt id', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, jwtid: 'jwtid' }); - - it('should check jwt id', function (done) { - jwt.verify(token, pub, { jwtid: 'jwtid' }, function (err, decoded) { - assert.isNotNull(decoded); - assert.isNull(err); - done(); - }); - }); - - it('should throw when invalid jwt id', function (done) { - jwt.verify(token, pub, { jwtid: 'wrongJwtid' }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - }); - - describe('when signing a token without jwt id', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm }); - - it('should check jwt id', function (done) { - jwt.verify(token, pub, { jwtid: 'jwtid' }, function (err, decoded) { - assert.isUndefined(decoded); - assert.isNotNull(err); - assert.equal(err.name, 'JsonWebTokenError'); - assert.instanceOf(err, jwt.JsonWebTokenError); - done(); - }); - }); - }); - describe('when verifying a malformed token', function () { it('should throw', function (done) { jwt.verify('fruit.fruit.fruit', pub, function (err, decoded) { From 5147852896755dc1291825e2e40556f964411fb2 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Fri, 14 Dec 2018 10:47:45 -0330 Subject: [PATCH 51/77] Add tests for private claims in the payload (#555) This change adds tests for private claims added to the payload during sign and ensures that after verifying the payload contains the expected claim. --- test/claim-private.tests.js | 73 +++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 test/claim-private.tests.js diff --git a/test/claim-private.tests.js b/test/claim-private.tests.js new file mode 100644 index 00000000..d21a7204 --- /dev/null +++ b/test/claim-private.tests.js @@ -0,0 +1,73 @@ +'use strict'; + +const expect = require('chai').expect; +const util = require('util'); +const testUtils = require('./test-utils'); + +function signWithPayload(payload, callback) { + testUtils.signJWTHelper(payload, 'secret', {algorithm: 'none'}, callback); +} + +describe('with a private claim', function() { + [ + true, + false, + null, + -1, + 0, + 1, + -1.1, + 1.1, + '', + 'private claim', + 'UTF8 - José', + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((privateClaim) => { + it(`should sign and verify with claim of ${util.inspect(privateClaim)}`, function (done) { + signWithPayload({privateClaim}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('privateClaim').to.deep.equal(privateClaim); + }); + }) + }); + }); + }); + + // these values JSON.stringify to null + [ + -Infinity, + Infinity, + NaN, + ].forEach((privateClaim) => { + it(`should sign and verify with claim of ${util.inspect(privateClaim)}`, function (done) { + signWithPayload({privateClaim}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.have.property('privateClaim', null); + }); + }) + }); + }); + }); + + // private claims with value undefined are not added to the payload + it(`should sign and verify with claim of undefined`, function (done) { + signWithPayload({privateClaim: undefined}, (e1, token) => { + testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.asyncCheck(done, () => { + expect(e1).to.be.null; + expect(e2).to.be.null; + expect(decoded).to.not.have.property('privateClaim'); + }); + }) + }); + }); +}); From da8f55c3c7b4dd0bfc07a2df228500fdd050242a Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Wed, 23 Jan 2019 11:16:28 +0100 Subject: [PATCH 52/77] ci: remove nsp from tests (#569) Fixes https://github.com/auth0/node-jsonwebtoken/issues/567 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2520dd8a..f019a611 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "scripts": { "lint": "eslint .", "coverage": "nyc mocha", - "test": "npm run lint && npm run coverage && nsp check && cost-of-modules" + "test": "npm run lint && npm run coverage && cost-of-modules" }, "repository": { "type": "git", From 0c24fe68cd2866cea6322016bf993cd897fefc98 Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Wed, 13 Feb 2019 15:49:01 +0000 Subject: [PATCH 53/77] Fix 'cert' token which isn't a cert (#554) Certs have pubkeys. We sign things with private keys. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6e653d3c..80132218 100644 --- a/README.md +++ b/README.md @@ -72,13 +72,13 @@ var token = jwt.sign({ foo: 'bar' }, 'shhhhh'); Synchronous Sign with RSA SHA256 ```js // sign with RSA SHA256 -var cert = fs.readFileSync('private.key'); -var token = jwt.sign({ foo: 'bar' }, cert, { algorithm: 'RS256'}); +var privateKey = fs.readFileSync('private.key'); +var token = jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256'}); ``` Sign asynchronously ```js -jwt.sign({ foo: 'bar' }, cert, { algorithm: 'RS256' }, function(err, token) { +jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' }, function(err, token) { console.log(token); }); ``` From 7b60c127ceade36c33ff33be066e435802001c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lio=E6=9D=8E=E6=AC=A7?= Date: Wed, 20 Feb 2019 20:22:27 +0800 Subject: [PATCH 54/77] Force use_strict during testing (#577) * Force use_strict during testing * Add string payload test cases to .iat tests --- package.json | 2 +- sign.js | 6 +++--- test/claim-iat.test.js | 26 ++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index f019a611..1c8ea5cd 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "scripts": { "lint": "eslint .", - "coverage": "nyc mocha", + "coverage": "nyc mocha --use_strict", "test": "npm run lint && npm run coverage && cost-of-modules" }, "repository": { diff --git a/sign.js b/sign.js index de80b024..4a493d3d 100644 --- a/sign.js +++ b/sign.js @@ -140,10 +140,10 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { var timestamp = payload.iat || Math.floor(Date.now() / 1000); - if (!options.noTimestamp) { - payload.iat = timestamp; - } else { + if (options.noTimestamp) { delete payload.iat; + } else if (isObjectPayload) { + payload.iat = timestamp; } if (typeof options.notBefore !== 'undefined') { diff --git a/test/claim-iat.test.js b/test/claim-iat.test.js index a1c63ba6..5bf8df7f 100644 --- a/test/claim-iat.test.js +++ b/test/claim-iat.test.js @@ -248,4 +248,30 @@ describe('issue at', function() { }); }); }); + + describe('with string payload', function () { + it('should not add iat to string', function (done) { + const payload = 'string payload'; + const options = {algorithm: 'none'}; + testUtils.signJWTHelper(payload, 'secret', options, (err, token) => { + const decoded = jwt.decode(token); + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.equal(payload); + }); + }); + }); + + it('should not add iat to stringified object', function (done) { + const payload = '{}'; + const options = {algorithm: 'none', header: {typ: 'JWT'}}; + testUtils.signJWTHelper(payload, 'secret', options, (err, token) => { + const decoded = jwt.decode(token); + testUtils.asyncCheck(done, () => { + expect(err).to.equal(null); + expect(JSON.stringify(decoded)).to.equal(payload); + }); + }); + }); + }); }); From 8737789dd330cf9e7870f4df97fd52479adbac22 Mon Sep 17 00:00:00 2001 From: Javier Espinosa Date: Wed, 20 Feb 2019 13:32:23 +0100 Subject: [PATCH 55/77] Add complete option in jwt.verify (#522) * Add complete option in verify * Remove comment * Update README.md Co-Authored-By: javespi * Move tests in a specific file --- README.md | 1 + test/option-complete.test.js | 53 ++++++++++++++++++++++++++++++++++++ verify.js | 10 +++++++ 3 files changed, 64 insertions(+) create mode 100644 test/option-complete.test.js diff --git a/README.md b/README.md index 80132218..442aa550 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues * `algorithms`: List of strings with the names of the allowed algorithms. For instance, `["HS256", "HS384"]`. * `audience`: if you want to check audience (`aud`), provide a value here. The audience can be checked against a string, a regular expression or a list of strings and/or regular expressions. > Eg: `"urn:foo"`, `/urn:f[o]{2}/`, `[/urn:f[o]{2}/, "urn:bar"]` +* `complete`: return an object with the decoded `{ payload, header, signature }` instead of only the usual content of the payload. * `issuer` (optional): string or array of strings of valid values for the `iss` field. * `ignoreExpiration`: if `true` do not validate the expiration of the token. * `ignoreNotBefore`... diff --git a/test/option-complete.test.js b/test/option-complete.test.js new file mode 100644 index 00000000..29320e8a --- /dev/null +++ b/test/option-complete.test.js @@ -0,0 +1,53 @@ +'use strict'; + +const jws = require('jws'); +const expect = require('chai').expect; +const path = require('path'); +const fs = require('fs'); +const testUtils = require('./test-utils') + +describe('complete option', function () { + const secret = fs.readFileSync(path.join(__dirname, 'priv.pem')); + const pub = fs.readFileSync(path.join(__dirname, 'pub.pem')); + + const header = { alg: 'RS256' }; + const payload = { iat: Math.floor(Date.now() / 1000 ) }; + const signed = jws.sign({ header, payload, secret, encoding: 'utf8' }); + const signature = jws.decode(signed).signature; + + [ + { + description: 'should return header, payload and signature', + complete: true, + }, + ].forEach((testCase) => { + it(testCase.description, function (done) { + testUtils.verifyJWTHelper(signed, pub, { typ: 'JWT', complete: testCase.complete }, (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded.header).to.have.property('alg', header.alg); + expect(decoded.payload).to.have.property('iat', payload.iat); + expect(decoded).to.have.property('signature', signature); + }); + }); + }); + }); + [ + { + description: 'should return payload', + complete: false, + }, + ].forEach((testCase) => { + it(testCase.description, function (done) { + testUtils.verifyJWTHelper(signed, pub, { typ: 'JWT', complete: testCase.complete }, (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded.header).to.be.undefined; + expect(decoded.payload).to.be.undefined; + expect(decoded.signature).to.be.undefined; + expect(decoded).to.have.property('iat', payload.iat); + }); + }); + }); + }); +}); diff --git a/verify.js b/verify.js index fa1339ae..6b459c75 100644 --- a/verify.js +++ b/verify.js @@ -203,6 +203,16 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { } } + if (options.complete === true) { + var signature = decodedToken.signature; + + return done(null, { + header: header, + payload: payload, + signature: signature + }); + } + return done(null, payload); }); }; From eefb9d9c6eec54718fa6e41306bda84788df7bec Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 20 Feb 2019 13:42:37 +0100 Subject: [PATCH 56/77] feat: add PS JWA support for applicable node versions (#573) --- .travis.yml | 3 +++ README.md | 9 ++++++--- lib/psSupported.js | 3 +++ package.json | 5 +++-- sign.js | 8 +++++++- test/async_sign.tests.js | 11 +++++++++++ test/jwt.asymmetric_signing.tests.js | 10 ++++++++++ test/rsa-public-key.tests.js | 15 ++++++++++++++- test/schema.tests.js | 6 ++++++ test/wrong_alg.tests.js | 11 +++++++++++ verify.js | 17 ++++++++++++----- 11 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 lib/psSupported.js diff --git a/.travis.yml b/.travis.yml index 7f2f9aa3..498d61d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,9 @@ language: node_js sudo: false node_js: + - "11" + - "10" + - "9" - "8" - "7" - "6" diff --git a/README.md b/README.md index 442aa550..ea0ee00a 100644 --- a/README.md +++ b/README.md @@ -340,9 +340,12 @@ alg Parameter Value | Digital Signature or MAC Algorithm HS256 | HMAC using SHA-256 hash algorithm HS384 | HMAC using SHA-384 hash algorithm HS512 | HMAC using SHA-512 hash algorithm -RS256 | RSASSA using SHA-256 hash algorithm -RS384 | RSASSA using SHA-384 hash algorithm -RS512 | RSASSA using SHA-512 hash algorithm +RS256 | RSASSA-PKCS1-v1_5 using SHA-256 hash algorithm +RS384 | RSASSA-PKCS1-v1_5 using SHA-384 hash algorithm +RS512 | RSASSA-PKCS1-v1_5 using SHA-512 hash algorithm +PS256 | RSASSA-PSS using SHA-256 hash algorithm (only node ^6.12.0 || >=8.0.0) +PS384 | RSASSA-PSS using SHA-384 hash algorithm (only node ^6.12.0 || >=8.0.0) +PS512 | RSASSA-PSS using SHA-512 hash algorithm (only node ^6.12.0 || >=8.0.0) ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm diff --git a/lib/psSupported.js b/lib/psSupported.js new file mode 100644 index 00000000..8c04144a --- /dev/null +++ b/lib/psSupported.js @@ -0,0 +1,3 @@ +var semver = require('semver'); + +module.exports = semver.satisfies(process.version, '^6.12.0 || >=8.0.0'); diff --git a/package.json b/package.json index 1c8ea5cd..0349fc45 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "url": "https://github.com/auth0/node-jsonwebtoken/issues" }, "dependencies": { - "jws": "^3.1.5", + "jws": "^3.2.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -44,7 +44,8 @@ "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", - "ms": "^2.1.1" + "ms": "^2.1.1", + "semver": "^5.6.0" }, "devDependencies": { "atob": "^2.1.2", diff --git a/sign.js b/sign.js index 4a493d3d..f649ce4f 100644 --- a/sign.js +++ b/sign.js @@ -1,4 +1,5 @@ var timespan = require('./lib/timespan'); +var PS_SUPPORTED = require('./lib/psSupported'); var jws = require('jws'); var includes = require('lodash.includes'); var isBoolean = require('lodash.isboolean'); @@ -8,11 +9,16 @@ var isPlainObject = require('lodash.isplainobject'); var isString = require('lodash.isstring'); var once = require('lodash.once'); +var SUPPORTED_ALGS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none'] +if (PS_SUPPORTED) { + SUPPORTED_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512'); +} + 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' }, + algorithm: { isValid: includes.bind(null, SUPPORTED_ALGS), 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' }, diff --git a/test/async_sign.tests.js b/test/async_sign.tests.js index b0948d3b..6eb7dc76 100644 --- a/test/async_sign.tests.js +++ b/test/async_sign.tests.js @@ -1,6 +1,7 @@ var jwt = require('../index'); var expect = require('chai').expect; var jws = require('jws'); +var PS_SUPPORTED = require('../lib/psSupported'); describe('signing a token asynchronously', function() { @@ -58,6 +59,16 @@ describe('signing a token asynchronously', function() { }); }); + if (PS_SUPPORTED) { + it('should return error when secret is not a cert for PS256', function(done) { + //this throw an error because the secret is not a cert and PS256 requires a cert. + jwt.sign({ foo: 'bar' }, secret, { algorithm: 'PS256' }, function (err) { + expect(err).to.be.ok; + done(); + }); + }); + } + it('should return error on wrong arguments', function(done) { //this throw an error because the secret is not a cert and RS256 requires a cert. jwt.sign({ foo: 'bar' }, secret, { notBefore: {} }, function (err) { diff --git a/test/jwt.asymmetric_signing.tests.js b/test/jwt.asymmetric_signing.tests.js index 49b9ed00..c56eea30 100644 --- a/test/jwt.asymmetric_signing.tests.js +++ b/test/jwt.asymmetric_signing.tests.js @@ -1,4 +1,5 @@ var jwt = require('../index'); +var PS_SUPPORTED = require('../lib/psSupported'); var fs = require('fs'); var path = require('path'); @@ -25,6 +26,15 @@ var algorithms = { } }; +if (PS_SUPPORTED) { + algorithms.PS256 = { + pub_key: loadKey('pub.pem'), + priv_key: loadKey('priv.pem'), + invalid_pub_key: loadKey('invalid_pub.pem') + }; +} + + describe('Asymmetric Algorithms', function(){ Object.keys(algorithms).forEach(function (algorithm) { diff --git a/test/rsa-public-key.tests.js b/test/rsa-public-key.tests.js index e2044fc3..6abafb84 100644 --- a/test/rsa-public-key.tests.js +++ b/test/rsa-public-key.tests.js @@ -1,8 +1,9 @@ var jwt = require('../'); +var PS_SUPPORTED = require('../lib/psSupported'); describe('public key start with BEGIN RSA PUBLIC KEY', function () { - it('should work', function (done) { + it('should work for RS family of algorithms', function (done) { var fs = require('fs'); var cert_pub = fs.readFileSync(__dirname + '/rsa-public-key.pem'); var cert_priv = fs.readFileSync(__dirname + '/rsa-private.pem'); @@ -12,4 +13,16 @@ describe('public key start with BEGIN RSA PUBLIC KEY', function () { jwt.verify(token, cert_pub, done); }); + if (PS_SUPPORTED) { + it('should work for PS family of algorithms', function (done) { + var fs = require('fs'); + var cert_pub = fs.readFileSync(__dirname + '/rsa-public-key.pem'); + var cert_priv = fs.readFileSync(__dirname + '/rsa-private.pem'); + + var token = jwt.sign({ foo: 'bar' }, cert_priv, { algorithm: 'PS256'}); + + jwt.verify(token, cert_pub, done); + }); + } + }); diff --git a/test/schema.tests.js b/test/schema.tests.js index 887b59f3..742d29e4 100644 --- a/test/schema.tests.js +++ b/test/schema.tests.js @@ -1,6 +1,7 @@ var jwt = require('../index'); var expect = require('chai').expect; var fs = require('fs'); +var PS_SUPPORTED = require('../lib/psSupported'); describe('schema', function() { @@ -21,6 +22,11 @@ describe('schema', function() { sign({algorithm: 'RS256'}); sign({algorithm: 'RS384'}); sign({algorithm: 'RS512'}); + if (PS_SUPPORTED) { + sign({algorithm: 'PS256'}); + sign({algorithm: 'PS384'}); + sign({algorithm: 'PS512'}); + } sign({algorithm: 'ES256'}); sign({algorithm: 'ES384'}); sign({algorithm: 'ES512'}); diff --git a/test/wrong_alg.tests.js b/test/wrong_alg.tests.js index 04ce48e8..8b6e2459 100644 --- a/test/wrong_alg.tests.js +++ b/test/wrong_alg.tests.js @@ -2,6 +2,7 @@ var fs = require('fs'); var path = require('path'); var jwt = require('../index'); var JsonWebTokenError = require('../lib/JsonWebTokenError'); +var PS_SUPPORTED = require('../lib/psSupported'); var expect = require('chai').expect; @@ -29,6 +30,16 @@ describe('when setting a wrong `header.alg`', function () { }); }); + if (PS_SUPPORTED) { + describe('signing with pub key as HS256 and whitelisting only PS256', function () { + it('should not verify', function () { + expect(function () { + jwt.verify(TOKEN, pub, {algorithms: ['PS256']}); + }).to.throw(JsonWebTokenError, /invalid algorithm/); + }); + }); + } + describe('signing with HS256 and checking with HS384', function () { it('should not verify', function () { expect(function () { diff --git a/verify.js b/verify.js index 6b459c75..1df99f8d 100644 --- a/verify.js +++ b/verify.js @@ -3,8 +3,18 @@ var NotBeforeError = require('./lib/NotBeforeError'); var TokenExpiredError = require('./lib/TokenExpiredError'); var decode = require('./decode'); var timespan = require('./lib/timespan'); +var PS_SUPPORTED = require('./lib/psSupported'); var jws = require('jws'); +var PUB_KEY_ALGS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512']; +var RSA_KEY_ALGS = ['RS256', 'RS384', 'RS512']; +var HS_ALGS = ['HS256', 'HS384', 'HS512']; + +if (PS_SUPPORTED) { + PUB_KEY_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512'); + RSA_KEY_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512'); +} + module.exports = function (jwtString, secretOrPublicKey, options, callback) { if ((typeof options === 'function') && !callback) { callback = options; @@ -102,11 +112,8 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { if (!options.algorithms) { options.algorithms = ~secretOrPublicKey.toString().indexOf('BEGIN CERTIFICATE') || - ~secretOrPublicKey.toString().indexOf('BEGIN PUBLIC KEY') ? - ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'] : - ~secretOrPublicKey.toString().indexOf('BEGIN RSA PUBLIC KEY') ? - ['RS256', 'RS384', 'RS512'] : - ['HS256', 'HS384', 'HS512']; + ~secretOrPublicKey.toString().indexOf('BEGIN PUBLIC KEY') ? PUB_KEY_ALGS : + ~secretOrPublicKey.toString().indexOf('BEGIN RSA PUBLIC KEY') ? RSA_KEY_ALGS : HS_ALGS; } From 1c0de55c4a650cf0e894d089c44b74afc91ff78e Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Wed, 20 Feb 2019 14:06:59 +0100 Subject: [PATCH 57/77] 8.5.0 --- CHANGELOG.md | 19 +++++++++++++++++++ package.json | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3196aaa9..ace80e96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file starting from version **v4.0.0**. This project adheres to [Semantic Versioning](http://semver.org/). +## 8.5.0 - 2019-02-20 + +### New Functionality + + - feat: add PS JWA support for applicable node versions (#573) ([eefb9d9c6eec54718fa6e41306bda84788df7bec](https://github.com/auth0/node-jsonwebtoken/commit/eefb9d9c6eec54718fa6e41306bda84788df7bec)), closes [#573](https://github.com/auth0/node-jsonwebtoken/issues/573) + - Add complete option in jwt.verify (#522) ([8737789dd330cf9e7870f4df97fd52479adbac22](https://github.com/auth0/node-jsonwebtoken/commit/8737789dd330cf9e7870f4df97fd52479adbac22)), closes [#522](https://github.com/auth0/node-jsonwebtoken/issues/522) + + ### Test Improvements + + - Add tests for private claims in the payload (#555) ([5147852896755dc1291825e2e40556f964411fb2](https://github.com/auth0/node-jsonwebtoken/commit/5147852896755dc1291825e2e40556f964411fb2)), closes [#555](https://github.com/auth0/node-jsonwebtoken/issues/555) + - Force use_strict during testing (#577) ([7b60c127ceade36c33ff33be066e435802001c94](https://github.com/auth0/node-jsonwebtoken/commit/7b60c127ceade36c33ff33be066e435802001c94)), closes [#577](https://github.com/auth0/node-jsonwebtoken/issues/577) + - Refactor tests related to jti and jwtid (#544) ([7eebbc75ab89e01af5dacf2aae90fe05a13a1454](https://github.com/auth0/node-jsonwebtoken/commit/7eebbc75ab89e01af5dacf2aae90fe05a13a1454)), closes [#544](https://github.com/auth0/node-jsonwebtoken/issues/544) + - ci: remove nsp from tests (#569) ([da8f55c3c7b4dd0bfc07a2df228500fdd050242a](https://github.com/auth0/node-jsonwebtoken/commit/da8f55c3c7b4dd0bfc07a2df228500fdd050242a)), closes [#569](https://github.com/auth0/node-jsonwebtoken/issues/569) + +### Docs + +- Fix 'cert' token which isn't a cert (#554) ([0c24fe68cd2866cea6322016bf993cd897fefc98](https://github.com/auth0/node-jsonwebtoken/commit/0c24fe68cd2866cea6322016bf993cd897fefc98)), closes [#554](https://github.com/auth0/node-jsonwebtoken/issues/554) + + ## 8.4.0 - 2018-11-14 ### New Functionality diff --git a/package.json b/package.json index 0349fc45..e31d21a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonwebtoken", - "version": "8.4.0", + "version": "8.5.0", "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", "nyc": { From 84e03ef70f9c44a3aef95a1dc122c8238854f683 Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Wed, 20 Feb 2019 15:07:21 +0100 Subject: [PATCH 58/77] README: fix markdown for algorithms table --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ea0ee00a..f966435e 100644 --- a/README.md +++ b/README.md @@ -343,9 +343,9 @@ HS512 | HMAC using SHA-512 hash algorithm RS256 | RSASSA-PKCS1-v1_5 using SHA-256 hash algorithm RS384 | RSASSA-PKCS1-v1_5 using SHA-384 hash algorithm RS512 | RSASSA-PKCS1-v1_5 using SHA-512 hash algorithm -PS256 | RSASSA-PSS using SHA-256 hash algorithm (only node ^6.12.0 || >=8.0.0) -PS384 | RSASSA-PSS using SHA-384 hash algorithm (only node ^6.12.0 || >=8.0.0) -PS512 | RSASSA-PSS using SHA-512 hash algorithm (only node ^6.12.0 || >=8.0.0) +PS256 | RSASSA-PSS using SHA-256 hash algorithm (only node ^6.12.0 OR >=8.0.0) +PS384 | RSASSA-PSS using SHA-384 hash algorithm (only node ^6.12.0 OR >=8.0.0) +PS512 | RSASSA-PSS using SHA-512 hash algorithm (only node ^6.12.0 OR >=8.0.0) ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm From e5874ae428ffc0465e6bd4e660f89f78b56a74a6 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 18 Mar 2019 12:17:04 +0100 Subject: [PATCH 59/77] fix: ensure correct PS signing and verification (#585) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e31d21a4..de1189b3 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "url": "https://github.com/auth0/node-jsonwebtoken/issues" }, "dependencies": { - "jws": "^3.2.1", + "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", From 7f1f8b4b842ca3168018ab1ef53001105a1a2948 Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Mon, 18 Mar 2019 12:25:21 +0100 Subject: [PATCH 60/77] 8.5.1 --- CHANGELOG.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ace80e96..54364a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file starting from version **v4.0.0**. This project adheres to [Semantic Versioning](http://semver.org/). +## 8.5.1 - 2019-03-18 + +### Bug fix + + - fix: ensure correct PS signing and verification (#585) ([e5874ae428ffc0465e6bd4e660f89f78b56a74a6](https://github.com/auth0/node-jsonwebtoken/commit/e5874ae428ffc0465e6bd4e660f89f78b56a74a6)), closes [#585](https://github.com/auth0/node-jsonwebtoken/issues/585) + +### Docs + + - README: fix markdown for algorithms table ([84e03ef70f9c44a3aef95a1dc122c8238854f683](https://github.com/auth0/node-jsonwebtoken/commit/84e03ef70f9c44a3aef95a1dc122c8238854f683)) + ## 8.5.0 - 2019-02-20 ### New Functionality diff --git a/package.json b/package.json index de1189b3..81d63da8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonwebtoken", - "version": "8.5.0", + "version": "8.5.1", "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", "nyc": { From a9e38b8bab4fc8532eccb9d97712bbf566a1fc6a Mon Sep 17 00:00:00 2001 From: Eduardo Diaz Date: Fri, 12 Apr 2019 19:47:26 +0200 Subject: [PATCH 61/77] ci: use circleci (#589) --- .circleci/config.yml | 71 ++++++++++++++++++++++++++++++++++++++++++++ .travis.yml | 11 ------- 2 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 .circleci/config.yml delete mode 100644 .travis.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..04670d73 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,71 @@ +version: 2.1 + +# Thanks to https://github.com/teppeis-sandbox/circleci2-multiple-node-versions + +commands: + test-nodejs: + steps: + - run: + name: Versions + command: npm version + - checkout + - run: + name: Install dependencies + command: npm install + - run: + name: Test + command: npm test + +jobs: + node-v4: + docker: + - image: node:4 + steps: + - test-nodejs + node-v5: + docker: + - image: node:5 + steps: + - test-nodejs + node-v6: + docker: + - image: node:6 + steps: + - test-nodejs + node-v7: + docker: + - image: node:7 + steps: + - test-nodejs + node-v8: + docker: + - image: node:8 + steps: + - test-nodejs + node-v9: + docker: + - image: node:9 + steps: + - test-nodejs + node-v10: + docker: + - image: node:10 + steps: + - test-nodejs + node-v11: + docker: + - image: node:11 + steps: + - test-nodejs + +workflows: + node-multi-build: + jobs: + - node-v4 + - node-v5 + - node-v6 + - node-v7 + - node-v8 + - node-v9 + - node-v10 + - node-v11 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 498d61d3..00000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: node_js -sudo: false -node_js: - - "11" - - "10" - - "9" - - "8" - - "7" - - "6" - - "5" - - "4" From 9fb90cae493b6c556feba04477109e1cbef7f149 Mon Sep 17 00:00:00 2001 From: Pedro Luiz Cabral Salomon Prado Date: Mon, 21 Oct 2019 08:23:57 -0300 Subject: [PATCH 62/77] style: add missing semicolon (#641) --- sign.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sign.js b/sign.js index f649ce4f..f8a2877e 100644 --- a/sign.js +++ b/sign.js @@ -9,7 +9,7 @@ var isPlainObject = require('lodash.isplainobject'); var isString = require('lodash.isstring'); var once = require('lodash.once'); -var SUPPORTED_ALGS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none'] +var SUPPORTED_ALGS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none']; if (PS_SUPPORTED) { SUPPORTED_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512'); } From 5ed1f061869b7d4e624a51789fd4a135ddb34b45 Mon Sep 17 00:00:00 2001 From: "Ibrahim P.G" <38604383+ibrahimpg@users.noreply.github.com> Date: Mon, 21 Oct 2019 06:25:55 -0500 Subject: [PATCH 63/77] docs: fix tiny style change in readme (#622) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f966435e..48310cc2 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Synchronous Sign with RSA SHA256 ```js // sign with RSA SHA256 var privateKey = fs.readFileSync('private.key'); -var token = jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256'}); +var token = jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' }); ``` Sign asynchronously From a6235fa561b5c30884c97ea0b30c3db3b546ae2c Mon Sep 17 00:00:00 2001 From: Cristofer Gonzales Date: Fri, 29 Nov 2019 07:55:04 -0300 Subject: [PATCH 64/77] Adds not to README on decoded payload validation (#646) Adds a note to the README that suggest that decoded payload should not be trusted and should be treated as user inputs. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 48310cc2..a0b142d4 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,8 @@ jwt.sign({ (Synchronous) If a callback is not supplied, function acts synchronously. Returns the payload decoded if the signature is valid and optional expiration, audience, or issuer are valid. If not, it will throw the error. +> __Warning:__ When the token comes from an untrusted source (e.g. user input or external requests), the returned decoded payload should be treated like any other user input; please make sure to sanitize and only work with properties that are expected + `token` is the JsonWebToken string `secretOrPublicKey` is a string or buffer containing either the secret for HMAC algorithms, or the PEM @@ -234,6 +236,9 @@ jwt.verify(token, getKey, options, function(err, decoded) { > __Warning:__ This will __not__ verify whether the signature is valid. You should __not__ use this for untrusted messages. You most likely want to use `jwt.verify` instead. +> __Warning:__ When the token comes from an untrusted source (e.g. user input or external request), the returned decoded payload should be treated like any other user input; please make sure to sanitize and only work with properties that are expected + + `token` is the JsonWebToken string `options`: From 88cb9df18a1d2a7b24f8cfeaa6f5f5b87d2c027f Mon Sep 17 00:00:00 2001 From: dviryamin Date: Mon, 10 Feb 2020 17:55:58 +0200 Subject: [PATCH 65/77] Replace tilde-indexOf with includes (#647) * Replace tilde-indexOf with includes * remove package lock --- verify.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/verify.js b/verify.js index 1df99f8d..8687eb59 100644 --- a/verify.js +++ b/verify.js @@ -111,9 +111,9 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { } if (!options.algorithms) { - options.algorithms = ~secretOrPublicKey.toString().indexOf('BEGIN CERTIFICATE') || - ~secretOrPublicKey.toString().indexOf('BEGIN PUBLIC KEY') ? PUB_KEY_ALGS : - ~secretOrPublicKey.toString().indexOf('BEGIN RSA PUBLIC KEY') ? RSA_KEY_ALGS : HS_ALGS; + options.algorithms = secretOrPublicKey.toString().includes('BEGIN CERTIFICATE') || + secretOrPublicKey.toString().includes('BEGIN PUBLIC KEY') ? PUB_KEY_ALGS : + secretOrPublicKey.toString().includes('BEGIN RSA PUBLIC KEY') ? RSA_KEY_ALGS : HS_ALGS; } From 5f10bf9957a2541828501cfecab0310908b2f62f Mon Sep 17 00:00:00 2001 From: hiramatsutaku Date: Tue, 24 Mar 2020 23:32:23 +0900 Subject: [PATCH 66/77] docs: add jwtid to options of jwt.verify (#704) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a0b142d4..da35fa52 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues > Eg: `"urn:foo"`, `/urn:f[o]{2}/`, `[/urn:f[o]{2}/, "urn:bar"]` * `complete`: return an object with the decoded `{ payload, header, signature }` instead of only the usual content of the payload. * `issuer` (optional): string or array of strings of valid values for the `iss` field. +* `jwtid` (optional): if you want to check JWT ID (`jti`), provide a string value here. * `ignoreExpiration`: if `true` do not validate the expiration of the token. * `ignoreNotBefore`... * `subject`: if you want to check subject (`sub`), provide a value here From 15a1bc449ab529d540eb9c2be4e093f9f5b0278d Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 4 Sep 2020 22:05:00 +0200 Subject: [PATCH 67/77] refactor: make decode non-enumerable see #741 --- index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 161eb2dd..a480f1dc 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,12 @@ module.exports = { - decode: require('./decode'), verify: require('./verify'), sign: require('./sign'), JsonWebTokenError: require('./lib/JsonWebTokenError'), NotBeforeError: require('./lib/NotBeforeError'), TokenExpiredError: require('./lib/TokenExpiredError'), }; + +Object.defineProperty(module.exports, 'decode', { + enumerable: false, + value: require('./decode'), +}); From a46097e962621ab2ba718d1da6025cdeba3597c8 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 4 Sep 2020 22:05:35 +0200 Subject: [PATCH 68/77] docs: make decode impossible to discover before verify see #741 --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index da35fa52..d76f8790 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,9 @@ jwt.verify(token, getKey, options, function(err, decoded) { ``` +
+Need to peak into a JWT without verifying it? (Click to expand) + ### jwt.decode(token [, options]) (Synchronous) Returns the decoded payload without verifying if the signature is valid. @@ -259,6 +262,8 @@ console.log(decoded.header); console.log(decoded.payload) ``` +
+ ## Errors & Codes Possible thrown errors during verification. Error is the first argument of the verification callback. From 37650031fd0bac1a5b0d682bbfcf8c1705917aa9 Mon Sep 17 00:00:00 2001 From: Mikael Viitaniemi Date: Mon, 9 Nov 2020 12:36:47 +0200 Subject: [PATCH 69/77] docs: fix spelling in README.md: Peak -> Peek (#754) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d76f8790..b0afff02 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,7 @@ jwt.verify(token, getKey, options, function(err, decoded) { ```
-Need to peak into a JWT without verifying it? (Click to expand) +Need to peek into a JWT without verifying it? (Click to expand) ### jwt.decode(token [, options]) From d71e383862fc735991fd2e759181480f066bf138 Mon Sep 17 00:00:00 2001 From: Andy Edwards Date: Thu, 18 Feb 2021 10:47:53 -0600 Subject: [PATCH 70/77] docs: document "invalid token" error closes #768 --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b0afff02..7eb8be16 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,8 @@ Error object: * name: 'JsonWebTokenError' * message: - * 'jwt malformed' + * 'invalid token' - the header or payload could not be parsed + * 'jwt malformed' - the token does not have three components (delimited by a `.`) * 'jwt signature is required' * 'invalid signature' * 'jwt audience invalid. expected: [OPTIONS AUDIENCE]' From 74d5719bd03993fcf71e3b176621f133eb6138c0 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Wed, 31 Mar 2021 02:00:10 -0700 Subject: [PATCH 71/77] docs: update references vercel/ms references (#770) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7eb8be16..834777e0 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,9 @@ encoded private key for RSA and ECDSA. In case of a private key with passphrase `options`: * `algorithm` (default: `HS256`) -* `expiresIn`: expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). +* `expiresIn`: expressed in seconds or a string describing a time span [vercel/ms](https://github.com/vercel/ms). > Eg: `60`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). -* `notBefore`: expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). +* `notBefore`: expressed in seconds or a string describing a time span [vercel/ms](https://github.com/vercel/ms). > Eg: `60`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). * `audience` * `issuer` @@ -147,7 +147,7 @@ As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues * `ignoreNotBefore`... * `subject`: if you want to check subject (`sub`), provide a value here * `clockTolerance`: number of seconds to tolerate when checking the `nbf` and `exp` claims, to deal with small clock differences among different servers -* `maxAge`: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). +* `maxAge`: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span [vercel/ms](https://github.com/vercel/ms). > Eg: `1000`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). * `clockTimestamp`: the time in seconds that should be used as the current time for all necessary comparisons. * `nonce`: if you want to check `nonce` claim, provide a string value here. It is used on Open ID for the ID Tokens. ([Open ID implementation notes](https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes)) From 7e6a86b1c25e5fd05733c52c118848341aba1c4e Mon Sep 17 00:00:00 2001 From: "sre-57-opslevel[bot]" <113727212+sre-57-opslevel[bot]@users.noreply.github.com> Date: Wed, 26 Oct 2022 10:36:39 +0100 Subject: [PATCH 72/77] Upload OpsLevel YAML (#849) Co-authored-by: sre-57-opslevel[bot] <113727212+sre-57-opslevel[bot]@users.noreply.github.com> --- opslevel.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 opslevel.yml diff --git a/opslevel.yml b/opslevel.yml new file mode 100644 index 00000000..aeeeea70 --- /dev/null +++ b/opslevel.yml @@ -0,0 +1,6 @@ +--- +version: 1 +repository: + owner: iam_protocols + tier: + tags: From 834503079514b72264fd13023a3b8d648afd6a16 Mon Sep 17 00:00:00 2001 From: Jake Lacey Date: Tue, 29 Nov 2022 14:32:41 +0000 Subject: [PATCH 73/77] fix(sign&verify)!: Remove default `none` support from `sign` and `verify` methods, and require it to be explicitly configured (#851) * fix(sign&verify)!: Remove default none support from sign and verify methods, and require it to be explicitly configured BREAKING CHANGE: Removes fallback for none algorithm for the verify method. --- test/claim-aud.test.js | 6 ++--- test/claim-exp.test.js | 39 +++++++++++++-------------- test/claim-iat.test.js | 31 +++++++++++----------- test/claim-iss.test.js | 22 +++++++-------- test/claim-jti.test.js | 14 +++++----- test/claim-nbf.test.js | 41 ++++++++++++++-------------- test/claim-private.tests.js | 8 +++--- test/claim-sub.tests.js | 14 +++++----- test/header-kid.test.js | 4 +-- test/jwt.hs.tests.js | 23 +++++++++------- test/option-maxAge.test.js | 10 +++---- test/option-nonce.test.js | 6 ++--- test/schema.tests.js | 4 +-- test/verify.tests.js | 53 ++++++++++++++++++++++++++++--------- verify.js | 2 +- 15 files changed, 153 insertions(+), 124 deletions(-) diff --git a/test/claim-aud.test.js b/test/claim-aud.test.js index 448da5c8..3a27fd89 100644 --- a/test/claim-aud.test.js +++ b/test/claim-aud.test.js @@ -6,7 +6,7 @@ const util = require('util'); const testUtils = require('./test-utils'); function signWithAudience(audience, payload, callback) { - const options = {algorithm: 'none'}; + const options = {algorithm: 'HS256'}; if (audience !== undefined) { options.audience = audience; } @@ -15,7 +15,7 @@ function signWithAudience(audience, payload, callback) { } function verifyWithAudience(token, audience, callback) { - testUtils.verifyJWTHelper(token, undefined, {audience}, callback); + testUtils.verifyJWTHelper(token, 'secret', {audience}, callback); } describe('audience', function() { @@ -47,7 +47,7 @@ describe('audience', function() { // undefined needs special treatment because {} is not the same as {aud: undefined} it('should error with with value undefined', function (done) { - testUtils.signJWTHelper({}, 'secret', {audience: undefined, algorithm: 'none'}, (err) => { + testUtils.signJWTHelper({}, 'secret', {audience: undefined, algorithm: 'HS256'}, (err) => { testUtils.asyncCheck(done, () => { expect(err).to.be.instanceOf(Error); expect(err).to.have.property('message', '"audience" must be a string or array'); diff --git a/test/claim-exp.test.js b/test/claim-exp.test.js index 94360f6b..fbdbc522 100644 --- a/test/claim-exp.test.js +++ b/test/claim-exp.test.js @@ -5,12 +5,10 @@ const expect = require('chai').expect; const sinon = require('sinon'); const util = require('util'); const testUtils = require('./test-utils'); - -const base64UrlEncode = testUtils.base64UrlEncode; -const noneAlgorithmHeader = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0'; +const jws = require('jws'); function signWithExpiresIn(expiresIn, payload, callback) { - const options = {algorithm: 'none'}; + const options = {algorithm: 'HS256'}; if (expiresIn !== undefined) { options.expiresIn = expiresIn; } @@ -49,7 +47,7 @@ describe('expires', function() { // undefined needs special treatment because {} is not the same as {expiresIn: undefined} it('should error with with value undefined', function (done) { - testUtils.signJWTHelper({}, undefined, {expiresIn: undefined, algorithm: 'none'}, (err) => { + testUtils.signJWTHelper({}, 'secret', {expiresIn: undefined, algorithm: 'HS256'}, (err) => { testUtils.asyncCheck(done, () => { expect(err).to.be.instanceOf(Error); expect(err).to.have.property( @@ -133,9 +131,10 @@ describe('expires', function() { {foo: 'bar'}, ].forEach((exp) => { it(`should error with with value ${util.inspect(exp)}`, function (done) { - const encodedPayload = base64UrlEncode(JSON.stringify({exp})); - const token = `${noneAlgorithmHeader}.${encodedPayload}.`; - testUtils.verifyJWTHelper(token, undefined, {exp}, (err) => { + const header = { alg: 'HS256' }; + const payload = { exp }; + const token = jws.sign({ header, payload, secret: 'secret', encoding: 'utf8' }); + testUtils.verifyJWTHelper(token, 'secret', { exp }, (err) => { testUtils.asyncCheck(done, () => { expect(err).to.be.instanceOf(jwt.JsonWebTokenError); expect(err).to.have.property('message', 'invalid exp value'); @@ -158,7 +157,7 @@ describe('expires', function() { it('should set correct "exp" with negative number of seconds', function(done) { signWithExpiresIn(-10, {}, (e1, token) => { fakeClock.tick(-10001); - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -170,7 +169,7 @@ describe('expires', function() { it('should set correct "exp" with positive number of seconds', function(done) { signWithExpiresIn(10, {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -183,7 +182,7 @@ describe('expires', function() { it('should set correct "exp" with zero seconds', function(done) { signWithExpiresIn(0, {}, (e1, token) => { fakeClock.tick(-1); - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -196,7 +195,7 @@ describe('expires', function() { it('should set correct "exp" with negative string timespan', function(done) { signWithExpiresIn('-10 s', {}, (e1, token) => { fakeClock.tick(-10001); - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -209,7 +208,7 @@ describe('expires', function() { it('should set correct "exp" with positive string timespan', function(done) { signWithExpiresIn('10 s', {}, (e1, token) => { fakeClock.tick(-10001); - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -222,7 +221,7 @@ describe('expires', function() { it('should set correct "exp" with zero string timespan', function(done) { signWithExpiresIn('0 s', {}, (e1, token) => { fakeClock.tick(-1); - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -267,7 +266,7 @@ describe('expires', function() { it('should set correct "exp" when "iat" is passed', function (done) { signWithExpiresIn(-10, {iat: 80}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -279,7 +278,7 @@ describe('expires', function() { it('should verify "exp" using "clockTimestamp"', function (done) { signWithExpiresIn(10, {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {clockTimestamp: 69}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {clockTimestamp: 69}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -293,7 +292,7 @@ describe('expires', function() { it('should verify "exp" using "clockTolerance"', function (done) { signWithExpiresIn(5, {}, (e1, token) => { fakeClock.tick(10000); - testUtils.verifyJWTHelper(token, undefined, {clockTimestamp: 6}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {clockTimestamp: 6}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -306,7 +305,7 @@ describe('expires', function() { it('should ignore a expired token when "ignoreExpiration" is true', function (done) { signWithExpiresIn('-10 s', {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {ignoreExpiration: true}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {ignoreExpiration: true}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -319,7 +318,7 @@ describe('expires', function() { it('should error on verify if "exp" is at current time', function(done) { signWithExpiresIn(undefined, {exp: 60}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.instanceOf(jwt.TokenExpiredError); @@ -331,7 +330,7 @@ describe('expires', function() { it('should error on verify if "exp" is before current time using clockTolerance', function (done) { signWithExpiresIn(-5, {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {clockTolerance: 5}, (e2) => { + testUtils.verifyJWTHelper(token, 'secret', {clockTolerance: 5}, (e2) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.instanceOf(jwt.TokenExpiredError); diff --git a/test/claim-iat.test.js b/test/claim-iat.test.js index 5bf8df7f..a3dd474a 100644 --- a/test/claim-iat.test.js +++ b/test/claim-iat.test.js @@ -5,24 +5,22 @@ const expect = require('chai').expect; const sinon = require('sinon'); const util = require('util'); const testUtils = require('./test-utils'); - -const base64UrlEncode = testUtils.base64UrlEncode; -const noneAlgorithmHeader = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0'; +const jws = require('jws'); function signWithIssueAt(issueAt, options, callback) { const payload = {}; if (issueAt !== undefined) { payload.iat = issueAt; } - const opts = Object.assign({algorithm: 'none'}, options); + const opts = Object.assign({algorithm: 'HS256'}, options); // async calls require a truthy secret // see: https://github.com/brianloveswords/node-jws/issues/62 testUtils.signJWTHelper(payload, 'secret', opts, callback); } -function verifyWithIssueAt(token, maxAge, options, callback) { +function verifyWithIssueAt(token, maxAge, options, secret, callback) { const opts = Object.assign({maxAge}, options); - testUtils.verifyJWTHelper(token, undefined, opts, callback); + testUtils.verifyJWTHelper(token, secret, opts, callback); } describe('issue at', function() { @@ -50,7 +48,7 @@ describe('issue at', function() { // undefined needs special treatment because {} is not the same as {iat: undefined} it('should error with iat of undefined', function (done) { - testUtils.signJWTHelper({iat: undefined}, 'secret', {algorithm: 'none'}, (err) => { + testUtils.signJWTHelper({iat: undefined}, 'secret', {algorithm: 'HS256'}, (err) => { testUtils.asyncCheck(done, () => { expect(err).to.be.instanceOf(Error); expect(err.message).to.equal('"iat" should be a number of seconds'); @@ -76,9 +74,10 @@ describe('issue at', function() { {foo: 'bar'}, ].forEach((iat) => { it(`should error with iat of ${util.inspect(iat)}`, function (done) { - const encodedPayload = base64UrlEncode(JSON.stringify({iat})); - const token = `${noneAlgorithmHeader}.${encodedPayload}.`; - verifyWithIssueAt(token, '1 min', {}, (err) => { + const header = { alg: 'HS256' }; + const payload = { iat }; + const token = jws.sign({ header, payload, secret: 'secret', encoding: 'utf8' }); + verifyWithIssueAt(token, '1 min', {}, 'secret', (err) => { testUtils.asyncCheck(done, () => { expect(err).to.be.instanceOf(jwt.JsonWebTokenError); expect(err.message).to.equal('iat required when maxAge is specified'); @@ -188,9 +187,9 @@ describe('issue at', function() { }, ].forEach((testCase) => { it(testCase.description, function (done) { - const token = jwt.sign({}, 'secret', {algorithm: 'none'}); + const token = jwt.sign({}, 'secret', {algorithm: 'HS256'}); fakeClock.tick(testCase.clockAdvance); - verifyWithIssueAt(token, testCase.maxAge, testCase.options, (err, token) => { + verifyWithIssueAt(token, testCase.maxAge, testCase.options, 'secret', (err, token) => { testUtils.asyncCheck(done, () => { expect(err).to.be.null; expect(token).to.be.a('object'); @@ -235,10 +234,10 @@ describe('issue at', function() { ].forEach((testCase) => { it(testCase.description, function(done) { const expectedExpiresAtDate = new Date(testCase.expectedExpiresAt); - const token = jwt.sign({}, 'secret', {algorithm: 'none'}); + const token = jwt.sign({}, 'secret', {algorithm: 'HS256'}); fakeClock.tick(testCase.clockAdvance); - verifyWithIssueAt(token, testCase.maxAge, testCase.options, (err) => { + verifyWithIssueAt(token, testCase.maxAge, testCase.options, 'secret', (err) => { testUtils.asyncCheck(done, () => { expect(err).to.be.instanceOf(jwt.JsonWebTokenError); expect(err.message).to.equal(testCase.expectedError); @@ -252,7 +251,7 @@ describe('issue at', function() { describe('with string payload', function () { it('should not add iat to string', function (done) { const payload = 'string payload'; - const options = {algorithm: 'none'}; + const options = {algorithm: 'HS256'}; testUtils.signJWTHelper(payload, 'secret', options, (err, token) => { const decoded = jwt.decode(token); testUtils.asyncCheck(done, () => { @@ -264,7 +263,7 @@ describe('issue at', function() { it('should not add iat to stringified object', function (done) { const payload = '{}'; - const options = {algorithm: 'none', header: {typ: 'JWT'}}; + const options = {algorithm: 'HS256', header: {typ: 'JWT'}}; testUtils.signJWTHelper(payload, 'secret', options, (err, token) => { const decoded = jwt.decode(token); testUtils.asyncCheck(done, () => { diff --git a/test/claim-iss.test.js b/test/claim-iss.test.js index ec82102a..1b1b72f9 100644 --- a/test/claim-iss.test.js +++ b/test/claim-iss.test.js @@ -6,7 +6,7 @@ const util = require('util'); const testUtils = require('./test-utils'); function signWithIssuer(issuer, payload, callback) { - const options = {algorithm: 'none'}; + const options = {algorithm: 'HS256'}; if (issuer !== undefined) { options.issuer = issuer; } @@ -44,7 +44,7 @@ describe('issuer', function() { // undefined needs special treatment because {} is not the same as {issuer: undefined} it('should error with with value undefined', function (done) { - testUtils.signJWTHelper({}, undefined, {issuer: undefined, algorithm: 'none'}, (err) => { + testUtils.signJWTHelper({}, 'secret', {issuer: undefined, algorithm: 'HS256'}, (err) => { testUtils.asyncCheck(done, () => { expect(err).to.be.instanceOf(Error); expect(err).to.have.property('message', '"issuer" must be a string'); @@ -92,7 +92,7 @@ describe('issuer', function() { describe('when signing and verifying a token', function () { it('should not verify "iss" if verify "issuer" option not provided', function(done) { signWithIssuer(undefined, {iss: 'foo'}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -105,7 +105,7 @@ describe('issuer', function() { describe('with string "issuer" option', function () { it('should verify with a string "issuer"', function (done) { signWithIssuer('foo', {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {issuer: 'foo'}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {issuer: 'foo'}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -117,7 +117,7 @@ describe('issuer', function() { it('should verify with a string "iss"', function (done) { signWithIssuer(undefined, {iss: 'foo'}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {issuer: 'foo'}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {issuer: 'foo'}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -129,7 +129,7 @@ describe('issuer', function() { it('should error if "iss" does not match verify "issuer" option', function(done) { signWithIssuer(undefined, {iss: 'foobar'}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {issuer: 'foo'}, (e2) => { + testUtils.verifyJWTHelper(token, 'secret', {issuer: 'foo'}, (e2) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); @@ -141,7 +141,7 @@ describe('issuer', function() { it('should error without "iss" and with verify "issuer" option', function(done) { signWithIssuer(undefined, {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {issuer: 'foo'}, (e2) => { + testUtils.verifyJWTHelper(token, 'secret', {issuer: 'foo'}, (e2) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); @@ -155,7 +155,7 @@ describe('issuer', function() { describe('with array "issuer" option', function () { it('should verify with a string "issuer"', function (done) { signWithIssuer('bar', {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {issuer: ['foo', 'bar']}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {issuer: ['foo', 'bar']}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -167,7 +167,7 @@ describe('issuer', function() { it('should verify with a string "iss"', function (done) { signWithIssuer(undefined, {iss: 'foo'}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {issuer: ['foo', 'bar']}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {issuer: ['foo', 'bar']}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -179,7 +179,7 @@ describe('issuer', function() { it('should error if "iss" does not match verify "issuer" option', function(done) { signWithIssuer(undefined, {iss: 'foobar'}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {issuer: ['foo', 'bar']}, (e2) => { + testUtils.verifyJWTHelper(token, 'secret', {issuer: ['foo', 'bar']}, (e2) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); @@ -191,7 +191,7 @@ describe('issuer', function() { it('should error without "iss" and with verify "issuer" option', function(done) { signWithIssuer(undefined, {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {issuer: ['foo', 'bar']}, (e2) => { + testUtils.verifyJWTHelper(token, 'secret', {issuer: ['foo', 'bar']}, (e2) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); diff --git a/test/claim-jti.test.js b/test/claim-jti.test.js index a10a9b59..9721f7c7 100644 --- a/test/claim-jti.test.js +++ b/test/claim-jti.test.js @@ -6,7 +6,7 @@ const util = require('util'); const testUtils = require('./test-utils'); function signWithJWTId(jwtid, payload, callback) { - const options = {algorithm: 'none'}; + const options = {algorithm: 'HS256'}; if (jwtid !== undefined) { options.jwtid = jwtid; } @@ -44,7 +44,7 @@ describe('jwtid', function() { // undefined needs special treatment because {} is not the same as {jwtid: undefined} it('should error with with value undefined', function (done) { - testUtils.signJWTHelper({}, undefined, {jwtid: undefined, algorithm: 'none'}, (err) => { + testUtils.signJWTHelper({}, 'secret', {jwtid: undefined, algorithm: 'HS256'}, (err) => { testUtils.asyncCheck(done, () => { expect(err).to.be.instanceOf(Error); expect(err).to.have.property('message', '"jwtid" must be a string'); @@ -92,7 +92,7 @@ describe('jwtid', function() { describe('when signing and verifying a token', function () { it('should not verify "jti" if verify "jwtid" option not provided', function(done) { signWithJWTId(undefined, {jti: 'foo'}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -105,7 +105,7 @@ describe('jwtid', function() { describe('with "jwtid" option', function () { it('should verify with "jwtid" option', function (done) { signWithJWTId('foo', {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {jwtid: 'foo'}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {jwtid: 'foo'}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -117,7 +117,7 @@ describe('jwtid', function() { it('should verify with "jti" in payload', function (done) { signWithJWTId(undefined, {jti: 'foo'}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {jetid: 'foo'}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {jetid: 'foo'}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -129,7 +129,7 @@ describe('jwtid', function() { it('should error if "jti" does not match verify "jwtid" option', function(done) { signWithJWTId(undefined, {jti: 'bar'}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {jwtid: 'foo'}, (e2) => { + testUtils.verifyJWTHelper(token, 'secret', {jwtid: 'foo'}, (e2) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); @@ -141,7 +141,7 @@ describe('jwtid', function() { it('should error without "jti" and with verify "jwtid" option', function(done) { signWithJWTId(undefined, {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {jwtid: 'foo'}, (e2) => { + testUtils.verifyJWTHelper(token, 'secret', {jwtid: 'foo'}, (e2) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); diff --git a/test/claim-nbf.test.js b/test/claim-nbf.test.js index 1aa5cda7..72397de1 100644 --- a/test/claim-nbf.test.js +++ b/test/claim-nbf.test.js @@ -5,12 +5,10 @@ const expect = require('chai').expect; const sinon = require('sinon'); const util = require('util'); const testUtils = require('./test-utils'); - -const base64UrlEncode = testUtils.base64UrlEncode; -const noneAlgorithmHeader = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0'; +const jws = require('jws'); function signWithNotBefore(notBefore, payload, callback) { - const options = {algorithm: 'none'}; + const options = {algorithm: 'HS256'}; if (notBefore !== undefined) { options.notBefore = notBefore; } @@ -49,7 +47,7 @@ describe('not before', function() { // undefined needs special treatment because {} is not the same as {notBefore: undefined} it('should error with with value undefined', function (done) { - testUtils.signJWTHelper({}, undefined, {notBefore: undefined, algorithm: 'none'}, (err) => { + testUtils.signJWTHelper({}, 'secret', {notBefore: undefined, algorithm: 'HS256'}, (err) => { testUtils.asyncCheck(done, () => { expect(err).to.be.instanceOf(Error); expect(err).to.have.property( @@ -133,9 +131,10 @@ describe('not before', function() { {foo: 'bar'}, ].forEach((nbf) => { it(`should error with with value ${util.inspect(nbf)}`, function (done) { - const encodedPayload = base64UrlEncode(JSON.stringify({nbf})); - const token = `${noneAlgorithmHeader}.${encodedPayload}.`; - testUtils.verifyJWTHelper(token, undefined, {nbf}, (err) => { + const header = { alg: 'HS256' }; + const payload = { nbf }; + const token = jws.sign({ header, payload, secret: 'secret', encoding: 'utf8' }); + testUtils.verifyJWTHelper(token, 'secret', {nbf}, (err) => { testUtils.asyncCheck(done, () => { expect(err).to.be.instanceOf(jwt.JsonWebTokenError); expect(err).to.have.property('message', 'invalid nbf value'); @@ -157,7 +156,7 @@ describe('not before', function() { it('should set correct "nbf" with negative number of seconds', function (done) { signWithNotBefore(-10, {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -170,7 +169,7 @@ describe('not before', function() { it('should set correct "nbf" with positive number of seconds', function (done) { signWithNotBefore(10, {}, (e1, token) => { fakeClock.tick(10000); - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -182,7 +181,7 @@ describe('not before', function() { it('should set correct "nbf" with zero seconds', function (done) { signWithNotBefore(0, {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -194,7 +193,7 @@ describe('not before', function() { it('should set correct "nbf" with negative string timespan', function (done) { signWithNotBefore('-10 s', {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -207,7 +206,7 @@ describe('not before', function() { it('should set correct "nbf" with positive string timespan', function (done) { signWithNotBefore('10 s', {}, (e1, token) => { fakeClock.tick(10000); - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -219,7 +218,7 @@ describe('not before', function() { it('should set correct "nbf" with zero string timespan', function (done) { signWithNotBefore('0 s', {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -264,7 +263,7 @@ describe('not before', function() { it('should set correct "nbf" when "iat" is passed', function (done) { signWithNotBefore(-10, {iat: 40}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -276,7 +275,7 @@ describe('not before', function() { it('should verify "nbf" using "clockTimestamp"', function (done) { signWithNotBefore(10, {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {clockTimestamp: 70}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {clockTimestamp: 70}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -289,7 +288,7 @@ describe('not before', function() { it('should verify "nbf" using "clockTolerance"', function (done) { signWithNotBefore(5, {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {clockTolerance: 6}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {clockTolerance: 6}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -302,7 +301,7 @@ describe('not before', function() { it('should ignore a not active token when "ignoreNotBefore" is true', function (done) { signWithNotBefore('10 s', {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {ignoreNotBefore: true}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {ignoreNotBefore: true}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -315,7 +314,7 @@ describe('not before', function() { it('should error on verify if "nbf" is after current time', function (done) { signWithNotBefore(undefined, {nbf: 61}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.instanceOf(jwt.NotBeforeError); @@ -327,7 +326,7 @@ describe('not before', function() { it('should error on verify if "nbf" is after current time using clockTolerance', function (done) { signWithNotBefore(5, {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {clockTolerance: 4}, (e2) => { + testUtils.verifyJWTHelper(token, 'secret', {clockTolerance: 4}, (e2) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.instanceOf(jwt.NotBeforeError); @@ -337,4 +336,4 @@ describe('not before', function() { }); }); }); -}); \ No newline at end of file +}); diff --git a/test/claim-private.tests.js b/test/claim-private.tests.js index d21a7204..b7f03687 100644 --- a/test/claim-private.tests.js +++ b/test/claim-private.tests.js @@ -5,7 +5,7 @@ const util = require('util'); const testUtils = require('./test-utils'); function signWithPayload(payload, callback) { - testUtils.signJWTHelper(payload, 'secret', {algorithm: 'none'}, callback); + testUtils.signJWTHelper(payload, 'secret', {algorithm: 'HS256'}, callback); } describe('with a private claim', function() { @@ -28,7 +28,7 @@ describe('with a private claim', function() { ].forEach((privateClaim) => { it(`should sign and verify with claim of ${util.inspect(privateClaim)}`, function (done) { signWithPayload({privateClaim}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -47,7 +47,7 @@ describe('with a private claim', function() { ].forEach((privateClaim) => { it(`should sign and verify with claim of ${util.inspect(privateClaim)}`, function (done) { signWithPayload({privateClaim}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -61,7 +61,7 @@ describe('with a private claim', function() { // private claims with value undefined are not added to the payload it(`should sign and verify with claim of undefined`, function (done) { signWithPayload({privateClaim: undefined}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; diff --git a/test/claim-sub.tests.js b/test/claim-sub.tests.js index 5cd33790..a65b39ec 100644 --- a/test/claim-sub.tests.js +++ b/test/claim-sub.tests.js @@ -6,7 +6,7 @@ const util = require('util'); const testUtils = require('./test-utils'); function signWithSubject(subject, payload, callback) { - const options = {algorithm: 'none'}; + const options = {algorithm: 'HS256'}; if (subject !== undefined) { options.subject = subject; } @@ -44,7 +44,7 @@ describe('subject', function() { // undefined needs special treatment because {} is not the same as {subject: undefined} it('should error with with value undefined', function (done) { - testUtils.signJWTHelper({}, undefined, {subject: undefined, algorithm: 'none'}, (err) => { + testUtils.signJWTHelper({}, 'secret', {subject: undefined, algorithm: 'HS256'}, (err) => { testUtils.asyncCheck(done, () => { expect(err).to.be.instanceOf(Error); expect(err).to.have.property('message', '"subject" must be a string'); @@ -92,7 +92,7 @@ describe('subject', function() { describe('when signing and verifying a token with "subject" option', function () { it('should verify with a string "subject"', function (done) { signWithSubject('foo', {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {subject: 'foo'}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {subject: 'foo'}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -104,7 +104,7 @@ describe('subject', function() { it('should verify with a string "sub"', function (done) { signWithSubject(undefined, {sub: 'foo'}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {subject: 'foo'}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {subject: 'foo'}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -116,7 +116,7 @@ describe('subject', function() { it('should not verify "sub" if verify "subject" option not provided', function(done) { signWithSubject(undefined, {sub: 'foo'}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {}, (e2, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', {}, (e2, decoded) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.null; @@ -128,7 +128,7 @@ describe('subject', function() { it('should error if "sub" does not match verify "subject" option', function(done) { signWithSubject(undefined, {sub: 'foo'}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {subject: 'bar'}, (e2) => { + testUtils.verifyJWTHelper(token, 'secret', {subject: 'bar'}, (e2) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); @@ -140,7 +140,7 @@ describe('subject', function() { it('should error without "sub" and with verify "subject" option', function(done) { signWithSubject(undefined, {}, (e1, token) => { - testUtils.verifyJWTHelper(token, undefined, {subject: 'foo'}, (e2) => { + testUtils.verifyJWTHelper(token, 'secret', {subject: 'foo'}, (e2) => { testUtils.asyncCheck(done, () => { expect(e1).to.be.null; expect(e2).to.be.instanceOf(jwt.JsonWebTokenError); diff --git a/test/header-kid.test.js b/test/header-kid.test.js index 42633ade..e419067a 100644 --- a/test/header-kid.test.js +++ b/test/header-kid.test.js @@ -6,7 +6,7 @@ const util = require('util'); const testUtils = require('./test-utils'); function signWithKeyId(keyid, payload, callback) { - const options = {algorithm: 'none'}; + const options = {algorithm: 'HS256'}; if (keyid !== undefined) { options.keyid = keyid; } @@ -44,7 +44,7 @@ describe('keyid', function() { // undefined needs special treatment because {} is not the same as {keyid: undefined} it('should error with with value undefined', function (done) { - testUtils.signJWTHelper({}, undefined, {keyid: undefined, algorithm: 'none'}, (err) => { + testUtils.signJWTHelper({}, 'secret', {keyid: undefined, algorithm: 'HS256'}, (err) => { testUtils.asyncCheck(done, () => { expect(err).to.be.instanceOf(Error); expect(err).to.have.property('message', '"keyid" must be a string'); diff --git a/test/jwt.hs.tests.js b/test/jwt.hs.tests.js index 5c12a734..a7741ff1 100644 --- a/test/jwt.hs.tests.js +++ b/test/jwt.hs.tests.js @@ -1,7 +1,8 @@ -var jwt = require('../index'); +const jwt = require('../index'); -var expect = require('chai').expect; -var assert = require('chai').assert; +const jws = require('jws'); +const expect = require('chai').expect; +const assert = require('chai').assert; describe('HS256', function() { @@ -42,19 +43,21 @@ describe('HS256', function() { }); it('should throw with secret and token not signed', function(done) { - var signed = jwt.sign({ foo: 'bar' }, secret, { algorithm: 'none' }); - var unsigned = signed.split('.')[0] + '.' + signed.split('.')[1] + '.'; - jwt.verify(unsigned, 'secret', function(err, decoded) { + const header = { alg: 'none' }; + const payload = { foo: 'bar' }; + const token = jws.sign({ header, payload, secret: 'secret', encoding: 'utf8' }); + jwt.verify(token, 'secret', function(err, decoded) { assert.isUndefined(decoded); assert.isNotNull(err); done(); }); }); - it('should work with falsy secret and token not signed', function(done) { - var signed = jwt.sign({ foo: 'bar' }, null, { algorithm: 'none' }); - var unsigned = signed.split('.')[0] + '.' + signed.split('.')[1] + '.'; - jwt.verify(unsigned, 'secret', function(err, decoded) { + it('should throw with falsy secret and token not signed', function(done) { + const header = { alg: 'none' }; + const payload = { foo: 'bar' }; + const token = jws.sign({ header, payload, secret: null, encoding: 'utf8' }); + jwt.verify(token, 'secret', function(err, decoded) { assert.isUndefined(decoded); assert.isNotNull(err); done(); diff --git a/test/option-maxAge.test.js b/test/option-maxAge.test.js index c76676fc..10340f46 100644 --- a/test/option-maxAge.test.js +++ b/test/option-maxAge.test.js @@ -11,7 +11,7 @@ describe('maxAge option', function() { let fakeClock; beforeEach(function() { fakeClock = sinon.useFakeTimers({now: 60000}); - token = jwt.sign({iat: 70}, undefined, {algorithm: 'none'}); + token = jwt.sign({iat: 70}, 'secret', {algorithm: 'HS256'}); }); afterEach(function() { @@ -37,8 +37,8 @@ describe('maxAge option', function() { }, ].forEach((testCase) => { it(testCase.description, function (done) { - expect(jwt.verify(token, undefined, {maxAge: '3s'})).to.not.throw; - jwt.verify(token, undefined, {maxAge: testCase.maxAge}, (err) => { + expect(jwt.verify(token, 'secret', {maxAge: '3s', algorithm: 'HS256'})).to.not.throw; + jwt.verify(token, 'secret', {maxAge: testCase.maxAge, algorithm: 'HS256'}, (err) => { expect(err).to.be.null; done(); }) @@ -54,11 +54,11 @@ describe('maxAge option', function() { {foo: 'bar'}, ].forEach((maxAge) => { it(`should error with value ${util.inspect(maxAge)}`, function (done) { - expect(() => jwt.verify(token, undefined, {maxAge})).to.throw( + expect(() => jwt.verify(token, 'secret', {maxAge, algorithm: 'HS256'})).to.throw( jwt.JsonWebTokenError, '"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60' ); - jwt.verify(token, undefined, {maxAge}, (err) => { + jwt.verify(token, 'secret', {maxAge, algorithm: 'HS256'}, (err) => { expect(err).to.be.instanceOf(jwt.JsonWebTokenError); expect(err.message).to.equal( '"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60' diff --git a/test/option-nonce.test.js b/test/option-nonce.test.js index 841bdc29..410c36b7 100644 --- a/test/option-nonce.test.js +++ b/test/option-nonce.test.js @@ -9,7 +9,7 @@ describe('nonce option', function () { let token; beforeEach(function () { - token = jwt.sign({ nonce: 'abcde' }, undefined, { algorithm: 'none' }); + token = jwt.sign({ nonce: 'abcde' }, 'secret', { algorithm: 'HS256' }); }); [ { @@ -18,7 +18,7 @@ describe('nonce option', function () { }, ].forEach((testCase) => { it(testCase.description, function (done) { - testUtils.verifyJWTHelper(token, undefined, { nonce: testCase.nonce }, (err, decoded) => { + testUtils.verifyJWTHelper(token, 'secret', { nonce: testCase.nonce }, (err, decoded) => { testUtils.asyncCheck(done, () => { expect(err).to.be.null; expect(decoded).to.have.property('nonce', 'abcde'); @@ -46,7 +46,7 @@ describe('nonce option', function () { { foo: 'bar' }, ].forEach((nonce) => { it(`should error with value ${util.inspect(nonce)}`, function (done) { - testUtils.verifyJWTHelper(token, undefined, { nonce }, (err) => { + testUtils.verifyJWTHelper(token, 'secret', { nonce }, (err) => { testUtils.asyncCheck(done, () => { expect(err).to.be.instanceOf(jwt.JsonWebTokenError); expect(err).to.have.property('message', 'nonce must be a non-empty string') diff --git a/test/schema.tests.js b/test/schema.tests.js index 742d29e4..75a3b0f5 100644 --- a/test/schema.tests.js +++ b/test/schema.tests.js @@ -19,6 +19,7 @@ describe('schema', function() { expect(function () { sign({ algorithm: 'foo' }); }).to.throw(/"algorithm" must be a valid string enum value/); + sign({ algorithm: 'none' }); sign({algorithm: 'RS256'}); sign({algorithm: 'RS384'}); sign({algorithm: 'RS512'}); @@ -33,7 +34,6 @@ describe('schema', function() { sign({algorithm: 'HS256'}); sign({algorithm: 'HS384'}); sign({algorithm: 'HS512'}); - sign({algorithm: 'none'}); }); it('should validate header', function () { @@ -73,4 +73,4 @@ describe('schema', function() { }); -}); \ No newline at end of file +}); diff --git a/test/verify.tests.js b/test/verify.tests.js index 29bbe104..9ef24e45 100644 --- a/test/verify.tests.js +++ b/test/verify.tests.js @@ -30,39 +30,68 @@ describe('verify', function() { }); }); - it('should be able to validate unsigned token', function (done) { + it('should not be able to verify unsigned token', function () { var header = { alg: 'none' }; var payload = { iat: Math.floor(Date.now() / 1000 ) }; var signed = jws.sign({ header: header, payload: payload, - secret: priv, + secret: 'secret', encoding: 'utf8' }); - jwt.verify(signed, null, {typ: 'JWT'}, function(err, p) { - assert.isNull(err); - assert.deepEqual(p, payload); - done(); - }); + expect(function () { + jwt.verify(signed, 'secret', {typ: 'JWT'}); + }).to.throw(JsonWebTokenError, /jwt signature is required/); }); - it('should not mutate options', function (done) { + it('should not be able to verify unsigned token', function () { var header = { alg: 'none' }; - var payload = { iat: Math.floor(Date.now() / 1000 ) }; - var options = {typ: 'JWT'}; + var 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) { + var header = { alg: 'none' }; + var payload = { iat: Math.floor(Date.now() / 1000 ) }; var signed = jws.sign({ header: header, payload: payload, - secret: priv, + 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, null, options, function(err) { + jwt.verify(signed, 'secret', options, function(err) { assert.isNull(err); assert.deepEqual(Object.keys(options).length, 1); done(); diff --git a/verify.js b/verify.js index 8687eb59..95fa3650 100644 --- a/verify.js +++ b/verify.js @@ -107,7 +107,7 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { } if (!hasSignature && !options.algorithms) { - options.algorithms = ['none']; + return done(new JsonWebTokenError('please specify "none" in "algorithms" to verify unsigned tokens')); } if (!options.algorithms) { From ecdf6cc6073ea13a7e71df5fad043550f08d0fa6 Mon Sep 17 00:00:00 2001 From: david-renaud-okta <97122670+david-renaud-okta@users.noreply.github.com> Date: Tue, 29 Nov 2022 11:58:59 -0500 Subject: [PATCH 74/77] fix!: Prevent accidental use of insecure key sizes & misconfiguration of secrets (#852) * fix!: Disable use of weak RSA key sizes for asymmetric algorithms Added checks to prevent invalid secrets from being used with the HS*** algorithms when signing and verifying Added checks to prevent the use of insecure asymmetric key sizes except when explicitly overriden via options Prevented Buffers containing malicious objects from being used as key material. BREAKING CHANGE: Requires node 12.x or later to allow use of `KeyObject` --- .circleci/config.yml | 48 +++++-------------- README.md | 47 +++++++++++-------- package.json | 14 ++---- sign.js | 81 ++++++++++++++++++++++---------- test/async_sign.tests.js | 16 +++++++ test/jwt.hs.tests.js | 25 +++++++++- test/jwt.malicious.tests.js | 39 ++++++++++++++++ test/rsa-public-key.tests.js | 22 ++++++++- test/schema.tests.js | 22 ++++----- verify.js | 91 +++++++++++++++++++++++------------- 10 files changed, 267 insertions(+), 138 deletions(-) create mode 100644 test/jwt.malicious.tests.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 04670d73..51f2d617 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,55 +17,31 @@ commands: command: npm test jobs: - node-v4: + node-v12: docker: - - image: node:4 + - image: node:12 steps: - test-nodejs - node-v5: + node-v14: docker: - - image: node:5 + - image: node:14 steps: - test-nodejs - node-v6: + node-v16: docker: - - image: node:6 + - image: node:16 steps: - test-nodejs - node-v7: + node-v18: docker: - - image: node:7 - steps: - - test-nodejs - node-v8: - docker: - - image: node:8 - steps: - - test-nodejs - node-v9: - docker: - - image: node:9 - steps: - - test-nodejs - node-v10: - docker: - - image: node:10 - steps: - - test-nodejs - node-v11: - docker: - - image: node:11 + - image: node:18 steps: - test-nodejs workflows: node-multi-build: jobs: - - node-v4 - - node-v5 - - node-v6 - - node-v7 - - node-v8 - - node-v9 - - node-v10 - - node-v11 \ No newline at end of file + - node-v12 + - node-v14 + - node-v16 + - node-v18 diff --git a/README.md b/README.md index 834777e0..05109073 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # jsonwebtoken -| **Build** | **Dependency** | -|-----------|---------------| +| **Build** | **Dependency** | +|-----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| | [![Build Status](https://secure.travis-ci.org/auth0/node-jsonwebtoken.svg?branch=master)](http://travis-ci.org/auth0/node-jsonwebtoken) | [![Dependency Status](https://david-dm.org/auth0/node-jsonwebtoken.svg)](https://david-dm.org/auth0/node-jsonwebtoken) | @@ -32,8 +32,9 @@ $ npm install jsonwebtoken > If `payload` is not a buffer or a string, it will be coerced into a string using `JSON.stringify`. -`secretOrPrivateKey` is a string, buffer, or object containing either the secret for HMAC algorithms or the PEM +`secretOrPrivateKey` is a string (utf-8 encoded), buffer, object, or KeyObject containing either the secret for HMAC algorithms or the PEM encoded private key for RSA and ECDSA. In case of a private key with passphrase an object `{ key, passphrase }` can be used (based on [crypto documentation](https://nodejs.org/api/crypto.html#crypto_sign_sign_private_key_output_format)), in this case be sure you pass the `algorithm` option. +When signing with RSA algorithms the minimum modulus length is 2048 except when the allowInsecureKeySizes option is set to true. Private keys below this size will be rejected with an error. `options`: @@ -50,6 +51,7 @@ encoded private key for RSA and ECDSA. In case of a private key with passphrase * `header` * `keyid` * `mutatePayload`: if true, the sign function will modify the payload object directly. This is useful if you need a raw reference to the payload after claims have been applied to it but before it has been encoded into a token. +* `allowInsecureKeySizes`: if true allows private keys with a modulus below 2048 to be used for RSA @@ -129,7 +131,7 @@ jwt.sign({ `token` is the JsonWebToken string -`secretOrPublicKey` is a string or buffer containing either the secret for HMAC algorithms, or the PEM +`secretOrPublicKey` is a string (utf-8 encoded), buffer, or KeyObject containing either the secret for HMAC algorithms, or the PEM encoded public key for RSA and ECDSA. If `jwt.verify` is called asynchronous, `secretOrPublicKey` can be a function that should fetch the secret or public key. See below for a detailed example @@ -137,7 +139,12 @@ As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues `options` -* `algorithms`: List of strings with the names of the allowed algorithms. For instance, `["HS256", "HS384"]`. +* `algorithms`: List of strings with the names of the allowed algorithms. For instance, `["HS256", "HS384"]`. + > If not specified a defaults will be used based on the type of key provided + > * secret - ['HS256', 'HS384', 'HS512'] + > * rsa - ['RS256', 'RS384', 'RS512'] + > * ec - ['ES256', 'ES384', 'ES512'] + > * default - ['RS256', 'RS384', 'RS512'] * `audience`: if you want to check audience (`aud`), provide a value here. The audience can be checked against a string, a regular expression or a list of strings and/or regular expressions. > Eg: `"urn:foo"`, `/urn:f[o]{2}/`, `[/urn:f[o]{2}/, "urn:bar"]` * `complete`: return an object with the decoded `{ payload, header, signature }` instead of only the usual content of the payload. @@ -347,21 +354,21 @@ jwt.verify(token, 'shhhhh', function(err, decoded) { Array of supported algorithms. The following algorithms are currently supported. -alg Parameter Value | Digital Signature or MAC Algorithm -----------------|---------------------------- -HS256 | HMAC using SHA-256 hash algorithm -HS384 | HMAC using SHA-384 hash algorithm -HS512 | HMAC using SHA-512 hash algorithm -RS256 | RSASSA-PKCS1-v1_5 using SHA-256 hash algorithm -RS384 | RSASSA-PKCS1-v1_5 using SHA-384 hash algorithm -RS512 | RSASSA-PKCS1-v1_5 using SHA-512 hash algorithm -PS256 | RSASSA-PSS using SHA-256 hash algorithm (only node ^6.12.0 OR >=8.0.0) -PS384 | RSASSA-PSS using SHA-384 hash algorithm (only node ^6.12.0 OR >=8.0.0) -PS512 | RSASSA-PSS using SHA-512 hash algorithm (only node ^6.12.0 OR >=8.0.0) -ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm -ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm -ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm -none | No digital signature or MAC value included +| alg Parameter Value | Digital Signature or MAC Algorithm | +|---------------------|------------------------------------------------------------------------| +| HS256 | HMAC using SHA-256 hash algorithm | +| HS384 | HMAC using SHA-384 hash algorithm | +| HS512 | HMAC using SHA-512 hash algorithm | +| RS256 | RSASSA-PKCS1-v1_5 using SHA-256 hash algorithm | +| RS384 | RSASSA-PKCS1-v1_5 using SHA-384 hash algorithm | +| RS512 | RSASSA-PKCS1-v1_5 using SHA-512 hash algorithm | +| PS256 | RSASSA-PSS using SHA-256 hash algorithm (only node ^6.12.0 OR >=8.0.0) | +| PS384 | RSASSA-PSS using SHA-384 hash algorithm (only node ^6.12.0 OR >=8.0.0) | +| PS512 | RSASSA-PSS using SHA-512 hash algorithm (only node ^6.12.0 OR >=8.0.0) | +| ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm | +| ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm | +| ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm | +| none | No digital signature or MAC value included | ## Refreshing JWTs diff --git a/package.json b/package.json index 81d63da8..8e4345c1 100644 --- a/package.json +++ b/package.json @@ -37,15 +37,9 @@ }, "dependencies": { "jws": "^3.2.2", - "lodash.includes": "^4.3.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", + "lodash": "^4.17.21", "ms": "^2.1.1", - "semver": "^5.6.0" + "semver": "^7.3.8" }, "devDependencies": { "atob": "^2.1.2", @@ -59,8 +53,8 @@ "sinon": "^6.0.0" }, "engines": { - "npm": ">=1.4.28", - "node": ">=4" + "npm": ">=6", + "node": ">=12" }, "files": [ "lib", diff --git a/sign.js b/sign.js index f8a2877e..3da5119b 100644 --- a/sign.js +++ b/sign.js @@ -1,20 +1,15 @@ -var timespan = require('./lib/timespan'); -var PS_SUPPORTED = require('./lib/psSupported'); -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 SUPPORTED_ALGS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none']; +const timespan = require('./lib/timespan'); +const PS_SUPPORTED = require('./lib/psSupported'); +const jws = require('jws'); +const {includes, isBoolean, isInteger, isNumber, isPlainObject, isString, once} = require('lodash') +const { KeyObject, createSecretKey, createPrivateKey } = require('crypto') + +const SUPPORTED_ALGS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none']; if (PS_SUPPORTED) { SUPPORTED_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512'); } -var sign_options_schema = { +const 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' }, @@ -26,10 +21,11 @@ var sign_options_schema = { 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' } + mutatePayload: { isValid: isBoolean, message: '"mutatePayload" must be a boolean' }, + allowInsecureKeySizes: { isValid: isBoolean, message: '"allowInsecureKeySizes" must be a boolean'} }; -var registered_claims_schema = { +const 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' } @@ -41,7 +37,7 @@ function validate(schema, allowUnknown, object, parameterName) { } Object.keys(object) .forEach(function(key) { - var validator = schema[key]; + const validator = schema[key]; if (!validator) { if (!allowUnknown) { throw new Error('"' + key + '" is not allowed in "' + parameterName + '"'); @@ -62,14 +58,14 @@ function validatePayload(payload) { return validate(registered_claims_schema, true, payload, 'payload'); } -var options_to_payload = { +const options_to_payload = { 'audience': 'aud', 'issuer': 'iss', 'subject': 'sub', 'jwtid': 'jti' }; -var options_for_objects = [ +const options_for_objects = [ 'expiresIn', 'notBefore', 'noTimestamp', @@ -87,10 +83,10 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { options = options || {}; } - var isObjectPayload = typeof payload === 'object' && + const isObjectPayload = typeof payload === 'object' && !Buffer.isBuffer(payload); - var header = Object.assign({ + const header = Object.assign({ alg: options.algorithm || 'HS256', typ: isObjectPayload ? 'JWT' : undefined, kid: options.keyid @@ -107,6 +103,32 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { return failure(new Error('secretOrPrivateKey must have a value')); } + if (secretOrPrivateKey != null && !(secretOrPrivateKey instanceof KeyObject)) { + try { + secretOrPrivateKey = createPrivateKey(secretOrPrivateKey) + } catch (_) { + try { + secretOrPrivateKey = createSecretKey(typeof secretOrPrivateKey === 'string' ? Buffer.from(secretOrPrivateKey) : secretOrPrivateKey) + } catch (_) { + return failure(new Error('secretOrPrivateKey is not valid key material')); + } + } + } + + if (header.alg.startsWith('HS') && secretOrPrivateKey.type !== 'secret') { + return failure(new Error((`secretOrPrivateKey must be a symmetric key when using ${header.alg}`))) + } else if (/^(?:RS|PS|ES)/.test(header.alg)) { + if (secretOrPrivateKey.type !== 'private') { + return failure(new Error((`secretOrPrivateKey must be an asymmetric key when using ${header.alg}`))) + } + if (!options.allowInsecureKeySizes && + !header.alg.startsWith('ES') && + secretOrPrivateKey.asymmetricKeyDetails !== undefined && //KeyObject.asymmetricKeyDetails is supported in Node 15+ + secretOrPrivateKey.asymmetricKeyDetails.modulusLength < 2048) { + return failure(new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`)); + } + } + if (typeof payload === 'undefined') { return failure(new Error('payload is required')); } else if (isObjectPayload) { @@ -120,7 +142,7 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { payload = Object.assign({},payload); } } else { - var invalid_options = options_for_objects.filter(function (opt) { + const invalid_options = options_for_objects.filter(function (opt) { return typeof options[opt] !== 'undefined'; }); @@ -144,7 +166,7 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { return failure(error); } - var timestamp = payload.iat || Math.floor(Date.now() / 1000); + const timestamp = payload.iat || Math.floor(Date.now() / 1000); if (options.noTimestamp) { delete payload.iat; @@ -177,7 +199,7 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { } Object.keys(options_to_payload).forEach(function (key) { - var claim = options_to_payload[key]; + const 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.')); @@ -186,7 +208,7 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { } }); - var encoding = options.encoding || 'utf8'; + const encoding = options.encoding || 'utf8'; if (typeof callback === 'function') { callback = callback && once(callback); @@ -198,9 +220,18 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { encoding: encoding }).once('error', callback) .once('done', function (signature) { + // TODO: Remove in favor of the modulus length check before signing once node 15+ is the minimum supported version + if(!options.allowInsecureKeySizes && /^(?:RS|PS)/.test(header.alg) && signature.length < 256) { + return callback(new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`)) + } callback(null, signature); }); } else { - return jws.sign({header: header, payload: payload, secret: secretOrPrivateKey, encoding: encoding}); + let signature = jws.sign({header: header, payload: payload, secret: secretOrPrivateKey, encoding: encoding}); + // TODO: Remove in favor of the modulus length check before signing once node 15+ is the minimum supported version + if(!options.allowInsecureKeySizes && /^(?:RS|PS)/.test(header.alg) && signature.length < 256) { + throw new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`) + } + return signature } }; diff --git a/test/async_sign.tests.js b/test/async_sign.tests.js index 6eb7dc76..eb31174e 100644 --- a/test/async_sign.tests.js +++ b/test/async_sign.tests.js @@ -2,6 +2,7 @@ var jwt = require('../index'); var expect = require('chai').expect; var jws = require('jws'); var PS_SUPPORTED = require('../lib/psSupported'); +const {generateKeyPairSync} = require("crypto"); describe('signing a token asynchronously', function() { @@ -59,6 +60,21 @@ describe('signing a token asynchronously', function() { }); }); + it('should not work for RS algorithms when modulus length is less than 2048 when allowInsecureKeySizes is false or not set', function(done) { + const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 1024 }); + + jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' }, function (err) { + expect(err).to.be.ok; + done(); + }); + }); + + it('should work for RS algorithms when modulus length is less than 2048 when allowInsecureKeySizes is true', function(done) { + const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 1024 }); + + jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256', allowInsecureKeySizes: true }, done); + }); + if (PS_SUPPORTED) { it('should return error when secret is not a cert for PS256', function(done) { //this throw an error because the secret is not a cert and PS256 requires a cert. diff --git a/test/jwt.hs.tests.js b/test/jwt.hs.tests.js index a7741ff1..1f5ec2fa 100644 --- a/test/jwt.hs.tests.js +++ b/test/jwt.hs.tests.js @@ -3,10 +3,33 @@ const jwt = require('../index'); const jws = require('jws'); const expect = require('chai').expect; const assert = require('chai').assert; +const { generateKeyPairSync } = require('crypto') describe('HS256', function() { - describe('when signing a token', function() { + describe("when signing using HS256", function () { + it('should throw if the secret is an asymmetric key', function () { + const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); + + expect(function () { + jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'HS256' }) + }).to.throw(Error, 'must be a symmetric key') + }) + + it('should throw if the payload is undefined', function () { + expect(function () { + jwt.sign(undefined, "secret", { algorithm: 'HS256' }) + }).to.throw(Error, 'payload is required') + }) + + it('should throw if options is not a plain object', function () { + expect(function () { + jwt.sign({ foo: 'bar' }, "secret", ['HS256']) + }).to.throw(Error, 'Expected "options" to be a plain object') + }) + }) + + describe('with a token signed using HS256', function() { var secret = 'shhhhhh'; var token = jwt.sign({ foo: 'bar' }, secret, { algorithm: 'HS256' }); diff --git a/test/jwt.malicious.tests.js b/test/jwt.malicious.tests.js new file mode 100644 index 00000000..d26ef415 --- /dev/null +++ b/test/jwt.malicious.tests.js @@ -0,0 +1,39 @@ +const jwt = require('../index'); +const crypto = require("crypto"); +const {expect} = require('chai'); +const JsonWebTokenError = require("../lib/JsonWebTokenError"); + +describe('when verifying a malicious token', function () { + // attacker has access to the public rsa key, but crafts the token as HS256 + // with kid set to the id of the rsa key, instead of the id of the hmac secret. + // const maliciousToken = jwt.sign( + // {foo: 'bar'}, + // pubRsaKey, + // {algorithm: 'HS256', keyid: 'rsaKeyId'} + // ); + // consumer accepts self signed tokens (HS256) and third party tokens (RS256) + const options = {algorithms: ['RS256', 'HS256']}; + + const { + publicKey: pubRsaKey + } = crypto.generateKeyPairSync('rsa', {modulusLength: 2048}); + + it('should not allow HMAC verification with an RSA key in KeyObject format', function () { + const maliciousToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InJzYUtleUlkIn0.eyJmb28iOiJiYXIiLCJpYXQiOjE2NTk1MTA2MDh9.cOcHI1TXPbxTMlyVTfjArSWskrmezbrG8iR7uJHwtrQ'; + + expect(() => jwt.verify(maliciousToken, pubRsaKey, options)).to.throw(JsonWebTokenError, 'must be a symmetric key'); + }) + + it('should not allow HMAC verification with an RSA key in PEM format', function () { + const maliciousToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InJzYUtleUlkIn0.eyJmb28iOiJiYXIiLCJpYXQiOjE2NTk1MTA2MDh9.cOcHI1TXPbxTMlyVTfjArSWskrmezbrG8iR7uJHwtrQ'; + + expect(() => jwt.verify(maliciousToken, pubRsaKey.export({type: 'spki', format: 'pem'}), options)).to.throw(JsonWebTokenError, 'must be a symmetric key'); + }) + + it('should not allow arbitrary execution from malicious Buffers containing objects with overridden toString functions', function () { + const token = jwt.sign({"foo": "bar"}, 'secret') + const maliciousBuffer = {toString: () => {throw new Error("Arbitrary Code Execution")}} + + expect(() => jwt.verify(token, maliciousBuffer)).to.throw(Error, 'not valid key material'); + }) +}) diff --git a/test/rsa-public-key.tests.js b/test/rsa-public-key.tests.js index 6abafb84..a5fdb769 100644 --- a/test/rsa-public-key.tests.js +++ b/test/rsa-public-key.tests.js @@ -1,5 +1,7 @@ -var jwt = require('../'); -var PS_SUPPORTED = require('../lib/psSupported'); +const jwt = require('../'); +const PS_SUPPORTED = require('../lib/psSupported'); +const expect = require('chai').expect; +const {generateKeyPairSync} = require('crypto') describe('public key start with BEGIN RSA PUBLIC KEY', function () { @@ -13,6 +15,22 @@ describe('public key start with BEGIN RSA PUBLIC KEY', function () { jwt.verify(token, cert_pub, done); }); + it('should not work for RS algorithms when modulus length is less than 2048 when allowInsecureKeySizes is false or not set', function (done) { + const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 1024 }); + + expect(function() { + jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256'}) + }).to.throw(Error, 'minimum key size'); + + done() + }); + + it('should work for RS algorithms when modulus length is less than 2048 when allowInsecureKeySizes is true', function (done) { + const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 1024 }); + + jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256', allowInsecureKeySizes: true}, done) + }); + if (PS_SUPPORTED) { it('should work for PS family of algorithms', function (done) { var fs = require('fs'); diff --git a/test/schema.tests.js b/test/schema.tests.js index 75a3b0f5..0a648f12 100644 --- a/test/schema.tests.js +++ b/test/schema.tests.js @@ -10,9 +10,9 @@ describe('schema', function() { var cert_rsa_priv = fs.readFileSync(__dirname + '/rsa-private.pem'); var cert_ecdsa_priv = fs.readFileSync(__dirname + '/ecdsa-private.pem'); - function sign(options) { + function sign(options, secret) { var isEcdsa = options.algorithm && options.algorithm.indexOf('ES') === 0; - jwt.sign({foo: 123}, isEcdsa ? cert_ecdsa_priv : cert_rsa_priv, options); + jwt.sign({foo: 123}, secret || (isEcdsa ? cert_ecdsa_priv : cert_rsa_priv), options); } it('should validate algorithm', function () { @@ -31,30 +31,30 @@ describe('schema', function() { sign({algorithm: 'ES256'}); sign({algorithm: 'ES384'}); sign({algorithm: 'ES512'}); - sign({algorithm: 'HS256'}); - sign({algorithm: 'HS384'}); - sign({algorithm: 'HS512'}); + sign({algorithm: 'HS256'}, 'superSecret'); + sign({algorithm: 'HS384'}, 'superSecret'); + sign({algorithm: 'HS512'}, 'superSecret'); }); it('should validate header', function () { expect(function () { - sign({ header: 'foo' }); + sign({ header: 'foo' }, 'superSecret'); }).to.throw(/"header" must be an object/); - sign({header: {}}); + sign({header: {}}, 'superSecret'); }); it('should validate encoding', function () { expect(function () { - sign({ encoding: 10 }); + sign({ encoding: 10 }, 'superSecret'); }).to.throw(/"encoding" must be a string/); - sign({encoding: 'utf8'}); + sign({encoding: 'utf8'},'superSecret'); }); it('should validate noTimestamp', function () { expect(function () { - sign({ noTimestamp: 10 }); + sign({ noTimestamp: 10 }, 'superSecret'); }).to.throw(/"noTimestamp" must be a boolean/); - sign({noTimestamp: true}); + sign({noTimestamp: true}, 'superSecret'); }); }); diff --git a/verify.js b/verify.js index 95fa3650..0b649db4 100644 --- a/verify.js +++ b/verify.js @@ -1,18 +1,20 @@ -var JsonWebTokenError = require('./lib/JsonWebTokenError'); -var NotBeforeError = require('./lib/NotBeforeError'); -var TokenExpiredError = require('./lib/TokenExpiredError'); -var decode = require('./decode'); -var timespan = require('./lib/timespan'); -var PS_SUPPORTED = require('./lib/psSupported'); -var jws = require('jws'); - -var PUB_KEY_ALGS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512']; -var RSA_KEY_ALGS = ['RS256', 'RS384', 'RS512']; -var HS_ALGS = ['HS256', 'HS384', 'HS512']; +const JsonWebTokenError = require('./lib/JsonWebTokenError'); +const NotBeforeError = require('./lib/NotBeforeError'); +const TokenExpiredError = require('./lib/TokenExpiredError'); +const decode = require('./decode'); +const timespan = require('./lib/timespan'); +const PS_SUPPORTED = require('./lib/psSupported'); +const jws = require('jws'); +const {KeyObject, createSecretKey, createPublicKey} = require("crypto"); + +const PUB_KEY_ALGS = ['RS256', 'RS384', 'RS512']; +const EC_KEY_ALGS = ['ES256', 'ES384', 'ES512']; +const RSA_KEY_ALGS = ['RS256', 'RS384', 'RS512']; +const HS_ALGS = ['HS256', 'HS384', 'HS512']; if (PS_SUPPORTED) { - PUB_KEY_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512'); - RSA_KEY_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512'); + PUB_KEY_ALGS.splice(PUB_KEY_ALGS.length, 0, 'PS256', 'PS384', 'PS512'); + RSA_KEY_ALGS.splice(RSA_KEY_ALGS.length, 0, 'PS256', 'PS384', 'PS512'); } module.exports = function (jwtString, secretOrPublicKey, options, callback) { @@ -28,7 +30,7 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { //clone this object since we are going to mutate it. options = Object.assign({}, options); - var done; + let done; if (callback) { done = callback; @@ -47,7 +49,7 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError('nonce must be a non-empty string')); } - var clockTimestamp = options.clockTimestamp || Math.floor(Date.now() / 1000); + const clockTimestamp = options.clockTimestamp || Math.floor(Date.now() / 1000); if (!jwtString){ return done(new JsonWebTokenError('jwt must be provided')); @@ -57,13 +59,13 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError('jwt must be a string')); } - var parts = jwtString.split('.'); + const parts = jwtString.split('.'); if (parts.length !== 3){ return done(new JsonWebTokenError('jwt malformed')); } - var decodedToken; + let decodedToken; try { decodedToken = decode(jwtString, { complete: true }); @@ -75,8 +77,8 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError('invalid token')); } - var header = decodedToken.header; - var getSecret; + const header = decodedToken.header; + let getSecret; if(typeof secretOrPublicKey === 'function') { if(!callback) { @@ -96,7 +98,7 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError('error in secret or public key callback: ' + err.message)); } - var hasSignature = parts[2].trim() !== ''; + const hasSignature = parts[2].trim() !== ''; if (!hasSignature && secretOrPublicKey){ return done(new JsonWebTokenError('jwt signature is required')); @@ -110,18 +112,41 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError('please specify "none" in "algorithms" to verify unsigned tokens')); } - if (!options.algorithms) { - options.algorithms = secretOrPublicKey.toString().includes('BEGIN CERTIFICATE') || - secretOrPublicKey.toString().includes('BEGIN PUBLIC KEY') ? PUB_KEY_ALGS : - secretOrPublicKey.toString().includes('BEGIN RSA PUBLIC KEY') ? RSA_KEY_ALGS : HS_ALGS; + if (secretOrPublicKey != null && !(secretOrPublicKey instanceof KeyObject)) { + try { + secretOrPublicKey = createPublicKey(secretOrPublicKey); + } catch (_) { + try { + secretOrPublicKey = createSecretKey(typeof secretOrPublicKey === 'string' ? Buffer.from(secretOrPublicKey) : secretOrPublicKey); + } catch (_) { + return done(new JsonWebTokenError('secretOrPublicKey is not valid key material')) + } + } + } + if (!options.algorithms) { + if (secretOrPublicKey.type === 'secret') { + options.algorithms = HS_ALGS; + } else if (['rsa', 'rsa-pss'].includes(secretOrPublicKey.asymmetricKeyType)) { + options.algorithms = RSA_KEY_ALGS + } else if (secretOrPublicKey.asymmetricKeyType === 'ec') { + options.algorithms = EC_KEY_ALGS + } else { + options.algorithms = PUB_KEY_ALGS + } } - if (!~options.algorithms.indexOf(decodedToken.header.alg)) { + if (options.algorithms.indexOf(decodedToken.header.alg) === -1) { return done(new JsonWebTokenError('invalid algorithm')); } - var valid; + if (header.alg.startsWith('HS') && secretOrPublicKey.type !== 'secret') { + return done(new JsonWebTokenError((`secretOrPublicKey must be a symmetric key when using ${header.alg}`))) + } else if (/^(?:RS|PS|ES)/.test(header.alg) && secretOrPublicKey.type !== 'public') { + return done(new JsonWebTokenError((`secretOrPublicKey must be an asymmetric key when using ${header.alg}`))) + } + + let valid; try { valid = jws.verify(jwtString, decodedToken.header.alg, secretOrPublicKey); @@ -133,7 +158,7 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError('invalid signature')); } - var payload = decodedToken.payload; + const payload = decodedToken.payload; if (typeof payload.nbf !== 'undefined' && !options.ignoreNotBefore) { if (typeof payload.nbf !== 'number') { @@ -154,10 +179,10 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { } if (options.audience) { - var audiences = Array.isArray(options.audience) ? options.audience : [options.audience]; - var target = Array.isArray(payload.aud) ? payload.aud : [payload.aud]; + const audiences = Array.isArray(options.audience) ? options.audience : [options.audience]; + const target = Array.isArray(payload.aud) ? payload.aud : [payload.aud]; - var match = target.some(function (targetAudience) { + const match = target.some(function (targetAudience) { return audiences.some(function (audience) { return audience instanceof RegExp ? audience.test(targetAudience) : audience === targetAudience; }); @@ -169,7 +194,7 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { } if (options.issuer) { - var invalid_issuer = + const invalid_issuer = (typeof options.issuer === 'string' && payload.iss !== options.issuer) || (Array.isArray(options.issuer) && options.issuer.indexOf(payload.iss) === -1); @@ -201,7 +226,7 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError('iat required when maxAge is specified')); } - var maxAgeTimestamp = timespan(options.maxAge, payload.iat); + const maxAgeTimestamp = timespan(options.maxAge, payload.iat); if (typeof maxAgeTimestamp === 'undefined') { return done(new JsonWebTokenError('"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60')); } @@ -211,7 +236,7 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { } if (options.complete === true) { - var signature = decodedToken.signature; + const signature = decodedToken.signature; return done(null, { header: header, From cd4163eb1407aab0b3148f91b0b9c26276b96c6b Mon Sep 17 00:00:00 2001 From: Jake Lacey Date: Fri, 2 Dec 2022 09:15:31 +0000 Subject: [PATCH 75/77] chore(ci): configure Github Actions jobs for Tests & Security Scanning (#856) * chore: add github actions test job * chore: configure semgrep ci job --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++++ .github/workflows/semgrep.yml | 18 ++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/semgrep.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..67a8de5e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: Tests + +on: push + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [12.x, 14.x, 15.x, 16.x, 18.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + + - name: Test execution + run: npm test diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 00000000..1ba433de --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,18 @@ +name: Semgrep + +on: + pull_request_target: {} + push: + branches: ["master", "main"] +jobs: + semgrep: + name: Scan + runs-on: ubuntu-latest + container: + image: returntocorp/semgrep + if: (github.actor != 'dependabot[bot]' && github.actor != 'snyk-bot') + steps: + - uses: actions/checkout@v3 + - run: semgrep ci + env: + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} From 5eaedbf2b01676d952336e73b4d2efba847d2d1b Mon Sep 17 00:00:00 2001 From: Jake Lacey Date: Mon, 12 Dec 2022 10:54:12 +0000 Subject: [PATCH 76/77] chore(ci): remove github test actions job (#861) --- .github/workflows/ci.yml | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 67a8de5e..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Tests - -on: push - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [12.x, 14.x, 15.x, 16.x, 18.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - - steps: - - uses: actions/checkout@v2 - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - - name: Install dependencies - run: npm install - - - name: Test execution - run: npm test From e1fa9dcc12054a8681db4e6373da1b30cf7016e3 Mon Sep 17 00:00:00 2001 From: Edgar Chirivella <115034055+edgarchirivella-okta@users.noreply.github.com> Date: Wed, 21 Dec 2022 13:36:01 +0100 Subject: [PATCH 77/77] 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 --- CHANGELOG.md | 18 +++ README.md | 4 +- lib/asymmetricKeyDetailsSupported.js | 3 + lib/rsaPssKeyDetailsSupported.js | 3 + lib/validateAsymmetricKey.js | 66 +++++++++ package.json | 2 +- sign.js | 12 +- test/dsa-private.pem | 36 +++++ test/dsa-public.pem | 36 +++++ test/jwt.asymmetric_signing.tests.js | 89 ++++++++---- test/prime256v1-private.pem | 5 + test/rsa-pss-invalid-salt-length-private.pem | 29 ++++ test/rsa-pss-private.pem | 29 ++++ test/schema.tests.js | 30 ++-- test/secp384r1-private.pem | 6 + test/secp521r1-private.pem | 7 + test/validateAsymmetricKey.tests.js | 142 +++++++++++++++++++ test/verify.tests.js | 117 +++++++++------ verify.js | 13 ++ 19 files changed, 557 insertions(+), 90 deletions(-) create mode 100644 lib/asymmetricKeyDetailsSupported.js create mode 100644 lib/rsaPssKeyDetailsSupported.js create mode 100644 lib/validateAsymmetricKey.js create mode 100644 test/dsa-private.pem create mode 100644 test/dsa-public.pem create mode 100644 test/prime256v1-private.pem create mode 100644 test/rsa-pss-invalid-salt-length-private.pem create mode 100644 test/rsa-pss-private.pem create mode 100644 test/secp384r1-private.pem create mode 100644 test/secp521r1-private.pem create mode 100644 test/validateAsymmetricKey.tests.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 54364a2f..572d767d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file starting from version **v4.0.0**. This project adheres to [Semantic Versioning](http://semver.org/). +## 9.0.0 - 2022-12-21 + + **Breaking changes: See [Migration from v8 to v9](https://github.com/auth0/node-jsonwebtoken/wiki/Migration-Notes:-v8-to-v9)** + +### Breaking changes + +- Removed support for Node versions 11 and below. +- The verify() function no longer accepts unsigned tokens by default. ([834503079514b72264fd13023a3b8d648afd6a16]https://github.com/auth0/node-jsonwebtoken/commit/834503079514b72264fd13023a3b8d648afd6a16) +- RSA key size must be 2048 bits or greater. ([ecdf6cc6073ea13a7e71df5fad043550f08d0fa6]https://github.com/auth0/node-jsonwebtoken/commit/ecdf6cc6073ea13a7e71df5fad043550f08d0fa6) +- Key types must be valid for the signing / verification algorithm + +### Security fixes + +- security: fixes `Arbitrary File Write via verify function` - CVE-2022-23529 +- security: fixes `Insecure default algorithm in jwt.verify() could lead to signature validation bypass` - CVE-2022-23540 +- security: fixes `Insecure implementation of key retrieval function could lead to Forgeable Public/Private Tokens from RSA to HMAC` - CVE-2022-23541 +- security: fixes `Unrestricted key type could lead to legacy keys usage` - CVE-2022-23539 + ## 8.5.1 - 2019-03-18 ### Bug fix diff --git a/README.md b/README.md index 05109073..4e20dd9c 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ $ npm install jsonwebtoken # Migration notes +* [From v8 to v9](https://github.com/auth0/node-jsonwebtoken/wiki/Migration-Notes:-v8-to-v9) * [From v7 to v8](https://github.com/auth0/node-jsonwebtoken/wiki/Migration-Notes:-v7-to-v8) # Usage @@ -52,6 +53,7 @@ When signing with RSA algorithms the minimum modulus length is 2048 except when * `keyid` * `mutatePayload`: if true, the sign function will modify the payload object directly. This is useful if you need a raw reference to the payload after claims have been applied to it but before it has been encoded into a token. * `allowInsecureKeySizes`: if true allows private keys with a modulus below 2048 to be used for RSA +* `allowInvalidAsymmetricKeyTypes`: if true, allows asymmetric keys which do not match the specified algorithm. This option is intended only for backwards compatability and should be avoided. @@ -158,7 +160,7 @@ As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues > Eg: `1000`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`). * `clockTimestamp`: the time in seconds that should be used as the current time for all necessary comparisons. * `nonce`: if you want to check `nonce` claim, provide a string value here. It is used on Open ID for the ID Tokens. ([Open ID implementation notes](https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes)) - +* `allowInvalidAsymmetricKeyTypes`: if true, allows asymmetric keys which do not match the specified algorithm. This option is intended only for backwards compatability and should be avoided. ```js // verify a token symmetric - synchronous diff --git a/lib/asymmetricKeyDetailsSupported.js b/lib/asymmetricKeyDetailsSupported.js new file mode 100644 index 00000000..a6ede56e --- /dev/null +++ b/lib/asymmetricKeyDetailsSupported.js @@ -0,0 +1,3 @@ +const semver = require('semver'); + +module.exports = semver.satisfies(process.version, '>=15.7.0'); diff --git a/lib/rsaPssKeyDetailsSupported.js b/lib/rsaPssKeyDetailsSupported.js new file mode 100644 index 00000000..7fcf3684 --- /dev/null +++ b/lib/rsaPssKeyDetailsSupported.js @@ -0,0 +1,3 @@ +const semver = require('semver'); + +module.exports = semver.satisfies(process.version, '>=16.9.0'); diff --git a/lib/validateAsymmetricKey.js b/lib/validateAsymmetricKey.js new file mode 100644 index 00000000..c10340b0 --- /dev/null +++ b/lib/validateAsymmetricKey.js @@ -0,0 +1,66 @@ +const ASYMMETRIC_KEY_DETAILS_SUPPORTED = require('./asymmetricKeyDetailsSupported'); +const RSA_PSS_KEY_DETAILS_SUPPORTED = require('./rsaPssKeyDetailsSupported'); + +const allowedAlgorithmsForKeys = { + 'ec': ['ES256', 'ES384', 'ES512'], + 'rsa': ['RS256', 'PS256', 'RS384', 'PS384', 'RS512', 'PS512'], + 'rsa-pss': ['PS256', 'PS384', 'PS512'] +}; + +const allowedCurves = { + ES256: 'prime256v1', + ES384: 'secp384r1', + ES512: 'secp521r1', +}; + +module.exports = function(algorithm, key) { + if (!algorithm || !key) return; + + const keyType = key.asymmetricKeyType; + if (!keyType) return; + + const allowedAlgorithms = allowedAlgorithmsForKeys[keyType]; + + if (!allowedAlgorithms) { + throw new Error(`Unknown key type "${keyType}".`); + } + + if (!allowedAlgorithms.includes(algorithm)) { + throw new Error(`"alg" parameter for "${keyType}" key type must be one of: ${allowedAlgorithms.join(', ')}.`) + } + + /* + * Ignore the next block from test coverage because it gets executed + * conditionally depending on the Node version. Not ignoring it would + * prevent us from reaching the target % of coverage for versions of + * Node under 15.7.0. + */ + /* istanbul ignore next */ + if (ASYMMETRIC_KEY_DETAILS_SUPPORTED) { + switch (keyType) { + case 'ec': + const keyCurve = key.asymmetricKeyDetails.namedCurve; + const allowedCurve = allowedCurves[algorithm]; + + if (keyCurve !== allowedCurve) { + throw new Error(`"alg" parameter "${algorithm}" requires curve "${allowedCurve}".`); + } + break; + + case 'rsa-pss': + if (RSA_PSS_KEY_DETAILS_SUPPORTED) { + const length = parseInt(algorithm.slice(-3), 10); + const { hashAlgorithm, mgf1HashAlgorithm, saltLength } = key.asymmetricKeyDetails; + + if (hashAlgorithm !== `sha${length}` || mgf1HashAlgorithm !== hashAlgorithm) { + throw new Error(`Invalid key for this operation, its RSA-PSS parameters do not meet the requirements of "alg" ${algorithm}.`); + } + + if (saltLength !== undefined && saltLength > length >> 3) { + throw new Error(`Invalid key for this operation, its RSA-PSS parameter saltLength does not meet the requirements of "alg" ${algorithm}.`) + } + } + break; + } + } +} diff --git a/package.json b/package.json index 8e4345c1..4f1e4e91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonwebtoken", - "version": "8.5.1", + "version": "9.0.0", "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", "nyc": { diff --git a/sign.js b/sign.js index 3da5119b..1aeeabc2 100644 --- a/sign.js +++ b/sign.js @@ -1,5 +1,6 @@ const timespan = require('./lib/timespan'); const PS_SUPPORTED = require('./lib/psSupported'); +const validateAsymmetricKey = require('./lib/validateAsymmetricKey'); const jws = require('jws'); const {includes, isBoolean, isInteger, isNumber, isPlainObject, isString, once} = require('lodash') const { KeyObject, createSecretKey, createPrivateKey } = require('crypto') @@ -22,7 +23,8 @@ const sign_options_schema = { 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' }, - allowInsecureKeySizes: { isValid: isBoolean, message: '"allowInsecureKeySizes" must be a boolean'} + allowInsecureKeySizes: { isValid: isBoolean, message: '"allowInsecureKeySizes" must be a boolean'}, + allowInvalidAsymmetricKeyTypes: { isValid: isBoolean, message: '"allowInvalidAsymmetricKeyTypes" must be a boolean'} }; const registered_claims_schema = { @@ -166,6 +168,14 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { return failure(error); } + if (!options.allowInvalidAsymmetricKeyTypes) { + try { + validateAsymmetricKey(header.alg, secretOrPrivateKey); + } catch (error) { + return failure(error); + } + } + const timestamp = payload.iat || Math.floor(Date.now() / 1000); if (options.noTimestamp) { diff --git a/test/dsa-private.pem b/test/dsa-private.pem new file mode 100644 index 00000000..e73003a1 --- /dev/null +++ b/test/dsa-private.pem @@ -0,0 +1,36 @@ +-----BEGIN DSA PRIVATE KEY----- +MIIGWAIBAAKCAgEArzbPbt//BQpsYsnoZR4R9nXgcuvcXoH8WZjRsb4ZPfVJGchG +7CfRMlG0HR34vcUpehNj5pAavErhfNnk1CEal0TyDsOkBY/+JG239zXgRzMYjSE6 +ptX5kj5pGv0uXVoozSP/JZblI8/Spd6TZkblLNAYOl3ssfcUGN4NFDXlzmiWvP+q +6ZUgE8tD7CSryicICKmXcVQIa6AG8ultYa6mBAaewzMbiIt2TUo9smglpEqGeHoL +CuLb3e7zLf0AhWDZOgTTfe1KFEiK6TXMe9HWYeP3MPuyKhS20GmT/Zcu5VN4wbr0 +bP+mTWk700oLJ0OPQ6YgGkyqBmh/Bsi/TqnpJWS/mjRbJEe3E2NmNMwmP4jwJ79V +JClp5Gg9kbM6hPkmGNnhbbFzn3kwY3pi9/AiqpGyr3GUPhXvP7fYwAu/A5ISKw8r +87j/EJntyIzm51fcm8Q0mq1IDt4tNkIOwJEIc45h9r7ZC1VAKkzlCa7XT04GguFo +JMaJBYESYcOAmbKRojo8P/cN4fPuemuhQFQplkFIM6FtG9cJMo2ayp6ukH9Up8tn +8j7YgE/m9BL9SnUIbNlti9j0cNgeKVn24WC38hw9D8M0/sR5gYyclWh/OotCttoQ +I8ySZzSvB4GARZHbexagvg1EdV93ctYyAWGLkpJYAzuiXbt7FayG7e2ifYkCIQDp +IldsAFGVaiJRQdiKsWdReOSjzH6h8cw6Co3OCISiOQKCAgEAnSU29U65jK3W2BiA +fKTlTBx2yDUCDFeqnla5arZ2njGsUKiP2nocArAPLQggwk9rfqufybQltM8+zjmE +zeb4mUCVhSbTH7BvP903U0YEabZJCHLx80nTywq2RgQs0Qmn43vs2U5EidYR0xj8 +CCNAH5gdzd9/CL1RYACHAf7zj4n68ZaNkAy9Jz1JjYXjP6IAxJh1W/Y0vsdFdIJ/ +dnuxsyMCUCSwDvSNApSfATO/tw+DCVpGgKo4qE8b8lsfXKeihuMzyXuSe/D98YN2 +UFWRTQ6gFxGrntg3LOn41RXSkXxzixgl7quacIJzm8jrFkDJSx4AZ8rgt/9JbThA +XF9PVlCVv7GL1NztUs4cDK+zsJld4O1rlI3QOz5DWq9oA+Hj1MN3L9IW3Iv2Offo +AaubXJhuv0xPWYmtCo06mPgSwkWPjDnGCbp1vuI8zPTsfyhsahuKeW0h8JttW4GB +6CTtC1AVWA1pJug5pBo36S5G24ihRsdG3Q5/aTlnke7t7H1Tkh2KuvV9hD5a5Xtw +cnuiEcKjyR0FWR81RdsAKh+7QNI3Lx75c95i22Aupon5R/Qkb05VzHdd299bb78c +x5mW8Dsg4tKLF7kpDAcWmx7JpkPHQ+5V9N766sfZ+z/PiVWfNAK8gzJRn/ceLQcK +C6uOhcZgN0o4UYrmYEy9icxJ44wCggIBAIu+yagyVMS+C5OqOprmtteh/+MyaYI+ +Q3oPXFR8eHLJftsBWev1kRfje1fdxzzx/k4SQMRbxxbMtGV74KNwRUzEWOkoyAHP +AAjhMio1mxknPwAxRjWDOSE0drGJPyGpI9ZfpMUtvekQO7MCGqa45vPldY10RwZC +VN66AIpxSF0MG1OEmgD+noHMI7moclw/nw+ZUPaIFxvPstlD4EsPDkdE0I6x3k3b +UXlWAYAJFR6fNf8+Ki3xnjLjW9da3cU/p2H7+LrFDP+kPUGJpqr4bG606GUcV3Cl +dznoqlgaudWgcQCQx0NPzi7k5O7PXr7C3UU0cg+5+GkviIzogaioxidvvchnG+UU +0y5nVuji6G69j5sUhlcFXte31Nte2VUb6P8umo+mbDT0UkZZZzoOsCpw+cJ8OHOV +emFIhVphNHqQt20Tq6WVRBx+p4+YNWiThvmLtmLh0QghdnUrJZxyXx7/p8K5SE9/ ++qU11t5dUvYS+53U1gJ2kgIFO4Zt6gaoOyexTt5f4Ganh9IcJ01wegl5WT58aDtf +hmw0HnOrgbWt4lRkxOra281hL74xcgtgMZQ32PTOy8wTEVTk03mmqlIq/dV4jgBc +Nh1FGQwGEeGlfbuNSB4nqgMN6zn1PmI7oCWLD9XLR6VZTebF7pGfpHtYczyivuxf +e1YOro6e0mUqAiEAx4K3cPG3dxH91uU3L+sS2vzqXEVn2BmSMmkGczSOgn4= +-----END DSA PRIVATE KEY----- diff --git a/test/dsa-public.pem b/test/dsa-public.pem new file mode 100644 index 00000000..659d96b7 --- /dev/null +++ b/test/dsa-public.pem @@ -0,0 +1,36 @@ +-----BEGIN PUBLIC KEY----- +MIIGSDCCBDoGByqGSM44BAEwggQtAoICAQCvNs9u3/8FCmxiyehlHhH2deBy69xe +gfxZmNGxvhk99UkZyEbsJ9EyUbQdHfi9xSl6E2PmkBq8SuF82eTUIRqXRPIOw6QF +j/4kbbf3NeBHMxiNITqm1fmSPmka/S5dWijNI/8lluUjz9Kl3pNmRuUs0Bg6Xeyx +9xQY3g0UNeXOaJa8/6rplSATy0PsJKvKJwgIqZdxVAhroAby6W1hrqYEBp7DMxuI +i3ZNSj2yaCWkSoZ4egsK4tvd7vMt/QCFYNk6BNN97UoUSIrpNcx70dZh4/cw+7Iq +FLbQaZP9ly7lU3jBuvRs/6ZNaTvTSgsnQ49DpiAaTKoGaH8GyL9OqeklZL+aNFsk +R7cTY2Y0zCY/iPAnv1UkKWnkaD2RszqE+SYY2eFtsXOfeTBjemL38CKqkbKvcZQ+ +Fe8/t9jAC78DkhIrDyvzuP8Qme3IjObnV9ybxDSarUgO3i02Qg7AkQhzjmH2vtkL +VUAqTOUJrtdPTgaC4WgkxokFgRJhw4CZspGiOjw/9w3h8+56a6FAVCmWQUgzoW0b +1wkyjZrKnq6Qf1Sny2fyPtiAT+b0Ev1KdQhs2W2L2PRw2B4pWfbhYLfyHD0PwzT+ +xHmBjJyVaH86i0K22hAjzJJnNK8HgYBFkdt7FqC+DUR1X3dy1jIBYYuSklgDO6Jd +u3sVrIbt7aJ9iQIhAOkiV2wAUZVqIlFB2IqxZ1F45KPMfqHxzDoKjc4IhKI5AoIC +AQCdJTb1TrmMrdbYGIB8pOVMHHbINQIMV6qeVrlqtnaeMaxQqI/aehwCsA8tCCDC +T2t+q5/JtCW0zz7OOYTN5viZQJWFJtMfsG8/3TdTRgRptkkIcvHzSdPLCrZGBCzR +Cafje+zZTkSJ1hHTGPwII0AfmB3N338IvVFgAIcB/vOPifrxlo2QDL0nPUmNheM/ +ogDEmHVb9jS+x0V0gn92e7GzIwJQJLAO9I0ClJ8BM7+3D4MJWkaAqjioTxvyWx9c +p6KG4zPJe5J78P3xg3ZQVZFNDqAXEaue2Dcs6fjVFdKRfHOLGCXuq5pwgnObyOsW +QMlLHgBnyuC3/0ltOEBcX09WUJW/sYvU3O1SzhwMr7OwmV3g7WuUjdA7PkNar2gD +4ePUw3cv0hbci/Y59+gBq5tcmG6/TE9Zia0KjTqY+BLCRY+MOcYJunW+4jzM9Ox/ +KGxqG4p5bSHwm21bgYHoJO0LUBVYDWkm6DmkGjfpLkbbiKFGx0bdDn9pOWeR7u3s +fVOSHYq69X2EPlrle3Bye6IRwqPJHQVZHzVF2wAqH7tA0jcvHvlz3mLbYC6miflH +9CRvTlXMd13b31tvvxzHmZbwOyDi0osXuSkMBxabHsmmQ8dD7lX03vrqx9n7P8+J +VZ80AryDMlGf9x4tBwoLq46FxmA3SjhRiuZgTL2JzEnjjAOCAgYAAoICAQCLvsmo +MlTEvguTqjqa5rbXof/jMmmCPkN6D1xUfHhyyX7bAVnr9ZEX43tX3cc88f5OEkDE +W8cWzLRle+CjcEVMxFjpKMgBzwAI4TIqNZsZJz8AMUY1gzkhNHaxiT8hqSPWX6TF +Lb3pEDuzAhqmuObz5XWNdEcGQlTeugCKcUhdDBtThJoA/p6BzCO5qHJcP58PmVD2 +iBcbz7LZQ+BLDw5HRNCOsd5N21F5VgGACRUenzX/Piot8Z4y41vXWt3FP6dh+/i6 +xQz/pD1Biaaq+GxutOhlHFdwpXc56KpYGrnVoHEAkMdDT84u5OTuz16+wt1FNHIP +ufhpL4iM6IGoqMYnb73IZxvlFNMuZ1bo4uhuvY+bFIZXBV7Xt9TbXtlVG+j/LpqP +pmw09FJGWWc6DrAqcPnCfDhzlXphSIVaYTR6kLdtE6ullUQcfqePmDVok4b5i7Zi +4dEIIXZ1KyWccl8e/6fCuUhPf/qlNdbeXVL2Evud1NYCdpICBTuGbeoGqDsnsU7e +X+Bmp4fSHCdNcHoJeVk+fGg7X4ZsNB5zq4G1reJUZMTq2tvNYS++MXILYDGUN9j0 +zsvMExFU5NN5pqpSKv3VeI4AXDYdRRkMBhHhpX27jUgeJ6oDDes59T5iO6Aliw/V +y0elWU3mxe6Rn6R7WHM8or7sX3tWDq6OntJlKg== +-----END PUBLIC KEY----- diff --git a/test/jwt.asymmetric_signing.tests.js b/test/jwt.asymmetric_signing.tests.js index c56eea30..a8472d52 100644 --- a/test/jwt.asymmetric_signing.tests.js +++ b/test/jwt.asymmetric_signing.tests.js @@ -1,17 +1,17 @@ -var jwt = require('../index'); -var PS_SUPPORTED = require('../lib/psSupported'); -var fs = require('fs'); -var path = require('path'); +const jwt = require('../index'); +const PS_SUPPORTED = require('../lib/psSupported'); +const fs = require('fs'); +const path = require('path'); -var expect = require('chai').expect; -var assert = require('chai').assert; -var ms = require('ms'); +const expect = require('chai').expect; +const assert = require('chai').assert; +const ms = require('ms'); function loadKey(filename) { return fs.readFileSync(path.join(__dirname, filename)); } -var algorithms = { +const algorithms = { RS256: { pub_key: loadKey('pub.pem'), priv_key: loadKey('priv.pem'), @@ -35,18 +35,17 @@ if (PS_SUPPORTED) { } -describe('Asymmetric Algorithms', function(){ - +describe('Asymmetric Algorithms', function() { Object.keys(algorithms).forEach(function (algorithm) { describe(algorithm, function () { - var pub = algorithms[algorithm].pub_key; - var priv = algorithms[algorithm].priv_key; + const pub = algorithms[algorithm].pub_key; + const priv = algorithms[algorithm].priv_key; // "invalid" means it is not the public key for the loaded "priv" key - var invalid_pub = algorithms[algorithm].invalid_pub_key; + const invalid_pub = algorithms[algorithm].invalid_pub_key; describe('when signing a token', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm }); + const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm }); it('should be syntactically valid', function () { expect(token).to.be.a('string'); @@ -73,13 +72,13 @@ describe('Asymmetric Algorithms', function(){ context('synchronous', function () { it('should validate with public key', function () { - var decoded = jwt.verify(token, pub); + const decoded = jwt.verify(token, pub); assert.ok(decoded.foo); assert.equal('bar', decoded.foo); }); it('should throw with invalid public key', function () { - var jwtVerify = jwt.verify.bind(null, token, invalid_pub) + const jwtVerify = jwt.verify.bind(null, token, invalid_pub) assert.throw(jwtVerify, 'invalid signature'); }); }); @@ -87,9 +86,8 @@ describe('Asymmetric Algorithms', function(){ }); describe('when signing a token with expiration', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: '10m' }); - it('should be valid expiration', function (done) { + const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: '10m' }); jwt.verify(token, pub, function (err, decoded) { assert.isNotNull(decoded); assert.isNull(err); @@ -99,8 +97,7 @@ describe('Asymmetric Algorithms', function(){ it('should be invalid', function (done) { // expired token - token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: -1 * ms('10m') }); - + const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: -1 * ms('10m') }); jwt.verify(token, pub, function (err, decoded) { assert.isUndefined(decoded); assert.isNotNull(err); @@ -113,7 +110,7 @@ describe('Asymmetric Algorithms', function(){ it('should NOT be invalid', function (done) { // expired token - token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: -1 * ms('10m') }); + const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm, expiresIn: -1 * ms('10m') }); jwt.verify(token, pub, { ignoreExpiration: true }, function (err, decoded) { assert.ok(decoded.foo); @@ -135,7 +132,7 @@ describe('Asymmetric Algorithms', function(){ }); describe('when decoding a jwt token with additional parts', function () { - var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm }); + const token = jwt.sign({ foo: 'bar' }, priv, { algorithm: algorithm }); it('should throw', function (done) { jwt.verify(token + '.foo', pub, function (err, decoded) { @@ -148,7 +145,7 @@ describe('Asymmetric Algorithms', function(){ describe('when decoding a invalid jwt token', function () { it('should return null', function (done) { - var payload = jwt.decode('whatever.token'); + const payload = jwt.decode('whatever.token'); assert.isNull(payload); done(); }); @@ -156,16 +153,16 @@ describe('Asymmetric Algorithms', function(){ describe('when decoding a valid jwt token', function () { it('should return the payload', function (done) { - var obj = { foo: 'bar' }; - var token = jwt.sign(obj, priv, { algorithm: algorithm }); - var payload = jwt.decode(token); + const obj = { foo: 'bar' }; + const token = jwt.sign(obj, priv, { algorithm: algorithm }); + const payload = jwt.decode(token); assert.equal(payload.foo, obj.foo); done(); }); it('should return the header and payload and signature if complete option is set', function (done) { - var obj = { foo: 'bar' }; - var token = jwt.sign(obj, priv, { algorithm: algorithm }); - var decoded = jwt.decode(token, { complete: true }); + const obj = { foo: 'bar' }; + const token = jwt.sign(obj, priv, { algorithm: algorithm }); + const decoded = jwt.decode(token, { complete: true }); assert.equal(decoded.payload.foo, obj.foo); assert.deepEqual(decoded.header, { typ: 'JWT', alg: algorithm }); assert.ok(typeof decoded.signature == 'string'); @@ -174,4 +171,38 @@ describe('Asymmetric Algorithms', function(){ }); }); }); + + describe('when signing a token with an unsupported private key type', function () { + it('should throw an error', function() { + const obj = { foo: 'bar' }; + const key = loadKey('dsa-private.pem'); + const algorithm = 'RS256'; + + expect(function() { + jwt.sign(obj, key, { algorithm }); + }).to.throw('Unknown key type "dsa".'); + }); + }); + + describe('when signing a token with an incorrect private key type', function () { + it('should throw a validation error if key validation is enabled', function() { + const obj = { foo: 'bar' }; + const key = loadKey('rsa-private.pem'); + const algorithm = 'ES256'; + + expect(function() { + jwt.sign(obj, key, { algorithm }); + }).to.throw(/"alg" parameter for "rsa" key type must be one of:/); + }); + + it('should throw an unknown error if key validation is disabled', function() { + const obj = { foo: 'bar' }; + const key = loadKey('rsa-private.pem'); + const algorithm = 'ES256'; + + expect(function() { + jwt.sign(obj, key, { algorithm, allowInvalidAsymmetricKeyTypes: true }); + }).to.not.throw(/"alg" parameter for "rsa" key type must be one of:/); + }); + }); }); diff --git a/test/prime256v1-private.pem b/test/prime256v1-private.pem new file mode 100644 index 00000000..31736657 --- /dev/null +++ b/test/prime256v1-private.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIMP1Xt/ic2jAHJva2Pll866d1jYL+dk3VdLytEU1+LFmoAoGCCqGSM49 +AwEHoUQDQgAEvIywoA1H1a2XpPPTqsRxSk6YnNRVsu4E+wTvb7uV6Yttvko9zWar +jmtM3LHDXk/nHn+Pva0KD+lby8gb2daHGg== +-----END EC PRIVATE KEY----- diff --git a/test/rsa-pss-invalid-salt-length-private.pem b/test/rsa-pss-invalid-salt-length-private.pem new file mode 100644 index 00000000..cbafa662 --- /dev/null +++ b/test/rsa-pss-invalid-salt-length-private.pem @@ -0,0 +1,29 @@ +-----BEGIN PRIVATE KEY----- +MIIE8gIBADBCBgkqhkiG9w0BAQowNaAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZI +hvcNAQEIMA0GCWCGSAFlAwQCAQUAogQCAgQABIIEpzCCBKMCAQACggEBAJy3FuDR +1qKXsC8o+0xDJbuJCnysT71EFDGQY2/b3cZmxW3rzDYLyE65t2Go1jeK5Kxs+kwS +1VxfefD8DifeDZN66wjRse4iWLcxmQB5FfishXOdozciimgXNvXJNS8X//feSofl +vDQaTUI0NJnw1qQ2CB0pgGInwajsRKpWnDOhfk3NA/cmGlmfhTtDSTxq0ReytUie +TjY7gy+S9YYm4bAgBcMeoup0GEPzYccK4+1yCmWzQZGFcrY1cuB9bL+vT7ajQFhe +WVKlp6z35GyBF2zI7gJSkHpUHaWV5+Z9aTr6+YP6U7xuCRvXQ/l6BEOUjt4Es2YG +3frgxeVbOs1gAakCAwEAAQKCAQAMvFxhnOwCfq1Ux9HUWsigOvzdMOuyB+xUMtXB +625Uh1mYG0eXRNHcg/9BMoVmMiVvVdPphsZMIX45dWJ5HvSffafIKbJ6FdR73s3+ +WdjNQsf9o1v2SRpSZ0CSLO3ji+HDdQ89iBAJc/G/ZZq4v/fRlIqIRC0ozO5SGhFi +fnNnRqH78d2KeJMX/g9jBZM8rJQCi+pb0keHmFmLJ5gZa4HokE8rWQJQY46PVYUH +W2BwEJToMl3MPC7D95soWVuFt3KHnIWhuma/tnCmd2AUvcMrdWq0CwStH3vuX4LB +vJug0toWkobt1tzZgzzCASb2EpzJj8UNxP1CzTQWsvl8OephAoGBAMVnmZeLHoh2 +kxn/+rXetZ4Msjgu19MHNQAtlMvqzwZLan0K/BhnHprJLy4SDOuQYIs+PYJuXdT7 +Yv2mp9kwTPz8glP9LAto4MDeDfCu0cyXmZb2VQcT/lqVyrwfx3Psqxm/Yxg62YKr +aQE8WqgZGUdOvU9dYU+7EmPlYpdGpPVlAoGBAMs7ks+12oE6kci3WApdnt0kk5+f +8fbQ0lp2vR3tEw8DURa5FnHWA4o46XvcMcuXwZBrpxANPNAxJJjMBs1hSkc8h4hd +4vjtRNYJpj+uBdDIRmdqTzbpWv+hv8Xpiol5EVgnMVs2UZWDjoxQ+mYa1R8tAUfj +ojzV2KBMWGCoHgj1AoGALki6JGQEBq72kpQILnhHUQVdC/s/s0TvUlldl+o4HBu2 +nhbjQL182YHuQ/kLenfhiwRO27QQ4A0JCrv2gt/mTTLPQ+4KU6qFd/MYhaQXoMay +xkh/aydu7cJNRIqW80E8ZM8Q5u91bEPQXO/PubYYzTVTAba9SDpud2mjEiEIMFkC +gYEAxINEQEgtkkuZ76UpIkzIcjkN7YlxJCFjZUnvL+KvTRL986TgyQ4RujOxwKx4 +Ec8ZwZX2opTKOt1p771IzorGkf87ZmayM9TpfLUz5dtVkD43pYOsOQKHlStIDgz2 +gltoo/6xwOrTFGlzCsa6eMR1U4Hm/SZlF8IHh2iLBFtLP4kCgYBqTi1XeWeVQVSA +y9Wolv9kMoRh/Xh6F2D8bTTybGshDVO+P4YLM4lLxh5UDZAd/VOkdf3ZIcUGv022 +lxrYbLbIEGckMCpkdHeZH/1/iuJUeiCrXeyNlQsXBrmJKr/0lENniJHGpiSEyvY5 +D8Oafyjd7ZjUmyBFvS4heQEC6Pjo3Q== +-----END PRIVATE KEY----- diff --git a/test/rsa-pss-private.pem b/test/rsa-pss-private.pem new file mode 100644 index 00000000..52b1c08e --- /dev/null +++ b/test/rsa-pss-private.pem @@ -0,0 +1,29 @@ +-----BEGIN PRIVATE KEY----- +MIIE8QIBADBBBgkqhkiG9w0BAQowNKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZI +hvcNAQEIMA0GCWCGSAFlAwQCAQUAogMCASAEggSnMIIEowIBAAKCAQEA00tEqqyF +VnyvcVA2ewVoSicCMdQXmWyYM82sBWX0wcnn0WUuZp1zjux4xTvQ71Lhx95OJCQZ +7r7b2192Im5ca37wNRbI6DhyXNdNVFXLFYlNAvgP+V0gIwlr6NgopdJqHCjYVv/g +GOoesRZaDdtV1A3O9CXdJ34x2HZh7nhwYK5hqZDhUW4rd+5GzIIzwCJfwgTQpkIc +18UeMMEoKJ6A0ixdpf43HqJ5fAB5nsbYFhyHpfiX1UO2EFJtSdbKEIbRmqcbNjG1 +tu1tjt6u8LI2coetLh/IYMbMfkyQz+eAUHLQCUb2R8BqLOL3hRqEsVTBo93UJlOs +VWC1fKaq+HOEWQIDAQABAoIBAAet23PagPQTjwAZcAlzjlvs5AMHQsj5gznqwSmR +ut3/e7SGrrOIXbv1iIQejZQ3w8CS/0MH/ttIRiRIaWTh9EDsjvKsU9FAxUNDiJTG +k3LCbTFCQ7kGiJWiu4XDCWMmwmLTRzLjlMjtr/+JS5eSVPcNKMGDI3D9K0xDLSxQ +u0DVigYgWOCWlejHCEU4yi6vBO0HlumWjVPelWb9GmihBDwCLUJtG0JA6H6rw+KS +i6SNXcMGVKfjEghChRp+HaMvLvMgU44Ptnj8jhlfBctXInBY1is1FfDSWxXdVbUM +1HdKXfV4A50GXSvJLiWP9ZZsaZ7NiBJK8IiJBXD72EFOzwECgYEA3RjnTJn9emzG +84eIHZQujWWt4Tk/wjeLJYOYtAZpF7R3/fYLVypX9Bsw1IbwZodq/jChTjMaUkYt +//FgUjF/t0uakEg1i+THPZvktNB8Q1E9NwHerB8HF/AD/jMALD+ejdLQ11Z4VScw +zyNmSvD9I84/sgpms5YVKSH9sqww2RkCgYEA9KYws3sTfRLc1hlsS25V6+Zg3ZCk +iGcp+zrxGC1gb2/PpRvEDBucZO21KbSRuQDavWIOZYl4fGu7s8wo2oF8RxOsHQsM +LJyjklruvtjnvuoft/bGAv2zLQkNaj+f7IgK6965gIxcLYL66UPCZZkTfL5CoJis +V0v2hBh1ES5bLUECgYEAuONeaLxNL9dO989akAGefDePFExfePYhshk91S2XLG+J ++CGMkjOioUsrpk3BMrwDSNU5zr8FP8/YH7OlrJYgCxN6CTWZMYb65hY7RskhYNnK +qvkxUBYSRH49mJDlkBsTZ93nLmvs7Kh9NHqRzBGCXjLXKPdxsrPKtj7qfENqBeEC +gYAC9dPXCCE3PTgw2wPlccNWZGY9qBdlkyH96TurmDj3gDnZ/JkFsHvW+M1dYNL2 +kx0Sd5JHBj/P+Zm+1jSUWEbBsWo+u7h8/bQ4/CKxanx7YefaWQESXjGB1P81jumH +einvqrVB6fDfmBsjIW/DvPNwafjyaoaDU+b6uDUKbS4rQQKBgCe0pvDl5lO8FM81 +NP7GoCIu1gKBS+us1sgYE65ZFmVXJ6b5DckvobXSjM60G2N5w2xaXEXJsnwMApf1 +SClQUsgNWcSXRwL+w0pIdyFKS25BSfwUNQ9n7QLJcYgmflbARTfB3He/10vbFzTp +G6ZAiKUp9bKFPzviII40AEPL2hPX +-----END PRIVATE KEY----- diff --git a/test/schema.tests.js b/test/schema.tests.js index 0a648f12..ebd553f6 100644 --- a/test/schema.tests.js +++ b/test/schema.tests.js @@ -6,31 +6,31 @@ var PS_SUPPORTED = require('../lib/psSupported'); 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'); + var cert_secp384r1_priv = fs.readFileSync(__dirname + '/secp384r1-private.pem'); + var cert_secp521r1_priv = fs.readFileSync(__dirname + '/secp521r1-private.pem'); - function sign(options, secret) { - var isEcdsa = options.algorithm && options.algorithm.indexOf('ES') === 0; - jwt.sign({foo: 123}, secret || (isEcdsa ? cert_ecdsa_priv : cert_rsa_priv), options); + function sign(options, secretOrPrivateKey) { + jwt.sign({foo: 123}, secretOrPrivateKey, options); } it('should validate algorithm', function () { expect(function () { - sign({ algorithm: 'foo' }); + sign({ algorithm: 'foo' }, cert_rsa_priv); }).to.throw(/"algorithm" must be a valid string enum value/); - sign({ algorithm: 'none' }); - sign({algorithm: 'RS256'}); - sign({algorithm: 'RS384'}); - sign({algorithm: 'RS512'}); + sign({ algorithm: 'none' }, null); + sign({algorithm: 'RS256'}, cert_rsa_priv); + sign({algorithm: 'RS384'}, cert_rsa_priv); + sign({algorithm: 'RS512'}, cert_rsa_priv); if (PS_SUPPORTED) { - sign({algorithm: 'PS256'}); - sign({algorithm: 'PS384'}); - sign({algorithm: 'PS512'}); + sign({algorithm: 'PS256'}, cert_rsa_priv); + sign({algorithm: 'PS384'}, cert_rsa_priv); + sign({algorithm: 'PS512'}, cert_rsa_priv); } - sign({algorithm: 'ES256'}); - sign({algorithm: 'ES384'}); - sign({algorithm: 'ES512'}); + sign({algorithm: 'ES256'}, cert_ecdsa_priv); + sign({algorithm: 'ES384'}, cert_secp384r1_priv); + sign({algorithm: 'ES512'}, cert_secp521r1_priv); sign({algorithm: 'HS256'}, 'superSecret'); sign({algorithm: 'HS384'}, 'superSecret'); sign({algorithm: 'HS512'}, 'superSecret'); diff --git a/test/secp384r1-private.pem b/test/secp384r1-private.pem new file mode 100644 index 00000000..82336b6a --- /dev/null +++ b/test/secp384r1-private.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDCez58vZHVp+ArI7/fe835GAtRzE0AtrxGgQAY1U/uk2SQOaSw1ph61 +3Unr0ygS172gBwYFK4EEACKhZANiAARtwlnIqYqZxfiWR+/EM35nKHuLpOjUHiX1 +kEpSS03C9XlrBLNwLQfgjpYx9Qvqh26XAzTe74DYjcc748R+zZD2YAd3lV+OcdRE +U+DWm4j5E6dlOXzvmw/3qxUcg3rRgR4= +-----END EC PRIVATE KEY----- diff --git a/test/secp521r1-private.pem b/test/secp521r1-private.pem new file mode 100644 index 00000000..397a3df0 --- /dev/null +++ b/test/secp521r1-private.pem @@ -0,0 +1,7 @@ +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIBlWXKBKKCgTgf7+NS09TMv7/NO3RtMBn9xTe+46oNNNK405lrZ9mz +WYtlsYvkdsc2Cx3v5V8JegaCOM+XtAZ0MNKgBwYFK4EEACOhgYkDgYYABAFNzaM7 +Zb9ug0p5KaZb5mjHrIshoVJSHaOXGtcjLVUakYVk0v9VsE+FKqyuLYcORUuAZdxl +ITAlC5e5JZ0o8NEKbAE+8oOrePrItR3IFBtWO15p7qiRa2dBB8oQklFrmQaJYn4K +fDV0hYpfu6ahpRNu2akR7aMXL/vXrptCH/n64q9KjA== +-----END EC PRIVATE KEY----- diff --git a/test/validateAsymmetricKey.tests.js b/test/validateAsymmetricKey.tests.js new file mode 100644 index 00000000..e0194b8e --- /dev/null +++ b/test/validateAsymmetricKey.tests.js @@ -0,0 +1,142 @@ +const validateAsymmetricKey = require('../lib/validateAsymmetricKey'); +const PS_SUPPORTED = require('../lib/psSupported'); +const ASYMMETRIC_KEY_DETAILS_SUPPORTED = require('../lib/asymmetricKeyDetailsSupported'); +const RSA_PSS_KEY_DETAILS_SUPPORTED = require('../lib/rsaPssKeyDetailsSupported'); +const fs = require('fs'); +const path = require('path'); +const { createPrivateKey } = require('crypto'); +const expect = require('chai').expect; + +function loadKey(filename) { + return createPrivateKey( + fs.readFileSync(path.join(__dirname, filename)) + ); +} + +const algorithmParams = { + RS256: { + invalidPrivateKey: loadKey('secp384r1-private.pem') + }, + ES256: { + invalidPrivateKey: loadKey('priv.pem') + } +}; + +if (PS_SUPPORTED) { + algorithmParams.PS256 = { + invalidPrivateKey: loadKey('secp384r1-private.pem') + }; +} + +describe('Asymmetric key validation', function() { + Object.keys(algorithmParams).forEach(function(algorithm) { + describe(algorithm, function() { + const keys = algorithmParams[algorithm]; + + describe('when validating a key with an invalid private key type', function () { + it('should throw an error', function () { + const expectedErrorMessage = /"alg" parameter for "[\w\d-]+" key type must be one of:/; + + expect(function() { + validateAsymmetricKey(algorithm, keys.invalidPrivateKey); + }).to.throw(expectedErrorMessage); + }); + }); + }); + }); + + describe('when the function has missing parameters', function() { + it('should pass the validation if no key has been provided', function() { + const algorithm = 'ES256'; + validateAsymmetricKey(algorithm); + }); + + it('should pass the validation if no algorithm has been provided', function() { + const key = loadKey('dsa-private.pem'); + validateAsymmetricKey(null, key); + }); + }); + + describe('when validating a key with an unsupported type', function () { + it('should throw an error', function() { + const algorithm = 'RS256'; + const key = loadKey('dsa-private.pem'); + const expectedErrorMessage = 'Unknown key type "dsa".'; + + expect(function() { + validateAsymmetricKey(algorithm, key); + }).to.throw(expectedErrorMessage); + }); + }); + + describe('Elliptic curve algorithms', function () { + const curvesAlgorithms = [ + { algorithm: 'ES256', curve: 'prime256v1' }, + { algorithm: 'ES384', curve: 'secp384r1' }, + { algorithm: 'ES512', curve: 'secp521r1' }, + ]; + + const curvesKeys = [ + { curve: 'prime256v1', key: loadKey('prime256v1-private.pem') }, + { curve: 'secp384r1', key: loadKey('secp384r1-private.pem') }, + { curve: 'secp521r1', key: loadKey('secp521r1-private.pem') } + ]; + + describe('when validating keys generated using Elliptic Curves', function () { + curvesAlgorithms.forEach(function(curveAlgorithm) { + curvesKeys + .forEach((curveKeys) => { + if (curveKeys.curve !== curveAlgorithm.curve) { + if (ASYMMETRIC_KEY_DETAILS_SUPPORTED) { + it(`should throw an error when validating an ${curveAlgorithm.algorithm} token for key with curve ${curveKeys.curve}`, function() { + expect(() => { + validateAsymmetricKey(curveAlgorithm.algorithm, curveKeys.key); + }).to.throw(`"alg" parameter "${curveAlgorithm.algorithm}" requires curve "${curveAlgorithm.curve}".`); + }); + } else { + it(`should pass the validation for incorrect keys if the Node version does not support checking the key's curve name`, function() { + expect(() => { + validateAsymmetricKey(curveAlgorithm.algorithm, curveKeys.key); + }).not.to.throw(); + }); + } + } else { + it(`should accept an ${curveAlgorithm.algorithm} token for key with curve ${curveKeys.curve}`, function() { + expect(() => { + validateAsymmetricKey(curveAlgorithm.algorithm, curveKeys.key); + }).not.to.throw(); + }); + } + }); + }); + }); + }); + + if (RSA_PSS_KEY_DETAILS_SUPPORTED) { + describe('RSA-PSS algorithms', function () { + const key = loadKey('rsa-pss-private.pem'); + + it(`it should throw an error when validating a key with wrong RSA-RSS parameters`, function () { + const algorithm = 'PS512'; + expect(function() { + validateAsymmetricKey(algorithm, key); + }).to.throw('Invalid key for this operation, its RSA-PSS parameters do not meet the requirements of "alg" PS512') + }); + + it(`it should throw an error when validating a key with invalid salt length`, function () { + const algorithm = 'PS256'; + const shortSaltKey = loadKey('rsa-pss-invalid-salt-length-private.pem'); + expect(function() { + validateAsymmetricKey(algorithm, shortSaltKey); + }).to.throw('Invalid key for this operation, its RSA-PSS parameter saltLength does not meet the requirements of "alg" PS256.') + }); + + it(`it should pass the validation when the key matches all the requirements for the algorithm`, function () { + expect(function() { + const algorithm = 'PS256'; + validateAsymmetricKey(algorithm, key); + }).not.to.throw() + }); + }); + } +}); diff --git a/test/verify.tests.js b/test/verify.tests.js index 9ef24e45..88500756 100644 --- a/test/verify.tests.js +++ b/test/verify.tests.js @@ -1,22 +1,22 @@ -var jwt = require('../index'); -var jws = require('jws'); -var fs = require('fs'); -var path = require('path'); -var sinon = require('sinon'); -var JsonWebTokenError = require('../lib/JsonWebTokenError'); +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'); -var assert = require('chai').assert; -var expect = require('chai').expect; +const assert = require('chai').assert; +const expect = require('chai').expect; describe('verify', function() { - var pub = fs.readFileSync(path.join(__dirname, 'pub.pem')); - var priv = fs.readFileSync(path.join(__dirname, 'priv.pem')); + 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) { - var header = { alg: 'RS256' }; - var payload = { iat: Math.floor(Date.now() / 1000 ) }; + const header = { alg: 'RS256' }; + const payload = { iat: Math.floor(Date.now() / 1000 ) }; - var signed = jws.sign({ + const signed = jws.sign({ header: header, payload: payload, secret: priv, @@ -31,10 +31,10 @@ describe('verify', function() { }); it('should not be able to verify unsigned token', function () { - var header = { alg: 'none' }; - var payload = { iat: Math.floor(Date.now() / 1000 ) }; + const header = { alg: 'none' }; + const payload = { iat: Math.floor(Date.now() / 1000 ) }; - var signed = jws.sign({ + const signed = jws.sign({ header: header, payload: payload, secret: 'secret', @@ -47,10 +47,10 @@ describe('verify', function() { }); it('should not be able to verify unsigned token', function () { - var header = { alg: 'none' }; - var payload = { iat: Math.floor(Date.now() / 1000 ) }; + const header = { alg: 'none' }; + const payload = { iat: Math.floor(Date.now() / 1000 ) }; - var signed = jws.sign({ + const signed = jws.sign({ header: header, payload: payload, secret: 'secret', @@ -63,10 +63,10 @@ describe('verify', function() { }); it('should be able to verify unsigned token when none is specified', function (done) { - var header = { alg: 'none' }; - var payload = { iat: Math.floor(Date.now() / 1000 ) }; + const header = { alg: 'none' }; + const payload = { iat: Math.floor(Date.now() / 1000 ) }; - var signed = jws.sign({ + const signed = jws.sign({ header: header, payload: payload, secret: 'secret', @@ -99,11 +99,11 @@ describe('verify', function() { }); describe('secret or token as callback', function () { - var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU'; - var key = 'key'; + const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU'; + const key = 'key'; - var payload = { foo: 'bar', iat: 1437018582, exp: 1437018592 }; - var options = {algorithms: ['HS256'], ignoreExpiration: true}; + 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) { @@ -114,7 +114,7 @@ describe('verify', function() { }); it('simple callback', function (done) { - var keyFunc = function(header, callback) { + const keyFunc = function(header, callback) { assert.deepEqual(header, { alg: 'HS256', typ: 'JWT' }); callback(undefined, key); @@ -128,7 +128,7 @@ describe('verify', function() { }); it('should error if called synchronously', function (done) { - var keyFunc = function(header, callback) { + const keyFunc = function(header, callback) { callback(undefined, key); }; @@ -140,7 +140,7 @@ describe('verify', function() { }); it('simple error', function (done) { - var keyFunc = function(header, callback) { + const keyFunc = function(header, callback) { callback(new Error('key not found')); }; @@ -153,7 +153,7 @@ describe('verify', function() { }); it('delayed callback', function (done) { - var keyFunc = function(header, callback) { + const keyFunc = function(header, callback) { setTimeout(function() { callback(undefined, key); }, 25); @@ -167,7 +167,7 @@ describe('verify', function() { }); it('delayed error', function (done) { - var keyFunc = function(header, callback) { + const keyFunc = function(header, callback) { setTimeout(function() { callback(new Error('key not found')); }, 25); @@ -184,17 +184,17 @@ describe('verify', function() { describe('expiration', function () { // { foo: 'bar', iat: 1437018582, exp: 1437018592 } - var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU'; - var key = 'key'; + const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU'; + const key = 'key'; - var clock; + 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 - var options = {algorithms: ['HS256']}; + const options = {algorithms: ['HS256']}; jwt.verify(token, key, options, function (err, p) { assert.equal(err.name, 'TokenExpiredError'); @@ -208,7 +208,7 @@ describe('verify', function() { it('should not error on expired token within clockTolerance interval', function (done) { clock = sinon.useFakeTimers(1437018594000); // iat + 12s, exp + 2s - var options = {algorithms: ['HS256'], clockTolerance: 5 } + const options = {algorithms: ['HS256'], clockTolerance: 5 } jwt.verify(token, key, options, function (err, p) { assert.isNull(err); @@ -218,16 +218,16 @@ describe('verify', function() { }); describe('option: clockTimestamp', function () { - var clockTimestamp = 1000000000; + const clockTimestamp = 1000000000; it('should verify unexpired token relative to user-provided clockTimestamp', function (done) { - var token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); + 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) { - var token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); + 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'); @@ -238,7 +238,7 @@ describe('verify', function() { }); }); it('should verify clockTimestamp is a number', function (done) { - var token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); + 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'); @@ -250,10 +250,10 @@ describe('verify', function() { describe('option: maxAge and clockTimestamp', function () { // { foo: 'bar', iat: 1437018582, exp: 1437018800 } exp = iat + 218s - var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODgwMH0.AVOsNC7TiT-XVSpCpkwB1240izzCIJ33Lp07gjnXVpA'; + const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODgwMH0.AVOsNC7TiT-XVSpCpkwB1240izzCIJ33Lp07gjnXVpA'; it('cannot be more permissive than expiration', function (done) { - var clockTimestamp = 1437018900; // iat + 318s (exp: iat + 218s) - var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1000y'}; + 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 @@ -267,4 +267,35 @@ describe('verify', function() { }); }); }); + + 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.'); + }); + }); }); diff --git a/verify.js b/verify.js index 0b649db4..cdbfdc45 100644 --- a/verify.js +++ b/verify.js @@ -3,6 +3,7 @@ const NotBeforeError = require('./lib/NotBeforeError'); const TokenExpiredError = require('./lib/TokenExpiredError'); const decode = require('./decode'); const timespan = require('./lib/timespan'); +const validateAsymmetricKey = require('./lib/validateAsymmetricKey'); const PS_SUPPORTED = require('./lib/psSupported'); const jws = require('jws'); const {KeyObject, createSecretKey, createPublicKey} = require("crypto"); @@ -49,6 +50,10 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError('nonce must be a non-empty string')); } + if (options.allowInvalidAsymmetricKeyTypes !== undefined && typeof options.allowInvalidAsymmetricKeyTypes !== 'boolean') { + return done(new JsonWebTokenError('allowInvalidAsymmetricKeyTypes must be a boolean')); + } + const clockTimestamp = options.clockTimestamp || Math.floor(Date.now() / 1000); if (!jwtString){ @@ -146,6 +151,14 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError((`secretOrPublicKey must be an asymmetric key when using ${header.alg}`))) } + if (!options.allowInvalidAsymmetricKeyTypes) { + try { + validateAsymmetricKey(header.alg, secretOrPublicKey); + } catch (e) { + return done(e); + } + } + let valid; try {