Skip to content
ecdsa.com
Node.js

jsonwebtoken (Node.js)

Auth0's jsonwebtoken, the most installed JWT package for Node.js: HMAC, RSA and ECDSA tokens on top of node:crypto, with algorithm checks tied to the key type since version 9 — but no EdDSA.
License
MIT
Checked version
9.0.3 ·
Source
github.com/auth0/node-jsonwebtoken
Documentation
github.com/auth0/node-jsonwebtoken#readme

Compatibility at a glance

JOSE and JWT

JWS algorithms
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512

jsonwebtoken signs and verifies JSON Web Tokens with three calls: jwt.sign(), jwt.verify() and jwt.decode(). It handles the JWS layer through the jws and jwa packages and does the cryptography with node:crypto, so keys can be PEM strings, buffers or KeyObject instances. jwt.decode() only parses the token — it never checks the signature, and code that reads claims from it trusts whatever the sender wrote.

Version 9 closed the classic algorithm-confusion paths, and the check confirmed it for 9.0.3: without an algorithms option the allowed list comes from the key type, an HS256 token keyed with the text of a public key is refused, and alg: none passes only when you list none and supply no key at all. Pass algorithms anyway — it documents the contract and keeps working if the key type ever changes.

The algorithm list stops at the RFC 7518 set: there is no EdDSA, Ed25519 or ES256K. A token from an issuer that signs with Ed25519 — common with jose, Go and Rust stacks — cannot be verified here; use jose instead. ES256 also requires a P-256 key, and signing with a P-384 key under that name throws rather than producing a token other libraries would reject.

ECDSA signatures inside the token are the 64-byte raw r‖s form that JWS mandates. When a token was assembled by hand from openssl dgst, Java's SHA256withECDSA or a cloud KMS, its signature is DER and verification fails with "ES256" signatures must be "64" bytes — paste the token into the JWT debugger to see the signature length, and fix it with the DER ⇄ raw converter.

Two defaults surprise many teams, one stricter and one looser than they assume: clockTolerance is zero, so a few seconds of clock drift between servers already yields jwt expired, and exp is optional, so a token issued without it never expires. Set expiresIn when signing and maxAge or an explicit exp requirement when verifying; check HMAC secrets with the JWT secret checker.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/jsonwebtoken/run.sh ran jsonwebtoken (Node.js) 9.0.3 on Node.js 24.22.0 in the container image node:24.22.0-bookworm-slim on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • JWS algorithms

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512

    HS256/384/512, RS256/384/512, PS256/384/512 and ES256/384/512 signed and verified in the check. EdDSA, Ed25519, Ed448, ES256K and ML-DSA were refused when signing.

    Recorded by the check
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512
    evidence jws-algorithms: ES256K: sign throws Error: "algorithm" must be a valid string enum value
    evidence jws-algorithms: EdDSA: sign throws Error: "algorithm" must be a valid string enum value
    evidence jws-algorithms: Ed25519: sign throws Error: "algorithm" must be a valid string enum value
    evidence jws-algorithms: Ed448: sign throws Error: "algorithm" must be a valid string enum value
    evidence jws-algorithms: ML-DSA-44: sign throws Error: "algorithm" must be a valid string enum value
  • ES256 signature encoding

    raw

    ES256 signatures in the token are 64 bytes of raw r‖s, as RFC 7518 requires; the library converts from the DER that node:crypto produces.

    Recorded by the check
    fact jws-ecdsa-encoding = raw
  • DER signature inside a JWS

    rejected

    A token whose ES256 signature was re-encoded as DER fails with "ES256" signatures must be "64" bytes, saw "72" (the number is the length of the DER blob).

    Recorded by the check
    fact jws-der-signature = rejected
    evidence jws-der-signature: TypeError: "ES256" signatures must be "64" bytes, saw "72"
  • Algorithm allow-list

    from-key

    Without algorithms, verification allows only the algorithms that fit the key: an HS256 token verified with a shared secret, an ES256 token with an EC public key, and an RS256 token against an EC key failed with invalid algorithm.

    Recorded by the check
    fact jwt-algorithm-allowlist = from-key
    evidence jwt-algorithm-allowlist: RS256 token against an EC key: JsonWebTokenError: invalid algorithm
  • Unsigned tokens (alg none)

    opt-in

    An unsigned token is refused with jwt signature is required whenever a key is supplied; it verifies only when algorithms contains none and the key argument is empty.

    Recorded by the check
    fact jwt-alg-none = opt-in
    evidence jwt-alg-none: with a key: JsonWebTokenError: jwt signature is required
    evidence jwt-alg-none: algorithms ["none"] and no key: verified
  • Public key used as an HMAC secret

    rejected

    An HS256 token whose HMAC key is the public key's PEM text was refused both without an algorithm list (invalid algorithm) and with algorithms: ["HS256"] (secretOrPublicKey must be a symmetric key when using HS256).

    Recorded by the check
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: no list: JsonWebTokenError: invalid algorithm
    evidence jwt-hmac-public-key: algorithms ["HS256"]: JsonWebTokenError: secretOrPublicKey must be a symmetric key when using HS256
  • Curve checked against alg

    enforced

    Signing ES256 with a P-384 key throws "alg" parameter "ES256" requires curve "prime256v1". — ES256, ES384 and ES512 are bound to P-256, P-384 and P-521.

    Recorded by the check
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: Error: "alg" parameter "ES256" requires curve "prime256v1".
  • Default clock skew

    0s

    No leeway by default: a token that expired three seconds earlier was rejected. Set clockTolerance in seconds to allow for drift.

    Recorded by the check
    fact jwt-clock-skew = 0s
  • Expiry claim required

    no

    A token without an exp claim verified successfully; expiry is enforced only when the claim is present or maxAge is set.

    Recorded by the check
    fact jwt-exp-required = no

Usage

Issue and verify an ES256 token, pinning the algorithm, issuer and audience on the verifying side.
import { generateKeyPairSync } from "node:crypto";
import jwt from "jsonwebtoken";

const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });

const token = jwt.sign({ sub: "user-42" }, privateKey, {
  algorithm: "ES256",
  expiresIn: "10m",
  issuer: "https://auth.example.com",
  audience: "api",
});

// Pin the algorithm, issuer and audience instead of trusting the token header.
const claims = jwt.verify(token, publicKey, {
  algorithms: ["ES256"],
  issuer: "https://auth.example.com",
  audience: "api",
});

console.log("sub:", claims.sub);

The snippet harness runs every sample verbatim in the container image node:24.22.0-bookworm-slim, and the run fails unless the output matches.

Setup
npm init -y >/dev/null && npm install --no-audit --no-fund jsonwebtoken@9.0.3
Command
node usage.mjs
Output must match (regular expression)
sub: user-42

Libraries