ecdsa.com

Verify an ES256 JWT · Node.js

How to verify an ES256 JWT in Node.js

Verifying an ES256 JWT in Node.js needs no JWT library: node:crypto verifies the signature once you know the JWS rules — the signed input is the ASCII of header.payload, and the signature is raw r‖s, which dsaEncoding handles natively.

Tested with Node.js 26 (OpenSSL 3.x backend).

Same recipe in:Node.jsPythonGoWebCrypto
verify-jwt.mjs — ES256 verification with node:crypto only
import { createPublicKey, verify } from "node:crypto";
 
function verifyEs256(token, publicKeyPem) {
const [header, payload, signature] = token.split(".");
 
const { alg } = JSON.parse(Buffer.from(header, "base64url").toString());
if (alg !== "ES256") throw new Error(`expected ES256, got ${alg}`);
 
const ok = verify(
"sha256",
Buffer.from(`${header}.${payload}`), // signed bytes: the ASCII text
{ key: createPublicKey(publicKeyPem), dsaEncoding: "ieee-p1363" },
Buffer.from(signature, "base64url"), // JWS sig: raw r‖s, 64 bytes
);
if (!ok) throw new Error("invalid signature");
 
const claims = JSON.parse(Buffer.from(payload, "base64url").toString());
if (claims.exp && claims.exp < Date.now() / 1000) throw new Error("token expired");
return claims;
}

How it works

  1. Pin the algorithm first: read alg from the header and require ES256. Never let the token choose the algorithm for you.
  2. The signature covers the base64url *text* header.payload — sign-input is those ASCII bytes, not the decoded JSON.
  3. dsaEncoding: "ieee-p1363" makes verify accept the 64-byte raw signature directly — no DER conversion step.
  4. Node's Buffer.from(str, "base64url") handles the unpadded base64url encoding JWS mandates.

Gotchas

  • Omitting the alg check invites algorithm-confusion: a token stamped HS256 and "verified" with the public key as an HMAC secret passes some naive implementations. Pin ES256 and reject everything else.
  • Without dsaEncoding the verification always fails — node defaults to DER, and no valid JWT carries a DER signature.
  • Signature validity is not token validity: exp, nbf, iss and aud checks are your job (the snippet shows exp only).

Related recipes