Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Session Management: Stateful vs. Stateless

Hello! Welcome to the next lesson in our journey to architect security products.

In our previous lesson, we designed a robust credential verification flow. We successfully authenticated a user by verifying their password against a secure hash (while protecting against timing attacks) and then confirming a second factor via TOTP. At the end of that flow, our system has high confidence in the user's identity.

This raises the crucial next question: now that the user is authenticated, how does our system remember them across subsequent requests and different pages? This is the fundamental problem of session management.

Today, we will address this by designing the system that creates and manages a user's logged-in state. Our learning outcome is to:

Design a session management system, comparing stateful (session ID) vs. stateless (JWT) approaches for secure issuance, validation, and revocation.

We will explore the two dominant architectural patterns for this:

  1. Stateful Sessions: The traditional approach where the server stores all session information and gives the client a simple pointer—a session ID.
  2. Stateless Sessions: A modern approach using JSON Web Tokens (JWTs), where the server packages the session information into a cryptographically signed token that the client holds.

For each approach, we will analyze the design of its issuance, validation, and revocation mechanisms, focusing on the trade-offs in scalability, security, and complexity.

1. The Stateful Approach: Session-ID Based Authentication

The classic approach to session management is to maintain the state on the server. The server creates a session record upon successful login, stores it, and gives the client a unique identifier for that record.

To see this flow in action, please watch the following short videos. They provide an excellent overview of the architecture.

Session Vs JWT: The Differences You May Not Know!

First, let's watch a segment from ByteByteGo that walks through the fundamental flow of session-based authentication.

Watch the section from the beginning until 01:48. Focus on the roles of the server, the client, and the session store (like Redis or a database).

JWT vs Sessions 🔥 — Why You Should Choose One Over the Other 🎯

This next video from Nutshell reinforces the concept and introduces some of the architectural challenges that arise in distributed systems.

Watch from 00:30 to 02:43. Pay close attention to the discussion on database lookups and the problem of 'sticky sessions' when scaling out.

Architectural Design

As you saw, the stateful session architecture can be summarized in the following flow:

  1. Issuance:

    • After a user successfully authenticates (e.g., password + MFA verified), the server generates a cryptographically secure, high-entropy, random string. This is the Session ID.
    • The server creates a session record containing the user's identity (user_id), permissions, expiration time, and other relevant metadata.
    • This record is stored in a server-side session store (e.g., a Redis cache or a dedicated database table), indexed by the Session ID.
    • The Session ID is sent to the client, typically within a secure cookie with the HttpOnly and Secure flags set.
  2. Validation:

    • For every subsequent request to a protected resource, the client's browser automatically includes the cookie containing the Session ID.
    • The server extracts the Session ID from the request.
    • It then performs a lookup in the session store using the Session ID as the key.
    • If a valid, non-expired session record is found, the server retrieves the associated user data and processes the request. If not, the request is rejected as unauthorized.
Session-based vs. JWT-based Authentication Comparison
This diagram visually contrasts the stateful session-based flow on the left with the stateless JWT-based flow on the right. Notice the critical role of the server-side 'Session Store' in the session-based model, which is absent in the JWT model.

Design Trade-offs

This architecture has clear advantages and disadvantages that are critical to understand from a system design perspective.

Pros:

  • Trivial Revocation: This is the strongest advantage. To invalidate a session (e.g., on user logout, password change, or suspected compromise), you simply delete the session record from the server-side store. The next time the client presents that Session ID, the lookup will fail, and access is immediately denied.
  • Minimal Data Exposure: The Session ID itself is an opaque token. It contains no information about the user. All sensitive session data remains securely on the server.
  • Session Data Flexibility: You can store large amounts of data in the session record and update it easily on the server without issuing a new token to the client.

Cons:

  • Scalability Bottleneck: The session store becomes a centralized dependency. In a large-scale distributed system, every authentication check requires a network call from the application server to the session store. This can increase latency and create a single point of failure.
  • State Management Overhead: You are responsible for building, maintaining, and scaling a highly available session store. As the Nutshell video noted, avoiding a centralized store by using in-memory sessions on each application server leads to the "sticky session" anti-pattern, which complicates load balancing and resilience.

2. The Stateless Approach: JSON Web Tokens (JWT)

To overcome the scalability limitations of stateful sessions, the industry developed a stateless approach using JSON Web Tokens (JWTs). The core idea is to eliminate the server-side session store by packaging the session data itself into a token that the client holds.

Stateless Sessions for Stateful Minds - JWTs Explained

This article from Auth0 provides a great first-principles explanation of the move from stateful to stateless authentication and dives into the technical structure of JWTs.

Please read the first two sections: 'The Benefits of Going Stateless' and 'The Technical Magic: JWTs, Digital Signatures and Encryption'. Focus on how cryptography replaces the need for a server-side store and understand the three parts of a JWT: Header, Payload, and Signature.

Architectural Design

