JSON Web Tokens (JWT)
Anatomy of a JWT: Understanding Headers, Payloads, and Claims
Learn how JWTs are structured into three distinct parts and how to effectively use registered, public, and private claims for identity propagation.
In this article
The Evolution of Web Identity: From Sessions to Statelessness
Traditional web applications rely on server-side sessions to maintain user state. When a user logs in, the server creates a record in its memory or a database and sends a unique session identifier back to the browser. This identifier is typically stored in a cookie and sent with every subsequent request to prove the user is still who they say they are.
While session-based authentication is effective for monolithic applications, it creates significant friction in modern distributed systems. As you scale horizontally across multiple server instances, you must either implement session affinity to keep users on the same server or synchronize session data across a cluster. This synchronization introduces latency and creates a single point of failure in your architecture.
JSON Web Tokens offer a stateless alternative by shifting the burden of state from the server to the client. Instead of the server remembering the user, the user carries a self-contained token that includes all the information the server needs to verify their identity. This allows any service in your architecture to authenticate a request without querying a central session store.
- Sessions require centralized storage like Redis or a database which adds infrastructure complexity.
- JWTs allow for horizontal scaling because any server can verify the token using a shared secret or public key.
- Sessions are susceptible to Cross-Site Request Forgery if not handled with specific cookie flags.
- JWTs are more flexible for mobile applications and cross-domain requests where cookies might be restricted.
By making identity portable, you decouple your authentication logic from your application state. This architectural shift is essential for building microservices that need to remain independent and resilient. When a service receives a token, it trusts the data inside because the token has been digitally signed by a trusted authority.
Understanding the Scalability Wall
In a stateful system, the memory usage of your application grows linearly with the number of active users. If you have a sudden spike in traffic, your session store may become a bottleneck that prevents the rest of your system from scaling. This is often referred to as the scalability wall, where adding more application servers no longer improves performance.
Stateless authentication removes this bottleneck by eliminating the need for a shared look-up table. Because the token contains the user identity and permissions, the application server only needs to perform a local mathematical operation to verify the signature. This operation is extremely fast and does not require any network input or output operations.
The Trust Model of Self-Contained Tokens
The core of the JWT philosophy is the concept of trust through cryptography. You do not need to check a database to see if a token is valid because the signature proves that the content has not been tampered with. If even a single character in the token payload is changed, the signature will no longer match and the server will reject the request.
A JWT is like a passport: the border agent does not call your home country to see if you are a citizen; they simply verify the security features and stamps on the passport itself to establish trust.
Dissecting the Three Pillars of a JWT
A JSON Web Token consists of three distinct parts separated by dots: the Header, the Payload, and the Signature. While these parts look like random strings of characters, they are actually Base64Url encoded JSON objects. It is a common mistake to assume that this encoding provides security or privacy for the data within.
Encoding is simply a way to transform data into a format that can be safely transmitted over the internet without being corrupted by special characters. Anyone who intercepts a JWT can decode the header and payload to see the raw JSON data inside. For this reason, you must never store sensitive information like passwords, social security numbers, or internal keys within a token payload.
The real security of a JWT comes from the third part, the signature. This is created by taking the encoded header and payload and signing them using a specific algorithm and a secret key known only to the server. This ensures that while everyone can read the token, only the person with the key can create or modify one.
The Header and Payload Mechanics
The header typically contains two pieces of information: the type of the token and the signing algorithm being used, such as HMAC SHA256 or RSA. This tells the receiving server how to process the token and which cryptographic tools are required to verify the signature.
The payload is where the actual data resides, organized into key-value pairs called claims. These claims can represent the user ID, their role, and the expiration time of the token. Because the payload is just a JSON object, it is highly extensible and can support complex data structures relevant to your specific application domain.
1// This logic demonstrates how the parts are concatenated before signing
2const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
3const payload = Buffer.from(JSON.stringify({ sub: 'user_123', admin: true })).toString('base64url');
4
5// The unsigned token is the first two parts joined by a dot
6const unsignedToken = `${header}.${payload}`;
7console.log(unsignedToken);The Signature: Ensuring Data Integrity
To generate the signature, the server runs the unsigned token string through a hashing algorithm combined with a secret salt. If you are using a symmetric algorithm like HS256, both the issuing server and the verifying server must share the same secret key. This is often the simplest approach for internal services managed by the same team.
For more complex scenarios where you have third-party consumers, asymmetric algorithms like RS256 are preferred. In this model, the identity provider signs the token with a private key, and the consuming services verify it using a public key. This prevents the consumers from being able to forge their own tokens while still allowing them to fully trust the data provided.
Practical Implementation and Security Strategy
Implementing JWTs requires more than just generating a string; it requires a strategy for token storage and propagation. On the client side, storing tokens in local storage is common but leaves the token vulnerable to Cross-Site Scripting attacks. If a malicious script runs on your page, it can easily read the token and send it to an external server.
A more secure approach is to store the JWT in an HttpOnly cookie. This prevents JavaScript from accessing the token while still allowing the browser to send it automatically with every request. However, this reintroduces the need for CSRF protection, which was one of the problems we were trying to avoid. You must weigh these trade-offs based on your specific security requirements.
In a microservice environment, the gateway service usually handles the initial authentication and then propagates the JWT to downstream services. This pattern, known as Identity Propagation, ensures that every service in the call chain knows exactly who the original user is. This is vital for auditing, rate limiting, and fine-grained access control across your entire infrastructure.
Handling the Revocation Problem
The biggest challenge with JWTs is that they are valid until they expire. If a user logs out or their account is compromised, you cannot easily invalidate a token that has already been issued without introducing state. One common solution is to maintain a small, high-speed blacklist of revoked token IDs in a cache like Redis.
Each JWT is given a unique identifier via the jti claim. When a user logs out, their jti is added to the blacklist for the remaining duration of the token's life. Services then check this cache during the verification process. This effectively makes the system stateful again, but the state is limited only to exceptions rather than every single active user.
Microservices Identity Flow
When Service A calls Service B, it should pass the user's JWT in the Authorization header. This allows Service B to act on behalf of the user with their specific permissions. This flow is much more secure than using a generic service-to-service API key because it respects the principle of least privilege for the specific user request.
1const verifyToken = (req, res, next) => {
2 const authHeader = req.headers['authorization'];
3 const token = authHeader && authHeader.split(' ')[1]; // Format: Bearer <token>
4
5 if (!token) return res.sendStatus(401);
6
7 jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
8 if (err) return res.sendStatus(403);
9 // Attach the user identity and claims to the request object
10 req.user = user;
11 next();
12 });
13};