{Inspector}

Developer Guide · 2026

JWT Cryptography Internals: The Real Difference Between HS256, RS256 and ES256

The alg header in every JWT names the signing algorithm — HS256, RS256 or ES256 — and it is not a cosmetic choice. The three are different branches of cryptography with different key types, sizes and attack surfaces. This guide walks through how each signature is computed, why the alg header can lie, and how to verify one by hand.

Benjamin Rotshtein

Written by Benjamin Rotshtein

Updated

What does the JWT signature actually do?

The signature is a cryptographic hash of the header and payload computed with a secret (HMAC) or a private key (RSA/ECDSA). Anyone holding the key can recompute it, so any tampering with the claims breaks verification instantly. That is how a stateless token proves it wasn’t modified — while staying readable by anyone, since only the signature is protected.

What the signature actually covers

A JWT is three Base64URL parts: header.payload.signature. The signature is computed over exactly two bytes: the encoded header, a dot, and the encoded payload — nothing else. Change one character of either part and the signature no longer matches. That is the entire guarantee: whoever holds the right key material is the only one who could have produced a signature that verifies against this exact header and payload. Anyone can read the claims; only the signer can bless them.

signingInput = base64url(header) + "." + base64url(payload)
signature   = sign(signingInput, key, alg)
token       = signingInput + "." + base64url(signature)

The three algorithm families

AlgorithmFamilyKey materialWho can verifyBest for
HS256HMAC-SHA256 (symmetric)One shared secret, 32+ bytesBoth signer and verifier hold the same secretSingle service, internal APIs
RS256RSA PKCS#1 v1.5 (asymmetric)Private key signs, public key verifiesAny service with the public key onlyMulti-service, microservices, third-party
ES256ECDSA with P-256 (asymmetric)Private key signs, public key verifiesAny service with the public key onlySmaller signatures, high throughput

HS256: one secret, two responsibilities

HMAC-SHA256 is a message authentication code: it takes the signing input and the shared secret, and produces a 32-byte tag. Verification is symmetric — the verifier re-runs the same HMAC with the same secret and compares tags. The convenience is speed and simplicity; the cost is that any service able to verify tokens is also able to mint them. If one microservice holding the secret is compromised, it can forge tokens for the entire system. The secret must be at least 32 random bytes (the JWA minimum for HS256); shorter, guessable or reused values are brute-forced in seconds with a dictionary attack.

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

const secret = Buffer.from(process.env.JWT_SECRET!, "utf8");
const tag     = createHmac("sha256", secret)
  .update(signingInput).digest();
const ok = timingSafeEqual(tag, Buffer.from(signature, "base64url"));

RS256: private signs, public verifies

RSA-SHA256 is asymmetric. The identity provider holds a 2048-bit (or larger) private key and signs the input; every other service holds only the public key and verifies. Verifiers can check authenticity but cannot forge tokens, which makes RS256 the default for multi-service and third-party setups (Auth0, Keycloak, Google). RSA PKCS#1 v1.5 signatures are deterministic and simple to verify, but signing is slower than ECDSA and signatures and keys are large.

ES256: the same property, with smaller keys

ECDSA with the P-256 curve is the asymmetric alternative with a much smaller footprint: a 256-bit key and a ~64-byte signature versus RSA’s 2048-bit key and 256-byte signature. It is the default for JWTs issued in constrained environments and modern identity flows. ECDSA signatures are non-deterministic (each signature uses a fresh random nonce), so two signatures of the same input will differ — which is expected and must not be treated as a verification failure.

The alg confusion attack

This is the classic JWT exploit, and it works because the algorithm is attacker-controlled data. The attack: take a valid RS256 token, rewrite the header to “alg”: “HS256” and re-sign it with the RS256 public key used as an HMAC secret. A server that (a) doesn’t pin the expected algorithm and (b) picks the key based on the header will compute an HMAC with the public key — and the forged token verifies. The defenses are simple: always check alg against an explicit allowlist before verifying, never derive key choice from the token, and keep key types strictly separated between environments.