As the article explains, JWTs achieve statelessness by creating a self-contained, verifiable token.

  1. Structure: A JWT consists of three parts, separated by dots (.):

    • Header: JSON object describing the token, including the signing algorithm (alg, e.g., HS256, RS256).
    • Payload: JSON object containing the "claims" about the user, such as sub (subject/user ID), exp (expiration time), and any custom data like roles or permissions.
    • Signature: A cryptographic signature computed over the header and payload. This is the most critical part; it guarantees the token's authenticity and integrity.
  2. Issuance:

    • After successful authentication, the server constructs the JWT Header and Payload.
    • It then signs the token using a secret key.
      • Symmetric (HMAC): Using an algorithm like HS256, the same secret key is used to both sign and verify the token. This is simpler but requires all verifying services to have the same secret.
      • Asymmetric (RSA/ECDSA): Using an algorithm like RS256, a private key is used to sign the token, and a corresponding public key is used to verify it. This is more secure for microservice architectures, as services only need the public key to verify tokens, while the private signing key remains protected on the authentication server.
    • The complete JWT is sent to the client.
  3. Validation:

    • The client stores the JWT and includes it in the Authorization header of subsequent requests (e.g., Authorization: Bearer <jwt>).
    • When the server receives the request, it performs the following checks in-memory, without any database calls:
      1. Parses the JWT.
      2. Verifies the signature using its secret (for HMAC) or public key (for RSA). If verification fails, the token has been tampered with and is rejected.
      3. Validates the claims in the payload, most importantly checking that the token has not expired (exp claim).
    • If all checks pass, the server trusts the claims in the payload and processes the request.

Design Trade-offs

Pros:

  • Excellent Scalability: Since there is no server-side session store, any service that has the verification key can validate a JWT independently. This is ideal for distributed systems and microservices, as it eliminates the central bottleneck and reduces latency.
  • Reduced Backend Chattiness: User information needed for authorization (like user ID or roles) can be placed directly in the JWT payload, reducing the need for database lookups on every request.

Cons:

  • Revocation is Hard: This is the single biggest challenge with JWTs. Once a JWT is issued, it is valid until it expires. You cannot simply "delete" it from a central store. If a token is compromised, an attacker can use it until it expires.
  • Token Size: JWTs contain more data than a simple session ID, which increases the size of every request header. This can be a concern in bandwidth-constrained environments.
  • Data Exposure: The JWT payload is Base64Url encoded, not encrypted. Anyone who intercepts the token can read the claims. Therefore, you must never store sensitive information in the payload unless you are using JWE (JSON Web Encryption) to encrypt the entire token.

3. Architecting a Solution for JWT Revocation

The difficulty of revoking JWTs is a critical design problem that you, as an architect, must solve. A purely stateless system offers no way to immediately invalidate a stolen token. To address this, we must introduce a small amount of state back into the system in a controlled manner.

Here are the primary architectural patterns for JWT revocation.

How to Handle JWT Revocation

The following article provides an excellent, practical overview of different strategies for handling JWT revocation, complete with code examples and a trade-off comparison.

Please read the introduction ('The JWT Revocation Problem'), then skim the different strategies outlined in the article. Pay close attention to the 'Comparison of Strategies' table, as it provides a concise summary of the trade-offs. Finally, read the 'Summary' section.

Let's discuss the most important strategies from an architectural standpoint.

Strategy 1: Short-Lived Access Tokens + Refresh Tokens

This is the most common and balanced approach. It doesn't provide immediate revocation but strikes a good balance between security and performance.

  • Access Token: A JWT with a very short lifetime (e.g., 5-15 minutes). This is the token used to access protected resources. If it's stolen, the window of exposure is small.
  • Refresh Token: A long-lived, opaque, and random string that is issued alongside the access token. It is stored securely by the client (e.g., in an HttpOnly cookie) and is associated with the user in a server-side database.
  • Flow: When the access token expires, the client sends the refresh token to a special endpoint (e.g., /api/token/refresh). The server validates the refresh token against its database. If valid, it issues a new access token.
  • Revocation: To "log out" the user or revoke their session, you simply invalidate the refresh token in the server-side database. The user can no longer get new access tokens. Their current access token will remain valid only until it expires in a few minutes.

Strategy 2: Token Blacklisting

This approach provides immediate revocation but sacrifices the pure statelessness of JWTs.

  • Flow: Each JWT is issued with a unique identifier claim (jti). When a token needs to be revoked, its jti is added to a "blacklist" (e.g., a Redis set).
  • Validation: During request validation, after verifying the JWT's signature, the server performs an extra check: it looks up the token's jti in the blacklist. If found, the token is rejected.
  • Trade-off: This brings back a database/cache lookup for every request, similar to stateful sessions. However, the lookup is often faster (checking for key existence in Redis is very fast), and you still get the other benefits of JWTs, like self-contained claims.

Conclusion

In this lesson, we have designed and compared the two primary architectures for session management. There is no single "best" solution; the choice depends entirely on your system's specific requirements.

Key Takeaways:

  • Stateful (Session ID): Choose this when immediate and easy revocation is your top priority and you can tolerate the architectural complexity and potential latency of a centralized session store. It is simpler to reason about for monolithic applications.
  • Stateless (JWT): Choose this for scalable, distributed systems (like microservices) where decoupling services and minimizing database lookups is crucial. Be prepared to implement a robust revocation strategy.
  • Hybrid Solutions are Common: The Access Token + Refresh Token pattern is a powerful hybrid. It gives you the stateless performance benefits of short-lived JWTs for most API calls while maintaining a stateful, revocable refresh token for managing the session lifecycle.

We have now established how a user proves their identity and how our IdP can manage their session. But an IdP's core value is not just authenticating users for its own applications, but for third-party applications as well. This is the concept of federated identity.

Preview of the Next Lesson:

In our next lesson, we will dive into one of the foundational protocols for enterprise federation: SAML. We will tackle the learning outcome: "Design the SAML assertion generation process, including attribute mapping and XML digital signature creation." We will see how the concepts of signed claims we learned with JWTs are applied in a different format (XML) to enable secure single sign-on across organizational boundaries.

Can't find a good explanation? Sign up and we'll make it for you

Sign up