JWT, OAuth2 & Securing APIs

Stateless Auth for REST APIs

18 min Lesson 1 of 13

Stateless Auth for REST APIs

Before you write a single line of JWT code you need to understand why the industry moved away from server-side sessions for REST APIs. Without that context, JWT just looks like a complicated cookie — and you will make the wrong design choices when it matters.

How Classic Session-Based Auth Works

In a traditional monolithic web application, authentication works like this:

  1. The user submits credentials (username + password).
  2. The server validates them, creates a session object in memory (or in a database), and stores the user's identity and roles there.
  3. The server sends back a Set-Cookie: JSESSIONID=abc123 header.
  4. Every subsequent request from the browser carries that cookie automatically.
  5. On each request the server looks up abc123 in its session store to find out who is making the request.

This works beautifully for a single server with a browser client. The problem is the phrase "looks up in its session store" — that lookup is the root of all the pain you will see in distributed systems.

The core assumption of session auth: The server that created the session is the server that will serve future requests — or all servers share the same session store. Break either assumption and auth breaks too.

Why Sessions Do Not Fit REST APIs

1. REST Is Defined as Stateless

Roy Fielding's original REST dissertation makes statelessness a hard architectural constraint: each request from a client must contain all the information necessary for the server to understand and process it. The server must not store any client context between requests. A session store violates this constraint directly — the server needs a side-channel lookup to understand the request.

This is not just philosophical. Statelessness is what makes REST services independently scalable, cacheable, and simple to reason about.

2. Horizontal Scaling Requires Shared State

Modern deployments run multiple instances of every service behind a load balancer. Consider a Spring Boot API deployed as three pods in Kubernetes:

[ Load Balancer ] / | \ [ Pod A ] [ Pod B ] [ Pod C ] (session (session (session store A) store B) store C)

A user authenticates against Pod A. Their session is in Pod A's in-memory store. The next request is routed to Pod B by the load balancer — Pod B knows nothing about that session and returns 401 Unauthorized. The user is suddenly logged out mid-workflow.

The standard fix — sticky sessions (pin each user to one pod) or a shared session store (Redis, a database) — patches the symptom but introduces new problems:

  • Sticky sessions break when a pod restarts or the cluster auto-scales. One failed node logs out all users pinned to it.
  • Shared session store turns the session store itself into a centralised bottleneck and single point of failure. Every authentication check requires a network round-trip to Redis or the database.
Spring Session + Redis does not solve the architectural problem. It shifts the bottleneck from an in-process store to an external one. Under high load the Redis cluster becomes the new constraint, and a Redis outage means a total auth outage. Stateless tokens eliminate the shared store entirely.

3. Microservices Span Trust Boundaries

In a microservices architecture an incoming request may fan out to an Order Service, an Inventory Service, and a Notification Service — each running as an independent process, possibly owned by a different team. Session cookies cannot propagate across service boundaries because:

  • Cookies are scoped to a domain; service-to-service calls are HTTP, not browser navigation.
  • There is no single session store all services can query without tight coupling.
  • Each service would need to call an auth service on every request, multiplying latency and coupling.

A self-contained, cryptographically signed token that each service can verify independently is the only practical solution.

4. Non-Browser Clients Do Not Handle Cookies Well

REST APIs are consumed by mobile apps, CLI tools, IoT devices, and other backend services. None of these have a browser's automatic cookie management. Implementing session cookies in an Android app or a Python script is awkward and error-prone. An Authorization: Bearer <token> header is universally supported by every HTTP client in every language.

What Stateless Auth Looks Like

With stateless authentication the server never persists any session data. Instead:

  1. The client authenticates once and receives a signed token.
  2. The client sends that token in the Authorization header of every subsequent request.
  3. The server validates the token's signature and extracts the identity and roles directly from the token — no database lookup required.
// What the client sends on every request GET /api/orders HTTP/1.1 Host: api.example.com Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... // What the server does — NO session store involved String token = request.getHeader("Authorization").substring(7); Claims claims = Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token); String userId = claims.getSubject(); // "user-42" List<String> roles = claims.get("roles", List.class); // ["ROLE_USER"]

The server's validation is purely computational — it verifies a cryptographic signature using a key it already holds in memory. No network I/O, no shared store, no sticky routing required.

The Trade-offs You Must Know

Stateless auth is not free. You are trading one set of problems for another. A working developer needs to be clear-eyed about both sides:

  • Token revocation is hard. A session can be invalidated instantly by deleting it from the store. A stateless token is valid until it expires. If a user logs out or is compromised, the token still works until expiry. The standard mitigations — short-lived tokens (5–15 minutes) plus refresh tokens, or a token blocklist — are covered in later lessons.
  • Token size grows with claims. A session ID is a tiny string; a JWT carrying roles, permissions, and user metadata is hundreds of bytes sent on every request. This matters at scale or on constrained networks.
  • Secret key management becomes critical. Anyone who holds your signing key can forge tokens. Key rotation, secure storage (not in source code), and algorithm selection are now your responsibility.
Design principle: Keep access tokens short-lived (15 minutes or less). This limits the blast radius of a stolen token without requiring a centralised revocation check on every request. You will implement this pattern with refresh tokens in Lesson 7.

Spring Security's Stateless Session Policy

By default, Spring Security creates an HttpSession and stores the SecurityContext in it. For a REST API you must explicitly disable this. The configuration below is the starting point for every stateless API you will build in this tutorial:

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // Disable CSRF — stateless APIs are not vulnerable to CSRF // because there are no session cookies for a browser to attach automatically .csrf(csrf -> csrf.disable()) // Tell Spring Security never to create or use an HttpSession .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() ); return http.build(); } }

The key line is SessionCreationPolicy.STATELESS. With this setting Spring Security will never write a JSESSIONID cookie and will never look for one. The SecurityContext must be populated on every request from the token — which is exactly what the JWT filter you will build in Lesson 4 does.

Why disable CSRF for a stateless API? CSRF attacks work by exploiting the browser's automatic cookie attachment. If your API does not use session cookies, there are no cookies to hijack. Keeping CSRF protection enabled on a pure JWT API adds overhead without adding security.

Summary

Server-side sessions are incompatible with REST's statelessness constraint, break under horizontal scaling, cannot cross service boundaries, and are awkward for non-browser clients. Stateless tokens solve all four problems at the cost of harder revocation and more careful key management. The remaining lessons in this tutorial build the complete solution: JWT structure and validation, a reusable authentication filter, role-based access control, refresh tokens, and finally OAuth2 for delegated authorisation.