Verifying a signature by hand

Split the token on dots, recompose the signing input from the first two parts exactly as the signer did, and call the verify function with the correct key type for the alg in the header. Node, Python and Go libraries do this for you — but only if you pass the key that matches the algorithm. Passing the wrong key type (or a key read from the header) is what enables alg confusion. When a token fails verification, reject it outright; do not fall back to “close enough” validation.

So which should you pick?

Single service, internal API, no third-party verifiers? HS256 with a random 32-byte secret is fast, simple and fine. More than one service, or anyone outside your team verifying tokens? RS256 — the public key is publishable, so consumers verify without ever holding minting power. Long-lived mobile apps, IoT, or tight token-size constraints? ES256 gives you the same asymmetric property at a fraction of the size. Whatever you choose, pin the algorithm and document a migration path — switching later without a plan is how confusion attacks sneak in.

Inspect the header and claims of any JWT

The first step of any crypto audit is seeing what a token actually claims. A JWT decoder shows you the alg and kid header values plus every claim — so you can confirm which algorithm your identity provider actually used, compare it with what your server expects, and spot an unexpected algorithm switch before it becomes a production incident.

Frequently asked questions

What is the difference between HS256, RS256 and ES256?

HS256 is HMAC-SHA256, a symmetric scheme where a single shared secret both signs and verifies. RS256 is RSA-SHA256, an asymmetric scheme where a private key signs and any service with the public key verifies. ES256 is ECDSA with the P-256 curve, also asymmetric but with far smaller keys and signatures. The choice is between one shared secret (fast, but every verifier can also sign) versus a public/private key pair (slower to sign, but verifiers cannot forge tokens).

Is HS256 secure, and how long should the secret be?

HS256 is secure if the secret is random and long enough. The JWA spec requires at least 32 bytes (256 bits) for HS256, 48 bytes for HS384 and 64 bytes for HS512. A short or reused secret — like 'secret' or a public value — makes brute-force trivial. The bigger risk is architectural: with HS256, every service that verifies tokens also holds the key needed to mint them, so one compromised microservice can forge tokens for all.

What is the JWT alg confusion (algorithm confusion) attack?

An attacker takes a valid RS256 token and changes the alg header to HS256. If the server does not pin the expected algorithm and naively uses the public key as an HMAC secret, the attacker can sign the modified header and payload with that same public key and the token verifies as valid. The fix is always to validate the alg header against an explicit allowlist (e.g. only RS256) before verifying, and to never mix key types.

Which algorithm should I use for my JWT?

For a single service, HS256 with a random 256-bit secret is fine and fastest. For any system with more than one service, or where a third party must verify tokens, use RS256 (or ES256 for smaller signatures). ES256 gives public-key security with signatures ~4x smaller than RSA, at the cost of more complex handling. Never change algorithms without a migration plan — that is exactly how alg confusion happens.

How do I verify a JWT signature in practice?

Split the token into header.payload.signature. Recompose the signing input as base64url(header) + '.' + base64url(payload), then compute the signature over exactly those bytes with the library's verify function and the correct key type for the algorithm. For HS256 that means an HMAC-SHA256 with the shared secret; for RS256/ES256 a signature verification with the public key. If verification fails, the token was tampered with — reject it.

Can I verify a JWT signature by decoding it in a browser?

You can verify an HS256 JWT in the browser if you have the secret, using the Web Crypto API. You cannot verify RS256 or ES256 signatures without the public key. Decoding the header and payload — the claims themselves — needs no key at all, which is what a token inspector does. It shows you the alg claim and lets you inspect claims locally, but signature verification requires the right key material.

See the alg and claims of any JWT — 100% in your browser

Decode any JWT and read its alg header and claims instantly, without the token ever leaving your machine. Check what your identity provider actually signed with.

Related guides