Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optionally validate the payload of a token before verification #972

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down
12 changes: 12 additions & 0 deletions lib/InvalidPayloadError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const JsonWebTokenError = require("./JsonWebTokenError");

const InvalidPayloadError = function (message, date) {
JsonWebTokenError.call(this, message);
this.name = "InvalidFormatError";
this.date = date;
};

InvalidPayloadError.prototype = Object.create(JsonWebTokenError.prototype);
InvalidPayloadError.prototype.constructor = InvalidPayloadError;

module.exports = InvalidPayloadError;
44 changes: 44 additions & 0 deletions test/payload_callback.test.js
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not keen on the double undefined, however had concerns around making any other changes to the function signature wrt backward compatibility

Copy link
Author

@georgejmx georgejmx Jul 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a repo that demos the feature https://github.com/georgejmx/demo-jsonwebtoken-payload-callback

Any more manual testing you need just let me know @Vero7979

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);
})
})
15 changes: 12 additions & 3 deletions verify.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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 = {};
Expand Down Expand Up @@ -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 {
Expand All @@ -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'));
Expand Down
Loading