JSON Web Tokens have become the default mechanism for authentication in modern web applications, particularly those with single-page frontends and API backends. Despite their widespread use, many developers treat JWTs as opaque strings — they copy library code, get authentication working, and move on without understanding what is inside the token or how the security model works. This leads to vulnerabilities. Understanding JWTs from the inside out helps you use them correctly and recognize when they are the wrong tool for the job.
The Three-Part Structure
A JWT is a string composed of three parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The three parts are:
- Header: Metadata about the token, primarily the signing algorithm and token type.
- Payload: The claims — the actual data the token carries.
- Signature: A cryptographic signature that verifies the header and payload have not been tampered with.
Each part is Base64url-encoded (not standard Base64 — the URL-safe variant that uses - and _ instead of + and /, with padding omitted). You can decode the header and payload with a simple Base64 decode. The signature is a binary value that only makes sense when verified against the signing key.
The Header
The header is a JSON object that typically contains two fields:
{
"alg": "HS256",
"typ": "JWT"
}
The alg field specifies the signing algorithm. The typ field is almost always "JWT". Some headers include a kid (key ID) field that tells the verifier which key to use, which is important in systems that rotate signing keys.
The Payload and Claims
The payload contains claims — statements about the user and additional metadata. Claims come in three categories:
Registered claims are predefined by the JWT specification (RFC 7519). They are optional but recommended:
iss(issuer): Who created the token, e.g., "auth.example.com".sub(subject): The user or entity the token represents, typically a user ID.aud(audience): The intended recipient(s) of the token, e.g., "api.example.com".exp(expiration): Unix timestamp after which the token is no longer valid.iat(issued at): Unix timestamp when the token was created.nbf(not before): Unix timestamp before which the token is not valid.jti(JWT ID): Unique identifier for the token, useful for preventing replay attacks.
Public claims are custom claims registered in the IANA JSON Web Token Claims registry to avoid naming conflicts. Private claims are custom claims agreed upon between parties, such as role, email, or permissions.
A typical payload looks like this:
{
"sub": "user_8f3k2j",
"name": "Jane Smith",
"email": "[email protected]",
"role": "admin",
"iat": 1775366400,
"exp": 1775370000
}
Signing Algorithms
The signature is what makes JWTs trustworthy. Without it, anyone could create a token with any claims they wanted. The two most common algorithm families are:
HMAC (HS256, HS384, HS512): Symmetric algorithms that use a single shared secret. The same key signs and verifies the token. HS256 uses HMAC with SHA-256. This is simpler to set up but requires that every service that needs to verify tokens has access to the secret key.
HMACSHA256(
base64urlEncode(header) + "." + base64urlEncode(payload),
secret
)
RSA (RS256, RS384, RS512): Asymmetric algorithms that use a private/public key pair. The issuer signs with the private key, and anyone can verify with the public key. This is ideal for distributed systems where multiple services need to verify tokens but only the auth server should create them.
There is also ES256 (ECDSA with P-256), which provides equivalent security to RS256 with shorter keys and signatures. It is increasingly preferred for new systems.
Token Validation Flow
When a server receives a JWT, it must perform these validation steps in order:
- Parse the token into its three Base64url-encoded parts.
- Decode the header and determine the signing algorithm.
- Verify the signature using the appropriate key and algorithm. If the signature does not match, reject the token immediately.
- Decode the payload and check the registered claims:
- Is
expin the future? If not, the token has expired. - Is
nbfin the past? If not, the token is not yet valid. - Does
issmatch the expected issuer? - Does
audinclude this service?
- Is
- Extract the custom claims (user ID, roles, permissions) and proceed with the request.
Every single one of these steps matters. Skipping any of them opens a security hole.
Common Security Mistakes
JWTs are frequently misused. Here are the most dangerous mistakes developers make:
1. Not validating the signature. This is the most critical error. If you decode the payload without verifying the signature, an attacker can forge any token they want. Some developers mistakenly treat JWT decoding as authentication.
2. The "alg: none" attack. The JWT spec allows an "alg": "none" header, meaning no signature. If your server blindly trusts the alg field from the token, an attacker can set it to "none", remove the signature, and the token passes validation. Always enforce a whitelist of acceptable algorithms on the server side.
3. Algorithm confusion attacks. If a server is configured to accept RS256 (asymmetric), an attacker might craft a token with "alg": "HS256" and sign it using the public key as the HMAC secret. Since the public key is, well, public, the attacker has the "secret." The server sees HS256, uses the public key as the HMAC key, and the signature verifies. The fix: never let the token dictate which algorithm to use. Configure accepted algorithms on the server.
4. Storing sensitive data in the payload. The payload is Base64url-encoded, not encrypted. Anyone with the token can decode and read the claims. Never put passwords, credit card numbers, or other secrets in a JWT payload.
5. Not checking expiration. If you skip the exp check, tokens are valid forever once issued. A stolen token becomes a permanent skeleton key.
6. Using long expiration times. JWTs cannot be revoked once issued (without additional infrastructure). A token valid for 30 days means a compromised token is exploitable for 30 days. Keep access token lifetimes short (5-15 minutes) and use refresh tokens for longer sessions.
JWTs vs. Session Cookies
JWTs and server-side sessions solve the same problem — maintaining authentication state — but with different trade-offs:
- JWTs are stateless: The server does not need to store session data. All the information is in the token. This is ideal for horizontally scaled APIs where any server instance should be able to handle any request.
- Sessions are stateful: The server stores session data (in memory, a database, or Redis) and the client holds only a session ID cookie. This allows easy revocation — just delete the session from the store.
- JWT revocation is hard: Since no server-side state exists by default, you cannot "log out" a JWT. You need a token blacklist, short expiration times, or a version counter checked against a database — all of which partially negate the stateless benefit.
- Cookie security is built-in: HTTP cookies support
HttpOnly(no JavaScript access),Secure(HTTPS only), andSameSite(CSRF protection) flags. JWTs stored inlocalStorageare vulnerable to XSS attacks.
When to Use JWTs
JWTs are a good fit when:
- You have a distributed architecture with multiple services that need to verify authentication without calling a central auth server on every request.
- You need short-lived authorization tokens (API access tokens with 5-15 minute lifetimes).
- You are implementing OAuth 2.0 or OpenID Connect flows.
- You need to pass verified claims between services (e.g., a gateway passing user roles to a microservice).
JWTs are a poor fit when:
- You need instant revocation (e.g., "log out everywhere" functionality).
- Your application is a traditional server-rendered monolith where session cookies work perfectly well.
- You are storing large amounts of data in the token (JWTs in cookies or headers add overhead to every request).
JWTs are a powerful tool, but they are not a universal solution. Use them where their stateless, self-contained nature is genuinely valuable, and pair them with short lifetimes and rigorous validation. When in doubt, server-side sessions with secure cookies remain a simpler, safer default for most web applications.