JWT Explained for Developers
A JWT is three base64-encoded strings separated by dots. What's in them, how signatures actually work, why "stateless auth" is a half-truth, and the mistakes that keep showing up in production code.
A JSON Web Token is three base64url-encoded strings glued together with dots. That's the entire format. Once you've seen one, you can spot them everywhere:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0In0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
The three sections are:
- Header — JSON describing the algorithm and token type.
- Payload — JSON containing the claims (who, what, when).
- Signature — a cryptographic proof that the first two parts haven't been tampered with.
That structure is straightforward. Everything that goes wrong with JWTs comes from misunderstanding what each piece guarantees — and, more often, what it does not.
Anatomy, in detail
Let's decode the example above.
Header
Base64url-decoding the first segment gives:
{ "alg": "HS256", "typ": "JWT" }
algis the signing algorithm. Common values:HS256(HMAC-SHA-256, symmetric),RS256(RSA-SHA-256, asymmetric),ES256(ECDSA, asymmetric).typis the token type. Almost always"JWT".
Sometimes you'll see kid (key ID), pointing at which key to use when the issuer rotates them.
Payload
The second segment decodes to a JSON object of claims:
{
"sub": "1234",
"iss": "https://auth.example.com",
"aud": "api.example.com",
"iat": 1715000000,
"exp": 1715003600,
"scope": "read:calendar"
}
Most of those keys are reserved by RFC 7519:
sub— subject. The user ID.iss— issuer. Who minted the token.aud— audience. Who the token is for. Reject tokens not addressed to you.iat— issued-at, Unix seconds.exp— expiration, Unix seconds.nbf— not-before. Token isn't valid before this time.jti— JWT ID. A unique identifier for the token, useful for revocation.
You can put any other JSON-serializable values in here. Don't put secrets. The payload is base64-encoded, not encrypted. Anyone who has the token can read everything in it.
Signature
This is where the security lives. The signature is:
HMAC-SHA256( base64url(header) + "." + base64url(payload), secret )
Or for RS256:
RSA-Sign( base64url(header) + "." + base64url(payload), privateKey )
The receiver re-computes the signature using the same input and the appropriate key. If it matches, the token wasn't tampered with. If it doesn't, the token is forged or modified, and you reject it.
A JWT is signed, not encrypted. If you need confidentiality (the payload contains sensitive data), you want a JWE — a different RFC, a different format — or, more pragmatically, you should keep the sensitive data on the server and only put a reference in the JWT.
Symmetric vs asymmetric: HS256 vs RS256
The single most consequential design decision when issuing JWTs.
HS256 (symmetric) uses one secret. The issuer signs with it; the verifier verifies with it. Whoever can verify can also forge. That's fine if the same service does both — your monolith issues a session token and verifies it on the next request. It's a problem the moment you have a separate API consuming tokens, because that API now has the issuing power too.
RS256 (asymmetric) uses an asymmetric key pair. The issuer signs with a private key. Consumers verify with the public key. Anyone can hold the public key and verify; only the issuer can mint new tokens.
For systems with more than one service, use RS256 (or ES256, which is smaller and faster). For a single monolith, HS256 is fine and simpler to operate.
The asymmetric choice is also what makes rotation possible — see Rotating JWT Signing Keys Without Logging Everyone Out.
The "stateless auth" half-truth
The pitch for JWTs in session contexts goes: the server doesn't need to store sessions; the token itself contains everything needed to authorize the request. This is true in a narrow sense and misleading in a broader one.
What's true: a JWT signed by your issuer carries the user's identity and permissions. Any service holding the public key can authorize the request without a database round-trip.
What's missing: revocation. If a user logs out, or you discover the token has been stolen, or you fire an employee — a stateless JWT keeps working until it expires. There's no list to remove it from.
Real systems solve this in a few ways, all of which add state back:
- Very short-lived access tokens (5-15 minutes) paired with a refresh token that is tracked server-side. The blast radius of a leaked access token is small; revocation happens at the refresh layer.
- A revocation list keyed by
jti. Every JWT verification checks a denylist. Cheap with Redis; harder to scale globally. - Token versioning. Embed a
tokenVersionin the JWT and store the user's current version in the database. Verification compares the two. Logout bumps the version, invalidating all old tokens. This adds a DB lookup but keeps revocation immediate.
"Stateless JWTs everywhere" is mostly a tutorial-grade architecture. Production auth systems are almost always hybrid.
How to verify a JWT correctly
The verification logic is more than "does the signature check out." A correct verifier does, in order:
- Parse the token into header, payload, signature.
- Reject if
algin the header is"none". Some libraries used to accept this. Thenonealgorithm means "no signature," which means "anyone can forge anything." This has been the source of multiple real CVEs. - Reject if
algdoesn't match what you expect. Don't trust the token to tell you which algorithm to use. Pin the algorithm in your verifier configuration. The "alg confusion" attack flips an asymmetric token into a symmetric one by changing the header, then signs it with the public key (which the attacker has). - Verify the signature with the expected key (resolve via
kidif rotating). - Check
exp— token must not be expired. Allow a small clock skew (60 seconds is typical). - Check
nbfif present — token must not be from the future. - Check
iss— must match the expected issuer string exactly. - Check
aud— must include your service's identifier. If a JWT issued forservice-Acan authorize requests toservice-B, you've built a confused deputy. - Only now trust the claims.
Don't write any of that by hand. Use a library you trust (jose for JavaScript, python-jose for Python, golang-jwt/jwt/v5 for Go). Audit the library's defaults — some accept alg=none by default and have to be configured not to.
Things that go wrong
The bugs that show up in real codebases:
Putting sensitive data in the payload. "We base64-encoded it, no one can read it." Anyone with atob() can read it. JWT payloads are public to anyone holding the token.
Storing JWTs in localStorage. They're now reachable by any XSS in your application. Once an attacker has the token, they have the session. Use HttpOnly cookies for session JWTs in browser contexts. We have a longer treatment in Common Authentication Vulnerabilities.
Long-lived JWTs. A 30-day access token sounds convenient. It is, for an attacker who steals one. Access tokens should expire in minutes, not days. Use refresh tokens for the long-lived part of the session, and keep those server-side.
Forgetting to rotate keys. Asymmetric signing keys should rotate periodically. Have a kid in your header and resolve to a key set (JWKS endpoint or local map). When you rotate, the new key is signed against; old keys remain valid until tokens signed with them expire.
Trusting the JWT for authorization decisions that change frequently. A user's role embedded in a 1-hour JWT keeps working for an hour after you revoke it. If you're encoding permissions that must be revocable in seconds, either keep the JWT short-lived enough to not matter, or do a per-request authorization lookup against your source of truth.
Cross-issuer confusion. Your auth server issues tokens for api.example.com. A separate auth server (say, Google for "log in with Google") also issues tokens. A bug in your verifier accepts both. Now Google tokens can authorize calls to your API. Pin the issuer.
When not to use JWTs
JWTs are popular enough that people reach for them by default. They're not always the right tool.
Browser sessions for a single web app: an opaque session cookie pointing at a server-side session record is simpler, smaller, and easier to revoke. JWTs are a win when you have multiple services consuming the same token across origins.
API keys for backend services: the auth pattern is "this caller has these capabilities," and you usually want it revocable in seconds. An opaque key looked up in a database is often a better fit.
Anywhere you're tempted to put sensitive data in the payload "because the client doesn't decode it." The client absolutely decodes it. The browser DevTools decodes it. Anyone who pastes the token into jwt.io decodes it.
The right framing: a JWT is a portable, signed assertion of facts. Use it where portability and signature actually buy you something. Don't use it as a generic session cookie when an opaque session ID would do.
What to read next
- Common Authentication Vulnerabilities — the token-handling patterns we keep seeing in code reviews.
- Rotating JWT Signing Keys Without Logging Everyone Out — the overlap window that makes a key swap a non-event.
- Session Management Best Practices — JWT or otherwise, the post-login session is where most bugs live.
JWTs are not magic, and they are not a replacement for thinking carefully about your session model. They're a sturdy primitive for carrying signed claims between services. Get the verifier right, keep them short-lived, and don't make them carry more than they should.
Written by
Emilian GheoneaSenior Blockchain & Full-Stack Software Engineer. I build EmbedAuth — an embeddable authentication platform for SaaS — and write about the auth problems most teams hit too late.
Related articles
JWT
Designing JWT Claims: What Belongs in a Token and What Doesn't
A practical guide to claim design — the standard claims and how to validate them, why token bloat becomes a 431 outage, PII and privacy in a base64 payload, namespacing custom claims, and versioning a payload you can never fully migrate.
Jul 29, 2026 9 minReadJWT
JWTs vs Opaque Tokens: Stop Choosing Based on Vibes
The real trade-off is where authorization state lives and how fast it can change. A decision framework covering revocation latency, introspection, hybrid designs, and the failure modes each choice hands you.
Jul 27, 2026 8 minReadJWT
Rotating JWT Signing Keys Without Logging Everyone Out
How JWKS and the kid header make key rotation a non-event — the overlap window, caching and thundering herds, emergency revocation, and the specific mistakes that turn a routine rotation into an outage.
Jul 25, 2026 9 minReadAuthentication
Refresh Token Rotation Explained: Detecting Token Theft
How refresh token rotation works, why token families enable automatic breach detection, the reuse-detection algorithm step by step, and the edge cases that trip teams up.
Jul 4, 2026 6 minRead