What is a JWT?
On this page
A JWT (JSON Web Token, pronounced “jot”) is a signed bundle of JSON used for authentication and authorization. The signature lets the receiver verify the contents weren’t tampered with, without calling back to the issuer to check.
That last property — stateless verification — is what makes JWTs popular and also what makes them dangerous when misused. This page covers both.
A complete JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIEV4YW1wbGUiLCJpYXQiOjE1MTYyMzkwMjJ9.
8LYVtC4tbE5e7P6V8PNRDLTCRk3lzvR4uClSrb7Wnyo
Three parts separated by dots:
- Header (base64url-encoded JSON) — what algorithm signed it
- Payload (base64url-encoded JSON) — claims about the user / session
- Signature — proof the first two haven’t been modified
You can paste any JWT into the decoder and see all three parts. Decoding is just base64 + JSON.parse — no secret needed.
What’s in each part
Header
{
"alg": "HS256",
"typ": "JWT"
}
Tells the receiver which algorithm to expect. HS256 is HMAC-SHA256.
Other common algorithms: RS256 (RSA-SHA256), ES256 (ECDSA-SHA256),
Ed25519 (formerly written EdDSA).
Payload
{
"sub": "user-12345",
"name": "Alice Example",
"iat": 1516239022
}
Contains “claims” — assertions about the subject. The JWT spec defines
seven registered claims (iss, sub, aud,
exp, nbf, iat, jti), but you can include any JSON.
Signature
The hard part. For HS256:
HMAC-SHA256(
base64url(header) + "." + base64url(payload),
secret
)
Then base64url-encode the result. The receiver re-computes this with the same secret; if it matches, the JWT is authentic. If it doesn’t, either the token was modified or the secret is wrong.
What JWTs are NOT
Three confusions worth clearing up:
- Not encrypted. The header and payload are plain base64 — anyone can read them. JWE (JSON Web Encryption) is the encrypted variant, but it’s less common. If your JWT contains a credit card number, you have a problem.
- Not opaque tokens. Anyone can decode a JWT without the secret. Useful for debugging, dangerous if you assume otherwise.
- Not the same as session tokens. Session tokens are usually random IDs that the server looks up in a database. JWTs are self-contained — the server reads the claims directly from the token.
Where JWTs fit well
- Stateless API authentication. A JWT lets a server validate the caller’s identity without a database lookup, which scales horizontally better than session tokens.
- Cross-service auth. Service A signs a token; service B verifies it with the public key. No shared session store needed.
- OIDC ID tokens. OpenID Connect uses JWTs for the ID token that conveys identity claims (email, name, sub).
- Short-lived access tokens. OAuth 2.0 access tokens are often JWTs.
- Single sign-on. OpenID Connect ID tokens are JWTs.
Where JWTs fit poorly
- Long-lived sessions. JWTs can’t be revoked easily — a stolen token is valid until it expires. Long expiry windows + no revocation = real risk. For session-style usage, prefer opaque random tokens with a server-side session store.
- Tokens carrying sensitive data. Anyone who reads the JWT reads the data. Don’t put SSNs, credit cards, or private notes in claims.
- Single-tenant systems with no scaling concern. Sessions are simpler and easier to revoke. JWT’s main benefit is statelessness.
- Use cases where you’ll need to invalidate tokens. Logout, password
change, security incident — sessions handle these naturally; JWTs
require workarounds (short expiry + refresh tokens, blocklist by
jti, key rotation).
See JWT vs session cookies for the full comparison.
Common security mistakes
1. Trusting alg without pinning
Old JWT libraries had a vulnerability: the receiver would read the
algorithm from the JWT itself, then verify with a key matched to that
algorithm. An attacker could forge a token with alg: HS256 and the
public key as the HMAC secret — the server accepted it.
Fix: always specify the expected algorithm explicitly:
jwt.verify(token, secret, { algorithms: ["HS256"] });
2. Accepting alg: none
The JWT spec defines an none algorithm that means “no signature.”
Some old libraries treated this as “signature passed.” Catastrophic.
Fix: modern libraries reject none by default. Verify your library
does, or pin algorithms explicitly.
3. Weak HMAC secrets
A 6-character secret can be brute-forced offline. HS256 needs at least 256 bits of entropy.
Fix: generate secrets with a CSPRNG:
import { randomBytes } from "crypto";
const secret = randomBytes(32).toString("base64");
4. Storing JWTs in localStorage
localStorage is accessible to any JavaScript on the page, including
malicious script injected via XSS. A stolen JWT is a stolen session.
Fix: store in HttpOnly cookies (inaccessible to JS) or use shorter expiry + secure refresh-token flow.
5. Long expiration with no revocation strategy
A 30-day JWT with no revocation means a stolen token gives 30 days of access. Even after password change.
Fix: short access token (15 min), long refresh token (days/weeks),
revocation list keyed by user or jti.
6. Putting too much in the payload
JWTs travel in headers. Servers and proxies limit header sizes, often in the range of several kilobytes depending on setup. A bloated JWT can break requests in production.
Fix: include only what’s needed for authorization decisions; look up the rest from the database.
How big is a JWT?
Typical signed JWTs are a few hundred bytes to about a kilobyte. Servers and proxies limit header sizes, often in the range of several kilobytes depending on setup, so keep tokens small.
Algorithms compared
| Algorithm | Type | Key | Use case |
|---|---|---|---|
HS256 | HMAC | Shared secret | Internal APIs where both sides share a key |
HS384 / HS512 | HMAC | Shared secret | Higher-security HMAC |
RS256 | RSA-SHA256 | Public/private keypair | Public verification (server signs, many can verify) |
RS384 / RS512 | RSA | Public/private keypair | Higher-security RSA |
ES256 | ECDSA-SHA256 | EC keypair | Modern public-key auth |
Ed25519 | EdDSA | OKP keypair (Ed25519) | Newer public-key option. Replaces the generic EdDSA name, now deprecated (RFC 9864) |
For symmetric setups (your service signs and your service verifies), HS256 is fine. For “anyone can verify with the public key,” use RS256 or ES256.
Tokens signed with Ed25519 used to carry "alg": "EdDSA". The
IANA JOSE registry
now marks EdDSA as Deprecated, because the name alone doesn’t say
which curve is used. Ed25519 is the fully specified replacement.
Deprecated here means a preferred replacement exists, not that EdDSA
is broken. Check that your library and every verifier accept the
Ed25519 name before you switch.
Try the tools
- JWT decoder — paste any JWT, see decoded contents with claim explanations
- Verify HMAC — verify HS256/384/512 signatures
- Sign HS256 — generate test tokens
- Standard claims — every registered claim explained
- JWT vs sessions — when to use each
Further reading
- RFC 7519: JSON Web Token — the spec
- RFC 7515: JSON Web Signature — signing details
- jwt.io introduction — the canonical primer