# auth0/node-jsonwebtoken — codebase unpack

> Canonical page: https://codevetter.com/unpack/jsonwebtoken

This is a deterministic Repo Unpack scan of auth0/node-jsonwebtoken — a structural map, not a review, security audit, or maintainer-endorsed description.

- Scanned commit: b924272f29192e12926b5414546f7c5bfcc9579d (2026-06-25)
- Last upstream commit: ci: replace npx changelog call with npm script and fix prepare-release workflow (#1037)
- Generated by codevetter 1.12.0 on 2026-09-22
- 84 files scanned · 238 KB

## Languages

- JavaScript: 46 files
- YAML: 6 files
- JSON: 5 files
- Markdown: 2 files

## Repository structure

- test/ — 52 files
- lib/ — 8 files
- .github/ — 5 files
- .husky/ — 1 files
- bin/ — 1 files

Probable source entrypoint: `index.js` (inferred from the tree, not upstream docs)

## Source layout — lib/

- lib/JsonWebTokenError.js — 1 file
- lib/NotBeforeError.js — 1 file
- lib/TokenExpiredError.js — 1 file
- lib/asymmetricKeyDetailsSupported.js — 1 file
- lib/psSupported.js — 1 file
- lib/rsaPssKeyDetailsSupported.js — 1 file
- lib/timespan.js — 1 file
- lib/validateAsymmetricKey.js — 1 file

## Package

- package.json: jsonwebtoken v9.0.3 — 24 declared dependencies

## Structural health signals

Average score 8.8/10 across 45 files; 0 hotspots. These are deterministic review leads, not proof of a bug.

## Recent history

- b924272f2919 (2026-06-25) ci: replace npx changelog call with npm script and fix prepare-release workflow (#1037)
- 02688b982132 (2026-05-01) ci: switch to conventionalcommits preset for semantic-release (#1020)
- cf33b5f59a19 (2026-03-18) ci: add semantic-release automation (#1015)
- 2db36ae04eae (2026-03-18) chore: add CODEOWNERS file (#1014)
- ed59e76ea37a (2025-12-04) chore: bump jws to 4.0.1 (#1007)

## Overview

jsonwebtoken is Auth0's reference Node implementation of RFC 7519 JSON Web Tokens, published to npm as `jsonwebtoken` v9.0.3. It is a tiny, dependency-light library (three public functions — `sign`, `verify`, `decode` — plus three error classes) that wraps `jws` for the actual JWS signing/verification and layers on claim semantics (exp/nbf/aud/iss/sub/jti/nonce/maxAge), option schema validation, and a hard-won set of key-confusion defenses. What is distinctive is how much of the code is defensive rather than functional: roughly half of sign.js and verify.js is dedicated to coercing untrusted key material into `crypto.KeyObject`s, rejecting alg/key-type mismatches (the classic RS256→HS256 confusion attack), and enforcing a 2048-bit RSA modulus floor. It targets any Node service that issues or consumes JWTs, and ships both callback-async and synchronous-throwing modes of every entrypoint.

## System map

This is a pure library with no server, no database, and no network I/O of its own — the only runtime boundary is the Node `crypto` module and the `jws` package. `index.js` is the whole public API surface: it re-exports `decode.js`, `verify.js`, `sign.js`, and the three error classes from `lib/`. Support modules in `lib/` are all single-purpose: feature-detection booleans gated on `process.version` via `semver`, a `ms`-backed timespan parser, and the asymmetric key/algorithm compatibility matrix. Build is nonexistent (plain CommonJS, no transpile); test is `mocha`, coverage is `nyc mocha --use_strict`, lint is `eslint .`. Deployment is npm publish driven by `semantic-release` in a two-workflow release dance on `master`.
- `index.js` is the sole entrypoint and exports exactly six symbols: `decode`, `verify`, `sign`, `JsonWebTokenError`, `NotBeforeError`, `TokenExpiredError` — there is no class, no factory, no configuration object. — [index.js#L1-8](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/index.js#L1-L8)
- All cryptography is delegated to the `jws` package (`^4.0.1`): `jws.decode` in decode.js, `jws.sign`/`jws.createSign` in sign.js, and `jws.verify` in verify.js. This repo contains no hashing, HMAC, or signature code of its own. — [decode.js#L1-5](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/decode.js#L1-L5) [sign.js#L232-251](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L232-L251) [verify.js#L165](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L165) [package.json#L40-51](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L40-L51)
- The runtime dependency set is deliberately minimal and old-school: `jws`, `ms`, `semver`, and seven separately-published single-function lodash packages (`lodash.includes`, `lodash.isboolean`, `lodash.isinteger`, `lodash.isnumber`, `lodash.isplainobject`, `lodash.isstring`, `lodash.once`). — [package.json#L40-51](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L40-L51) [sign.js#L5-11](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L5-L11)
- Node version capability is resolved at require-time, not call-time, by three boolean modules that `semver.satisfies(process.version, ...)`: `lib/psSupported.js` (`^6.12.0 || >=8.0.0`), `lib/asymmetricKeyDetailsSupported.js` (`>=15.7.0`), and `lib/rsaPssKeyDetailsSupported.js` (`>=16.9.0`). — [lib/psSupported.js#L1-3](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/psSupported.js#L1-L3) [lib/asymmetricKeyDetailsSupported.js#L1-3](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/asymmetricKeyDetailsSupported.js#L1-L3) [lib/rsaPssKeyDetailsSupported.js#L1-3](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/rsaPssKeyDetailsSupported.js#L1-L3)
- PS256/PS384/PS512 are spliced into the algorithm allow-lists at module load only when `PS_SUPPORTED` — into `SUPPORTED_ALGS` at index 3 in sign.js, and appended to both `PUB_KEY_ALGS` and `RSA_KEY_ALGS` in verify.js. — [sign.js#L14-17](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L14-L17) [verify.js#L11-19](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L11-L19)
- The error hierarchy is hand-rolled ES5 prototype inheritance, not `class extends Error`: `JsonWebTokenError` calls `Error.call(this, message)` and manually sets `this.message`, and `NotBeforeError`/`TokenExpiredError` do `Object.create(JsonWebTokenError.prototype)` and add a `date`/`expiredAt` field. — [lib/JsonWebTokenError.js#L1-14](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/JsonWebTokenError.js#L1-L14) [lib/NotBeforeError.js#L1-13](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/NotBeforeError.js#L1-L13) [lib/TokenExpiredError.js#L1-13](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/TokenExpiredError.js#L1-L13)
- `lib/validateAsymmetricKey.js` owns the key-type↔algorithm compatibility matrix (`allowedAlgorithmsForKeys` for `ec`/`rsa`/`rsa-pss`) and the ES-curve mapping (`allowedCurves`: ES256→prime256v1, ES384→secp384r1, ES512→secp521r1). It is the single shared gate called by both sign and verify. — [lib/validateAsymmetricKey.js#L4-30](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/validateAsymmetricKey.js#L4-L30) [sign.js#L177-183](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L177-L183) [verify.js#L154-160](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L154-L160)
- `lib/timespan.js` is the only time-arithmetic module; it converts a `ms`-parseable string or a raw number into an absolute epoch-seconds timestamp, and returns `undefined` (not an error) for unparseable input, pushing error construction onto its three callers. — [lib/timespan.js#L1-18](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/timespan.js#L1-L18) [sign.js#L193-215](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L193-L215) [verify.js#L242-245](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L242-L245)
- Developer commands are `npm test` (bare `mocha`), `npm run coverage` (`nyc mocha --use_strict`), `npm run lint` (`eslint .`), and `npm run update-changelog` (`conventional-changelog -p conventionalcommits -i CHANGELOG.md`). `npm run prepare` installs husky hooks. — [package.json#L21-27](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L21-L27)
- CI is five GitHub Actions workflows: `test.yml` (PR-triggered matrix over Node 16/18/20/22/24), `commitlint.yml` (PR-triggered conventional-commit validation), `prepare-release.yml` and `release.yml` (both on push to master, mutually exclusive via a `chore(release):` head-commit-message guard), and `sca-scan.yml` (Snyk, via a pinned reusable workflow from `auth0/devsecops-tooling`). — [.github/workflows/test.yml#L1-38](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/test.yml#L1-L38) [.github/workflows/commitlint.yml](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/commitlint.yml) [.github/workflows/prepare-release.yml#L1-20](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/prepare-release.yml#L1-L20) [.github/workflows/release.yml#L1-25](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/release.yml#L1-L25) [.github/workflows/sca-scan.yml](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/sca-scan.yml)
- Publishing is `semantic-release` configured in `.releaserc.json` on the `master` branch, with conventional-commits analysis, npm publish from repo root, a `prepareCmd` of `git diff --exit-code`, and a `verifyReleaseCmd` that runs a ReversingLabs `rl-wrapper` scan over the `npm pack` tarball before release. — [.releaserc.json#L1-32](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.releaserc.json#L1-L32) [.github/workflows/release.yml#L58-72](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/release.yml#L58-L72)
- The published npm tarball is narrowed by the `files` allow-list to `lib`, `decode.js`, `sign.js`, `verify.js` — tests, `bin/`, and workflows never ship. `.npmignore` additionally excludes `opslevel.yml`. — [package.json#L72-77](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L72-L77) [.npmignore](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.npmignore)
- Repo ownership/routing is declared for Auth0 internal tooling: `CODEOWNERS` assigns everything to `@auth0/project-iam-protocols-engineer-codeowner` and `opslevel.yml` tags the repo to the `iam_protocols` owner. — [CODEOWNERS](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/CODEOWNERS) [opslevel.yml](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/opslevel.yml)
- `package.json` declares `engines.node >= 12`, but the test matrix only exercises Node 16 through 24 — the 12/14 support claim is unverified by CI. — [package.json#L68-71](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L68-L71) [.github/workflows/test.yml#L16-19](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/test.yml#L16-L19)

## Feature catalog

The user-facing surface is three functions, each with a dual sync/async calling convention selected by whether a trailing callback is supplied. `sign` turns a payload plus key into a compact JWS, mapping option names onto registered claim names and computing time-based claims. `verify` is the security-critical path: it re-decodes, resolves a key (possibly via an async key-lookup callback for JWKS/kid workflows), enforces an algorithm allow-list, checks key/alg compatibility, verifies the signature, then runs seven independent claim checks. `decode` is the deliberately unauthenticated escape hatch. Twelve signing algorithms plus `none` are supported, and a set of explicit backwards-compatibility escape valves (`allowInsecureKeySizes`, `allowInvalidAsymmetricKeyTypes`) let callers opt out of the newer hardening.
- `jwt.sign(payload, secretOrPrivateKey, [options, callback])` supports thirteen options validated by `sign_options_schema`: `expiresIn`, `notBefore`, `audience`, `algorithm`, `header`, `encoding`, `issuer`, `subject`, `jwtid`, `noTimestamp`, `keyid`, `mutatePayload`, `allowInsecureKeySizes`, `allowInvalidAsymmetricKeyTypes` — and rejects any key not in that schema with `"<key>" is not allowed in "options"`. — [sign.js#L19-34](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L19-L34) [sign.js#L42-63](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L42-L63)
- Four sign options are sugar that write registered claims, via the `options_to_payload` table: `audience`→`aud`, `issuer`→`iss`, `subject`→`sub`, `jwtid`→`jti`. Supplying both the option and the raw claim is an error (`Bad "options.<x>" option. The payload already has an "<claim>" property.`). — [sign.js#L69-74](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L69-L74) [sign.js#L217-225](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L217-L225)
- Non-object payloads (strings, numbers, Buffers) are supported for signing but reject the seven object-only options listed in `options_for_objects` with `invalid <opts> option for <typeof> payload`; a Buffer payload round-trips as its string form. — [sign.js#L76-84](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L76-L84) [sign.js#L152-160](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L152-L160) [test/buffer.tests.js#L1-10](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/buffer.tests.js#L1-L10) [test/non_object_values.tests.js#L6-18](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/non_object_values.tests.js#L6-L18)
- `mutatePayload: false` (the default) shallow-clones the payload with `Object.assign({}, payload)` before adding `iat`/`exp`/`nbf`, which is what makes signing a frozen or sealed payload work. — [sign.js#L149-151](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L149-L151) [test/issue_147.tests.js#L6-11](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/issue_147.tests.js#L6-L11)
- `options.encoding` (default `utf8`) is passed through to `jws`, so `{encoding: 'binary'}` produces a differently-encoded payload segment for non-ASCII characters. — [sign.js#L227](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L227) [sign.js#L246](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L246) [test/encoding.tests.js#L11-25](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/encoding.tests.js#L11-L25)
- `jwt.verify(token, secretOrPublicKey, [options, callback])` supports thirteen options: `algorithms`, `audience`, `complete`, `issuer`, `jwtid`, `ignoreExpiration`, `ignoreNotBefore`, `subject`, `clockTolerance`, `maxAge`, `clockTimestamp`, `nonce`, `allowInvalidAsymmetricKeyTypes`. — [verify.js#L45-57](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L45-L57) [verify.js#L176-249](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L176-L249) [README.md#L145-165](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L145-L165)
- `secretOrPublicKey` may be a key-lookup function `(header, cb)` — the JWKS/`kid` integration point — but only when a callback is also supplied; otherwise verify fails with `verify must be called asynchronous if secret or public key is provided as a callback`. — [verify.js#L88-99](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L88-L99) [verify.js#L101-103](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L101-L103)
- Audience matching accepts a string, a `RegExp`, or an array of either, and matches if ANY token `aud` entry matches ANY expected audience — an any-to-any `some`/`some` cross product. — [verify.js#L194-207](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L194-L207) [README.md#L151-152](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L151-L152)
- `clockTolerance` (seconds of slack) and `clockTimestamp` (override for "now") apply to `nbf`, `exp`, and `maxAge` uniformly; `clockTimestamp` must be a number or verify fails early with `clockTimestamp must be a number`. — [verify.js#L45-47](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L45-L47) [verify.js#L57](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L57) [verify.js#L180](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L180) [verify.js#L189](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L189) [verify.js#L246](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L246)
- `maxAge` is an independent expiry ceiling computed from `payload.iat`, requires a numeric `iat` (`iat required when maxAge is specified`), and cannot loosen `exp` — a token past `exp` still fails even with `maxAge: '1000y'`. — [verify.js#L237-249](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L237-L249) [test/verify.tests.js#L251-269](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/verify.tests.js#L251-L269)
- `complete: true` on either `verify` or `decode` returns `{header, payload, signature}` instead of the bare payload — this is how callers reach `kid` and `alg` for key selection. — [verify.js#L251-259](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L251-L259) [decode.js#L19-28](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/decode.js#L19-L28)
- `jwt.decode(token, options)` performs no signature verification at all, and silently swallows a `JSON.parse` failure on the payload — returning the raw string payload rather than throwing. — [decode.js#L3-17](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/decode.js#L3-L17)
- Unsigned (`alg: none`) tokens are supported on both sides but require explicit opt-in on verify: `please specify "none" in "algorithms" to verify unsigned tokens`, and passing a key alongside a signature-less token yields `jwt signature is required`. — [verify.js#L106-118](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L106-L118) [test/verify.tests.js#L32-80](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/verify.tests.js#L32-L80)
- Two named backwards-compatibility escape hatches exist and are documented as discouraged: `allowInsecureKeySizes` (permits RSA modulus < 2048) and `allowInvalidAsymmetricKeyTypes` (skips the `validateAsymmetricKey` gate). — [sign.js#L132-137](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L132-L137) [sign.js#L177-183](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L177-L183) [verify.js#L154-160](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L154-L160) [README.md#L38](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L38) [README.md#L165](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L165)

## Data flow

There are exactly two input boundaries — the caller-supplied `(payload | jwtString, key, options)` tuple — and one output boundary, either a returned/thrown value or a `(err, result)` callback invocation. No state is owned across calls: the library is entirely stateless, with the only module-level mutable structures being the algorithm allow-list arrays built at require time. The central transform on both paths is key normalization: whatever the caller passes (string, Buffer, PEM, JWK-ish object) is funnelled through `crypto.createPrivateKey`/`createPublicKey` with a `createSecretKey` fallback into a `KeyObject`, and every downstream decision reads `key.type`, `key.asymmetricKeyType`, and `key.asymmetricKeyDetails` off that normalized object. Claim data flows one way on sign (options → payload properties → JWS) and one way on verify (JWS → decoded payload → claim assertions → returned payload).
- On sign, key material normalization is a try/catch cascade: if not already a `KeyObject`, attempt `createPrivateKey(secretOrPrivateKey)`, fall back to `createSecretKey(Buffer.from(...))` for strings, and on double failure emit `secretOrPrivateKey is not valid key material`. — [sign.js#L114-124](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L114-L124)
- Verify mirrors that cascade with `createPublicKey` then `createSecretKey`, producing `secretOrPublicKey is not valid key material` — this is what converts a hostile object with a throwing `toString` into a clean error instead of arbitrary execution. — [verify.js#L120-130](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L120-L130) [test/jwt.malicious.tests.js#L33-38](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/jwt.malicious.tests.js#L33-L38)
- The JWS header is assembled by `Object.assign` with a fixed precedence: computed `{alg, typ, kid}` first (alg defaults to `HS256`, `typ` is `'JWT'` only for object payloads, `kid` comes from `options.keyid`), then `options.header` spread last so it can override any of them including `alg`. — [sign.js#L94-101](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L94-L101)
- A single `timestamp` value — `payload.iat || Math.floor(Date.now()/1000)` — is computed once and is the base for both `nbf` and `exp`, guaranteeing the three time claims are internally consistent within one sign call. — [sign.js#L185-215](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L185-L215)
- On verify, the token is parsed twice: once shallowly as `jwtString.split('.')` for arity (`jwt malformed` when length !== 3) and signature-presence checks on `parts[2]`, and once fully through `decode(jwtString, {complete: true})`. — [verify.js#L67-83](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L67-L83) [verify.js#L106](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L106)
- `options` is defensively shallow-cloned on entry to verify (`Object.assign({}, options)`) specifically because the function later writes `options.algorithms` when the caller omitted it — caller-supplied objects are never mutated. — [verify.js#L31-32](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L31-L32) [verify.js#L132-142](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L132-L142)
- When `options.algorithms` is absent, the allow-list is derived from the normalized key: `secret`→HS_ALGS, `rsa`/`rsa-pss`→RSA_KEY_ALGS, `ec`→EC_KEY_ALGS, everything else→PUB_KEY_ALGS. This binds the accepted alg set to the key material, not to the token. — [verify.js#L132-142](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L132-L142) [README.md#L146-150](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L146-L150)
- The token-supplied `header.alg` flows into `jws.verify(jwtString, decodedToken.header.alg, secretOrPublicKey)`, but only after passing the allow-list membership check and the key-type compatibility check — the ordering at lines 144→152→165 is the whole defense. — [verify.js#L144-165](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L144-L165)
- The verified `payload` object returned to the caller is the same object produced by `decode`, not a copy — all seven claim checks read from it without transforming it, so callers receive attacker-influenced data verbatim (the README explicitly warns to sanitize it). — [verify.js#L174](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L174) [verify.js#L251-261](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L251-L261) [README.md#L132](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L132)
- Both functions funnel every error through a single sink — `failure(err)` in sign and `done(err, data)` in verify — which dispatches to `callback(err)` when async or `throw err` when sync, so sync and async modes are guaranteed to produce identical errors. — [sign.js#L103-108](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L103-L108) [verify.js#L34-43](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L34-L43) [test/test-utils.js#L28-37](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/test-utils.js#L28-L37)
- `decode` normalizes the payload channel: `jws.decode` may return a string payload, which `decode` opportunistically `JSON.parse`s and keeps only if the result is a non-null object — otherwise the string passes through unchanged. — [decode.js#L7-17](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/decode.js#L7-L17)
- `timespan(time, iat)` is the shared conversion from relative to absolute time on both paths: it feeds `payload.nbf`/`payload.exp` on sign and `maxAgeTimestamp` on verify, using `ms()` for strings and plain addition for numbers. — [lib/timespan.js#L3-17](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/timespan.js#L3-L17) [sign.js#L195](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L195) [sign.js#L207](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L207) [verify.js#L242](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L242)
- The only file-system reads in the entire repo are in tests, loading PEM fixtures from `test/*.pem` — the library itself performs zero file or network I/O. — [test/jwt.asymmetric_signing.tests.js#L10-12](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/jwt.asymmetric_signing.tests.js#L10-L12) [test/schema.tests.js#L9-12](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/schema.tests.js#L9-L12) [test/verify.tests.js#L12-13](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/verify.tests.js#L12-L13)

## Behavior traces

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)

## Testing signals

Testing is the strongest part of the repo: ~258 `it()` blocks across 30 mocha files in `test/`, backed by chai assertions, sinon fake timers, and 17 checked-in PEM fixtures covering RSA, RSA-PSS, EC (three curves), DSA, and deliberately-invalid keys. The signature move is `test/test-utils.js`, which runs every sign/verify twice — once sync, once async — under a frozen clock and asserts the two produce byte-identical tokens or deep-equal errors; this is why the sync/async parity invariant actually holds. Coverage is configured with hard nyc thresholds, but the CI workflow runs bare `mocha`, so those thresholds and eslint are never enforced on PRs. Test naming is split between a per-claim convention (`claim-*.test.js`, `option-*.test.js`) and older topic/regression files (`*.tests.js`, `issue_*.tests.js`).
- The framework stack is mocha 5 + chai 4 + sinon 6, with `atob` used to independently decode base64url segments in encoding tests. There is no `.mocharc`, so mocha's default `./test/*.js` glob picks up every file in `test/`. — [package.json#L52-67](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L52-L67) [test/encoding.tests.js#L3](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/encoding.tests.js#L3) [test/verify.tests.js#L5-9](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/verify.tests.js#L5-L9)
- `test/test-utils.js` exports `signJWTHelper` and `verifyJWTHelper`, both of which freeze time with `sinon.useFakeTimers({now: Date.now()})`, run the sync call in a try/catch, then run the async call and assert equality of token or error before restoring the clock. — [test/test-utils.js#L59-118](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/test-utils.js#L59-L118)
- `expectEqualError` exists because chai does not deep-equal errors (the helper cites chaijs/chai#1009); it compares `message` and `name` explicitly plus every enumerable property, which is how `expiredAt`/`date` payloads on the error subclasses get verified. — [test/test-utils.js#L27-37](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/test-utils.js#L27-L37)
- Option/claim validation is tested exhaustively by table-driven loops over ~15 bad values (`true, false, null, -1.1, Infinity, NaN, ' ', '', [], ['foo'], {}, {foo:'bar'}` …), applied per-claim in `claim-exp.test.js`, `header-kid.test.js`, and siblings. — [test/claim-exp.test.js#L19-45](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/claim-exp.test.js#L19-L45) [test/header-kid.test.js#L18-40](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/header-kid.test.js#L18-L40)
- `test/claim-aud.test.js` is the largest test file at 436 lines and 36 `it()` blocks, reflecting that audience matching (string/regex/array cross-product) is the most branch-heavy claim check. — [test/claim-aud.test.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/claim-aud.test.js) [verify.js#L194-207](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L194-L207)
- `test/jwt.asymmetric_signing.tests.js` is parameterized: it builds an `algorithms` map of RS256/ES256 (plus PS256 when `PS_SUPPORTED`) each with `pub_key`, `priv_key`, and an `invalid_pub_key` that is deliberately not the pair of `priv_key`, then runs the same describe block per algorithm. — [test/jwt.asymmetric_signing.tests.js#L13-46](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/jwt.asymmetric_signing.tests.js#L13-L46)
- Security regressions are explicitly locked in by `test/jwt.malicious.tests.js`: HMAC-with-RSA-key confusion (both KeyObject and PEM form) and the malicious-`toString` Buffer-impersonation RCE vector. — [test/jwt.malicious.tests.js#L21-38](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/jwt.malicious.tests.js#L21-L38)
- Historical bugs are pinned by dedicated regression files: `issue_70.tests.js` (PEM headers), `issue_147.tests.js` (signing an `Object.seal`ed payload), `issue_304.tests.js`, and `rsa-public-key.tests.js`. — [test/issue_70.tests.js#L1-15](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/issue_70.tests.js#L1-L15) [test/issue_147.tests.js#L6-11](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/issue_147.tests.js#L6-L11) [test/issue_304.tests.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/issue_304.tests.js) [test/rsa-public-key.tests.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/rsa-public-key.tests.js)
- `test/validateAsymmetricKey.tests.js` unit-tests the key-matrix module directly (not through sign/verify), including the no-key and no-algorithm short-circuit paths and the PS/curve branches gated on the feature-detection booleans. — [test/validateAsymmetricKey.tests.js#L1-50](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/validateAsymmetricKey.tests.js#L1-L50) [lib/validateAsymmetricKey.js#L16-21](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/validateAsymmetricKey.js#L16-L21)
- nyc is configured with `check-coverage: true` and thresholds of 95% lines, 95% statements, 100% functions, 95% branches, excluding `./test/**` — a genuinely strict bar. — [package.json#L6-20](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L6-L20)
- GAP: `test.yml` runs only `npm test` (bare mocha). Neither `npm run coverage` nor `npm run lint` appears in any workflow, so the nyc thresholds and the eslint ruleset are advisory only and can silently regress. — [.github/workflows/test.yml#L31-38](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/test.yml#L31-L38) [package.json#L21-27](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L21-L27)
- A known-broken case is parked rather than fixed: `it.skip('should work with none algorithm where secret is falsy')` in async_sign, annotated with a link to node-jws issue #62 — the async `alg: none` path with a falsy secret is untested and known not to work. — [test/async_sign.tests.js#L45-52](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/async_sign.tests.js#L45-L52)
- A coverage hole is deliberately carved out with `/* istanbul ignore next */` around the entire `ASYMMETRIC_KEY_DETAILS_SUPPORTED` block in validateAsymmetricKey, with a comment explaining it would otherwise sink coverage on Node < 15.7.0 — meaning EC curve matching and RSA-PSS parameter checks are excluded from the coverage numbers. — [lib/validateAsymmetricKey.js#L32-65](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/validateAsymmetricKey.js#L32-L65)
- Test fixtures include intentionally-bad keys used as negative cases: `invalid_pub.pem`, `ecdsa-public-invalid.pem`, `rsa-pss-invalid-salt-length-private.pem`, and `dsa-public.pem` (to assert `Unknown key type "dsa".`). — [test/verify.tests.js#L272-282](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/verify.tests.js#L272-L282) [lib/validateAsymmetricKey.js#L24-26](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/validateAsymmetricKey.js#L24-L26)

## Risk map

As a library whose entire job is security, nearly every line in verify.js is a security-sensitive path, and the ordering of its checks is the load-bearing invariant — any reordering that lets `jws.verify` run before the algorithm allow-list check reintroduces the classic JWT confusion attacks. The most concrete correctness defect is in sign.js's `options_to_payload` loop, where `return failure(...)` inside a `forEach` callback does not abort the enclosing function, so in async mode the callback can fire twice. Secondary risks cluster around Node-version-conditional code that CI does not fully exercise, deliberately silent error swallowing in `decode`, a very old eslint pinned at v4, and release workflows that handle several third-party secrets. Blast radius for any change here is unusually wide: this package sits in the dependency tree of a large fraction of Node auth stacks.
- CORRECTNESS BUG: in the `options_to_payload` loop, `return failure(...)` only returns from the `forEach` arrow — the outer sign function continues. In async mode `failure` invokes the raw callback with an error at line 221, and since `callback` is not yet wrapped by `once()` (that happens at line 230), signing proceeds and the callback is invoked a second time with a valid token. — [sign.js#L217-225](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L217-L225) [sign.js#L229-244](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L229-L244)
- SECURITY-CRITICAL ORDERING: the allow-list check (`options.algorithms.indexOf(header.alg) === -1`), the key-type symmetry check, and `validateAsymmetricKey` all sit between `decode` and `jws.verify`. Any edit that moves `jws.verify` earlier, or that trusts `header.alg` before line 144, reopens the RS256→HS256 confusion class. — [verify.js#L144-172](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L144-L172) [test/jwt.malicious.tests.js#L21-31](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/jwt.malicious.tests.js#L21-L31)
- `options.header` is `Object.assign`ed LAST when building the sign header, so a caller passing `{header: {alg: 'none'}}` silently overrides `options.algorithm` — and the downstream key/alg checks then run against the overridden value. This is by design but is a sharp edge for callers who build headers from user input. — [sign.js#L97-101](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L97-L101) [sign.js#L126-138](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L126-L138)
- `decode` swallows all `JSON.parse` errors in an empty `catch (e) { }`, returning the raw string payload instead. Callers that assume an object back from `decode` will see a string on malformed tokens with no signal. — [decode.js#L10-17](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/decode.js#L10-L17)
- The `signature.length < 256` heuristic is an acknowledged stopgap for the 2048-bit modulus rule on Node < 15, duplicated in both the sync and async branches with matching `TODO: Remove ...` comments — a two-site invariant that can drift. — [sign.js#L239-241](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L239-L241) [sign.js#L247-249](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L247-L249)
- No source file uses `'use strict'`, so sloppy-mode semantics apply — e.g. `delete payload.iat` on a primitive string payload under `noTimestamp` is a silent no-op rather than a TypeError. Notably `npm run coverage` runs mocha with `--use_strict` while `npm test` does not, so the two commands exercise different semantics. — [sign.js#L187-191](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L187-L191) [package.json#L23-24](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L23-L24)
- The EC curve check dereferences `key.asymmetricKeyDetails.namedCurve` without a null guard; it is protected only by the module-load `ASYMMETRIC_KEY_DETAILS_SUPPORTED` semver boolean, and that whole block is excluded from coverage by `istanbul ignore next`. — [lib/validateAsymmetricKey.js#L38-48](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/validateAsymmetricKey.js#L38-L48)
- The verified payload returned by `verify` is the same object `decode` produced, unsanitized and fully attacker-controlled in content; the README carries an explicit warning to treat it as user input, which implies downstream misuse is a known recurring problem. — [verify.js#L174-261](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L174-L261) [README.md#L132](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L132)
- `allowInvalidAsymmetricKeyTypes: true` disables `validateAsymmetricKey` on both sign and verify, and a test asserts it lets an ES256-labelled token be processed with an RSA key — a documented footgun kept only for backwards compatibility. — [verify.js#L154-160](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L154-L160) [test/verify.tests.js#L293-300](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/verify.tests.js#L293-L300) [README.md#L165](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L165)
- `release.yml` handles six secrets (`PRODSEC_TOOLS_ARN`, `PRODSEC_TOOLS_USER`, `PRODSEC_TOOLS_TOKEN`, `RLSECURE_LICENSE`, `RLSECURE_SITE_KEY`, `SIGNAL_HANDLER_TOKEN`) and embeds credentials in a pip `--index-url`. Mitigations are in place — `persist-credentials: false`, `mask-aws-account-id: true`, SHA-pinned actions — but this job is the highest-value supply-chain target in the repo. — [.github/workflows/release.yml#L21-72](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/release.yml#L21-L72)
- `prepare-release.yml` parses the next version out of semantic-release's human-readable stdout with `awk '/The next release version is/{print $NF}'` — a brittle coupling to log wording that would silently produce an empty version if the message text changes. — [.github/workflows/prepare-release.yml#L36-52](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/prepare-release.yml#L36-L52)
- DEAD CODE: `bin/changelog` is a standalone conventional-changelog script with hardcoded `releaseCount: 19` and a commented-out `preset: 'jshint'`, but nothing references it — `package.json` and `prepare-release.yml` both go through the `update-changelog` npm script instead (per commit b924272). — [bin/changelog#L1-32](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/bin/changelog#L1-L32) [package.json#L26](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L26) [.github/workflows/prepare-release.yml#L56-59](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/prepare-release.yml#L56-L59)
- STALE TOOLING: `eslint` is pinned at `^4.19.1` (with `ecmaVersion: 6` in the parser options, below the ES2018+ syntax the sources actually use), and `nsp@^2.6.2` — a long-deprecated security tool — remains a devDependency with no script invoking it. — [package.json#L60-64](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L60-L64) [.eslintrc.json#L3-5](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.eslintrc.json#L3-L5)
- The README still advertises Travis CI and david-dm badges even though CI has moved to GitHub Actions, and the TODO section admits `X.509 certificate chain is not checked` — a real, documented limitation for anyone using x509 PEMs as verification keys. — [README.md#L3-6](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L3-L6) [README.md#L382-384](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L382-L384) [.github/workflows/test.yml](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/test.yml)
- The `files` allow-list omits `index.js` despite it being `main`; npm force-includes the `main` file so the package works today, but the list is one refactor (e.g. moving the entry to `lib/`) away from shipping a broken tarball. (inferred) — [package.json#L5](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L5) [package.json#L72-77](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L72-L77)

## Extension points

This library has no plugin system by design — a JWT implementation with third-party extension hooks would be a security liability. What it does have are a handful of declarative tables that are the intended edit sites for new capability, and one genuine runtime hook. The tables are the two option schemas and the `options_to_payload` map in sign.js, the four algorithm arrays in verify.js, and the `allowedAlgorithmsForKeys`/`allowedCurves` matrices in `lib/validateAsymmetricKey.js` — adding an algorithm means touching all three files coherently. The one caller-facing hook is verify's function-valued `secretOrPublicKey`, which is where JWKS clients plug in. Feature detection for new Node capabilities follows a one-file-per-boolean convention in `lib/`.
- PRIMARY RUNTIME HOOK: `secretOrPublicKey` as a `(header, secretCallback)` function is the documented integration point for JWKS/`kid`-based key resolution; internally the static-key case is normalized into the same shape so there is exactly one code path after line 101. — [verify.js#L88-101](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L88-L101) [README.md#L138-139](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L138-L139)
- Adding a new sign option means one entry in `sign_options_schema` with an `isValid` predicate and a `message` — the generic `validate(schema, allowUnknown, object, parameterName)` walker requires no other change, and unknown options are rejected because `validateOptions` passes `allowUnknown: false`. — [sign.js#L19-34](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L19-L34) [sign.js#L42-63](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L42-L63)
- `registered_claims_schema` is the parallel table for payload claims and is validated with `allowUnknown: true`, so arbitrary custom claims pass through while `iat`/`exp`/`nbf` are type-enforced — this asymmetry is the extension point for application-specific claims. — [sign.js#L36-40](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L36-L40) [sign.js#L65-67](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L65-L67) [test/claim-private.tests.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/claim-private.tests.js)
- `options_to_payload` is a two-column map from option name to claim name; adding a new registered-claim shortcut is a one-line addition there, and the conflict-detection and copy logic applies automatically. — [sign.js#L69-74](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L69-L74) [sign.js#L217-225](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L217-L225)
- `options_for_objects` is the companion list declaring which options are meaningless for non-object payloads; any new object-only option must be added here too or it will be silently accepted on a string payload. — [sign.js#L76-84](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L76-L84) [sign.js#L152-160](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L152-L160)
- Algorithm support is defined in four arrays: `SUPPORTED_ALGS` (sign.js) and `PUB_KEY_ALGS` / `EC_KEY_ALGS` / `RSA_KEY_ALGS` / `HS_ALGS` (verify.js). A new algorithm requires edits in both files plus `allowedAlgorithmsForKeys`. — [sign.js#L14-17](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L14-L17) [verify.js#L11-19](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L11-L19) [lib/validateAsymmetricKey.js#L4-8](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/validateAsymmetricKey.js#L4-L8)
- `allowedAlgorithmsForKeys` maps `asymmetricKeyType` → permitted `alg` values and is the single authority on key/algorithm pairing; an unrecognized key type throws `Unknown key type "<type>"`, so new key types must be registered here or they are rejected outright. — [lib/validateAsymmetricKey.js#L4-30](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/validateAsymmetricKey.js#L4-L30) [test/verify.tests.js#L272-282](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/verify.tests.js#L272-L282)
- `allowedCurves` maps ES algorithms to required named curves and is the place to add a curve; it is consulted only under `ASYMMETRIC_KEY_DETAILS_SUPPORTED`. — [lib/validateAsymmetricKey.js#L10-14](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/validateAsymmetricKey.js#L10-L14) [lib/validateAsymmetricKey.js#L39-48](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/validateAsymmetricKey.js#L39-L48)
- New Node-version feature gates follow a one-module-per-flag convention: a three-line file exporting `semver.satisfies(process.version, '<range>')`, consumed as a module-scope constant. Three such files already exist. — [lib/psSupported.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/psSupported.js) [lib/asymmetricKeyDetailsSupported.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/asymmetricKeyDetailsSupported.js) [lib/rsaPssKeyDetailsSupported.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/rsaPssKeyDetailsSupported.js)
- New error types extend the hierarchy by the ES5 pattern in `lib/NotBeforeError.js`: call `JsonWebTokenError.call(this, message)`, set `this.name`, add the payload field, then `prototype = Object.create(JsonWebTokenError.prototype)` and restore `constructor`. New types must also be re-exported from `index.js` to be reachable. — [lib/NotBeforeError.js#L1-13](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/NotBeforeError.js#L1-L13) [lib/TokenExpiredError.js#L1-13](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/TokenExpiredError.js#L1-L13) [index.js#L5-7](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/index.js#L5-L7)
- New verify-time claim checks slot into the linear block between `jws.verify` and the return, following the established `if (options.X) { if (payload.y !== options.X) return done(new JsonWebTokenError(...)) }` shape used by issuer/subject/jwtid/nonce. — [verify.js#L209-235](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L209-L235)
- Test convention for new options/claims: add a `test/option-<name>.test.js` or `test/claim-<name>.test.js` file that uses `testUtils.signJWTHelper`/`verifyJWTHelper` plus `asyncCheck`, so sync/async parity is enforced for free. — [test/option-nonce.test.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/option-nonce.test.js) [test/option-maxAge.test.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/option-maxAge.test.js) [test/claim-jti.test.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/claim-jti.test.js) [test/test-utils.js#L120-125](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/test-utils.js#L120-L125)
- Release automation is itself extensible through `.releaserc.json`'s plugin array; the `@semantic-release/exec` entry is where additional pre-publish gates (currently `git diff --exit-code` and the ReversingLabs scan) would be added. — [.releaserc.json#L20-30](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.releaserc.json#L20-L30)

## Contributor handoff

Treat this as a security library first and a JavaScript project second: the check ordering in verify.js and the guard cascade in sign.js are the product, and reordering them is never a safe refactor even when it looks like cleanup. Style is intentionally conservative CommonJS ES5/ES6 with no build step, no TypeScript, no async/await, and no strict mode — match it rather than modernize it. Read the four root JS files and `lib/validateAsymmetricKey.js` before touching anything; they are under 600 lines combined. Commits must be Conventional Commits because commitlint gates PRs via husky locally and a workflow in CI, and because semantic-release derives the version from them.
- READ FIRST, in order: `index.js`, `sign.js`, `verify.js`, `decode.js`, `lib/validateAsymmetricKey.js`. That is the entire library — under 600 lines total — and everything else in `lib/` is a three-line helper. — [index.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/index.js) [sign.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js) [verify.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js) [decode.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/decode.js) [lib/validateAsymmetricKey.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/validateAsymmetricKey.js)
- RUN: `npm test` for the fast loop (mocha over `test/`), `npm run coverage` before proposing a merge (nyc enforces 95/95/100/95 and will fail the command), and `npm run lint` manually since no workflow runs it. — [package.json#L21-27](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L21-L27) [package.json#L6-20](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L6-L20) [.github/workflows/test.yml#L31-38](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/test.yml#L31-L38)
- COMMIT FORMAT: Conventional Commits are mandatory. `.husky/commit-msg` runs `npx --no -- commitlint --edit $1` locally and `commitlint.yml` re-validates every commit in a PR range; `@commitlint/config-conventional` is the ruleset. — [.husky/commit-msg](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.husky/commit-msg) [.commitlintrc.json](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.commitlintrc.json) [.github/workflows/commitlint.yml#L18-20](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/commitlint.yml#L18-L20)
- STYLE: 2-space indent enforced by both `.editorconfig` and the eslint `indent` rule; no semicolon rule, no quote rule. eslint is pinned at v4 with `ecmaVersion: 6`, so avoid syntax newer than ES2015 in new code paths or the lint run will fail to parse. — [.editorconfig](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.editorconfig) [.eslintrc.json#L1-17](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.eslintrc.json#L1-L17) [package.json#L60](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L60)
- TRAP — check ordering: in verify.js, lines 144 (allow-list), 148-152 (key-type symmetry), and 154-160 (key/alg matrix) must all remain before line 165 (`jws.verify`). Moving or short-circuiting any of them is a vulnerability, not a refactor. — [verify.js#L144-172](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L144-L172)
- TRAP — dual calling convention: every change to sign or verify must work identically in sync (throws) and async (callback) mode. `test/test-utils.js` asserts byte-equality of tokens and deep-equality of errors across the two, so a one-sided fix will fail loudly. — [test/test-utils.js#L59-118](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/test-utils.js#L59-L118) [sign.js#L103-108](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L103-L108) [verify.js#L34-43](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L34-L43)
- TRAP — `return` inside `forEach`: the existing `options_to_payload` loop already has this bug (a `return failure(...)` that does not abort the function). Do not copy that pattern into new claim-copy logic, and be aware that fixing it changes observable async callback behavior. — [sign.js#L217-225](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L217-L225)
- TRAP — Node-conditional code: three `semver.satisfies` booleans are evaluated at module load. Code behind `ASYMMETRIC_KEY_DETAILS_SUPPORTED` and `RSA_PSS_KEY_DETAILS_SUPPORTED` will not execute on your local Node unless it is new enough, and is excluded from coverage — verify such changes against the CI matrix (16/18/20/22/24), not just locally. — [lib/asymmetricKeyDetailsSupported.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/asymmetricKeyDetailsSupported.js) [lib/rsaPssKeyDetailsSupported.js](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/rsaPssKeyDetailsSupported.js) [lib/validateAsymmetricKey.js#L32-65](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/validateAsymmetricKey.js#L32-L65) [.github/workflows/test.yml#L16-19](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/test.yml#L16-L19)
- SAFE EDIT BOUNDARY: adding a claim check, a validated option, or a test is low-risk and well-patterned. Changing key coercion (`createPrivateKey`/`createPublicKey`/`createSecretKey` cascades), the algorithm arrays, or `validateAsymmetricKey` is high-risk and touches published security behavior. — [sign.js#L114-138](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L114-L138) [verify.js#L120-160](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/verify.js#L120-L160) [lib/validateAsymmetricKey.js#L4-30](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/lib/validateAsymmetricKey.js#L4-L30)
- DO NOT EDIT casually: `CHANGELOG.md` is generated by `npm run update-changelog` during the prepare-release workflow, and `package.json` `version` is bumped by `npm version --no-git-tag-version` in that same workflow. Hand-editing either will conflict with the release automation. — [.github/workflows/prepare-release.yml#L53-59](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/prepare-release.yml#L53-L59) [package.json#L26](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/package.json#L26)
- RELEASE SHAPE: pushing to master triggers `prepare-release.yml`, which opens a `release/vX.Y.Z` PR titled `chore(release): X.Y.Z`; merging that PR matches the `startsWith(..., 'chore(release):')` guard and triggers `release.yml` to run `npx semantic-release`. The two workflows are mutually exclusive by that guard. — [.github/workflows/prepare-release.yml#L19-20](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/prepare-release.yml#L19-L20) [.github/workflows/release.yml#L17](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/release.yml#L17) [.github/workflows/prepare-release.yml#L61-79](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/.github/workflows/prepare-release.yml#L61-L79)
- New public API or option changes must be reflected in `README.md`, which is the only user documentation and is structured as `jwt.sign` / `jwt.verify` / `jwt.decode` / Errors & Codes / Algorithms supported. — [README.md#L23-25](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L23-L25) [README.md#L126](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L126) [README.md#L246](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L246) [README.md#L276](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L276) [README.md#L355](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L355)
- Any PR touching `lib/`, `sign.js`, or `verify.js` will require review from `@auth0/project-iam-protocols-engineer-codeowner` — the CODEOWNERS glob is `*`, so every path is covered. — [CODEOWNERS](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/CODEOWNERS)
- Do not add PEM fixtures casually: 17 already exist covering RSA, RSA-PSS (valid and invalid salt length), EC on three curves, DSA, and invalid public keys — reuse them via the `loadKey(filename)` helper convention. — [test/jwt.asymmetric_signing.tests.js#L10-12](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/jwt.asymmetric_signing.tests.js#L10-L12) [test/validateAsymmetricKey.tests.js#L10-14](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/test/validateAsymmetricKey.tests.js#L10-L14)
- Security vulnerabilities must not be filed as GitHub issues — the README directs them to Auth0's Responsible Disclosure Program. An agent discovering one should report it to the user rather than opening a public issue. — [README.md#L386-388](https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/README.md#L386-L388)

Source repository: https://github.com/auth0/node-jsonwebtoken
Full evidence corpus: https://github.com/Codevetter/codevetter/tree/main/benchmarks/repo-unpacks/jsonwebtoken.json

## Public product links

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