Skip to content

fix: prevent callback from being called twice on payload conflict#1017

Open
abhu85 wants to merge 1 commit intoauth0:masterfrom
abhu85:fix/callback-executed-twice
Open

fix: prevent callback from being called twice on payload conflict#1017
abhu85 wants to merge 1 commit intoauth0:masterfrom
abhu85:fix/callback-executed-twice

Conversation

@abhu85
Copy link

@abhu85 abhu85 commented Mar 20, 2026

Summary

Fixes #1000

When jwt.sign() is called with a callback and the payload contains a property that conflicts with options (e.g., payload has iss but options has issuer), the callback was being called twice:

  1. First with the error: Bad "options.issuer" option. The payload already has an "iss" property.
  2. Then with the signed token (ignoring the conflict)

Root Cause

In sign.js around line 217, a forEach loop was used to check for payload/options conflicts:

Object.keys(options_to_payload).forEach(function (key) {
  // ...
  if (typeof payload[claim] !== 'undefined') {
    return failure(new Error('Bad "options.' + key + '" option...'));
  }
  // ...
});

The return inside a forEach callback only exits the current iteration, not the enclosing function. The loop continued and execution reached the async signing code, causing the callback to be invoked a second time.

Fix

Replace forEach with for...of so that return properly exits the enclosing function:

for (const key of Object.keys(options_to_payload)) {
  // ...
  if (typeof payload[claim] !== 'undefined') {
    return failure(new Error('Bad "options.' + key + '" option...'));
  }
  // ...
}

Test Plan

  • Added test cases that verify the callback is called exactly once for all four conflicting properties (iss/issuer, sub/subject, aud/audience, jti/jwtid)
  • All existing tests pass (515 passing)
  • Manually verified the fix with the reproduction case from the issue

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 auth0#1000
@abhu85 abhu85 requested a review from a team as a code owner March 20, 2026 13:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

jwt.sign() callback is executed twice for "The payload already has an "..." property" errors

1 participant