JWT Anatomy
Three base64url segments separated by dots — what is in each one, and why 'encoded' is not 'encrypted'.
eyJhbGciOiJSUzI1NiIsImtpZCI6ImsxIn0.eyJzdWIiOiI0MiJ9.MEUCIQ...A JSON Web Token is three base64url-encoded parts joined by dots:
header.payload.signatureHeader
Describes how the token is signed. alg is the algorithm, kid is the key id (so the verifier knows which of several public keys to use).
{"alg": "RS256", "typ": "JWT", "kid": "k1"}Payload (the claims)
The facts being asserted. Registered claims have standard meanings; you can add your own.
{
"iss": "https://dfg.com", // issuer
"sub": "42", // subject: the user
"aud": "https://dfg.com/api", // audience: who may accept it
"exp": 1754313000, // expires at (seconds since epoch)
"iat": 1754312400, // issued at
"jti": "c1f9...", // unique token id
"scope": "orders:read"
}Signature
Computed over base64url(header) + "." + base64url(payload) with the signing key. Change one byte of either part and verification fails.
The rule people miss
A JWT is signed, not encrypted. Anyone holding the token can read the payload — it is base64, not a cipher. Never put a password, a card number, or an internal secret in a JWT. Signing guarantees integrity and origin, nothing about confidentiality. (Encrypted variants exist — JWE — and are rarely what you want.)
Keep it small
The token travels in a header on every request. Some proxies cap header size around 8KB, and a bloated token slows every call. Put the user id and the scopes in; look everything else up.
Example
# Decode a JWT with nothing but base64 — no key needed. That is the point.
TOKEN='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBbGljZSIsImV4cCI6MTc1NDMxMzAwMH0.7mZ9Xk2rQ8vT1cB4nL6pY0dF3sJ5wA2eR8uI1oK7hG4'
# header
echo "$TOKEN" | cut -d. -f1 | base64 -d 2>/dev/null
# {"alg":"HS256","typ":"JWT"}
# payload — readable by anyone holding the token
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null
# {"sub":"42","name":"Alice","exp":1754313000}
# The signature is the only part you cannot produce without the key.When to use it
- A support engineer decodes a customer's token to check which audience and scopes it carries, without ever needing the signing key.
- A code review rejects a token that embedded the user's full address and phone number, since every holder of the token can read them.
- An API gateway reads the kid header to pick the right public key during a key rotation window where two keys are valid.
More examples
Building and reading the parts by hand
Writing it once by hand makes the security model obvious: the signature protects the bytes, it does not hide them.
import { createHmac } from 'crypto';
const b64url = (obj) =>
Buffer.from(JSON.stringify(obj)).toString('base64url');
const header = { alg: 'HS256', typ: 'JWT' };
const payload = {
iss: 'https://dfg.com',
sub: '42',
aud: 'https://dfg.com/api',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 600,
scope: 'orders:read',
};
const signingInput = `${b64url(header)}.${b64url(payload)}`;
const signature = createHmac('sha256', SECRET)
.update(signingInput)
.digest('base64url');
const token = `${signingInput}.${signature}`;
// Reading it back requires no key at all:
const [h, p] = token.split('.');
console.log(JSON.parse(Buffer.from(p, 'base64url').toString()));
// { iss: 'https://dfg.com', sub: '42', ... }Decoding without verifying — and when that is OK
Decoding on the client to schedule a refresh is fine. Decoding on the server to read the user id — which happens more often than you would hope — is a complete authentication bypass.
// A client may decode its OWN token to read the expiry and schedule a refresh.
function decodePayload(token) {
try {
return JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
} catch { return null; }
}
const claims = decodePayload(accessToken);
const msLeft = claims.exp * 1000 - Date.now();
setTimeout(refresh, Math.max(0, msLeft - 30_000)); // refresh 30s early
// ⚠️ This is a UX optimisation ONLY. A client-side decode proves nothing:
// the payload is attacker-controllable if the token came from anywhere else.
// The SERVER must always verify the signature before trusting a single claim.
Discussion