Decoding a JWT Is Not the Same as Verifying It
Every JWT bug in production reduces to the same mistake: trusting a decoded token without verifying its signature. The difference, the consequences, and how to do it right.
On this page β
A JWT looks like one string. It's actually three things welded together with periods. Knowing the difference between reading those three things and verifying them is the line between auth-as-intended and security-as-disaster.
This post is the difference, in plain terms, with the production consequences of getting it wrong.
What's in a JWT, exactly
A JWT (JSON Web Token, RFC 7519) is a string of three Base64URL-encoded sections separated by dots:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0.signature
βββββββ header ββββββββββββββββββββ payload βββββββββββββββ signature ββ
- Header: a small JSON object with the signing algorithm. Decodes to something like
{"alg":"HS256","typ":"JWT"}. - Payload (a.k.a. claims): JSON with whatever the issuer wants: user ID, expiry, audience, custom data.
- Signature: cryptographic bytes computed from
Base64URL(header) + "." + Base64URL(payload)using the algorithm and key specified in the header.
The first two parts are not encrypted. They're encoded. Anyone who has the JWT can read them, instantly, for free.
Try it now. Take any JWT and paste into the JWT Decoder. The header and payload appear immediately. No key required. No password. Nothing.
Decoding β verifying
Decoding means: split on dots, Base64URL-decode each part, parse the JSON. Anyone can do this. It tells you what the token claims to be.
Verifying means: re-compute the signature from the header+payload using the issuer's key, and check that it matches the signature in the token. This requires the key. It tells you that the token was actually issued by who it says, and the contents haven't been changed.
The vital insight: decoded data is just a claim, not a fact.
A decoded JWT might say {"sub": "admin", "role": "superuser"}. Without verification, you have no idea if:
- The issuer actually granted that role
- Anyone could have hand-crafted this token and pasted in any claims they wanted
- The token expired three years ago
A verified JWT, in contrast, proves the issuer signed exactly these claims at the time of issuance, and the signature hasn't been tampered with.
The production failure mode
Every "we got hacked through our JWT auth" story I've read traces back to one of these:
Failure mode 1: The decode-and-trust
// π« Catastrophically wrong
const payload = JSON.parse(atob(token.split('.')[1]));
if (payload.role === 'admin') {
// grant admin
}
Anyone can craft a token with {"role": "admin"} and base64-encode it. No signature is checked. The "auth" is theater.
This pattern shows up because:
- Devs use jwt-decoder tools (like ours!) for debugging, then accidentally translate the same logic into server code.
- Frameworks sometimes have a
jwt.decode()method (no verify) and ajwt.verify()method (with key). People grab the convenient one. - The token "works": it round-trips, the right fields appear, so it ships.
Failure mode 2: The "alg: none" attack
// π« Naive verifier
const verified = jwt.verify(token, key, { algorithms: undefined });
Some JWT libraries default to allowing the special alg: "none" algorithm, which means no signature is required. An attacker crafts a JWT with {"alg":"none"} and an empty signature, and the verifier accepts it.
The fix: always specify allowed algorithms explicitly:
// β
Correct
const verified = jwt.verify(token, key, { algorithms: ['HS256'] });
Most modern libraries (jsonwebtoken 8+, jose) reject alg: none by default, but check yours.
Failure mode 3: HMAC vs RSA key confusion
If your verifier accepts both symmetric (HS*) and asymmetric (RS*, ES*) algorithms with the same key lookup, an attacker can:
- Get your RSA public key (it's public: they're in your
.well-known/jwks.json). - Craft a JWT with
{"alg":"HS256"}and sign it with your public key as the HMAC secret. - Your verifier looks up "the key for HS256": gets the public key, uses it as HMAC. Signature checks out. Token accepted.
The fix: lock the algorithm per issuer. If your server expects RS256, only accept RS256.
Failure mode 4: Expiry not checked
// π« Verifier ignores exp claim
jwt.verify(token, key, { algorithms: ['HS256'] });
// proceed without checking payload.exp
Some libraries verify the signature but don't enforce the exp (expiration) claim unless you ask. A token signed two years ago is "valid" forever.
The fix: most libraries have ignoreExpiration: false as default but verify yours. Always check exp. Most also support clockTolerance for small clock skew between issuer and verifier, keep this small (30-60 seconds).
Failure mode 5: Trusting the issuer URL from the token
JWS supports headers like jku (JWK Set URL) or x5u (X.509 URL) that tell the verifier where to fetch the key from. If your verifier blindly fetches whatever URL the token says, an attacker controls the URL β controls the key β controls the signature.
The fix: never trust jku or x5u from the token. Hardcode (or strict-allowlist) which JWKS endpoint you fetch keys from.
How to verify correctly
In Node.js with jsonwebtoken:
import jwt from 'jsonwebtoken';
try {
const claims = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // explicit, no fallback
issuer: 'https://issuer.example.com', // verify iss claim
audience: 'my-api', // verify aud claim
clockTolerance: 30, // 30 sec clock skew
});
// claims is now trustworthy
} catch (err) {
// signature invalid, expired, wrong issuer, wrong audience, etc.
}
Note what verification also does in modern libraries:
- Validates the algorithm matches expectations
- Checks
exp(expiration) andnbf(not-before) - Validates
iss(issuer) andaud(audience) if you supply expected values - Computes signature with the correct algorithm
In Python with pyjwt:
import jwt
claims = jwt.decode(
token,
public_key,
algorithms=['RS256'],
issuer='https://issuer.example.com',
audience='my-api',
)
(Python's decode() with a key actually verifies. The verify=False parameter disables it. Don't pass verify=False outside of debugging.)
When decoding-without-verifying is OK
There are exactly two cases where decoding without verifying makes sense:
-
You're a debugging tool. Xerobit's JWT Decoder shows you what's in a token without needing the signing key. You should never paste your signing key into a third-party web tool. The trade-off: you see the claims but can't prove they're authentic: and the tool labels this clearly.
-
You're inspecting your own freshly-issued token. When debugging your own auth flow, you can decode tokens you just issued because you trust the issuance step. This is for development; never extends to production.
In every other case, decode without verify = no security. Period.
A simple mental model
A JWT is a paper letter. Decoding is reading the letter. Verifying is checking the wax seal.
You can read any letter that lands in your hands. The text is plain and visible. But that text is just a claim, anyone could have written it.
The seal proves who wrote it. To verify the seal, you need the writer's seal-stamp (the signing key). Without that, the seal is just decoration.
A well-crafted forgery looks identical to a real letter. The only difference is the seal. Skipping the verify step is reading a forged letter and acting on it.
Tools for the job
- JWT Decoder: for debugging your own tokens. Decodes; never verifies (intentionally).
- Server-side:
jsonwebtoken,jose, orpassport-jwt(Node);pyjwt(Python);jose-jwt(.NET);JJWT(Java). - For machine-to-machine OAuth flows where the server needs to fetch keys, use a JWKS-aware library that caches and respects
kidheaders correctly.
The one rule
Never trust the contents of a JWT until you've verified the signature with the issuer's key.
That's it. Internalize that, and 95% of JWT security problems disappear.
Further reading
- JWT Security Checklist for 2026: the full pre-deploy checklist
- Base64: How It Actually Works Under the Hood: the encoding JWTs are built on
- RFC 7519: JSON Web Token (JWT): the spec itself
- RFC 7515: JSON Web Signature (JWS): the signing layer
Keep reading
Base64: How It Actually Works Under the Hood
Base64 is everywhere, in JWTs, data URLs, email attachments. This is the byte-level walkthrough of what it does, why it grows files by 33%, and the URL-safe variant.
JWT Security Checklist for 2026
Twelve checks every JWT implementation should pass before shipping. The actual checklist used by security teams, with the failure mode each prevents.
MD5 Is Dead. Use These Instead.
MD5 was broken in 2004 and is trivially cracked for passwords. Here's what to use instead for checksums, passwords, and message authentication, and why it matters.
JWT Authentication Flow: Login, Token Storage, Refresh Tokens
JWT authentication involves issuing tokens on login, sending them with requests, and refreshing before expiry. Covers the full flow in Node.js, secure storage (httpOnly cookies...
JWT Best Practices: Secure JSON Web Token Implementation
JWT implementation mistakes lead to authentication bypasses and security vulnerabilities. Here's how to sign, validate, and store JWTs correctly, covering algorithm confusion...
JWT Standard Claims: iss, sub, aud, exp, nbf, iat, jti Explained
JWT registered claims define standard metadata for tokens: issuer (iss), subject (sub), audience (aud), expiration (exp), not-before (nbf), issued-at (iat), and JWT ID (jti)....
JWT Refresh Token: Implement Sliding Sessions with Refresh Tokens
Refresh tokens extend JWT sessions without requiring re-login. Learn how to implement a refresh token flow with short-lived access tokens, secure httpOnly refresh token...
JWT Security Best Practices: Common Vulnerabilities and How to Avoid Them
JWT vulnerabilities include algorithm confusion attacks, weak secrets, missing expiry, and storing tokens in localStorage. Learn how to implement JWTs securely: algorithm...
JWT Token Structure: Header, Payload, and Signature Explained
A JWT has three Base64URL-encoded parts: header, payload, and signature, separated by dots. Here's how each part works, what claims mean, and how to read and decode a JWT token.
JWT vs Session Authentication: When to Use Each
JWT and session authentication have different tradeoffs. Sessions store state server-side; JWTs are stateless. Learn when stateless JWTs are genuinely useful, when sessions are...
Related tools
JWT Decoder
Decode and inspect JSON Web Tokens. Local-only, tokens never leave your browser.
QR Code Generator
Generate QR codes for URLs, text, Wi-Fi, contact cards. Custom size, colors, error correction. Download as PNG or SVG. 100% client-side.
Barcode Generator
Generate Code 128, Code 39, EAN-13, UPC-A, EAN-8 and ITF barcodes as SVG with automatic check digit calculation.
Morse Code Translator
Translate text to Morse code and Morse back to English, live. Play it as audio at any speed, with a full alphabet chart.
Written by Mian Ali Khalid. More guides in Encoding & Crypto.