Updated April 2026

How to Decode and Validate a JWT

Quick Answer

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.

Decode your JWT →

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

ClaimWhat to check
algNever "none". Prefer RS256 (asymmetric) over HS256 (symmetric) for public APIs.
typShould be "JWT". "at+JWT" for OAuth 2.0 access tokens.
kidKey ID — tells the server which key to use for verification.

What to check in the payload

ClaimWhat to check
expUnix timestamp. Must be in the future — new Date(exp * 1000) to read it.
iatIssued at time. Should be recent — tokens issued far in the past may indicate replay attacks.
issIssuer. Must match your expected auth server URL exactly.
audAudience. Must match your API identifier — prevent tokens from one service being used on another.
subSubject — 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 →