diff --git a/README.md b/README.md index 3e0edda..4534f8b 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ encoded private key for RSA and ECDSA. * `algorithm` (default: `HS256`) * `expiresInMinutes` or `expiresInSeconds` +* `notBeforeMinutes` or `notBeforeSeconds` * `audience` * `subject` * `issuer` @@ -37,7 +38,7 @@ encoded private key for RSA and ECDSA. If `payload` is not a buffer or a string, it will be coerced into a string using `JSON.stringify`. -If any `expiresInMinutes`, `audience`, `subject`, `issuer`, `jwtid`, `subject` are not provided, there is no default. The jwt generated won't include those properties in the payload. +If any `expiresInMinutes`, `notBeforeMinutes`, `audience`, `subject`, `issuer`, `jwtid`, `subject` are not provided, there is no default. The jwt generated won't include those properties in the payload. Additional headers can be provided via the `headers` object. @@ -60,6 +61,7 @@ var token = jwt.sign({ foo: 'bar' }, cert, { algorithm: 'RS256'}); `options`: * `ignoreExpiration` +* `ignoreNotBefore` * `audience` * `issuer` * `jwtid` @@ -81,6 +83,7 @@ encoded public key for RSA and ECDSA. * `audience`: if you want to check audience (`aud`), provide a value here * `issuer`: if you want to check issuer (`iss`), provide a value here * `ignoreExpiration`: if `true` do not validate the expiration of the token. +* `ignoreNotBefore`: if `true` do not validate the not before of the token. * `jwtid`: if you want to check JWT ID (`jti`), provide a value here * `subject`: if you want to check subject (`sub`), provide a value here diff --git a/index.js b/index.js index 945804a..204a165 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ var jws = require('jws'); var JsonWebTokenError = module.exports.JsonWebTokenError = require('./lib/JsonWebTokenError'); +var NotBeforeError = module.exports.NotBeforeError = require('./lib/NotBeforeError'); var TokenExpiredError = module.exports.TokenExpiredError = require('./lib/TokenExpiredError'); module.exports.decode = function (jwt, options) { @@ -53,6 +54,14 @@ module.exports.sign = function(payload, secretOrPrivateKey, options) { if (!options.noTimestamp) { payload.iat = payload.iat || timestamp; } + + var notBeforeSeconds = options.notBeforeMinutes ? + options.notBeforeMinutes * 60 : + options.notBeforeSeconds; + + if (notBeforeSeconds) { + payload.nbf = timestamp + notBeforeSeconds; + } var expiresInSeconds = options.expiresInMinutes ? options.expiresInMinutes * 60 : @@ -167,6 +176,14 @@ module.exports.verify = function(jwtString, secretOrPublicKey, options, callback } catch(err) { return done(err); } + + if (typeof payload.nbf !== 'undefined' && !options.ignoreNotBefore) { + if (typeof payload.nbf !== 'number') { + return done(new JsonWebTokenError('invalid nbf value')); + } + if (payload.nbf >= Math.floor(Date.now() / 1000)) + return done(new NotBeforeError('jwt not active', new Date(payload.nbf * 1000))); + } if (typeof payload.exp !== 'undefined' && !options.ignoreExpiration) { if (typeof payload.exp !== 'number') { diff --git a/lib/NotBeforeError.js b/lib/NotBeforeError.js new file mode 100644 index 0000000..7b30084 --- /dev/null +++ b/lib/NotBeforeError.js @@ -0,0 +1,13 @@ +var JsonWebTokenError = require('./JsonWebTokenError'); + +var NotBeforeError = function (message, date) { + JsonWebTokenError.call(this, message); + this.name = 'NotBeforeError'; + this.date = date; +}; + +NotBeforeError.prototype = Object.create(JsonWebTokenError.prototype); + +NotBeforeError.prototype.constructor = NotBeforeError; + +module.exports = NotBeforeError; \ No newline at end of file diff --git a/test/jwt.rs.tests.js b/test/jwt.rs.tests.js index 89d231c..18af156 100644 --- a/test/jwt.rs.tests.js +++ b/test/jwt.rs.tests.js @@ -88,6 +88,43 @@ describe('RS256', function() { }); }); + describe('when signing a token with not before', function() { + var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: 'RS256', notBeforeMinutes: -10 }); + + 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: 'RS256', notBeforeMinutes: 10 }); + + 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 NOT be invalid', function(done) { + // not active token + token = jwt.sign({ foo: 'bar' }, priv, { algorithm: 'RS256', notBeforeMinutes: 10 }); + + 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: 'RS256', audience: 'urn:foo' });