How to Decode a JWT — and 7 Security Mistakes That Get Tokens Forged
What's inside a JSON Web Token, how to decode and verify one safely, and the real-world JWT vulnerabilities — alg:none, key confusion, weak secrets — with the fix for each.
By Abdessamad Bettal··6 min read
Key takeaways
- A JWT is three Base64URL segments — header, payload, signature — joined by dots. The header and payload are encoded, not encrypted: anyone holding the token can read them.
- The signature only proves the token wasn't modified by someone without the key. Never put secrets or personal data in the payload unless you use encrypted JWTs (JWE).
- Most JWT breaches come from verification bugs: accepting
alg: none, letting the token choose its own algorithm (RS256→HS256 key confusion), weak HMAC secrets, and skippingexp,audandisschecks. - Fix them by pinning the expected algorithm on the server, using a random secret of at least 256 bits for HS256 (or an asymmetric key), and validating every registered claim.
- Decode production tokens only in tools that run locally. Pasting a live token into a hosted debugger hands someone a working credential.
JSON Web Tokens (RFC 7519) carry identity and permissions between services: after login, your API hands the browser a JWT, and every later request presents it instead of a session cookie. Because the token is self-contained, the server can verify it without a database lookup — which is also why mistakes in verification are so dangerous.
Anatomy of a JWT
Here is a real HS256 token, signed with the secret correct-horse-battery-staple:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzgyNzEiLCJuYW1lIjoiQWRhIExvdmVsYWNlIiwicm9sZSI6ImVkaXRvciIsImlhdCI6MTc5MzAwMDAwMCwiZXhwIjoxNzkzMDAzNjAwfQ.-tqKwYSnLeraycJh-328i7yJO6BpVQ3Dkj3yBjUSeqASplit on the dots and Base64URL-decode the first two parts:
// Header — how the token is signed
{ "alg": "HS256", "typ": "JWT" }
// Payload — the claims
{
"sub": "user_8271",
"name": "Ada Lovelace",
"role": "editor",
"iat": 1793000000, // issued at: 2026-10-26 07:33:20 UTC
"exp": 1793003600 // expires at: 2026-10-26 08:33:20 UTC (1 hour later)
}The third segment is the signature: HMAC-SHA256(secret, base64url(header) + "." + base64url(payload)), itself Base64URL-encoded. Change one character of the payload — say editor to admin — and the signature no longer matches.
Paste the token into our JWT decoder to see the claims, with iat and exp rendered as dates. Enter the secret to verify the signature — then change one letter of the secret and watch verification fail. The decoder runs in your browser, so it's safe to use with real tokens.
Decoding on the command line
# Payload is the 2nd segment. Base64URL uses - and _ and drops padding,
# so translate and re-pad before decoding:
echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | awk '{ while (length($0) % 4) $0 = $0 "="; print }' | base64 -dSigning algorithms: HS256 vs RS256 vs ES256 vs EdDSA
| alg | Type | Who can verify | Notes |
|---|---|---|---|
| HS256 | HMAC + shared secret | Anyone with the secret — who can also forge | Simple; fine when one service issues and verifies |
| RS256 | RSA signature | Anyone with the public key | Ubiquitous; large signatures |
| ES256 | ECDSA P-256 | Anyone with the public key | Small and fast; needs a sound RNG |
| EdDSA | Ed25519 | Anyone with the public key | Modern, deterministic; growing support |
| none | Unsigned | — | Never accept |
Choose asymmetric signing (RS256, ES256, EdDSA) when tokens are verified by services other than the issuer — each verifier gets only the public key, so a compromised microservice can't mint tokens. HS256 is fine when the same service issues and checks tokens.
The 7 mistakes behind most JWT vulnerabilities
1. Accepting alg: none
The JWT spec defines an "unsecured" token with "alg": "none" and an empty signature. Early libraries honoured whatever the header said, so an attacker could take any token, edit the payload, set alg to none, strip the signature — and be accepted. Modern libraries reject none by default, but custom verification code still gets this wrong.
2. Letting the token pick the algorithm (key confusion)
A server expects RS256 and verifies with its RSA public key. An attacker changes the header to HS256 and signs the forged token with HMAC, using that public key — which is public — as the HMAC secret. A library that does verify(token, key) and trusts the header's alg will compute HMAC with the same public key and accept it. This class of bug (for example CVE-2015-9235 in Node's jsonwebtoken) is why you must pin the algorithm:
// jose (Node, Deno, browsers)
const { payload } = await jwtVerify(token, publicKey, {
algorithms: ["RS256"], // never derive this from the token
issuer: "https://auth.example.com",
audience: "api.example.com",
});3. Weak HMAC secrets
An HS256 token is an offline brute-force target: an attacker who has one token can test candidate secrets as fast as their hardware computes HMAC-SHA-256, with no rate limit — hashcat has a dedicated mode (16500) for it. The secret in our example, correct-horse-battery-staple, is a famous phrase and would fall to a wordlist almost instantly. RFC 7518 requires an HS256 key of at least 256 bits; use a random one from a secret token generator, not a memorable phrase.
4. Skipping claim validation
A valid signature only means *we* issued this token at some point. You must also check:
exp— reject expired tokens (allow a small clock-skew leeway, e.g. 30–60 seconds).nbf— not valid before this time.iss— the issuer you expect.aud— that the token was meant for *this* service. Without it, a token issued for a low-privilege app may be replayed against your admin API.
5. Putting sensitive data in the payload
Base64URL is an encoding, not encryption. Email addresses, internal IDs, and permission lists in a payload are visible to the user, to browser extensions, and in any log that captures an Authorization header. If the payload must be confidential, use JWE (encrypted JWT, RFC 7516) — or keep the data server-side and put only an opaque ID in the token.
6. Trusting header-supplied keys (kid, jku, jwk)
Headers like jku (a URL to fetch keys from) and jwk (an embedded public key) let a token tell you how to verify itself — which an attacker will happily exploit by pointing them at their own key. The kid (key ID) header is fine to *select* among keys you already trust, but has caused SQL injection and path traversal when used directly in a query or file path. Only resolve keys from an allow-listed JWKS URL you configured.
7. Long-lived tokens with no revocation
A stateless JWT is valid until exp, even after the user logs out or is fired. Keep access tokens short-lived (5–15 minutes), pair them with refresh tokens that *are* checked against a database and can be revoked, and rotate signing keys on a schedule.
Where should the browser store a JWT?
localStorage is readable by any JavaScript on the page, so a single XSS bug exfiltrates every token. An HttpOnly, Secure, SameSite cookie can't be read by JavaScript at all, at the cost of needing CSRF protection (SameSite=Lax or Strict covers most cases). For browser apps, cookies are generally the safer default; for native apps and service-to-service calls, an Authorization: Bearer header is normal.
A verification checklist
- Use a maintained library (jose, PyJWT, java-jwt, golang-jwt) — never hand-parse.
- Pin the accepted algorithm(s) in server configuration.
- Use an asymmetric algorithm when more than one service verifies tokens; otherwise a random ≥256-bit HMAC secret.
- Validate
exp,nbf,issandaudon every request. - Fetch public keys only from a JWKS URL you configured; ignore
jkuandjwkheaders. - Keep access tokens short-lived and refresh tokens revocable.
- Keep personal data out of the payload, or use JWE.
- Follow RFC 8725, *JSON Web Token Best Current Practices*, which documents these attacks.
Frequently asked questions
Is a JWT encrypted?
+
Standard signed JWTs (JWS) are not encrypted. The header and payload are Base64URL-encoded and readable by anyone who has the token. Only the signature is cryptographic, and it guarantees integrity, not confidentiality. Encrypted JWTs (JWE) exist but are less common.
Is it safe to paste a JWT into an online decoder?
+
Only if the decoder runs entirely in your browser. A JWT is a bearer credential, so sending a live token to a remote server gives that server the ability to act as you until the token expires. The encrypt.tools JWT decoder decodes and verifies locally.
Should I use HS256 or RS256?
+
Use HS256 when the same service issues and verifies tokens, with a random secret of at least 256 bits. Use RS256, ES256 or EdDSA when other services verify tokens, so they only need the public key and cannot forge tokens.
What is the JWT alg none attack?
+
An attacker edits a token's payload, sets the header algorithm to none and removes the signature. Vulnerable libraries that trust the header accept the unsigned token as valid. Prevent it by configuring the server with an explicit list of allowed algorithms.
How long should a JWT be valid?
+
Access tokens are typically valid for 5 to 15 minutes. Use longer-lived refresh tokens, stored securely and checked against a server-side record, to obtain new access tokens and to support revocation.
Tools used in this guide
Encoding vs Encryption vs Hashing: The Difference, With Examples
Encoding, encryption and hashing all turn data into gibberish, but only one keeps secrets. Learn the difference with real Base64, AES and SHA-256 examples and when to use each.
6 MIN READSYMMETRIC · AESAES-GCM vs CBC vs CTR vs ECB: Which AES Mode Should You Use?
AES is only as safe as its mode. Compare ECB, CBC, CTR and GCM — how each works, how each fails, and why AES-GCM (or ChaCha20-Poly1305) is the right default in 2026.
7 MIN READHASHING · SECURITYIs MD5 Still Safe? When MD5 and SHA-1 Are (and Aren't) OK to Use
MD5 and SHA-1 are broken — but not for everything. Learn exactly which attacks exist, where MD5 and SHA-1 are still acceptable, where they are dangerous, and what to use instead.
5 MIN READ