Skip to content
LocalOnly
Developer Utilities

How to Verify a JWT in Node.js and Python

Decoding a JWT shows its claims; verifying it proves they are genuine. Here are the checks a correct verification makes, written out in Node.js and Python so you can see each step.

Updated October 18, 20263 min readTool runs in your browser

To verify a JWT in Node.js or Python, you check its signature and then its claims, in that order. The JWT Decoder shows what a token contains, and its page explains why decoding proves nothing about whether the token is genuine. This guide covers the step that does.

In production, use a maintained library such as jose for Node.js or PyJWT for Python. The code below spells out the checks for an HMAC-signed token (HS256) so you know what that library must do, and what to look for when reviewing code that verifies tokens.

The checks a JWT verification must make

  1. Algorithm. Accept only the algorithm you expect. Never take it from the token's own header, or an attacker can choose none or switch algorithms.
  2. Signature. Recompute it over the first two parts with your key and compare in constant time.
  3. Expiry. Reject the token when exp is in the past, and when nbf (not before) is in the future.
  4. Issuer and audience. iss must be the identity provider you trust, and aud must name your service, so a token issued for another application is refused.

Verify a JWT in Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyHs256(token, secret, { issuer, audience }) {
  const [header, payload, signature] = token.split(".");
  if (JSON.parse(Buffer.from(header, "base64url")).alg !== "HS256") throw new Error("unexpected alg");

  const expected = createHmac("sha256", secret).update(header + "." + payload).digest();
  const actual = Buffer.from(signature, "base64url");
  if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) throw new Error("bad signature");

  const claims = JSON.parse(Buffer.from(payload, "base64url"));
  const now = Math.floor(Date.now() / 1000);
  if (claims.exp <= now) throw new Error("expired");
  if (claims.nbf && claims.nbf > now) throw new Error("not yet valid");
  if (claims.iss !== issuer || claims.aud !== audience) throw new Error("wrong issuer or audience");
  return claims;
}

Tested against three tokens: a valid one returned its claims; one with the payload changed to "sub": "admin" failed with bad signature; and one with "alg": "none" and an empty signature failed with unexpected alg. timingSafeEqual matters: a normal comparison stops at the first different byte, and the time it takes can leak how much of a forged signature was right.

Verify a JWT in Python

import base64, hashlib, hmac, json, time

def b64url(data):
    return base64.urlsafe_b64decode(data + "=" * (-len(data) % 4))

def verify_hs256(token, secret, issuer, audience):
    header, payload, signature = token.split(".")
    if json.loads(b64url(header))["alg"] != "HS256":
        raise ValueError("unexpected alg")
    expected = hmac.new(secret.encode(), f"{header}.{payload}".encode(), hashlib.sha256).digest()
    if not hmac.compare_digest(expected, b64url(signature)):
        raise ValueError("bad signature")
    claims = json.loads(b64url(payload))
    if claims["exp"] <= time.time():
        raise ValueError("expired")
    if claims.get("nbf", 0) > time.time():
        raise ValueError("not yet valid")
    if claims.get("iss") != issuer or claims.get("aud") != audience:
        raise ValueError("wrong issuer or audience")
    return claims

hmac.compare_digest is Python's constant-time comparison. The padding helper is needed because JWT segments drop the = characters that Python's decoder expects.

Tokens signed with RS256 or ES256

Most identity providers sign with a private key (RS256 or ES256) and publish the public keys at a JWKS URL. The checks are the same, but the signature step uses the public key matching the token's kid header. Here a library is not optional: it fetches and caches the keys, picks the right one and handles key rotation. Still pin the algorithm in its options.

Frequently asked questions

Command Palette

Search for a tool or command