json-web-token

JSON Web Token (JWT) encode/decode for Node. Zero runtime dependencies,
timing-safe signature verification, synchronous result-object API.
Install
npm install json-web-token
pnpm add json-web-token
yarn add json-web-token
Quick start
import { encode, decode } from "json-web-token";
const secret = "TOPSECRETTTTT";
const payload = { iss: "me", aud: "you", iat: Date.now() };
const { error, value: token } = encode(secret, payload);
if (error) throw error;
const { error: e2, value: decoded, header } = decode(secret, token);
if (e2) throw e2;
console.log(decoded, header);
The library is synchronous — both encode and decode return their
result immediately. If you want async ergonomics, wrap them yourself:
const tokenP = Promise.resolve(encode(secret, payload));
const { value: token } = encode(secret, {
payload: { iss: "me", aud: "you" },
header: { kid: "my-key-id" },
}, "HS512");
Header keys you provide are merged with the defaults — typ and alg are
always set by the library and cannot be overridden through this surface.
Locking decode to a specific algorithm
const { error, value } = decode(publicKey, token, { algorithms: ["RS256"] });
Any token whose header.alg is outside the list is rejected before any
signature work happens.
API
function encode(
key: string | Buffer,
data: unknown,
algorithm?: string,
): EncodeResult;
function decode(
key: string | Buffer,
token: string,
options?: DecodeOptions,
): DecodeResult;
interface DecodeOptions {
algorithms?: string[];
}
function getAlgorithms(): string[];
class JWTError extends Error { }
EncodeResult is { error: JWTError | null; value: string | null }.
DecodeResult is { error: JWTError | null; value: unknown; header?: JWTHeader }.
Security notes
- CVE-2023-48238 (algorithm confusion) is fixed. v4 refuses to verify
any token whose algorithm family does not match the key handed to
decode. PEM-encoded keys (anything starting with -----BEGIN) can
only be paired with the asymmetric algorithms (RS*); plain secrets
(string or Buffer without PEM markers) can only be paired with the
HMAC algorithms (HS*). This blocks the classic RS256→HS256 swap
where an attacker re-signs a token with HMAC using the server's RSA
public key as the HMAC secret.
- Optional algorithm allowlist. Safety-conscious callers can pass
{ algorithms: ["RS256"] } (or any subset) to decode to reject
any token whose header.alg is outside that list, in addition to the
key-type guard above.
- Timing-safe HMAC verify — v4 compares signatures with
crypto.timingSafeEqual on length-checked Buffers, removing the
timing side-channel that was present in v3's string === compare.
alg: 'none' is rejected in both encode and decode.
- Claim validation is out of scope.
exp, nbf, iat, iss,
aud, sub are not validated automatically. Check them in your own
code on the decoded payload.
Supported algorithms
HS256, HS384, HS512, RS256.
Migrating from v3
The { error, value, [header] } return shape and getAlgorithms() /
JWTError are unchanged. Callback overloads have been removed —
v4 is sync-only. If you used the callback form in v3:
jwt.encode(secret, payload, (err, token) => { ... });
const { error, value: token } = jwt.encode(secret, payload);
if (error) { }
Other changes worth knowing:
| Min Node | >=8 | >=18 |
| Runtime deps | 4 (base64-url, is.object, json-parse-safe, xtend) | none |
| Call style | callback OR result-object | result-object only |
| HMAC verify | string === (timing-leaky) | crypto.timingSafeEqual |
| Algorithm confusion | vulnerable (CVE-2023-48238) | fixed — key-type / alg-family guard on encode + decode |
| Algorithm allowlist | none | optional algorithms in decode options |
| Module formats | CJS only | ESM + CJS via exports map |
| Types | hand-written index.d.ts (loose any) | TS source, generated .d.mts / .d.cts |
| Base64url | base64-url package | Node native Buffer |
| Build | hand-edited index.js | tsup from TS |
| Test runner | mocha + nyc | vitest with v8 coverage |
| Linter | standard | biome |
| CI | Travis (Node 8/10/12) | GitHub Actions (Node 20/22/24) |
Size
Zero runtime dependencies. What ships in the npm tarball:
ESM runtime (index.mjs) | 4 880 B | 1 599 B |
CJS runtime (index.cjs) | 4 941 B | 1 609 B |
Types (.d.mts / .d.cts) | 5 238 B | 1 167 B |
| Sourcemaps (debug-only, not loaded) | 27 610 B | 4 511 B |
Only one of the two runtime files is loaded by your bundler / Node, so
the real cost in your app is ~1.6 kB gzipped.
License
ISC © @joaquimserafim