Understanding and Decoding JSON Web Tokens (JWT)
Learn JWT structure, base64url encoding, and standard claims — how to decode tokens in JavaScript, why decoding is not verifying, and security best practices.
JSON Web Tokens are everywhere in modern authentication — in your Authorization
headers, your cookies, and your OAuth flows. They look like opaque gibberish, but
they are mostly just JSON you can read. This guide explains exactly what a JWT
contains, how to decode one, and the security pitfall that trips up even
experienced developers.
Anatomy of a JWT
A JWT is three base64url-encoded strings joined by dots:
header.payload.signature
Each segment has a job:
- Header — metadata about the token, mainly the signing algorithm.
- Payload — the claims: who the user is, when the token expires, who issued it.
- Signature — a cryptographic seal proving the first two parts were not tampered with.
Here is a real-looking token (line-wrapped for readability; in practice it is one line):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
base64url encoding
The segments are encoded with base64url, a URL-safe variant of base64. Two
characters differ: + becomes -, and / becomes _. Padding = characters
are usually stripped. This matters because standard base64 decoders may choke on
a raw JWT segment — you sometimes need to swap the characters back and re-add
padding before decoding.
base64url is encoding, not encryption. Anyone who has the token can read the header and payload. Keep reading — that fact is the whole security lesson here.
Decoding the header
Decoding the first segment yields the header JSON:
{
"alg": "HS256",
"typ": "JWT"
}
The alg field tells the recipient which algorithm signed the token (here,
HMAC-SHA256). The typ is almost always JWT. Some tokens add a kid (key ID)
to select the right verification key.
Decoding the payload
The second segment decodes to the claims:
{
"sub": "1234567890",
"name": "Jane Doe",
"iat": 1516239022,
"exp": 1516242622
}
Standard registered claims
The JWT spec reserves several short claim names. You will see these constantly:
iss— issuer: who created the token (e.g.https://auth.example.com).sub— subject: the principal the token is about, usually a user ID.aud— audience: who the token is intended for (an API or client ID).exp— expiration: a Unix timestamp after which the token is invalid.iat— issued at: when the token was created.nbf— not before: the token is invalid before this time.
The exp, iat, and nbf values are seconds since the Unix epoch, not
milliseconds. A token with these claims might look like:
{
"iss": "https://auth.example.com",
"sub": "user_42",
"aud": "billing-api",
"iat": 1516239022,
"nbf": 1516239022,
"exp": 1516242622,
"scope": "read:invoices write:invoices"
}
Everything outside the registered set — like name or scope above — is a
custom claim you define for your application.
Decoding a JWT in JavaScript
In the browser, atob decodes base64. You split on dots, fix the base64url
characters, and parse the JSON. A small helper:
function decodeJwt(token) {
const [headerB64, payloadB64] = token.split(".");
const fromBase64Url = (segment) => {
// restore standard base64, then add padding
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
const padded = base64.padEnd(
base64.length + ((4 - (base64.length % 4)) % 4),
"="
);
return JSON.parse(atob(padded));
};
return {
header: fromBase64Url(headerB64),
payload: fromBase64Url(payloadB64),
};
}
const { header, payload } = decodeJwt(token);
console.log(header.alg); // "HS256"
console.log(payload.sub); // "1234567890"
In Node.js you can decode without any third-party library using Buffer:
const payload = JSON.parse(
Buffer.from(token.split(".")[1], "base64url").toString()
);
console.log(payload.exp);
Note that Node's base64url encoding handles the character swapping and padding
for you. To check whether a token is expired, compare exp against the current
time in seconds:
const isExpired = payload.exp * 1000 < Date.now();
You can do all of this interactively — paste a token into the JWT decoder to see the header and payload as formatted JSON without writing any code.
Decoding is NOT verifying
This is the single most important point in this article. Decoding a JWT only reads the base64url data. It does not check the signature, so it tells you nothing about whether the token is authentic.
Anyone can craft a token with whatever payload they like:
# an attacker base64url-encodes their own header and payload
# the result decodes "successfully" but the signature is garbage
To trust a token, the server must verify the signature using the secret (for
HMAC algorithms like HS256) or the public key (for RS256/ES256). Only the
party holding the signing key can produce a valid signature, and only signature
verification proves the claims were not altered. Verification also re-checks
exp, nbf, iss, and aud. Never make an authorization decision based on a
decoded-but-unverified token.
A short rule of thumb: decode in the client for display, verify on the server for trust.
Security best practices
- Never trust an unverified token. Always verify the signature server-side before honoring any claim.
- Use short expiry. Keep
exptight — minutes to an hour for access tokens — and use refresh tokens for longer sessions. A leaked short-lived token is worth little. - Never put secrets in the payload. Remember, the payload is readable by anyone. No passwords, no API keys, no PII you would not print on a postcard.
- Validate
alg. Reject tokens whose algorithm does not match what you expect, and never acceptalg: none. - Check
audandiss. Make sure the token was minted by your issuer and intended for your service. - Store tokens carefully. Prefer
HttpOnly,Securecookies overlocalStorageto reduce XSS exposure.
Conclusion
A JWT is three base64url segments — header, payload, signature — and the first two are plain JSON you can read at a glance. Knowing the standard claims and how to decode them in JavaScript demystifies the whole flow. Just remember the line that separates a working auth system from a breach: decoding shows you what a token says, but only verifying the signature proves you can believe it.
Want to inspect a token right now? Paste it into the JWT decoder to see the decoded header and payload, then pretty-print the result with the JSON formatter. Find more developer tools and guides on the JSONPost blog.
Keep reading
Validating JSON with JSON Schema
Learn how JSON Schema works, how to generate a schema from sample data, and how to validate documents with clear, path-based error messages.
JSON vs JSON5 vs JSONC — What's the Difference?
Comments, trailing commas, and unquoted keys — understand JSON5 and JSONC, where each is used, and how to convert them to strict JSON.
How to Format JSON (and Why It Matters)
A practical guide to beautifying, indenting, and minifying JSON — and when to use each, with tips for debugging messy API responses.