From b9f842861a2ae36a781c6cd279ca9130370ae94c Mon Sep 17 00:00:00 2001 From: jaideeppyne Date: Wed, 19 Aug 2026 07:43:22 +0530 Subject: [PATCH] fix: respect an "iat" of 0 instead of overwriting with current time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signing a token with `iat: 0` (the Unix epoch) silently discarded the value and used the current time instead, because both `sign.js` and `lib/timespan.js` used `iat || Math.floor(Date.now() / 1000)` — and `0` is falsy. Since that timestamp is also the base `timespan()` uses to derive `exp`/`nbf`, those claims were corrupted too whenever `iat` was 0. Guard with an explicit numeric check (`typeof iat === 'number' && !isNaN(iat)`) at both sites. `0` is now preserved, while the existing contract is kept: `NaN` still falls back to the current time and `Infinity` still serializes to `null`. Both sites must change, otherwise `timespan()` re-applies its own `0 || …` and `exp`/`nbf` stay wrong. Fixes #874. --- lib/timespan.js | 2 +- sign.js | 2 +- test/claim-iat.test.js | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/timespan.js b/lib/timespan.js index e5098690..cd8fb1e4 100644 --- a/lib/timespan.js +++ b/lib/timespan.js @@ -1,7 +1,7 @@ var ms = require('ms'); module.exports = function (time, iat) { - var timestamp = iat || Math.floor(Date.now() / 1000); + var timestamp = (typeof iat === 'number' && !isNaN(iat)) ? iat : Math.floor(Date.now() / 1000); if (typeof time === 'string') { var milliseconds = ms(time); diff --git a/sign.js b/sign.js index 82bf526e..7ffb7a03 100644 --- a/sign.js +++ b/sign.js @@ -182,7 +182,7 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { } } - const timestamp = payload.iat || Math.floor(Date.now() / 1000); + const timestamp = (typeof payload.iat === 'number' && !isNaN(payload.iat)) ? payload.iat : Math.floor(Date.now() / 1000); if (options.noTimestamp) { delete payload.iat; diff --git a/test/claim-iat.test.js b/test/claim-iat.test.js index a3dd474a..1799c5b6 100644 --- a/test/claim-iat.test.js +++ b/test/claim-iat.test.js @@ -110,6 +110,14 @@ describe('issue at', function() { expectedIssueAt: 100, options: {} }, + { + // regression for #874: iat of 0 (the Unix epoch) is a valid number and + // must be preserved, not treated as falsy and overwritten with "now". + description: 'should sign with provided time for "iat" of 0', + iat: 0, + expectedIssueAt: 0, + options: {} + }, // TODO an iat of -Infinity should fail validation { description: 'should set null "iat" when given -Infinity', @@ -153,6 +161,21 @@ describe('issue at', function() { }); }); }); + + // regression for #874: an "iat" of 0 must anchor "exp"/"nbf" too, not "now". + // The bug also affected timespan() (lib/timespan.js), which independently + // applied the same `iat || now` fallback, so exp/nbf were derived from "now". + it('should derive "exp" and "nbf" from an "iat" of 0', function (done) { + signWithIssueAt(0, {expiresIn: 86400, notBefore: 60}, (err, token) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + const decoded = jwt.decode(token); + expect(decoded.iat).to.equal(0); + expect(decoded.exp).to.equal(86400); + expect(decoded.nbf).to.equal(60); + }); + }); + }); }); describe('when verifying a token', function() {