A JWT is not encrypted, and decoding it is not verifying it
published
TL;DR
A JWT is three base64url strings joined by dots. The first two — header and payload — are plain JSON that anyone can read. Decoding them takes no key and proves nothing. Only verifying the third segment, the signature, against a key tells you the token is authentic and untampered. Confusing decode with verify is how the alg: none and algorithm-confusion attacks work.
The structure
A token looks like xxxxx.yyyyy.zzzzz:
header— base64url JSON, e.g.{"alg":"HS256","typ":"JWT"}payload— base64url JSON with the claims, e.g.{"sub":"123","role":"admin","exp":1718000000}signature— bytes produced by signingheader.payloadwith a key, then base64url-encoded
base64url is base64 with +// swapped for -/_ and padding stripped (RFC 7515 §2). It is an encoding, not encryption. You can read the first two parts in three lines:
const [header, payload] = token.split('.');
const decode = (s) =>
JSON.parse(atob(s.replace(/-/g, '+').replace(/_/g, '/')));
console.log(decode(header)); // { alg: 'HS256', typ: 'JWT' }
console.log(decode(payload)); // { sub: '123', role: 'admin', exp: 1718000000 }
No key, no library, no permission. The JWT decoder does exactly this, locally — so never put a secret in a JWT payload expecting it to be hidden.
Decode is not verify
| Operation | Needs a key? | What it proves |
|---|---|---|
| Decode | No | What the token claims — nothing about authenticity |
| Verify | Yes | The signature matches header.payload, so the issuer signed it and nobody altered it |
Verification recomputes the signature over header.payload using the algorithm and key, then compares it to zzzzz. If you decode a token, read role: "admin", and trust it without verifying, an attacker just edits the payload to say admin and re-encodes it — the unsigned read accepts it. Authorization decisions must come from a verified token, server-side, every time.
The alg:none attack
RFC 7519 defines an algorithm literally named none — “unsecured JWT”, no signature. It exists for cases where integrity is guaranteed by other means. The attack: take a valid token, change the header to {"alg":"none"}, keep your tampered payload, and send an empty signature:
{"alg":"none","typ":"JWT"}.{"sub":"123","role":"admin"}.
A library that reads alg from the attacker-supplied header and dispatches verification accordingly will see none, skip the signature check, and accept the forged token. This was a real, widespread library bug disclosed in 2015 (Auth0: critical vulnerabilities in JSON Web Token libraries) and it still resurfaces in hand-rolled verifiers.
Algorithm confusion: RS256 → HS256
A subtler variant. RS256 uses an asymmetric keypair: the server signs with a private key and verifies with a public key (which is, by design, public). HS256 is symmetric: the same secret signs and verifies. If a verifier picks the algorithm from the token header, an attacker can:
- Take the server’s public RSA key (often published at a JWKS endpoint).
- Forge a token with header
{"alg":"HS256"}and a malicious payload. - Sign it with HMAC-SHA256 using the public key string as the HMAC secret.
The server, told HS256, verifies HMAC using that same public key as the secret — and it matches. The public key was never meant to be a secret, so the attacker had everything needed. RFC 8725 (JWT Best Current Practices) names this explicitly.
What to do
- Pin the algorithm on the verifier. Decide server-side that tokens are, say, RS256, and reject anything else. Do not let the token’s own
algheader select the verification path. - Reject
alg: noneoutright unless you have a deliberate, documented reason. - Use a maintained library that implements RFC 8725, and call its verify function with an explicit allowed-algorithms list — not its decode function.
- Always verify before trusting any claim, including
exp,iss,aud, androle. - Keep secrets out of the payload. It is readable by anyone holding the token.
Caveats
- A signed JWT guarantees integrity and authenticity, not confidentiality. If the payload must be hidden, you need JWE (encrypted JWTs), not a plain JWS.
- Short expirations limit the blast radius of a leaked token but do not replace signature verification.
nonehas legitimate uses in fully trusted, transport-secured channels — the bug is verifiers honoring it when they expect a signed token.