How to Decode and Validate a JWT
A JWT has three base64url-encoded parts separated by dots: header, payload, signature. Decode the header and payload client-side by base64-decoding each part. Validate: check exp is in the future, alg is not "none", and iss matches your expected issuer. Signature verification requires the secret or public key on the server.
JWTs fail silently — expired tokens cause invalid_grant errors, wrong algorithms cause auth failures, missing claims break authorization. Decode the token first before debugging the auth flow.
JWT structure
Every JWT has three parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← Header (base64url) .eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTcwMH0 ← Payload (base64url) .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQ ← Signature
Decode manually in the browser console
// Paste in browser DevTools console
const token = 'your.jwt.here';
const [header, payload] = token.split('.');
const decode = str => JSON.parse(atob(str.replace(/-/g,'+').replace(/_/g,'/')));
console.log('Header:', decode(header));
console.log('Payload:', decode(payload));
What to check in the header
| Claim | What to check |
|---|---|
alg | Never "none". Prefer RS256 (asymmetric) over HS256 (symmetric) for public APIs. |
typ | Should be "JWT". "at+JWT" for OAuth 2.0 access tokens. |
kid | Key ID — tells the server which key to use for verification. |
What to check in the payload
| Claim | What to check |
|---|---|
exp | Unix timestamp. Must be in the future — new Date(exp * 1000) to read it. |
iat | Issued at time. Should be recent — tokens issued far in the past may indicate replay attacks. |
iss | Issuer. Must match your expected auth server URL exactly. |
aud | Audience. Must match your API identifier — prevent tokens from one service being used on another. |
sub | Subject — usually the user ID. Use this to identify the user, not email (which can change). |
The none algorithm attack
Some early JWT libraries accepted "alg": "none" — meaning no signature required. An attacker could modify the payload and remove the signature entirely. Always reject tokens with alg: none on the server:
# Node.js — jsonwebtoken
jwt.verify(token, secret, { algorithms: ['RS256'] });
// Explicitly allowlist algorithms — rejects 'none' automatically
# Python — PyJWT
jwt.decode(token, key, algorithms=['RS256'])
# Never pass algorithms=None
Check expiry in code
// JavaScript
const payload = JSON.parse(atob(token.split('.')[1]));
const isExpired = payload.exp * 1000 < Date.now();
if (isExpired) { // Refresh the token or redirect to login
}
Signature verification
Signature verification requires the secret (HS256) or public key (RS256) — this must happen on the server, never in the browser. The JWT Debugger decodes and validates claims client-side but cannot verify the signature.
# Node.js
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// Throws JsonWebTokenError if signature invalid, expired, wrong issuer
Decode and validate your JWT →