CodeVetter
← All unpacks
CodeVetter · Repo Unpack evidence

auth0/node-jsonwebtoken

Scanned commit b924272f29 dated 2026-06-25 — last upstream commit. Generated by codevetter 1.12.0 on 2026-09-22.

github.com/auth0/node-jsonwebtoken ↗ · 84 files · 238 KB

jsonwebtoken | Build | Dependency | |-----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------

Deep analysis by claude-opus-5 via the claude CLI on 2026-09-22 (345s) — every claim below links to its cited source files.

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`.

All 14 system map claims →

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.

All 14 feature catalog claims →

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).

All 13 data flow claims →

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.

All 14 behavior traces claims →

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`).

All 14 testing signals claims →

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.

All 15 risk map claims →

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/`.

All 13 extension points claims →

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.

All 15 contributor handoff claims →

Languages

LanguageFilesShare of source
JavaScript46100%
YAML6docs/config
JSON5docs/config
Markdown2docs/config

Start reading here

Probable source entrypoint: index.js — inferred from the directory tree and manifest, not the upstream docs.

Source roots

lib/ (8 files)

Source layout — lib/

PathFiles
lib/JsonWebTokenError.js1
lib/NotBeforeError.js1
lib/TokenExpiredError.js1
lib/asymmetricKeyDetailsSupported.js1
lib/psSupported.js1
lib/rsaPssKeyDetailsSupported.js1
lib/timespan.js1
lib/validateAsymmetricKey.js1

Repository structure

Top-level pathFiles
test/52
lib/8
.github/5
.husky/1
bin/1

Docs found: README.md

Package

package.json — jsonwebtoken v9.0.3, 24 declared dependencies.

@commitlint/cli@commitlint/config-conventional@semantic-release/execatobchaiconventional-changelogconventional-changelog-conventionalcommitseslinthuskyjwslodash.includeslodash.isboolean

CI & config entrypoints

Structural health signals

Average score 8.8/10 across 45 analyzed source files; 0 hotspots surfaced.

These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.

Recent history