Home / Blog / JWTs Demystified: What's Inside That Long Token

JWTs Demystified: What's Inside That Long Token

If you have ever worked with a modern web API, you have met the string: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c — a JSON Web Token, or JWT. It looks like gibberish, but it is one of the most readable security formats ever designed.

This article opens one up: the three parts, the encoding that is not encryption, the claims vocabulary, what the signature actually guarantees — and the critical warning every tutorial buries too deep: never put secrets in the payload.

Three parts, two dots

Every JWT has exactly three sections separated by dots: header.payload.signature. The header says how the token is secured — typically {"alg":"HS256","typ":"JWT"}, meaning HMAC with SHA-256. The payload carries the claims: statements about the user and the token itself, like a user ID and an expiry time. The signature is cryptographic proof that the first two parts have not been tampered with.

The design goal was stateless authentication. A server can verify a JWT using only a shared secret or public key — no database lookup, no session store. That is why JWTs power so many APIs and single-sign-on flows: any service holding the key can trust the token independently, which scales beautifully across microservices.

The trade-off is right there in the design: because verification needs no lookup, revocation is hard. A stolen token stays valid until it expires, which is why short lifetimes and refresh-token patterns exist.

Base64url: readable, not secret

The header and payload are Base64url-encoded — a URL-safe variant of Base64 that avoids +, / and = characters. Encoding is not encryption: it is a reversible transformation, like writing in all caps. Anyone with the token can decode the payload in seconds, in a browser console or a free online tool.

This surprises newcomers constantly. Paste a JWT into a debugger and it cheerfully displays the contents: names, emails, roles, expiry. That transparency is intentional — the payload is meant to be read by whoever holds the token — but it means the payload must never contain anything confidential.

Test it yourself sometime with a sample token (never a real one): split on the dots, Base64-decode the middle part, and read your own claims. The exercise takes thirty seconds and permanently fixes the concept.

Claims: the payload's vocabulary

Claims are the name-value pairs inside the payload, and a few are standardised. “sub” (subject) identifies the user, “iss” (issuer) says who created the token, “exp” (expiry) is a Unix timestamp after which the token must be rejected, “iat” (issued at) records creation, and “nbf” (not before) sets earliest validity.

Applications add custom claims: roles, permissions, tenant IDs, feature flags. This is convenient — the API learns what the user may do without a database hit — but every claim enlarges the token, which travels on every request. Bloated tokens slow every call, so keep claims minimal and stable.

One subtlety: claims are assertions by the issuer, not verified facts about the world. “role: admin” means the token issuer says so; the signature is what makes that statement trustworthy, which brings us to the part that does the real security work.

The signature is the whole point

The signature is computed over header.payload using the issuer's secret (HMAC) or private key (RSA/ECDSA). Change even one character of the payload and the signature stops matching — verification fails. That tamper-evidence is the entire security guarantee: the payload can travel through untrusted hands because any modification is detectable.

With HMAC (HS256), the same secret both signs and verifies, so it must stay on trusted servers only. With RSA (RS256), the private key signs and anyone with the public key verifies — ideal for tokens consumed by many services or third parties. Choosing badly here is a classic vulnerability: some infamous attacks tricked servers into verifying an RS256 token with HMAC using the public key as the secret.

Verification must also check the claims, not just the maths: expiry enforced, issuer expected, audience correct, algorithm exactly the one you configured (never “none”). A valid signature on an expired token is still a rejected token.

Never put secrets in the payload

This is the rule to tattoo somewhere visible: JWT payloads are signed, not encrypted. Passwords, API keys, personal data, internal URLs — none of it belongs in claims. The token will be logged, cached, pasted into bug reports and decoded by anyone curious. Assume every claim is public.

If you genuinely need confidentiality in a token, the answer is JWE (JSON Web Encryption), a different standard that actually encrypts — but most applications do not need it. The better pattern is to keep sensitive data server-side and put only an opaque identifier in the token.

Storage matters too. Tokens in browser localStorage are readable by any script on the page, making cross-site scripting attacks devastating; httpOnly cookies with SameSite attributes are the safer default for web apps. And keep lifetimes short — minutes to hours for access tokens — with refresh tokens handling long sessions. A leaked token with a 15-minute life is an incident; one with a 30-day life is a disaster.

Frequently asked questions

Are JWTs encrypted?

No — standard JWTs (JWS) are signed, not encrypted. The payload is Base64url-encoded, which anyone can decode. The signature guarantees the contents have not been altered, but it does nothing to hide them. For actual confidentiality you need JWE, a separate standard.

How long should a JWT live?

As short as practical: typically 5–15 minutes for access tokens in web apps. Long-lived tokens magnify the damage of theft since they cannot easily be revoked. Use short-lived access tokens plus longer-lived, rotatable refresh tokens stored securely for extended sessions.

What happens if the signing secret leaks?

Anyone with the secret can forge tokens with any claims — including admin roles — that your servers will accept as genuine. Treat it like a production incident: rotate the secret immediately, invalidate outstanding tokens, and investigate. This is why secrets live in vaults, never in code repositories.

JWT vs traditional session cookies?

Sessions store state on the server and hand the client an opaque ID; JWTs put the state in the token itself. Sessions are easy to revoke but need shared storage to scale. JWTs scale effortlessly but are hard to revoke. Many systems use both: JWTs for stateless API auth, sessions where instant revocation matters.

Conclusion

A JWT is a signed envelope, not a locked safe: readable by design, trustworthy because of the signature, dangerous the moment you forget which of those is true. Keep payloads public-safe, verify signatures strictly, keep lifetimes short — and never, ever put a secret in the payload.