How to Decode a JWT Without the Secret (And Why Verification Still Matters)
One of the most common misconceptions about JSON Web Tokens is that they are encrypted. They are not. JWT headers and payloads are only Base64URL-encoded — anyone can decode them and read the claims inside without knowing the signing secret.
This guide explains what decoding reveals, how to do it safely during development, and why decoding without verification must never replace proper signature validation in production.
JWT Structure: Three Dot-Separated Parts
A JWT has three segments separated by dots:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyM30.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
HEADER PAYLOAD SIGNATURE| Part | Encoding | Requires Secret? |
|---|---|---|
| Header | Base64URL | No |
| Payload | Base64URL | No |
| Signature | HMAC/RSA | Yes (to verify) |
The header typically contains the algorithm (alg) and token type (typ). The JWT payload contains claims like sub (subject), exp (expiration), and custom application data.
Decode vs Verify: The Critical Difference
Decoding converts Base64URL back to JSON. It proves nothing about authenticity.
Verifying checks the cryptographic signature using your secret or public key. Only verified tokens should be trusted.
// WRONG — reads claims without checking authenticity
const payload = jwt.decode(token);
console.log(payload.userId); // Could be forged!
// CORRECT — verifies signature before trusting claims
const payload = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
console.log(payload.userId); // Cryptographically authenticatedAn attacker can craft a token with any payload they want. Without signature verification, your API will accept it.
How to Decode a JWT Manually
In JavaScript
function decodeJwtPart(part) {
const base64 = part.replace(/-/g, '+').replace(/_/g, '/');
const json = atob(base64);
return JSON.parse(json);
}
const [headerB64, payloadB64] = token.split('.');
const header = decodeJwtPart(headerB64);
const payload = decodeJwtPart(payloadB64);
console.log('Algorithm:', header.alg);
console.log('Subject:', payload.sub);
console.log('Expires:', new Date(payload.exp * 1000));In Python
import base64, json
def decode_part(part):
padding = 4 - len(part) % 4
part += '=' * padding
return json.loads(base64.urlsafe_b64decode(part))
header_b64, payload_b64, _ = token.split('.')
header = decode_part(header_b64)
payload = decode_part(payload_b64)Using the JWT Validator Tool
For quick inspection during development, paste your token into the JWT Validator. It decodes the header and payload instantly and optionally verifies the signature if you provide your secret.
What You Can Learn From Decoding
Decoding is genuinely useful for debugging:
- Check expiration — is
expin the past? - Inspect claims — does
submatch the expected user? - Verify algorithm — is
algset to HS256 as expected? - Audit custom claims — roles, permissions, tenant IDs
| Claim | Purpose | Safe to Trust Without Verify? |
|---|---|---|
| `sub` | User identifier | No |
| `exp` | Expiration timestamp | No |
| `iat` | Issued-at timestamp | No |
| `role` | Authorization level | No |
| `alg` (header) | Signing algorithm | Never trust from token |
Every claim in a decoded token is untrusted until the signature is verified.
Common Development Mistakes
Trusting Decoded Claims in Middleware
// DANGEROUS — attacker sets role: 'admin' in forged payload
app.use((req, res, next) => {
const payload = jwt.decode(req.headers.authorization?.split(' ')[1]);
req.user = payload;
next();
});Using jwt.decode() for Authorization
Libraries expose decode() for debugging, not authentication. Always use verify() in request handlers.
Ignoring the Algorithm Header
Never use the alg value from the token to select your verification method. Always specify the expected algorithm explicitly:
jwt.verify(token, secret, { algorithms: ['HS256'] });When Decoding Without Verification Is Acceptable
There are narrow cases where decode-only is fine:
- Local debugging — inspecting your own tokens during development
- Logging and monitoring — extracting
suborjtifor audit trails (but never for authorization decisions) - Client-side display — showing the user's name from a token that was already verified server-side
In every authorization path — API middleware, route guards, background jobs — verification is mandatory.
Production Validation Checklist
Use this checklist for every token your API accepts:
1. Extract the token from the Authorization: Bearer header
2. Verify the signature with your secret or public key
3. Explicitly require algorithm HS256 (or RS256)
4. Check exp — reject expired tokens
5. Validate iss and aud if your architecture uses them
6. Check custom claims (role, scope) only after steps 1–5 pass
The JWT Validator helps you test each step during development.
Base64URL: Why JWTs Look Like Gibberish
JWTs use Base64URL encoding — a URL-safe variant of Base64 that replaces + with - and / with _, and omits padding =. This makes tokens safe to pass in URLs and HTTP headers without additional encoding.
Understanding Base64URL explains why JWTs are readable but not secure by obscurity. Encoding is not encryption.