# Behavior traces — auth0/node-jsonwebtoken

> Canonical page: https://codevetter.com/unpack/jsonwebtoken/behavior-traces

# Behavior traces — auth0/node-jsonwebtoken

Scanned commit b924272f29192e12926b5414546f7c5bfcc9579d (2026-06-25). Generated by codevetter codevetter 1.12.0 on 2026-09-22.

Three flows matter: module load (feature detection), signing, and verification. Module load is unusual in that it bakes Node-version capability into module-scope constants, so the same code path behaves differently on Node 14 vs Node 16 without any runtime branch. Signing is a long linear gauntlet of eleven guards before any crypto happens. Verification is the inverse-ordered gauntlet, and its ordering is load-bearing: algorithm allow-listing and key-type checks must both precede `jws.verify`. Both entry functions branch at the very end into callback vs synchronous return.
- MODULE LOAD: `require('jsonwebtoken')` → index.js requires decode/verify/sign → sign.js requires `lib/psSupported` → `semver.satisfies(process.version, '^6.12.0 || >=8.0.0')` → if true, `SUPPORTED_ALGS.splice(3, 0, 'PS256','PS384','PS512')` executes at module scope. Algorithm support is therefore frozen at first require. — [index.js#L1-8](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/index.js#L1-L8) [sign.js#L2](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L2) [sign.js#L14-17](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L14-L17) [lib/psSupported.js#L1-3](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/psSupported.js#L1-L3)
- SIGN step 1-2: normalize arity (`typeof options === 'function'` → shift to callback), compute `isObjectPayload` as `typeof payload === 'object' && !Buffer.isBuffer(payload)`, then build `header` via `Object.assign`. — [sign.js#L86-101](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L86-L101)
- SIGN step 3-4: reject missing key unless `algorithm === 'none'`, then coerce to `KeyObject` via `createPrivateKey` → `createSecretKey` fallback. — [sign.js#L110-124](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L110-L124)
- SIGN step 5: enforce symmetry — `header.alg.startsWith('HS')` requires `key.type === 'secret'`; `/^(?:RS|PS|ES)/` requires `key.type === 'private'`; and for non-ES RSA algs, `asymmetricKeyDetails.modulusLength < 2048` is rejected (guarded by an `!== undefined` check for pre-Node-15 where the field is absent). — [sign.js#L126-138](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L126-L138)
- SIGN step 6-8: `validatePayload` (allowUnknown=true, checks only iat/exp/nbf are numbers) → clone payload unless `mutatePayload` → reject `exp`+`expiresIn` and `nbf`+`notBefore` double-specification → `validateOptions` (allowUnknown=false). — [sign.js#L65-67](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L65-L67) [sign.js#L140-175](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L140-L175)
- SIGN step 9-11: `validateAsymmetricKey(header.alg, key)` unless opted out → compute `timestamp` and apply `noTimestamp`/`iat` → `timespan` for `nbf` and `exp` → copy the four `options_to_payload` claims. — [sign.js#L177-225](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L177-L225) [lib/validateAsymmetricKey.js#L16-30](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/validateAsymmetricKey.js#L16-L30)
- SIGN terminal branch (async): wrap callback in `once()`, then `jws.createSign({header, privateKey, payload, encoding})` with `.once('error', callback)` and `.once('done', ...)`; the done handler re-checks `signature.length < 256` as a pre-Node-15 proxy for the 2048-bit modulus rule. — [sign.js#L229-244](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L229-L244)
- SIGN terminal branch (sync): `jws.sign({header, payload, secret, encoding})` returns the token, followed by the identical `signature.length < 256` check which `throw`s rather than calling back. Both branches carry a `TODO: Remove ... once node 15+ is the minimum supported version`. — [sign.js#L239-251](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L239-L251)
- VERIFY step 1-4: arity normalization → clone options → build `done` (callback or throw-on-error closure) → validate `clockTimestamp`/`nonce`/`allowInvalidAsymmetricKeyTypes` shapes → compute `clockTimestamp` → reject falsy or non-string `jwtString` → split into exactly 3 parts. — [verify.js#L21-71](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L21-L71)
- VERIFY step 5-6: `decode(jwtString, {complete: true})` inside a try/catch (returns `invalid token` if falsy), then choose `getSecret` — either the caller's key-lookup function or a trivial wrapper that immediately yields the static key. All remaining logic runs inside that callback. — [verify.js#L73-101](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L73-L101)
- VERIFY step 7: the three-way signature/key presence matrix — signature-without-key → `secret or public key must be provided`; key-without-signature → `jwt signature is required`; neither-plus-no-algorithms → `please specify "none" in "algorithms"`. — [verify.js#L106-118](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L106-L118) [test/undefined_secretOrPublickey.tests.js#L8-17](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/undefined_secretOrPublickey.tests.js#L8-L17)
- VERIFY step 8-10 (the security core, in this exact order): coerce key → derive default `algorithms` from key type → reject `header.alg` not in the list (`invalid algorithm`) → reject HS-alg-with-non-secret-key and RS/PS/ES-alg-with-non-public-key → `validateAsymmetricKey` → only then `jws.verify`. — [verify.js#L120-172](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L120-L172)
- VERIFY step 11 (claims, in order): `nbf` (type check then `> clockTimestamp + clockTolerance` → `NotBeforeError`) → `exp` (→ `TokenExpiredError`) → `audience` → `issuer` → `subject` → `jwtid` → `nonce` → `maxAge` (→ `TokenExpiredError('maxAge exceeded')`) → return payload or the `complete` triple. — [verify.js#L176-261](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L176-L261)
- KEY-CONFUSION TRACE: an attacker-forged `{alg: HS256, kid: rsaKeyId}` token presented with the legitimate RSA public key reaches line 148, where `header.alg.startsWith('HS') && key.type !== 'secret'` throws `secretOrPublicKey must be a symmetric key when using HS256` — before `jws.verify` is ever called. The test asserts this for both KeyObject and PEM forms. — [verify.js#L148-152](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L148-L152) [test/jwt.malicious.tests.js#L21-31](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/jwt.malicious.tests.js#L21-L31)

Every claim links to its cited files at the pinned commit. Citations identify source evidence; they do not establish runtime correctness. Independent CodeVetter scan — not reviewed by upstream maintainers.

## Public product links

- [CodeVetter](https://codevetter.com/)
- [Download](https://codevetter.com/download)
- [Documentation](https://codevetter.com/docs/)
- [Source](https://github.com/Codevetter/codevetter)
