Understanding the Need for JWT (The Problem with Sessions)
1. Introduction
Before JSON Web Tokens (JWTs) became popular, web applications relied heavily on sessions for authentication. At first, this seemed simple and effective — a user logs in, the server remembers them, and everything works smoothly.
But as applications evolved — from single servers to distributed systems and microservices — this approach started to break down. Managing user sessions across multiple servers became complex, costly, and difficult to scale.
This section walks you through why sessions fail to scale and how this problem naturally led to the creation of stateless authentication with tokens.
2. The Classic Web Authentication Model
Let’s start with how authentication traditionally worked in a simple, single-server web application.
Step-by-Step Flow:
- A user (say Alice) logs in using her username and password.
- The server validates these credentials against its user database.
- If correct, the server creates a session to remember Alice.
- The session details (like her user ID and login timestamp) are stored on the server in memory or in a session file.
- The server sends a session ID to Alice’s browser, usually inside a cookie.
- Every subsequent request Alice makes automatically includes that cookie.
- The server checks the session ID from the cookie against its stored sessions.
- If found and valid, Alice is authenticated.
Textual Diagram
[Browser] ---> Login (username/password)
|
v
[Server] ---> Creates Session ---> Stores session in memory
|
v
<--- Sends Cookie: session_id=abc123
Future requests:
Browser -> Cookie(session_id=abc123)
Server -> Finds session_id in memory -> Valid user
This system works perfectly for small, single-server applications.
3. The Problem: Scaling Sessions
Now imagine your application grows.
You add more servers to handle traffic. A load balancer distributes incoming requests between these servers.
Here’s where the problem begins.
Scenario:
- Alice logs in through Server A.
- Server A creates a session for her (
session_id=abc123) and stores it in memory. - On her next request, the load balancer routes her to Server B.
- Server B receives her cookie with
session_id=abc123— but it has no idea who Alice is because her session is stored in Server A’s memory.
Alice’s authentication fails, even though she’s logged in.
This issue arises because sessions are stateful — they rely on in-memory data that isn’t shared across all servers.
Textual Diagram
Step 1:
[User] -> [Server A] -> Creates Session (session_id=123)
|
+--> Stores in memory
Step 2:
[User] -> [Load Balancer] -> [Server B]
Server B checks session_id=123 -> Not Found ❌
Authentication fails
This architecture simply doesn’t scale well across multiple servers or distributed environments.
4. Attempts to Fix Session-Based Authentication
Developers tried several approaches to make sessions work in multi-server environments. Let’s review them.
4.1 Solution 1: Sticky Sessions
In this setup, the load balancer is configured to always route a user to the same server.
So if Alice logs in through Server A, all her requests continue to go to Server A.
Advantages:
- Simple to implement.
- Works without major architectural changes.
Disadvantages:
- If Server A crashes, Alice’s session is lost.
- Load balancing becomes uneven because certain servers handle more users.
- Hard to scale dynamically (new servers don’t automatically share session data).
Textual Diagram
[User] -> [Load Balancer]
|
+--> [Server A] (User Alice - all requests)
+--> [Server B] (Other users)
Sticky sessions “fix” the routing issue but not the scalability or fault tolerance problems.
4.2 Solution 2: Centralized Session Store
Another popular fix is to use a shared session storage mechanism like Redis, Memcached, or a shared database.
Here’s how it works:
- When a user logs in, the server creates a session and stores it in a shared store instead of local memory.
- Any other server can now access the same session using the session ID.
Advantages:
- Sessions are available to all servers.
- Users can access any node in the cluster without losing their session.
Disadvantages:
- The system is still stateful — session data exists in an external store.
- Introduces a new single point of failure (the session store).
- Adds latency, as every request now hits Redis or the database.
- More maintenance overhead.
Textual Diagram
+---------------------------------------+
| Centralized Session Store |
| (Redis / Memcached / Database) |
+---------------------------------------+
^ ^ ^
/ \ \
[Server A] [Server B] [Server C]
| | |
+---> Stores sessions +--------------+
This approach solves the “session not found” problem but introduces new complexity and infrastructure overhead.
5. The Root Problem: Statefulness
The fundamental issue with all these methods is statefulness.
Every server must remember something about every logged-in user — either directly in memory or indirectly through a shared session store.
In a world of stateless HTTP, this design contradicts the very nature of the web.
HTTP was designed so that every request is independent and self-contained.
But sessions break that rule by requiring servers to maintain memory of past interactions.
When applications moved to:
- Cloud environments
- Microservice architectures
- Containerized deployments (like Kubernetes)
…this stateful dependency became a huge scalability bottleneck.
The solution?
Make authentication stateless — so that each request contains everything needed for validation without relying on server memory.
This brings us to tokens.
6. The Rise of Token-Based Authentication
With token-based authentication, the server doesn’t store any session information.
Instead, it issues a token that the client keeps and sends along with each request.
Here’s how it works:
- The user logs in with valid credentials.
- The server creates a token that contains all relevant user information (like ID, role, and expiration).
- The token is digitally signed so that the server can verify its authenticity later.
- The token is sent back to the client.
- On every request, the client includes this token in the HTTP header.
- Any server can now validate the token without checking a central database or store.
This makes the entire authentication process stateless, scalable, and efficient.
Textual Diagram
[Client] ---> Login (username/password)
|
v
[Server] ---> Creates Token (includes user info)
|
v
<--- Sends Token to Client
Future requests:
[Client] ---> HTTP Header: Authorization: Bearer <token>
[Server] ---> Verifies Signature -> Grants Access
7. Why Stateless Authentication Matters
In a stateless system:
- No session data is stored on the server.
- Every request carries its own authentication information.
- Any server can verify and respond independently.
This design perfectly fits modern architectures like:
- Microservices
- Cloud auto-scaling environments
- Serverless APIs
And at the core of this stateless design lies one standard — JSON Web Tokens (JWT).
Introduction to JWT and Its Structure
1. What is JWT?
A JSON Web Token (JWT) is a compact, self-contained, and URL-safe way of securely transmitting information between two parties — usually a client (like a web app or mobile app) and a server (like an API).
JWTs solve one key problem: how to verify identity and authorization in a stateless way.
Instead of relying on server memory or database lookups, JWTs carry their own proof of authenticity. They contain user information (called claims) and are digitally signed, which means their contents cannot be modified without detection.
In essence, JWT = a digitally signed JSON object that says:
“Here’s who I am, and here’s proof that a trusted server verified me.”
2. The Anatomy of a JWT
A JWT consists of three parts, separated by dots (.):
Header.Payload.Signature
For example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiJhbGljZTEyMyIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcwMDAwMDAwMH0. TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
Each of these three parts is Base64URL-encoded JSON.
Let’s understand what each section means.
3. The Header
The header defines metadata about the token — it specifies how the token is signed and what type of token it is.
A typical JWT header looks like this (before encoding):
{
"alg": "HS256",
"typ": "JWT"
}
Explanation:
- alg – Algorithm used for signing. Common options include:
HS256→ HMAC with SHA-256 (symmetric)RS256→ RSA with SHA-256 (asymmetric)
- typ – Token type, always “JWT”.
Once encoded using Base64URL, this header becomes:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Textual Diagram
Header (JSON) → Base64URL Encode → eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
4. The Payload
The payload is the core of the JWT. It contains claims, which are statements about the user or token.
Claims can include:
- Who the user is
- When the token expires
- What permissions or roles they have
Here’s an example payload before encoding:
{
"sub": "alice123",
"role": "admin",
"exp": 1735689600
}
Explanation:
- sub → Subject or unique identifier of the user (Alice).
- role → Role or access level (admin, user, etc.).
- exp → Expiration time (in Unix timestamp format).
Once Base64URL-encoded, it becomes:
eyJzdWIiOiJhbGljZTEyMyIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTczNTY4OTYwMH0
So far we have:
Header.Payload eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiJhbGljZTEyMyIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTczNTY4OTYwMH0
5. The Signature
The signature is what makes a JWT trustworthy.
It ensures that no one can alter the token without detection.
To create the signature:
- Take the encoded header and payload.
- Join them with a dot (
.). - Apply a cryptographic hash function using a secret key (for HS256) or a private key (for RS256).
Conceptually:
signature = sign( base64(header) + "." + base64(payload), secret )
The output is again Base64URL-encoded to produce the final token:
Header.Payload.Signature
Example (simplified)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiJhbGljZTEyMyIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTczNTY4OTYwMH0. TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
This final part — the signature — guarantees:
- The token was created by a trusted source (since only the issuer knows the secret key).
- The data hasn’t been modified (any change invalidates the signature).
6. Visualizing the JWT Structure
Here’s a textual diagram that shows the JWT at a glance:
-------------------------------------------------------------
| Header | Payload | Signature |
-------------------------------------------------------------
| {"alg": "HS256", "typ": "JWT"} |
| {"sub": "alice123", "role": "admin", "exp": 1735689600} |
| sign(Base64UrlEncode(header) + "." + Base64UrlEncode(payload), secret) |
-------------------------------------------------------------
↓ Base64URL Encoded and Joined ↓
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJhbGljZTEyMyIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTczNTY4OTYwMH0.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
7. Important Note: JWTs Are Not Encrypted
Many developers misunderstand this crucial point.
JWTs are encoded, not encrypted.
That means anyone with access to the token can decode it and read its payload.
Example:
If you paste the token above into jwt.io, you’ll immediately see the payload in readable JSON.
Therefore:
- Never store sensitive data (like passwords or bank details) inside a JWT.
- Only include non-sensitive claims like
userId,role, orexp.
The token’s integrity is protected by its signature, not its secrecy.
8. Example: Creating and Decoding a JWT
Let’s walk through a simple example to see how it works in practice.
Step 1: Header and Payload
Header:
{
"alg": "HS256",
"typ": "JWT"
}
Payload:
{
"sub": "john_doe",
"role": "user",
"exp": 1735689600
}
Step 2: Base64URL Encoding
Header Encoded: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 Payload Encoded: eyJzdWIiOiJqb2huX2RvZSIsInJvbGUiOiJ1c2VyIiwiZXhwIjoxNzM1Njg5NjAwfQ
Step 3: Creating the Signature
Signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret_key )
Step 4: Complete JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiJqb2huX2RvZSIsInJvbGUiOiJ1c2VyIiwiZXhwIjoxNzM1Njg5NjAwfQ. rTJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
9. Why JWT Is Compact and Efficient
JWTs are designed to be lightweight and transmittable over the web:
- They are text-based (JSON format).
- They can be safely sent in URLs, HTTP headers, or cookies.
- They avoid server lookups — each server can validate independently.
This makes them ideal for:
- RESTful APIs
- Mobile backends
- Microservice ecosystems
- Cross-domain authentication
Signing, Verification, and Algorithms (HS256, RS256, ES256)
1. Introduction
At this stage, you already understand what a JSON Web Token (JWT) looks like — it consists of a header, a payload, and a signature.
But what makes a JWT trustworthy is not its structure — it’s the signature.
Without the signature, a JWT would just be a base64-encoded JSON object that anyone could modify. The signature ensures that:
- The token was created by a legitimate source.
- The contents (claims) have not been tampered with.
This part of the guide explains how JWT signatures are created, what cryptographic algorithms power them, and how servers verify their authenticity.
2. What is the Purpose of Signing a JWT?
Signing a JWT serves two critical functions:
- Authenticity: Confirms that the token was issued by a trusted authority (e.g., your authentication server).
- Integrity: Ensures that the token hasn’t been modified after issuance.
When a server signs a JWT, it uses a secret or private key. Any system with the corresponding verification key can later confirm that the token is valid and unaltered.
3. How JWT Signing Works (Conceptual Flow)
Here’s how a signed JWT is created:
- The server takes the Base64URL-encoded header and payload.
- It concatenates them with a dot:
<base64_header>.<base64_payload> - The server then applies a signing algorithm using a secret key (for symmetric algorithms like HS256) or a private key (for asymmetric algorithms like RS256).
- The result is another Base64URL-encoded string — the signature.
- The complete JWT becomes:
<base64_header>.<base64_payload>.<base64_signature>
4. How JWT Verification Works (Server Side)
When a client sends the JWT back to a server (e.g., in an API request), the server must verify it before granting access.
The verification process works as follows:
- The server extracts the header, payload, and signature from the JWT.
- It recomputes the signature using the same algorithm and the same secret or public key.
- If the computed signature matches the token’s signature, the token is valid.
- If it doesn’t match, it means the token was tampered with or forged, and the request is rejected.
5. Textual Diagram – JWT Signing and Verification Flow
JWT Creation (on Auth Server)
--------------------------------------------------------
| Header | Payload | Secret / Private Key |
--------------------------------------------------------
↓
base64Encode(header) + "." + base64Encode(payload)
↓
Sign using algorithm (HS256 / RS256)
↓
Add Signature
↓
Final JWT = header.payload.signature
JWT Verification (on API Server)
--------------------------------------------------------
| Extract Header & Payload | Verify Signature using Key |
--------------------------------------------------------
↓
Recompute signature locally
↓
Compare with token’s signature
↓
If matches → Token valid ✅
If mismatch → Token invalid ❌
6. Types of Signing Algorithms
JWT supports several signing algorithms, grouped mainly into two categories — symmetric and asymmetric.
6.1 Symmetric Signing (HS256)
Symmetric algorithms use one shared secret key for both signing and verification.
Example Algorithm:
HS256 (HMAC with SHA-256)
Working:
- The authentication server uses a shared secret key to sign the token.
- The same key is used by all servers to verify it.
Advantages:
- Simple and fast to compute.
- Easy to implement when all systems are internal.
Disadvantages:
- Requires key sharing between multiple servers or services.
- If the key is leaked, anyone can forge valid tokens.
Textual Diagram:
Symmetric Signing (HS256) ------------------------------- Auth Server → uses secret key → Sign token API Server → uses same key → Verify token -------------------------------
Use Case:
HS256 works well for small-scale systems where all services trust each other and share a single secret (like monolithic backends or limited internal microservices).
6.2 Asymmetric Signing (RS256, ES256)
Asymmetric algorithms use two different keys:
- A private key for signing (kept secret by the issuer).
- A public key for verification (shared openly with services that need to verify).
Common Algorithms:
RS256: RSA with SHA-256ES256: ECDSA (Elliptic Curve Digital Signature Algorithm) with SHA-256
Working:
- The authentication server signs the JWT using its private key.
- Any other service can verify the JWT using the public key.
- Since the public key cannot be used to sign new tokens, only the issuer can create valid JWTs.
Advantages:
- Public key can be freely shared — no secret leakage risk.
- Ideal for distributed and public systems (e.g., Google, Auth0, AWS Cognito).
- Allows third-party verification without exposing secrets.
Disadvantages:
- Slightly slower due to complex cryptography.
- Requires key management and rotation.
Textual Diagram:
Asymmetric Signing (RS256) --------------------------------------------- Auth Server: Uses PRIVATE KEY → Sign JWT Other Servers / Clients: Use PUBLIC KEY → Verify JWT Only the private key holder can issue tokens. ---------------------------------------------
7. Key Distribution: JWKS (JSON Web Key Set)
When using asymmetric algorithms, servers need access to the public keys to verify tokens.
These keys are typically exposed through a JWKS (JSON Web Key Set) endpoint.
For example:
https://auth.example.com/.well-known/jwks.json
A JWKS file looks like this:
{
"keys": [
{
"kty": "RSA",
"kid": "abc123",
"use": "sig",
"alg": "RS256",
"n": "public_key_modulus_here",
"e": "AQAB"
}
]
}
Each token’s header includes a kid (Key ID) that tells the verifier which public key to use.
Flow Diagram:
[JWT Header]
{
"alg": "RS256",
"kid": "abc123"
}
[Server Verification]
→ Fetch key with kid=abc123 from JWKS
→ Verify token using corresponding public key
This mechanism allows automatic key rotation and easy management of public keys in distributed systems.
8. Comparing HS256 and RS256
| Feature | HS256 (Symmetric) | RS256 (Asymmetric) |
|---|---|---|
| Keys Used | One shared secret key | Private key (sign), public key (verify) |
| Security Risk | If leaked, attacker can sign tokens | Public key can be shared safely |
| Performance | Fast (light computation) | Slightly slower |
| Best For | Internal apps | Distributed or public apps |
| Key Rotation | Manual | Supported via JWKS |
9. Example: Signing and Verifying JWT with HS256
Signing (on the Auth Server):
import jwt
payload = {"user_id": "alice", "role": "admin"}
secret_key = "my_super_secret"
token = jwt.encode(payload, secret_key, algorithm="HS256")
print(token)
Verification (on the API Server):
decoded = jwt.decode(token, secret_key, algorithms=["HS256"]) print(decoded)
If anyone tampers with the token, the verification will fail.
10. Example: Signing and Verifying JWT with RS256
Signing (Auth Server with Private Key):
import jwt
private_key = open("private.pem").read()
payload = {"user_id": "alice", "role": "admin"}
token = jwt.encode(payload, private_key, algorithm="RS256")
print(token)
Verification (API Server with Public Key):
public_key = open("public.pem").read()
decoded = jwt.decode(token, public_key, algorithms=["RS256"])
print(decoded)
Here, even if someone obtains the public key, they cannot forge a token because signing requires the private key.
JWT Authentication Flow and Practical Implementation
1. Introduction
Now that we understand how JWTs are structured, signed, and verified, it’s time to see how they work in practice.
A JWT is not just a random token — it’s a self-contained access pass that allows stateless authentication between a client and server.
Once the client obtains a JWT after successful login, it can send it with every subsequent request without the need for repeated authentication or session lookups.
In this part, we’ll examine:
- How JWT authentication works step-by-step.
- What happens when a user logs in.
- How servers issue, validate, and expire tokens.
- How refresh tokens extend session lifetimes.
- Where and how JWTs should be stored.
2. The JWT Authentication Workflow
Let’s go through the complete flow of JWT-based authentication, comparing it to the traditional session-based model.
Step 1: User Logs In
The process starts when a user (say Alice) provides her credentials — typically a username and password.
POST /login
{
"username": "alice123",
"password": "mypassword"
}
The authentication server validates these credentials against the user database.
Step 2: Server Issues a JWT
If the credentials are valid, the server generates a JWT.
This token includes:
- The user’s ID or username (
sub) - The user’s role (
role) - The expiration timestamp (
exp) - Optional metadata (like issued time
iator audienceaud)
The server signs the token using its secret or private key (depending on HS256 or RS256).
Example payload:
{
"sub": "alice123",
"role": "admin",
"iat": 1735680000,
"exp": 1735689600
}
The encoded JWT might look like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiJhbGljZTEyMyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTczNTY4MDAwMCwiZXhwIjoxNzM1Njg5NjAwfQ. TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
The server then sends this token back to the client as part of the login response.
HTTP/1.1 200 OK
{
"token": "<JWT_TOKEN>"
}
Step 3: Client Stores the Token
Once the client (like a browser or mobile app) receives the token, it must store it securely.
Common storage options:
- LocalStorage – Simple and persistent, but vulnerable to XSS attacks.
- SessionStorage – Safer, but expires when the browser closes.
- HTTP-only cookies – Most secure option for web apps since they are not accessible via JavaScript.
Stored token example (browser localStorage):
localStorage.setItem("jwt", token);
Step 4: Client Sends JWT with Each Request
Now that the client has a valid token, it includes it in the Authorization header of every subsequent HTTP request to protected APIs.
GET /api/user/profile Authorization: Bearer <JWT_TOKEN>
This makes authentication stateless — the server doesn’t need to remember who logged in; the token carries that proof.
Step 5: Server Verifies the JWT
When the server receives a request, it extracts the token from the Authorization header and performs verification:
- Decode the JWT to access the header and payload.
- Validate the signature using the shared secret (HS256) or public key (RS256).
- Check token validity:
- Expiration (
exp) - Issuer (
iss) - Audience (
aud)
- Expiration (
If all checks pass, the request is authenticated, and the server processes it.
If verification fails, the server returns an HTTP 401 Unauthorized response.
Step 6: Access Granted
Once verified, the server uses the information inside the token (like the user’s role) to decide what the user is allowed to do.
Example response:
HTTP/1.1 200 OK
{
"username": "alice123",
"email": "alice@example.com",
"role": "admin"
}
All of this happens without any session lookups in a database or cache.
Textual Diagram: JWT Authentication Flow
JWT Authentication Process ------------------------------------------------------------- 1. Client logs in with username & password 2. Server verifies credentials 3. Server issues signed JWT 4. Client stores JWT securely 5. Client sends JWT in Authorization header 6. Server verifies token and grants access -------------------------------------------------------------
3. JWT Validation Logic
The token validation process is the backbone of secure authentication.
Here’s what the server typically checks when a JWT is presented:
- Signature Verification:
Ensures that the token was issued by a trusted authority and has not been modified. - Expiration Check (
exp):
Rejects tokens that have expired. - Issued At (
iat) and Not Before (nbf):
Ensures the token is not used before its valid timeframe. - Audience (
aud) and Issuer (iss):
Ensures that the token was meant for this particular API and issued by a trusted service.
4. Example: Token Validation in Node.js (Express + jsonwebtoken)
Here’s a simple example showing how a server validates a JWT.
const jwt = require('jsonwebtoken');
function verifyToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, process.env.SECRET_KEY, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
This middleware extracts the token, verifies it, and attaches the user information to the request.
5. Token Expiration and Refresh Tokens
JWTs typically have a short lifetime for security reasons — often a few minutes to a few hours.
When a token expires, the user must either log in again or use a refresh token to obtain a new one.
What is a Refresh Token?
A refresh token is a long-lived token issued alongside the access (JWT) token.
It is used only to request new access tokens once they expire.
Key differences:
| Token Type | Lifetime | Purpose |
|---|---|---|
| Access Token (JWT) | Short (e.g., 15 min) | Used for accessing APIs |
| Refresh Token | Long (e.g., 7 days or more) | Used to get new access tokens |
Refresh Token Flow
- User logs in and receives both access token and refresh token.
- Access token expires after some time.
- Client sends the refresh token to get a new access token.
- Server verifies the refresh token and issues a fresh JWT.
Textual Diagram:
Access & Refresh Token Workflow ---------------------------------------------------------- 1. Login -> Receive Access Token + Refresh Token 2. Access Token expires after short time 3. Client sends Refresh Token to /token endpoint 4. Server verifies and issues new Access Token ----------------------------------------------------------
Refresh tokens should be stored securely on the server or as HTTP-only cookies to prevent theft.
6. Token Revocation
Since JWTs are stateless, once issued, they remain valid until they expire.
This means that revoking (invalidating) a token before its expiration is not straightforward.
Common Revocation Strategies:
- Short Expiration + Refresh Tokens
Issue short-lived tokens that expire quickly.
Users automatically get new ones when needed. - Token Blacklist
Maintain a list of revoked tokens (identified by a claim likejti) and reject them during verification. - Key Rotation
Regularly change signing keys. Tokens signed with old keys automatically become invalid.
7. Storage and Security Best Practices
To ensure JWTs remain secure in production, follow these best practices:
- Use HTTPS Only:
Always transmit tokens over secure HTTPS connections. - Use HTTP-only Cookies (if possible):
Prevents JavaScript access, protecting against XSS attacks. - Short Expiry Times:
Limit the lifetime of JWTs to reduce risk exposure if stolen. - Avoid Sensitive Data:
Never store confidential details like passwords or personal info in the payload. - Rotate Keys Regularly:
For RS256/ES256, use JWKS for automatic key rotation. - Validate All Claims:
Always checkiss,aud,exp, andiatto prevent misuse.
8. Example: Full JWT Authentication Sequence
Here’s the complete flow combining login, token generation, and verification.
Step 1 – Login
POST /login
{
"username": "alice",
"password": "mypassword"
}
Step 2 – Response
HTTP/1.1 200 OK
{
"access_token": "<JWT>",
"refresh_token": "<REFRESH_TOKEN>"
}
Step 3 – Access Protected Route
GET /api/user/profile Authorization: Bearer <JWT>
Step 4 – Token Expired
HTTP/1.1 401 Unauthorized
Step 5 – Refresh
POST /token
{
"refresh_token": "<REFRESH_TOKEN>"
}
Step 6 – Server Issues New JWT
HTTP/1.1 200 OK
{
"access_token": "<NEW_JWT>"
}
Security, Use Cases, and Best Practices
1. Introduction
While JSON Web Tokens (JWTs) solve many scalability and session management problems, security remains a critical consideration.
JWTs are compact, stateless, and easy to validate — but those same features can become liabilities if implemented incorrectly.
In this section, we’ll explore:
- Common JWT security vulnerabilities.
- How to store and handle tokens securely.
- When and where to use JWTs.
- Best practices for production environments.
By understanding these, you can ensure your JWT-based authentication remains both scalable and secure.
2. JWT Security Principles
Before diving into attacks and best practices, let’s restate the core security principles of JWTs:
- Integrity over Confidentiality:
JWTs are signed, not necessarily encrypted.
The signature ensures data integrity (unchanged content), not secrecy. - Trust the Signature, Not the Payload:
The token’s payload can be decoded by anyone — only the signature proves authenticity. - Short Lifetimes Reduce Risk:
Since tokens are self-contained, short expiration times reduce damage if a token is stolen. - Key Security Equals Token Security:
The signing key (secret or private key) must be protected — if compromised, all tokens become forgeable.
3. Common JWT Vulnerabilities
Even with cryptographic signing, JWTs are not immune to security risks.
Below are common mistakes developers make and how to avoid them.
3.1 Algorithm Confusion Attack
JWTs specify the signing algorithm in the header. If not properly validated, an attacker can modify it to “none” or a weaker algorithm.
Example Attack:
- Attacker intercepts a valid JWT.
- Modifies the header:
{ "alg": "none" } - Removes the signature part.
- If the server doesn’t enforce verification, it accepts this forged token as valid.
Prevention:
- Always explicitly verify the algorithm on the server side.
- Reject tokens using the “none” algorithm or unexpected alg values.
- Configure your JWT library to disallow unsigned tokens.
3.2 Secret Key Exposure
If your JWT secret (used in HS256) leaks — perhaps through logs, GitHub repositories, or environment variables — anyone can forge tokens.
Prevention:
- Store secrets in secure vaults (AWS Secrets Manager, HashiCorp Vault, etc.).
- Never hardcode secrets in code or configuration files.
- Rotate keys regularly and invalidate old ones.
3.3 Token Theft via XSS or LocalStorage
Tokens stored in browser LocalStorage or JavaScript-accessible cookies can be stolen using cross-site scripting (XSS).
Prevention:
- Prefer HTTP-only, Secure cookies for storing JWTs.
- Implement Content Security Policy (CSP) headers.
- Sanitize all user inputs to prevent XSS injection.
3.4 Replay Attacks
Attackers might reuse stolen tokens to impersonate legitimate users until the token expires.
Prevention:
- Keep JWTs short-lived (e.g., 15 minutes).
- Use refresh tokens to issue new access tokens.
- Maintain a token revocation list if necessary.
- Optionally track token IDs (
jti) and invalidate them on logout.
3.5 Improper Audience or Issuer Validation
If your service accepts JWTs without verifying the iss (issuer) or aud (audience) claims, tokens from other systems might be mistakenly accepted.
Prevention:
- Always validate the
issandaudclaims. - Match them against expected values configured on your server.
3.6 Long-Lived Tokens Without Revocation
Because JWTs are self-contained and stateless, once issued they remain valid until they expire.
If a long-lived token is stolen, it can be used freely.
Prevention:
- Use short expiry durations.
- Combine with refresh tokens.
- Maintain a revocation mechanism (blacklist or key rotation).
4. Token Storage Strategies
Where you store JWTs on the client side determines how vulnerable your app is to attacks.
Option 1: LocalStorage
localStorage.setItem("token", jwt);
- Persistent across sessions.
- Accessible via JavaScript → vulnerable to XSS.
Option 2: SessionStorage
sessionStorage.setItem("token", jwt);
- Safer than LocalStorage (cleared on tab close).
- Still accessible via JavaScript.
Option 3: HTTP-only Secure Cookie
Best option for web applications.
Set-Cookie: token=<JWT>; HttpOnly; Secure; SameSite=Strict;
- Not accessible via JavaScript.
- Protected from XSS and CSRF when used correctly.
Summary Table
| Storage Option | Persistent | XSS Safe | CSRF Safe | Recommended |
|---|---|---|---|---|
| LocalStorage | Yes | No | Yes | No |
| SessionStorage | No | No | Yes | No |
| HTTP-only Cookie | Configurable | Yes | With SameSite | Yes ✅ |
5. Key Management and Rotation
In large systems, especially those using RS256 or ES256, key management is critical.
Key Management Best Practices:
- Use JWKS endpoints for sharing public keys with services.
- Include
kid(Key ID) in JWT headers to identify the signing key. - Rotate keys periodically (e.g., every 90 days).
- Revoke tokens signed with old keys immediately after rotation.
Textual Diagram:
JWT Verification Using JWKS --------------------------------------------------- 1. Token Header → "kid": "key123" 2. API fetches key "key123" from JWKS endpoint 3. Verifies signature using public key 4. If key rotated → use new "kid" ---------------------------------------------------
6. Use Cases of JWT
JWTs are flexible and widely used in various authentication and authorization contexts.
6.1 Single Sign-On (SSO)
JWTs are ideal for SSO systems where multiple services or domains trust the same identity provider (IDP).
A token issued by the IDP can be used across services without reauthentication.
6.2 API Authentication
In RESTful APIs and microservices, JWTs allow stateless authentication where each request carries its own proof of identity.
6.3 Mobile and SPA Authentication
In mobile apps or single-page applications (SPAs), JWTs provide lightweight identity handling without server-side session storage.
6.4 Authorization Between Microservices
In distributed systems, one service can call another with a JWT, proving the request originates from an authenticated source.
7. JWT Best Practices Checklist
Below is a practical checklist for using JWT securely in production.
7.1 Token Creation
- Use short-lived access tokens.
- Always include essential claims (
iss,aud,exp,iat). - Sign tokens with strong algorithms (RS256 or ES256 preferred).
7.2 Token Verification
- Validate the signature, issuer, and audience.
- Reject expired tokens (
expclaim). - Reject tokens signed with unexpected algorithms.
7.3 Storage
- Prefer HTTP-only cookies.
- Never store JWTs in LocalStorage.
- Use HTTPS for all token transmission.
7.4 Key Management
- Protect private or secret keys.
- Rotate keys regularly.
- Use JWKS endpoints for key distribution.
7.5 Logout and Revocation
- Use short-lived tokens.
- Implement refresh tokens and a blacklist mechanism.
- Invalidate tokens on logout by rotating secrets or using
jtitracking.
8. Example: Secure JWT Flow (with Refresh Tokens)
Step-by-Step:
- User logs in → server issues:
- Access Token (expires in 15 min)
- Refresh Token (expires in 7 days)
- Client stores refresh token in an HTTP-only cookie.
- When access token expires, client sends the refresh token to renew it.
- Server verifies the refresh token and issues a new JWT.
Textual Diagram:
Secure JWT Workflow ---------------------------------------------------------- 1. Login → Issue Access + Refresh Token 2. Store Refresh Token in HTTP-only Cookie 3. Use Access Token for API requests 4. On expiry → use Refresh Token to renew 5. Rotate keys and validate claims regularly ----------------------------------------------------------
9. Common Myths About JWT
Let’s clear up a few misconceptions.
| Myth | Reality |
|---|---|
| JWTs are encrypted. | No — they are Base64URL-encoded and signed. Anyone can read them. |
| JWTs can be revoked easily. | No — you must implement expiration or blacklists. |
| Long-lived tokens are safe. | No — shorter lifetimes improve security. |
| JWTs replace OAuth. | JWTs are a building block within OAuth — not a replacement. |
10. Advantages of JWT
- Stateless Authentication:
No need for session storage — servers can verify tokens independently. - Scalability:
Works seamlessly across distributed systems and microservices. - Compact and URL-safe:
Easy to transmit via HTTP headers, URLs, or cookies. - Cross-Domain Support:
Ideal for Single Sign-On (SSO) and federated identity systems. - Built-in Expiration:
Tokens automatically expire after a predefined time.
11. Disadvantages of JWT
- Difficult Revocation:
Once issued, tokens remain valid until they expire unless explicitly blacklisted. - Potential Token Theft:
If stored insecurely, tokens can be stolen and reused. - Increased Payload Size:
Each request carries the entire token, slightly increasing bandwidth usage. - Risk of Misconfiguration:
Weak signing algorithms or long-lived tokens can compromise the entire system.
