What Is a JWT (JSON Web Token)?
If you have ever inspected a web request and seen a long string of gibberish split by two dots, you have met a JSON Web Token. JWTs are how a huge share of the modern web handles “this user is logged in.” They are also routinely misunderstood in ways that cause real security holes. This guide explains what a JWT is, what it is not, and the traps that catch people building with them.
The problem JWTs solve
HTTP is stateless: every request arrives with no memory of the last one. So how does a server know that this request is from someone who logged in five minutes ago?
The traditional answer is a session. The server generates a random session ID, stores the user's details against it in a database, and gives the ID to the browser as a cookie. Every request sends the ID back, and the server looks it up.
JWTs invert this. Instead of storing the user's data on the server and handing out a pointer to it, the server puts the data in the token itself, signs it, and hands the whole thing over. The server then needs to store nothing at all — it can verify the token mathematically. This is why JWTs are called self-contained or stateless.
The three parts
A JWT is three Base64-encoded chunks joined by dots: header.payload.signature
1. Header — metadata, chiefly which signing algorithm is used:
{ "alg": "HS256", "typ": "JWT" }
2. Payload — the actual data, called claims:
{ "sub": "1234", "name": "Alice", "role": "admin", "exp": 1799999999 }
Some claim names are standardised:
- sub — subject: who the token is about (usually a user ID).
- exp — expiry, as a Unix timestamp. After this moment the token is invalid.
- iat — issued at.
- iss — issuer: who created the token.
- aud — audience: who it is intended for.
3. Signature — the header and payload, hashed together with a secret key that only the server knows. This is the part that makes the whole thing trustworthy.
The most important thing to understand
A JWT is signed, not encrypted. The payload is fully readable by anyone.
Base64 is encoding, not encryption. It has no key and no secret — it exists to make binary data safe to transport, and anyone can decode it in one line of code, or by pasting the token into a decoder.
People see the random-looking string and assume it is protected. It is not. If you put an email address, a phone number or an internal ID in a JWT payload, you have published it to whoever holds the token.
So what does the signature actually give you? Integrity, not secrecy. Anyone canread the token, but nobody can changeit without the secret key. Flip “role” from user to admin and the signature no longer matches, so the server rejects it.
Rule: never put anything secret in a JWT payload.
The alg:none attack
A notorious vulnerability, and a perfect illustration of why the details matter.
The JWT spec permits an algorithm value of none, meaning “this token is unsigned.” It was intended for cases where security is handled elsewhere.
Naive libraries simply read the algorithm from the token's own header and do what it says. So an attacker takes a valid token, edits the payload to make themselves an administrator, sets "alg": "none", deletes the signature — and the server, obediently following the header, performs no verification and accepts it.
The token is telling the server how to check the token. Which is like asking a stranger to validate their own ID.
The defence: the server must decide which algorithm it expectsand reject anything else. Never trust the header's claim about how to verify itself. Modern libraries guard against this, but it still appears in older code.
The revocation problem
This is the genuine, unavoidable weakness of JWTs, and the one to weigh before choosing them.
Because the server stores nothing, it has no list of valid tokens — so it cannot easily invalidate one. A token is valid until it expires, full stop.
Which means:
- A user clicks “log out” — their token still works.
- You ban a user — their existing token keeps working.
- A token is stolen — you cannot cancel it.
- You demote an admin — their token still says
admin.
With server-side sessions, you delete the row and access ends instantly. With JWTs, you cannot. The standard mitigations are all compromises:
- Short expiry times — 5 to 15 minutes — so a stolen token is useful only briefly, paired with a longer-lived refresh token to get new ones.
- A blocklist of revoked tokens — which reintroduces the server-side state that JWTs existed to eliminate, undermining the whole point.
When to use a JWT — and when not to
Good fit:
- Stateless APIs, especially across multiple servers where sharing session state is awkward.
- Single sign-on across several services that share a trusted issuer.
- Short-lived, tightly scoped access tokens.
Poor fit:
- A conventional web app with a login form. An old-fashioned session cookie is simpler, safer and revocable instantly. JWTs are frequently used here purely because they sound modern.
- Anywhere you need to revoke access immediately.
- Storing anything sensitive.
Common mistakes to avoid
- Putting secrets in the payload. It is readable by anyone.
- Trusting the
algheader. Pin the algorithm server-side. - Not checking
exp. An expired token must be rejected. - A weak signing secret. It can be brute-forced. Use a long random key.
- Very long expiry times — a year-long token you cannot revoke is a liability.
- Storing tokens in localStorage, where any cross-site scripting flaw can read them. An httpOnly cookie is safer.
Frequently asked questions
Can I decode a JWT without the secret? Yes — anyone can read the payload. You need the secret only to verify or create a valid token.
Are JWTs secure? The mechanism is sound. Most JWT vulnerabilities come from how they are used, not from the standard itself.
How long should a JWT last? Access tokens: minutes. Refresh tokens: days or weeks, stored securely and revocable.
JWT or session cookie? If you have one server and a normal login, use a session. Reach for JWTs when statelessness genuinely buys you something.
Decode a JWT now
Use our JWT Decoderto inspect a token's header and payload in your browser — nothing is sent anywhere, so it is safe to paste real tokens. The encoding itself is Base64, and the exp claim is Unix time.