How to decode a JWT in JavaScript (and why decoding isn't verifying)
A snip.tools guide · runs alongside the JWT decoder
Prefer to just run it? Use the free JWT decoder — no install, in your browser.
Open the JWT decoderA JSON Web Token (JWT) is the compact string an auth server hands a client to prove who they are. It often looks encrypted, but its first two parts are just Base64-encoded JSON you can read in a few lines of JavaScript. Knowing how to decode one — and, crucially, understanding the difference between decoding and verifying — saves a lot of debugging. (To paste a token and inspect every claim instantly, our in-browser JWT decoder does this without sending the token anywhere.)
The three parts of a token
A JWT is three Base64url segments separated by dots. The header says which algorithm signed it, the payload carries the claims (who the user is, when it expires), and the signature is a cryptographic check over the first two parts:
// A JWT is three Base64url strings joined by dots:
// header . payload . signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 // header → {"alg":"HS256","typ":"JWT"}
.eyJzdWIiOiIxMjMiLCJuYW1lIjoiQWRhIn0 // payload → {"sub":"123","name":"Ada"}
.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gF // signature (binary, not JSON) The header and payload are not secret — anyone holding the token can read them. Only the signature requires a key, and only to verify, not to read.
Decoding the payload
Decoding is: split on the dots, take the payload segment, convert it from Base64url to standard Base64, run
it through atob, and parse the JSON. One detail trips people up — JWTs use the
URL-safe Base64 alphabet, swapping +// for -/_,
so you must translate those two characters before atob will accept them:
// Decode the payload of a JWT in the browser — no library needed.
function decodeJwt(token) {
const [, payload] = token.split('.'); // [header, payload, signature]
const json = atob(
payload.replace(/-/g, '+').replace(/_/g, '/'), // Base64url → Base64
);
return JSON.parse(json);
}
decodeJwt(token); // { sub: "123", name: "Ada", iat: 1718000000, exp: 1718003600 } Handling non-ASCII claims correctly
Plain atob mishandles any multi-byte character (an accented name, CJK text), because it predates
Unicode-aware strings. If your claims might contain non-ASCII, decode the bytes through
TextDecoder instead — the same UTF-8-safe technique covered in
Base64 in JavaScript:
// Names and text can contain non-ASCII, so decode UTF-8 safely:
function decodeJwtSafe(token) {
const [, payload] = token.split('.');
const b64 = payload.replace(/-/g, '+').replace(/_/g, '/');
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
return JSON.parse(new TextDecoder().decode(bytes));
} Reading the standard claims
The payload's well-known fields are defined by the spec and are worth memorising. The time-based ones —
exp (expiry) and iat (issued-at) — are seconds since the Unix
epoch, so multiply by 1000 before handing them to JavaScript's millisecond Date:
// Standard claims are seconds since the Unix epoch — multiply by 1000 for JS.
const claims = decodeJwtSafe(token);
const isExpired = claims.exp * 1000 < Date.now();
const issuedAt = new Date(claims.iat * 1000);
// exp = expires, iat = issued-at, sub = subject, iss = issuer, aud = audience Decoding is not verifying — this is the important part
Everything above reads the token; none of it proves the token is genuine. Anyone can craft a
string with a payload claiming "role":"admin" and Base64url-encode it. What stops that is the
signature: the server recomputes it using a secret (HMAC) or a public key (RSA/ECDSA) and rejects the token
if it doesn't match. So two firm rules follow:
- Never trust an unverified token to make a security decision. Decoding in the browser is fine for showing a username or checking expiry for UX, but authorization must be enforced server-side against a verified signature.
- Signature verification belongs on the server, because it needs the signing secret (for
HMAC) which must never ship to the client. Use a vetted library (
jose,jsonwebtoken) rather than hand-rolling the crypto.
And never put secrets in a JWT
Because the payload is readable by anyone holding the token, a JWT is the wrong place for anything sensitive — no passwords, no API keys, no private personal data. The token authenticates; it doesn't conceal. If you need confidentiality, that's encryption's job, not a JWT's.
To inspect a token's header, payload and claim timestamps without writing any of this, paste it into the JWT decoder — it runs entirely in your browser, so the token never leaves your machine. The decoding itself leans on Base64; for the full UTF-8-safe treatment see the Base64 in JavaScript guide.
Try it now: the JWT decoder does everything in this guide in your browser — nothing is uploaded. Browse more guides or the full tool list.