How to Read a JWT, and Why You Should Never Put Secrets in One
4 min read3 October 2026By Muhammad Abdullah
A JWT looks like an opaque string of random characters, and that appearance misleads people into treating it as secret. It is not. A standard JWT is encoded, not encrypted, which means anyone holding one can read most of what is inside. Understanding that single fact explains both how to read a token and what you must never put in it.
A JWT is three parts separated by dots
Every JWT has the shape header.payload.signature. The first two parts are JSON objects encoded with Base64url; the third is a cryptographic signature over the first two.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiIsImV4cCI6MTc5MDAwMDAwMH0.dGhpc2lzYXNpZ25hdHVyZQ
\__________________/ \_____________________________________/ \____________________/
header payload signatureThe header says how the token was signed. The payload carries the claims: who the token is about, who issued it, and when it expires. The signature lets a server that holds the key confirm that nobody altered the first two parts.
Decoding it yourself
Because the header and payload are only Base64url, decoding them needs no key. Base64url swaps two characters relative to ordinary Base64, so convert those back, decode the bytes, and parse the JSON.
function decodePart(part) {
const base64 = part.replace(/-/g, "+").replace(/_/g, "/");
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
return JSON.parse(new TextDecoder().decode(bytes));
}
const [header, payload] = token.split(".").slice(0, 2).map(decodePart);The detour through TextDecoder matters. Calling JSON.parse(atob(...)) directly works for plain ASCII but garbles any non-Latin text in the payload, because atob returns raw bytes rather than decoded UTF-8.
The standard claims
A handful of short claim names are defined by the specification and mean the same thing in every token.
iss: the issuer, who created the token.sub: the subject, usually the user the token is about.aud: the audience, the service the token is meant for.exp: the expiry time, after which the token must be rejected.iat: issued at, when the token was created.nbf: not before, a time before which the token is not yet valid.
Reading the expiry
exp, iat and nbf are Unix timestamps in seconds, not milliseconds. JavaScript dates use milliseconds, so multiply by a thousand before converting, or every token will appear to expire in January 1970.
const expires = new Date(payload.exp * 1000);
const expired = expires < new Date();An expired token is not necessarily broken. Most systems issue short-lived access tokens on purpose and refresh them, so finding exp in the past is often entirely normal.
Decoding is not verifying
Reading a token tells you what it claims. It tells you nothing about whether those claims are true. Anyone can write a JSON object, encode it, and attach a fake signature. Only a check of the signature against the correct key proves the token was issued by who it says and was not modified.
So decode freely to inspect and debug, but never make an access decision based on a payload you have not verified. Verification belongs on your server, using a maintained JWT library and the signing key, which should never be pasted into a browser tool.
Why secrets do not belong in the payload
Since the payload is readable by anyone holding the token, anything you put there is effectively public. Tokens end up in browser storage, server logs, analytics tools and error reports. A password, an API key, or personal data in a payload is data you have published.
The 'none' algorithm trap
The header's alg field names the signing algorithm. The specification includes a value of none, meaning no signature at all. A poorly written verifier that trusts the header could accept a forged token simply because it declares itself unsigned. Good libraries reject none by default and let you pin the algorithm you expect. Make sure yours does.
Common questions
Is a JWT encrypted?
A standard signed JWT is not. Its header and payload are only Base64url encoded, so anyone can read them. Encrypted JWTs exist (JWE) but are a separate, less common format.
Can I trust the data in a decoded JWT?
Not until the signature has been verified on your server with the correct key. Decoding only shows what the token claims.
Why is the exp value such a large number?
It is a Unix timestamp in seconds. Multiply by 1000 to get the milliseconds JavaScript dates expect.