The Problem JWT Solves
Traditional web applications use server-side sessions: when a user logs in, the server creates a session record and sends a session ID cookie to the browser. Every subsequent request looks up that session in storage to identify the user.
This works fine for a single server, but creates headaches at scale:
- Multiple servers need access to the same session store (Redis, a database)
- Microservices need to share session state across service boundaries
- Mobile apps and SPAs communicate via stateless APIs where cookies are awkward
JSON Web Tokens address these problems by moving authentication state into the token itself. The server no longer needs to look anything up — it just verifies the signature.
What a JWT Looks Like
A JWT is a compact string of three dot-separated, Base64URL-encoded sections:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiJ1c2VyXzEyMyIsIm5hbWUiOiJBbGljZSIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxNjI0MjYyMn0
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The three parts are:
- Header — algorithm and token type
- Payload — the claims (data)
- Signature — cryptographic proof of authenticity
The Header
{
"alg": "HS256",
"typ": "JWT"
}
alg specifies the signing algorithm. typ is always JWT.
The Payload (Claims)
{
"sub": "user_123",
"name": "Alice",
"role": "admin",
"iat": 1716239022,
"exp": 1716242622
}
Claims are statements about the subject (typically the logged-in user). There are three categories:
- Registered claims — standardised names defined by the JWT spec:
sub,iss,aud,exp,iat,nbf - Public claims — names registered with IANA to avoid collisions:
name,email, etc. - Private claims — custom fields agreed between your services:
role,tenant_id, etc.
The Signature
The signature is computed over the Base64URL-encoded header and payload:
HMACSHA256(
base64url(header) + "." + base64url(payload),
secret
)
Anyone can decode a JWT header and payload — they are only Base64URL-encoded, not encrypted. The signature is what guarantees that the content has not been modified since it was issued. A valid signature tells the server: "I signed this, and it hasn't changed."
Signing Algorithms
| Algorithm | Type | Use when | |---|---|---| | HS256 / HS384 / HS512 | HMAC (symmetric) | Single service, shared secret | | RS256 / RS384 / RS512 | RSA (asymmetric) | Distributed systems, multiple verifiers | | ES256 / ES384 / ES512 | ECDSA (asymmetric) | Same as RSA, smaller key sizes | | PS256 / PS384 / PS512 | RSA-PSS (asymmetric) | RSA with probabilistic signing | | EdDSA | Ed25519 / Ed448 | Modern, high-performance asymmetric | | none | None | Unsecured (never use in production) |
HMAC (HS256) uses the same secret to sign and verify. Easy to set up, but every service that needs to verify tokens must have the secret.
RSA / ECDSA uses a private key to sign and a public key to verify. Services only need the public key to check tokens, which is much safer — the private key never leaves the auth server.
JWT Authentication Flow
1. User logs in with credentials
2. Auth server verifies credentials and issues a JWT signed with its private key
3. Client stores the JWT (typically in memory or localStorage)
4. Client sends the JWT in the Authorization header on every request:
Authorization: Bearer <token>
5. Resource server verifies the signature with the public key
6. If valid and not expired, the request is processed
JWT vs Session Tokens
| | JWT | Session | |---|---|---| | State stored | In the token (client) | On the server | | Scalability | Horizontal scaling easy | Requires shared session store | | Revocation | Hard (must use a blocklist) | Easy (delete the session) | | Size | Larger (claims add bytes) | Tiny (just an ID) | | Best for | Microservices, APIs, mobile | Monolith, server-rendered apps |
Common Security Pitfalls
Never accept "alg": "none". A malicious actor can create a JWT without a signature
by setting the algorithm to none. Always validate that the algorithm matches what your
server expects.
Validate the exp claim. Expired tokens should be rejected. A token with no exp
claim never expires — be careful when issuing tokens without an expiry.
Validate the iss and aud claims. If your system has multiple token issuers, check
that the iss matches your expected issuer and aud matches your service. This prevents
tokens issued for one service being accepted by another.
Do not store sensitive data in the payload. Anyone with the token can decode the payload. If you need to transmit sensitive data, use JWE (JSON Web Encryption) instead.
Use short expiry times. Because JWTs are stateless, you cannot instantly revoke one. Use short expiry (15–60 minutes) and refresh tokens for long-lived sessions.
Inspecting a JWT
Paste any JWT into the JWT Decoder to see the decoded header and payload instantly. You can also verify the signature by providing your secret or public key.
To create a signed JWT for testing, use the JWT Encoder. It supports all major algorithms and runs entirely in your browser.
Related Tools
- JWT Decoder — Decode and verify JWT tokens in your browser.
- JWT Encoder — Create and sign JWT tokens with HS256, RS256, ES256, and more.
- Base64 Decoder — Manually decode the Base64URL-encoded parts of a JWT.