What a JWT actually is
A JSON Web Token is a string. That is the whole trick. Instead of handing a client a session id and keeping the session on the server, you hand the client a signed statement — “this is user 4821, these are their scopes, this expires at 14:05” — and let them carry it. Any service that trusts the signing key can read the statement and act on it without calling a database.
That makes JWTs very convenient and slightly dangerous, for the same reason: the data is in the client's hands. A token you issue can be read by whoever holds it, cannot be withdrawn before it expires, and will be accepted by anything that trusts the signature. Most JWT problems in production come from forgetting one of those three facts.
A token looks like three blocks of gibberish separated by dots:
eyJhbGciOiJIUzI1NiIsImtpZCI6ImtleS0yIn0.eyJzdWIiOiI0ODIxIiwiZXhwIjoxNzY1MTExMTExfQ.3xS7_c9Nn-aQ
It is not encrypted. The first two blocks are plain JSON in Base64url, which is a URL-safe
alphabet that swaps + and / for - and _ and
drops the trailing = padding. Anyone can turn them back into JSON in a second — which
is exactly what a JWT decoder does.
The three parts, and what each one does
Split the token on the dots and you have the header, the payload and the signature.
The header
Small, and almost always about keys. alg names the signing algorithm,
typ is usually JWT, and kid is a key id that tells the
verifier which public key to fetch when the issuer has more than one. If a token suddenly stops
validating after a key rotation, kid is where you look first.
The payload
The claims: the actual statement being made. Some are registered by the spec
(iss, sub, aud, exp, nbf,
iat, jti) and the rest are whatever your issuer decided to add —
scope, roles, tenant_id, an email address, a feature
flag.
The signature
A MAC or a digital signature over the first two parts, exactly as they appear in the string. That last detail matters: the signature covers the encoded bytes, so re-serialising the JSON and re-encoding it will produce a token that no longer validates even if the data is identical. Never rebuild a token to “tidy it up”.
How to decode a token, and what to look at first
Paste the whole thing into a decoder, dots included. If you copied it out of a request, strip
the Bearer prefix — or use a tool that strips it for you. Then read, in this
order:
- Did it decode at all? If the decoder says the parts are not valid Base64url JSON, the token was truncated or mangled in transit. A cookie that hit a size limit and a header that got line-wrapped both look like this.
exp. Expiry explains the majority of “it worked five minutes ago” reports, and it is the first thing to rule out.audandiss. A token minted for a different audience is a perfectly valid token that your API is right to reject.- The custom claims. Only once the standard ones check out is it worth arguing
about whether
scopecontains what you expected.
Decode locally wherever you can. A token is a live credential, so pasting one into a random site is the same class of mistake as pasting a password: any tool worth using decodes in the browser and sends nothing anywhere.
The claims that decide whether a token works
Five claims do almost all the work, and each one can reject a request on its own.
exp— expiry, as a Unix timestamp in seconds. A millisecond value here is a classic bug: it lands in the year 57000 and the token never expires. A good decoder shows both the real date and how long ago it passed.nbf— not before. The token is invalid until this moment. Rarely used deliberately, occasionally set by accident on a machine whose clock is ahead.iat— issued at. Useful for rejecting tokens that are technically unexpired but suspiciously old, and for spotting a clock problem wheniatis in the future.aud— audience. The intended recipient. An API that skips this check will happily accept a token your other service issued for itself, which is a real privilege-escalation path between microservices.iss— issuer. Pin it to the exact issuer you expect, and fetch keys only from that issuer's published key set.
jti is the odd one out: a unique token id. It only means something if you store
and check it, but it is what makes revoking a single token possible at all.
Decoding is not verifying
This is the point that bites people. Decoding reads Base64url and shows you JSON. It tells you what a token says. It tells you nothing about whether that statement is true.
Anyone can write {"sub":"1","role":"admin"}, Base64url it, bolt any string on the
end as a signature and hand it to you. It will decode perfectly in any decoder, including ours.
What stops it working is the verification step: recomputing the signature with the issuer's key
and refusing the token if it does not match. That step needs a secret or a public key, it belongs
on your server, and no browser tool can do it for you.
So treat a decoder as a readout, never as evidence. “I decoded it and it says admin” is not a security finding. “My server verified it and it says admin” is.
Why a valid-looking token gets a 401
When the token decodes cleanly and the API still refuses it, it is almost always one of these:
- Expired. Check
expagainst the clock on the server, not yours. - Clock skew. A server a couple of minutes ahead will reject tokens that have only just been issued. Most libraries allow a small leeway for exactly this reason; if yours is set to zero, that is worth revisiting.
- Wrong audience. Common when one identity provider serves several APIs.
- Key rotation. The issuer signed with a new key, the verifier still has the
old key set cached, and
kidno longer resolves. - The token never arrived. A missing or malformed
Authorizationheader, a dropped cookie, or a proxy stripping headers. Check what the server actually received before blaming the token — the HTTP status code lookup and our guide to status codes are useful for telling a 401 apart from a 403 that means something else entirely.
401 or 403?
A 401 means the credential is missing, expired or unverifiable: fix the token. A
403 means the credential was fine and the answer is still no: fix the permissions, or
the scope claim. Services that return 401 for an authorisation failure send people
off to debug a token that was never the problem.
Choosing and checking the signing algorithm
Two families matter in practice. HS256 is an HMAC with a shared secret: whoever can verify can also sign, which is fine inside one service and a poor fit across organisational boundaries. RS256 and ES256 use a private key to sign and a public key to verify, so an API can validate tokens it could never have minted itself.
Then there is alg: none, an unsigned token that the specification permits and that
you should never accept. It exists in the standard, which means it has existed in libraries, which
means it has been exploited: a verifier that takes the algorithm from the token's own header can
be told “this one is unsigned, just trust it”. A related trick hands an RS256 verifier
an HS256 token signed with the public key as the HMAC secret.
The defence is one line of policy: decide which algorithms you accept before you look
at the token, and reject anything else. The header is a hint about which key to use, not
an instruction about whether to check the signature. If your decoder flags alg: none
in red, that is why.
What should never go in a payload
Because the payload is encoded rather than encrypted, every claim is public to whoever holds the token — and tokens end up in browser storage, in logs, in error trackers, in support tickets and in screenshots. Keep out of the payload:
- Anything you would not put in a URL: passwords, API keys, card numbers, health details.
- Internal identifiers you would rather not publish — database primary keys, internal hostnames, licence terms.
- Large blobs. Every request carries the token; a kilobyte of claims is a kilobyte on every call, and oversized tokens are a reliable way to hit header size limits in a proxy.
Keep tokens out of URLs too. A token in a query string lands in server logs, in
Referer headers and in browser history. Send it in the
Authorization header, and make sure your site sets a sensible
Referrer-Policy — our guide
to security headers covers that, and the security headers checker will tell you what you are sending today.
Expiry, refresh and the revocation problem
A JWT cannot be called back. Once issued, it is valid until exp, whatever happens
to the account behind it. Disable a user, and their access token keeps working until it runs out.
That is the price of not checking a database on every request.
The standard answer is two tokens. A short-lived access token — minutes, not days — carries the claims and does the work. A long-lived refresh token is stored server-side, checked against a database every time it is used, and can be revoked instantly. Logging someone out then means invalidating the refresh token and waiting out the short window before the access token dies on its own.
Pick the expiry window deliberately: it is the gap between revoking access and access actually
stopping. Fifteen minutes is a common compromise. Hours are hard to justify; days mean you have no
revocation at all. And if you need instant revocation, keep a deny-list of jti
values — at which point you are checking a store on every request again, and a plain session may
be the simpler design.
Whatever you choose, the debugging loop is the same: decode the token, read exp
and aud, confirm the clock, then verify the signature on the server. Four steps, in
that order, and almost every token problem falls out in the first two.
Related tools and guides:
- JWT Decoder - Decode a JSON Web Token and read its header, payload, claims and expiry.
- Base64 Encoder & Decoder - Encode text to Base64 or decode it back, with URL-safe output and UTF-8 handled.
- Base64 Explained: How Encoding and Decoding Really Work - How Base64 turns bytes into text
Frequently Asked Questions
Can I decode a JWT without the secret key?⌄
Yes. The header and payload are Base64url-encoded, not encrypted, so anyone holding the token can read them. The key is only needed to verify the signature. That is also why you must never put sensitive data in a payload.
Is it safe to paste a JWT into an online decoder?⌄
Only if the decoding happens in your browser. A token is a live credential until it expires, so a tool that uploads it to a server is handling your credential. Our decoder runs entirely in the page and sends the token nowhere, but the safest habit is to use expired or test tokens whenever you can.
Why does my token decode fine but still return 401?⌄
Decoding proves only that the string is well formed. The usual culprits are an exp that has passed, a clock difference between the issuer and the verifier, an aud that names a different API, or a kid the verifier cannot resolve after a key rotation.
What is the difference between JWT and a session cookie?⌄
A session cookie is a pointer: the server keeps the state and looks it up on every request, so it can be revoked instantly. A JWT carries the state itself, so no lookup is needed — and nothing can be withdrawn before it expires. JWTs scale better across services; sessions give you control.
Should I use HS256 or RS256?⌄
HS256 shares one secret, so anyone who can verify can also sign. That is acceptable inside a single service. RS256 and ES256 sign with a private key and verify with a public one, which is what you want as soon as more than one party is involved. Either way, pin the accepted algorithms in your verifier and never trust the alg value in the token.
15+ years of hands-on experience in web development and security. Focused on practical web security: keeping websites secure, reliable and easy to troubleshoot. BuildToolForge exists to make these checks free, private and understandable. Tools run in your browser wherever possible, and nothing you enter into a tool is stored.
How we write our guides: Every guide is written from hands-on experience, checked against the official standards, and tested with our own tools on real domains. We update guides when standards or tools change. Found a mistake? Email contact@buildtoolforge.com.