diff --git a/README.md b/README.md index 4e20dd9..0c950d5 100644 --- a/README.md +++ b/README.md @@ -123,13 +123,13 @@ jwt.sign({ }, 'secret', { expiresIn: '1h' }); ``` -### jwt.verify(token, secretOrPublicKey, [options, callback]) +### jwt.verify(token, secretOrPublicKey, [options, callback, payloadCallback]) (Asynchronous) If a callback is supplied, function acts asynchronously. The callback is called with the decoded payload if the signature is valid and optional expiration, audience, or issuer are valid. If not, it will be called with the error. (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 +> __Warning:__ When the token comes from an untrusted source (e.g. user input or external requests) and a payloadCallback is not specified, 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 @@ -162,6 +162,10 @@ As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues * `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. +`payloadCallback` + +A function to specify custom validation of the token payload obtained from decoding the token. This function is applied before token verification. Throwing an error in this function will throw an error in `jwt.verify` before the token is verified, allowing sanitizing the token payload without needing to manually call `jwt.decode` before or after validation + ```js // verify a token symmetric - synchronous var decoded = jwt.verify(token, 'shhhhh'); @@ -221,6 +225,17 @@ jwt.verify(token, cert, { algorithms: ['RS256'] }, function (err, payload) { // if token alg != RS256, err == invalid signature }); +// alo verify token payload +var cert = fs.readFileSync('public.pem'); // get public key +jwt.verify(token, cert, { algorithms: ['RS256'] }, function (err, payload) { + // if token alg != RS256, err == invalid signature +}, function (payload) { + // specify custom payload schema validation here without needing to decode separately + if (payload.foo !== 'bar' || typeof payload.userId !== 'number') { + throw new Error('token does not have the correct schema'); + } +}); + // 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'); @@ -351,6 +366,30 @@ jwt.verify(token, 'shhhhh', function(err, decoded) { }); ``` +### InvalidPayloadError +Thrown if the payload validation callback failed. The error message will be inherited from the error thrown in the `payloadCallback` or defaults to *'invalid token payload'* if for example a number was thrown and not an error + +Error object example: + +* name: 'InvalidPayloadError' +* message: 'invalid token payload' + +```js +jwt.verify(token, 'shhhhh', function(err, decoded) { + if (err) { + /* + err = { + name: 'InvalidPayloadError', + message: 'foo property not equal to bar', + } + */ + } +}, function (payload) { + if (payload.foo !== 'bar') { + throw new Error('foo property not equal to bar'); + } +}); +``` ## Algorithms supported diff --git a/lib/InvalidPayloadError.js b/lib/InvalidPayloadError.js new file mode 100644 index 0000000..af4fc96 --- /dev/null +++ b/lib/InvalidPayloadError.js @@ -0,0 +1,11 @@ +const JsonWebTokenError = require("./JsonWebTokenError"); + +const InvalidPayloadError = function (message) { + JsonWebTokenError.call(this, message); + this.name = "InvalidPayloadError"; +}; + +InvalidPayloadError.prototype = Object.create(JsonWebTokenError.prototype); +InvalidPayloadError.prototype.constructor = InvalidPayloadError; + +module.exports = InvalidPayloadError; diff --git a/test/payload_callback.test.js b/test/payload_callback.test.js new file mode 100644 index 0000000..9406908 --- /dev/null +++ b/test/payload_callback.test.js @@ -0,0 +1,44 @@ +'use strict'; + +const jwt = require('../index'); +const assert = require('chai').assert; +const expect = require('chai').expect; + +describe('payload callback', function() { + const KEY = 'somethingSECRET'; + const TEST_ERROR_MESSAGE = 'bar not car!'; + let testPayloadCallback; + + beforeEach(function() { + testPayloadCallback = function (payload) { + if (payload.foo !== 'bar') { + throw new Error(TEST_ERROR_MESSAGE); + } + } + }); + + it('should check that the payload satisfies the provided callback', function () { + const token = jwt.sign({ foo: 'bar' }, KEY); + const result = jwt.verify(token, KEY, undefined, undefined, testPayloadCallback); + expect(result.foo).to.equal('bar'); + }); + + it('should be compatible with async token verification', function () { + const testCallback = function (err, decoded) { + if (err) { + assert.fail(err.message); + } + expect(decoded.foo).to.equal('bar'); + } + + const token = jwt.sign({ foo: 'bar' }, KEY); + jwt.verify(token, KEY, {}, testCallback, testPayloadCallback); + }); + + it('should throw when the payload callback rejects', function () { + const token = jwt.sign({ foo: 'car' }, KEY); + expect(function () { + jwt.verify(token, KEY, undefined, undefined, testPayloadCallback) + }).to.throw(TEST_ERROR_MESSAGE); + }) +}) \ No newline at end of file diff --git a/verify.js b/verify.js index cdbfdc4..76faf34 100644 --- a/verify.js +++ b/verify.js @@ -1,6 +1,7 @@ const JsonWebTokenError = require('./lib/JsonWebTokenError'); const NotBeforeError = require('./lib/NotBeforeError'); const TokenExpiredError = require('./lib/TokenExpiredError'); +const InvalidPayloadError = require('./lib/InvalidPayloadError'); const decode = require('./decode'); const timespan = require('./lib/timespan'); const validateAsymmetricKey = require('./lib/validateAsymmetricKey'); @@ -18,7 +19,7 @@ if (PS_SUPPORTED) { RSA_KEY_ALGS.splice(RSA_KEY_ALGS.length, 0, 'PS256', 'PS384', 'PS512'); } -module.exports = function (jwtString, secretOrPublicKey, options, callback) { +module.exports = function (jwtString, secretOrPublicKey, options, callback, payloadCallback) { if ((typeof options === 'function') && !callback) { callback = options; options = {}; @@ -159,6 +160,16 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { } } + const payload = decodedToken.payload; + if (typeof payloadCallback === 'function') { + try { + payloadCallback(payload); + } catch (e) { + const message = e instanceof Error ? e.message : 'invalid token payload'; + return done(new InvalidPayloadError(message)); + } + } + let valid; try { @@ -171,8 +182,6 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError('invalid signature')); } - const payload = decodedToken.payload; - if (typeof payload.nbf !== 'undefined' && !options.ignoreNotBefore) { if (typeof payload.nbf !== 'number') { return done(new JsonWebTokenError('invalid nbf value'));