From 8e446e498c9bcb212b13ad967575f7b1ec27a886 Mon Sep 17 00:00:00 2001 From: Abhishek Chauhan Date: Fri, 20 Mar 2026 08:35:06 -0500 Subject: [PATCH] fix: prevent callback from being called twice on payload conflict When payload contains a property that conflicts with options (e.g., payload has 'iss' but options has 'issuer'), the callback was being called twice: first with the error, then with the signed token. The root cause was using `forEach` with `return failure()`. The `return` inside a `forEach` callback only exits the current iteration, not the enclosing function. The loop continued and the function proceeded to call the callback again via `jws.createSign`. This fix replaces `forEach` with `for...of` so that `return` properly exits the enclosing function when a payload conflict is detected. Fixes #1000 --- sign.js | 4 +- test/issue_1000.tests.js | 112 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 test/issue_1000.tests.js diff --git a/sign.js b/sign.js index 82bf526..da1377a 100644 --- a/sign.js +++ b/sign.js @@ -214,7 +214,7 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { } } - Object.keys(options_to_payload).forEach(function (key) { + for (const key of Object.keys(options_to_payload)) { const claim = options_to_payload[key]; if (typeof options[key] !== 'undefined') { if (typeof payload[claim] !== 'undefined') { @@ -222,7 +222,7 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) { } payload[claim] = options[key]; } - }); + } const encoding = options.encoding || 'utf8'; diff --git a/test/issue_1000.tests.js b/test/issue_1000.tests.js new file mode 100644 index 0000000..2b4630c --- /dev/null +++ b/test/issue_1000.tests.js @@ -0,0 +1,112 @@ +'use strict'; + +const jwt = require('../'); +const expect = require('chai').expect; + +describe('issue 1000 - callback executed twice on payload conflict', function() { + it('should call the callback only once when payload has conflicting "iss" property', function(done) { + let callCount = 0; + + jwt.sign( + { iss: 'bar', iat: 1757476476 }, + 'secret', + { algorithm: 'HS256', issuer: 'foo' }, + (err, token) => { + callCount++; + + // The callback should only be called once + if (callCount > 1) { + done(new Error(`Callback was called ${callCount} times instead of once`)); + return; + } + + // Give time for potential second callback + setTimeout(() => { + expect(callCount).to.equal(1); + expect(err).to.be.instanceOf(Error); + expect(err.message).to.equal('Bad "options.issuer" option. The payload already has an "iss" property.'); + expect(token).to.be.undefined; + done(); + }, 50); + } + ); + }); + + it('should call the callback only once when payload has conflicting "sub" property', function(done) { + let callCount = 0; + + jwt.sign( + { sub: 'bar' }, + 'secret', + { algorithm: 'HS256', subject: 'foo' }, + (err, token) => { + callCount++; + + if (callCount > 1) { + done(new Error(`Callback was called ${callCount} times instead of once`)); + return; + } + + setTimeout(() => { + expect(callCount).to.equal(1); + expect(err).to.be.instanceOf(Error); + expect(err.message).to.equal('Bad "options.subject" option. The payload already has an "sub" property.'); + expect(token).to.be.undefined; + done(); + }, 50); + } + ); + }); + + it('should call the callback only once when payload has conflicting "aud" property', function(done) { + let callCount = 0; + + jwt.sign( + { aud: 'bar' }, + 'secret', + { algorithm: 'HS256', audience: 'foo' }, + (err, token) => { + callCount++; + + if (callCount > 1) { + done(new Error(`Callback was called ${callCount} times instead of once`)); + return; + } + + setTimeout(() => { + expect(callCount).to.equal(1); + expect(err).to.be.instanceOf(Error); + expect(err.message).to.equal('Bad "options.audience" option. The payload already has an "aud" property.'); + expect(token).to.be.undefined; + done(); + }, 50); + } + ); + }); + + it('should call the callback only once when payload has conflicting "jti" property', function(done) { + let callCount = 0; + + jwt.sign( + { jti: 'bar' }, + 'secret', + { algorithm: 'HS256', jwtid: 'foo' }, + (err, token) => { + callCount++; + + if (callCount > 1) { + done(new Error(`Callback was called ${callCount} times instead of once`)); + return; + } + + setTimeout(() => { + expect(callCount).to.equal(1); + expect(err).to.be.instanceOf(Error); + expect(err.message).to.equal('Bad "options.jwtid" option. The payload already has an "jti" property.'); + expect(token).to.be.undefined; + done(); + }, 50); + } + ); + }); +});