From eefb9d9c6eec54718fa6e41306bda84788df7bec Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 20 Feb 2019 13:42:37 +0100 Subject: [PATCH] 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 7f2f9aa..498d61d 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 442aa55..ea0ee00 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 0000000..8c04144 --- /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 1c8ea5c..0349fc4 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 4a493d3..f649ce4 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 b0948d3..6eb7dc7 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 49b9ed0..c56eea3 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 e2044fc..6abafb8 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 887b59f..742d29e 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 04ce48e..8b6e245 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 6b459c7..1df99f8 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; }