{Inspector}

Ultimate Guide · 2026

JWT Signature Verification: HS256 vs RS256, Explained with Code

Anyone can Base64-decode a JWT — the header and payload are not encrypted. What makes a token trustworthy is the signature: a cryptographic check proving the token was issued by someone holding the key and was not tampered with. This guide covers the three algorithm families, what each one proves, and how to verify with standard-library code.

Benjamin Rotshtein

Written by Benjamin Rotshtein

Updated

How does JWT signature verification work?

Verification recomputes the signature over the header and payload with the same secret or public key and compares it to the third segment. HS256 uses a shared secret and SHA-256; RS256 uses an RSA private key to sign and a public key to verify. If the recomputed value matches, the token was not tampered with since signing.

What the signature actually protects

A JWT has three dot-separated parts: header.payload.signature. The signature is computed over the first two parts only, so it cannot be forged without the key and breaks the moment either segment changes. Decoding the payload requires no key at all — that is why you should never place secrets in a JWT payload. Try it: paste any token into the JWT decoder and you will see the readable claims immediately.

The three algorithm families

AlgorithmFamilyKey modelVerifierUsed for
HS256 / HS384 / HS512HMAC + SHAOne shared secret (symmetric)Anyone holding the secret can both sign and verifySingle service, trusted clients, internal APIs
RS256 / RS384 / RS512RSAPrivate key signs, public key verifies (asymmetric)Anyone with the public key can verify, but cannot signIssuer/audience split — e.g. auth server vs API servers
ES256 / ES384 / ES512ECDSA (P-256 etc.)Private key signs, public key verifies (asymmetric)Same trust model as RSA with much smaller keysMemory-constrained clients, hardware tokens, modern defaults

The three verification steps

  1. 1

    Take the token's first two segments

    A JWT is header.payload.signature. Verification re-derives the signature from exactly the same two segments: the Base64URL-encoded header and payload, joined with a dot.

  2. 2

    Recompute the digest with the key

    For HS256, the server computes HMAC-SHA256(header.payload, secret). For RS256/ES256 it does not recompute — it uses the public key to check that the signature was produced by the matching private key.

  3. 3

    Reject the token if the signature does not match

    If the bytes do not match, the token has been tampered with — changed payload, wrong secret, replayed token — and it must be rejected before any claim is trusted.

HS256 verification, line by line (Node.js)

const crypto = require("crypto");

function verifyHs256(token, secret) {
  const [header, payload, signature] = token.split(".");
  if (!header || !payload || !signature) return false;

  // Recompute the HMAC over the exact signed input
  const expected = crypto
    .createHmac("sha256", secret)
    .update(header + "." + payload)
    .digest("base64url");

  // Constant-time comparison defeats timing attacks
  const received = signature;
  const a = Buffer.from(expected);
  const b = Buffer.from(received);
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

The same idea in Python

import base64
import hashlib
import hmac

def verify_hs256(token: str, secret: str) -> bool:
    header, payload, signature = token.split(".")
    signing_input = f"{header}.{payload}".encode()

    digest = hmac.new(secret.encode(), signing_input, hashlib.sha256).digest()
    expected = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()

    return hmac.compare_digest(expected, signature)

What about RS256 and ES256?

RSA and ECDSA verification never recompute the signature. Instead the verifier uses the public key to check the mathematical property that the signature could only have been produced by the matching private key. That is why RS256 is the standard choice for identity providers (Auth0, Okta, Keycloak): the signing key stays on the issuer, while every API server only needs the public keys, typically fetched from a JWKS endpoint. ES256 uses the same asymmetric model with P-256 keys — faster and smaller, which is why it is common in mobile and hardware tokens.

The attacks verification prevents

Frequently asked questions

What does a JWT signature actually prove?

It proves the header and payload have not been modified since the token was signed by a party holding the key. With HS256 that key is a shared secret. With RS256/ES256 it is the private key corresponding to the public key used for verification. The signature does not encrypt anything — the payload remains readable by anyone.

HS256 or RS256 — which should I use?

Use HS256 when the issuer and verifier are the same service (or fully trust each other) — it is simpler and faster. Use RS256 (or ES256) whenever more than one service verifies the token, because the signing key never needs to be shared: verifiers get only the public key. RS256 also protects you if a verifier leaks a key.

Can I verify a JWT signature without the secret?

No. HS256 verification requires the shared secret itself. RS256/ES256 verification requires the public key — which is public, so anyone can verify, but nobody can sign. This asymmetry is exactly why RS256 is preferred when an issuer must stay a single point of trust.

How do you verify a JWT in Node.js, Python or Go?

The trusted pattern is to use the language's standard library. In Node: crypto.createHmac('sha256', secret).update(token.split('.')[0] + '.' + token.split('.')[1]).digest() compared to the Base64URL-decoded signature. In Python: hmac.new(secret, header_payload, hashlib.sha256).digest(). In Go: crypto/hmac and crypto/sha256. Production code should use an audited library such as jsonwebtoken, PyJWT or golang-jwt that also enforces the algorithm, exp and nbf claims.

Why do libraries force an explicit algorithm whitelist?

Older libraries guessed the algorithm from the token header, which allowed the 'alg:none' attack and algorithm-confusion attacks (HS256 tokens verified as RS256 using the public key as the HMAC secret). Whitelisting alg values closes both holes. Never accept 'none', and always check exp.

See the claims your tokens expose — 100% in your browser

Decode any JWT locally and inspect the exact header and payload before you decide what to ship. Nothing leaves your machine.

Related guides