What is JWT? A Complete Developer's Guide (2026)
If you've authenticated against any modern API in the last few years, you've handled a JWT — those eyJ... strings that come back in the response body and get tacked onto every subsequent request. JWT (JSON Web Token) is the de-facto standard for stateless authentication in single-page apps, mobile backends, and microservice architectures. It's compact, self-contained, and works without a server-side session store.
This guide breaks down the anatomy of a JWT, walks through the three signing algorithms you'll actually meet (HS256, RS256, ES256), compares it head-to-head with traditional session cookies, and flags the four security mistakes that show up in production code all too often.
1. The Three-Part Structure
A JWT is just three Base64URL-encoded strings joined by dots:
header.payload.signature
Each segment has a job. Together they let a receiver verify that the token came from a trusted issuer and hasn't been tampered with. The format is defined in RFC 7519.
Header
Declares the signing algorithm and token type:
{
"alg": "HS256",
"typ": "JWT"
}
After Base64URL encoding, that JSON becomes eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. Note: Base64URL is the URL-safe variant (uses - and _ instead of + and /, drops padding) — see the Base64 guide for details.
Payload (Claims)
The payload carries the actual data — known as "claims." There are three flavors you should know about, and they often get conflated in tutorials:
- Registered claims — predefined names from the RFC. Recommended but optional. The ones you'll see in the wild:
iss(issuer) — who minted the tokensub(subject) — usually the user IDaud(audience) — intended recipientexp(expiration) — Unix timestamp, after which the token is invalidiat(issued at) — when it was mintednbf(not before) — earliest time it's validjti(JWT ID) — unique identifier, useful for replay defense
- Public claims — custom names registered in the IANA JWT Registry or namespaced with a URI to avoid collisions.
- Private claims — anything you and your consumers agree on, like
user_idorrole. Convenient, but the spec offers no guarantees about interoperability.
Signature
The signature is what makes the token trustworthy. For HS256 it's calculated like this:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
The server recomputes this hash on every request and rejects tokens whose signature doesn't match. If even a single character in the header or payload changes, the signature will no longer verify.
2. HS256 vs RS256 vs ES256
The alg field in the header picks the signing algorithm. The three you'll meet most often:
| Algorithm | Type | Key model | Best for |
|---|---|---|---|
| HS256 | Symmetric HMAC | One shared secret | Single-service apps, internal APIs |
| RS256 | Asymmetric RSA | Private signs, public verifies | Multi-service verification, OAuth 2.0, OIDC |
| ES256 | Elliptic curve | Private signs, public verifies | Mobile/IoT, short signatures, high security |
Rule of thumb: HS256 for a single backend signing and verifying its own tokens (simpler is better). RS256 once you have multiple services that need to verify tokens minted by an auth server — the public key can be distributed freely without compromising security. ES256 when signature size or CPU cost matters (mobile clients, edge devices).
3. A Complete Example
Here's the canonical example from jwt.io, which you can paste into any decoder to verify:
Header (Base64URL-encoded):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload (Base64URL-encoded):
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
Full token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Three segments, two dots, one signed payload. Decoded, the payload reads:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
4. JWT vs Session Cookies
The classic question: when to reach for a JWT, and when to stick with the tried-and-true server-side session cookie? Here's the honest comparison.
| Dimension | JWT | Session cookie |
|---|---|---|
| State model | Stateless — token carries everything | Stateful — server stores session |
| Scalability | Trivial — any node can verify | Needs a shared store (Redis) |
| Revocation | Hard — needs a blacklist or short expiry | Easy — delete the session record |
| Size per request | Larger (header + payload + signature) | Tiny — opaque session ID |
| CSRF risk | None if sent in Authorization header | Needs CSRF tokens or SameSite cookies |
In short: JWTs shine for stateless, distributed systems and cross-service auth. Sessions still win for traditional web apps with one server, easy logout, and tight revocation requirements.
5. Anatomy of a Login Request
Here's what a typical JWT-based login flow looks like, end to end:
┌──────────┐ ┌──────────┐
│ Client │ │ Server │
└────┬─────┘ └────┬─────┘
│ │
│ POST /login {user, password} │
│ ──────────────────────────────────>│
│ │ verify creds
│ │ sign JWT
│ 200 OK {token: "eyJhbG..."} │
│ <──────────────────────────────────│
│ │
│ GET /api/profile │
│ Authorization: Bearer eyJhbG... │
│ ──────────────────────────────────>│
│ │ verify signature
│ │ read claims
│ 200 OK {name: "John", ...} │
│ <──────────────────────────────────│
│ │
The server never stores the token. The client holds it (in memory or a cookie — see security below) and presents it on every request.
6. Four Security Mistakes You Must Avoid
1. Storing the token in localStorage
Any XSS bug gives an attacker localStorage.getItem("token"). Store tokens in an httpOnly, Secure, SameSite=Lax cookie instead — JavaScript can't read those, and the browser won't send them cross-site.
2. Trusting the alg header blindly
The classic alg=none attack: an attacker replaces the header with {"alg":"none"} and strips the signature. If your library accepts that, anyone can forge a valid token. Equally dangerous is the algorithm-confusion attack, where a server expecting RS256 is tricked into verifying with HS256 using the public key as a symmetric secret. Always pin the expected algorithm on the server side and never let the header dictate it.
3. Skipping TLS
A JWT in an HTTP request body is plaintext. Anyone on the network path reads it. Always serve auth endpoints over HTTPS, and reject plain-HTTP callbacks for refresh tokens.
4. Long-lived access tokens, no rotation
A token valid for 30 days with no rotation is a 30-day window for an attacker. Keep access tokens short (15–60 minutes) and pair them with refresh tokens. Rotate the refresh token on every use so a stolen one is detected quickly.
7. Decode a JWT Locally with DevToolbox
When you're debugging an API and need to peek inside a token — say, to check whether the exp claim is why auth is failing — the JWT Decoder runs entirely in your browser. Paste the token, see the header and payload decoded instantly, and the signature is verified locally so nothing leaves your machine.
Decode any JWT in one click
Header + payload parsed in your browser. No network calls. No token logging.
Open JWT Decoder →Frequently Asked Questions
Can I store sensitive data in the JWT payload?
No. The payload is just Base64URL-encoded JSON — anyone who has the token can read it with a single line of code. The signature guarantees it wasn't tampered with, not that it's secret. Keep sensitive data on the server and look it up using an ID from the claims.
How do I revoke a JWT before it expires?
The honest answer is that pure JWTs don't support revocation well. In practice, teams do one of three things: (1) maintain a blacklist of revoked jti values in Redis, checked on every request; (2) keep access tokens very short (minutes) and rely on a refresh-token system, so a forced logout just deletes the refresh token; (3) maintain a "tokens-issued-after" timestamp per user and reject anything older on logout.
JWT vs OAuth 2.0 — what's the difference?
They live at different layers. OAuth 2.0 is an authorization framework — a protocol for delegating access ("this app can act on your behalf with this other service"). JWT is a token format — a way to encode claims inside a compact, signed string. The two often appear together: OAuth 2.0 flows frequently use JWTs as the access-token format (then called "JOSE" or "JWT bearer tokens"). JWT alone is not an authorization protocol.
Is JWT a good fit for API authentication?
Yes, especially for stateless REST or GraphQL APIs served by multiple nodes. It's the right choice when the auth service is separate from the resource servers and you don't want every request hitting a central session store. Just follow the four security practices above — short expiry, httpOnly storage, pinned algorithm, HTTPS only.
Which JWT library should I use?
Pick the one with the most downloads and the most recent commit in your language's ecosystem. A few battle-tested choices: Node.js — jsonwebtoken. Python — PyJWT. Java — jjwt or nimbus-jose-jwt. Go — golang-jwt/jwt. Ruby — ruby-jwt. Avoid rolling your own crypto.
Wrapping Up
JWT is a small specification that punches above its weight. Three segments, a signature, and you've got a portable way to convey identity across services. Get the algorithm choice right for your architecture, treat the token like a password, and you'll avoid 95% of the JWT-related security incidents that show up in the news.
When you're staring at a 401 response and need to know what's actually inside the token, skip the CLI one-liner and use the DevToolbox JWT Decoder — it's the fastest way to check the header, claims, and signature status in one place.