Definition
- Session-based authentication – a stateful scheme where the server creates a session record after login and stores it server-side in memory or a database.
- Session ID – an opaque identifier sent to the client in a
Set-Cookieheader; the client returns it on every request. - Stateful – the authoritative state lives on the server; the cookie is just a pointer to it.
- Scalability problem – sessions in one server's memory are invisible to other instances behind a load balancer.
- CSRF vulnerability – the browser sends the cookie automatically, so a malicious site can forge requests that carry the victim's valid cookie.
When Rajesh enters National Bank of India, the receptionist writes his full details in a big register and hands him a small token numbered 42. Every time Rajesh approaches a counter he just shows token 42, and the staff flips to page 42 in the register to see who he is. The token itself is meaningless: all the real information sits in the bank's register. The problem is that if Rajesh goes to a different branch that does not have the same register, nobody knows who token 42 is.
Understanding the Concepts
What is session-based authentication?
Session-based authentication is a stateful approach where the server stores the user's identity after login and gives the client only an opaque session identifier. The server is the source of truth. Every request requires the server to look up the session record to know who the caller is.
The flow is: the user submits credentials, the server validates them, creates a session, and returns a Set-Cookie header with the session ID. The browser automatically attaches this cookie on every subsequent request to the same domain.
package com.bank.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SessionLoginController {
@PostMapping("/login")
public String login(@RequestParam String username,
@RequestParam String password,
HttpServletRequest request) {
// Validate credentials (simplified)
if ("rajesh".equals(username) && "securePass2024".equals(password)) {
HttpSession session = request.getSession(true);
session.setAttribute("user", "rajesh");
session.setAttribute("role", "CUSTOMER");
session.setMaxInactiveInterval(1800); // 30 minutes
return "Logged in. Session ID: " + session.getId();
}
return "Invalid credentials.";
}
@GetMapping("/account/balance")
public String getBalance(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("user") == null) {
return "401: Please log in.";
}
String user = (String) session.getAttribute("user");
return "Balance for " + user + ": 125000.75 INR";
}
}
POST /login?username=rajesh&password=securePass2024
--> Logged in. Session ID: 3F2AB7C9D1E4...
Response Header: Set-Cookie: JSESSIONID=3F2AB7C9D1E4...; HttpOnly; Secure
GET /account/balance (cookie sent automatically)
--> Balance for rajesh: 125000.75 INR
How does session-based auth work internally?
The servlet container (Tomcat, Jetty) manages sessions. When request.getSession(true) is called, the container generates a high-entropy session ID, creates an in-memory map entry, and returns a Set-Cookie: JSESSIONID=... header. On subsequent requests, the container reads the JSESSIONID cookie, looks up the map, and attaches the session object to the request.
The session is essentially a server-side Map keyed by the session ID. Attributes like username, role, and last-access time are stored in this map.
package com.bank.security;
import jakarta.servlet.http.HttpSession;
import java.util.Enumeration;
public class SessionInspector {
public static void inspect(HttpSession session) {
System.out.println("Session ID: " + session.getId());
System.out.println("Created: " + session.getCreationTime());
System.out.println("Last accessed: " + session.getLastAccessedTime());
System.out.println("Max inactive: " + session.getMaxInactiveInterval() + "s");
Enumeration<String> names = session.getAttributeNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
System.out.println(" " + name + " = " + session.getAttribute(name));
}
}
}
Session ID: 3F2AB7C9D1E4
Created: 1730000000000
Last accessed: 1730000060000
Max inactive: 1800s
user = rajesh
role = CUSTOMER
What is the scalability problem with sticky sessions?
Because the session lives in one server's memory, if you run three instances behind a load balancer, a user authenticated on instance A will not be recognized on instance B. The two workarounds are sticky sessions and a shared session store.
Sticky sessions configure the load balancer to always route a user to the same instance. This hurts failover: if instance A goes down, the user's session is lost. It also creates uneven load distribution.
A shared session store like Redis externalizes sessions so every instance can access them. Spring Session makes this transparent.
package com.bank.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class RedisSessionConfig {
// Sessions are now stored in Redis, not in Tomcat memory.
// Any instance can read any session.
// Trade-off: adds a network hop and a Redis dependency.
}
Without Redis (sticky sessions):
Instance A: has rajesh's session
Instance B: does not know rajesh --> 401
Instance A crashes --> session lost
With Redis:
Instance A: writes session to Redis
Instance B: reads session from Redis --> knows rajesh
Instance A crashes --> session survives in Redis
What is the CSRF vulnerability?
Because the browser sends the session cookie automatically on every request to the domain, a malicious site can trick the browser into sending a forged request that carries the victim's valid cookie. The server sees a valid session and executes the request.
<!-- Malicious page on evil.com --syn-kw">>
syn-kw"><form action="https://nationalbank.in/api/transfer" method="POST"syn-kw">>
syn-kw"><input type="hidden" name="toAccount" value="ATTACKER_ACCT" /syn-kw">>
syn-kw"><input type="hidden" name="amount" value="50000" /syn-kw">>
syn-kw"></formsyn-kw">>
syn-kw"><scriptsyn-kw">>document.forms[0].submit();syn-kw"></scriptsyn-kw">>
When Rajesh visits this page while logged into the bank, his browser auto-submits the form with his valid session cookie attached. The bank sees a seemingly valid transfer request.
The defenses are anti-CSRF tokens and the SameSite cookie attribute.
package com.bank.config;
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.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@Configuration
public class CsrfConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
}
How is session-based auth different from token-based auth?
| Aspect | Session-Based | Token-Based (JWT) |
|---|---|---|
| State | Server stores session | Client holds self-contained token |
| Storage | Server memory or Redis | Client (cookie, header, storage) |
| Scalability | Needs shared store or sticky sessions | Any instance validates independently |
| Revocation | Instant: delete the session | Hard: token valid until expiry |
| CSRF risk | High (cookie auto-sent) | Low if token sent in Authorization header |
| Network | Lookup on every request (unless cached) | No lookup needed (signature check only) |
| Best for | Server-rendered monoliths | Stateless microservices, SPAs |
package com.bank.security;
// Session-based: server stores state, looks up on every request
public class SessionApproach {
public String identify(String sessionId) {
// Must query session store (memory or Redis)
Object user = sessionStore.get(sessionId);
return user != null ? user.toString() : "unknown";
}
}
// Token-based: no server state, validate signature locally
public class TokenApproach {
public String identify(String jwtToken) {
// No database call needed: verify signature + check expiry
Claims claims = Jwts.parser().verifyWith(key).build()
.parseSignedClaims(jwtToken).getPayload();
return claims.getSubject(); // "rajesh"
}
}
What are the advantages of session-based auth?
The biggest advantage is instant server-side revocation. If fraud is detected on Rajesh's account, the bank simply deletes his session and he is immediately logged out everywhere. With stateless JWTs, you cannot do this without maintaining a revocation list, which reintroduces server-side state.
Another advantage is that the session cookie carries no identity information. A captured session ID reveals nothing about the user until the server is queried, whereas a captured JWT exposes all its claims because the payload is only Base64-encoded.
package com.bank.security;
import jakarta.servlet.http.HttpSession;
public class SessionRevocationService {
public void revokeForFraud(HttpSession session) {
String user = (String) session.getAttribute("user");
System.out.println("Fraud detected for " + user + ". Revoking session.");
session.invalidate(); // Immediate logout
System.out.println("Session invalidated. User must re-authenticate.");
}
}
Fraud detected for rajesh. Revoking session.
Session invalidated. User must re-authenticate.
What are common mistakes?
A common mistake is not rotating the session ID after login, which enables session fixation attacks. An attacker sets a known session ID before the victim logs in, and after login the session is promoted to an authenticated one that the attacker already knows.
// WRONG: session ID not rotated after login
public void loginWrong(HttpServletRequest request, String username) {
HttpSession session = request.getSession(); // reuses existing ID
session.setAttribute("user", username);
// Attacker who planted the original session ID now has access
}
// CORRECT: rotate session ID on authentication
public void loginCorrect(HttpServletRequest request, String username) {
request.getSession().invalidate(); // destroy old session
HttpSession newSession = request.getSession(true); // new ID
newSession.setAttribute("user", username);
// Old session ID is now useless
}
Another mistake is storing sessions without setting HttpOnly, Secure, and SameSite flags on the cookie, which exposes the session ID to JavaScript theft and cross-site attacks.
What should a senior engineer know?
Senior engineers know that session-based auth is not obsolete. It is the right choice for server-rendered banking portals where instant revocation matters. The combination of Spring Session with Redis, HttpOnly Secure SameSite cookies, CSRF tokens, and session rotation after login makes it production-ready.
For microservices, token-based auth is preferred because it eliminates the shared session store. The architecture choice is: sessions for monoliths, tokens for distributed systems, and a hybrid when both exist.
Visual Overview
Login Flow:
┌────────┐ credentials ┌────────────┐
│ Client ├──────────────────►│ Server │
│(Rajesh)│ │ │
│ │◄──────────────────┤ Creates │
│ │ Set-Cookie: │ session │
│ │ JSESSIONID=42 │ in memory │
└────────┘ └─────┬──────┘
│
▼
┌──────────────┐
│ Session Store │
│ ID=42 │
│ user=rajesh │
│ role=CUST │
└──────────────┘
Subsequent Requests:
┌────────┐ Cookie: ID=42 ┌────────────┐
│ Client ├─────────────────►│ Server │
│ │ │ Lookup 42 │
│ │◄─────────────────┤ Found! │
│ │ 200 OK │ user=raj │
└────────┘ └────────────┘
Multi-Instance Problem:
┌────────┐
│ Client │
└───┬────┘
│ Cookie: ID=42
▼
┌──────────────┐
│ Load Balancer│
└──┬───────┬───┘
│ │
▼ ▼
┌──────┐ ┌──────┐
│Inst A│ │Inst B│
│ID=42 │ │ ??? │ ◄── Does not have session 42
│rajesh│ │ │
└──────┘ └──────┘
Solution: Redis shared store
┌──────┐ ┌──────┐
│Inst A│ │Inst B│
└──┬───┘ └──┬───┘
│ │
▼ ▼
┌──────────────┐
│ Redis │
│ ID=42 │
│ user=rajesh │
└──────────────┘
Key Takeaway
Session-based authentication is a stateful approach where, after login, the server creates a session record holding the user's identity and keeps it server-side in memory or a database, while sending only an opaque session identifier back to the client in a cookie. On every later request the browser automatically returns the cookie, and the server looks up the session to recover who the user is. The important point is that the real state lives on the server; the cookie is just a claim check.
In the National Bank of India project, this worked well for a single server, but the moment the system scaled to three instances behind a load balancer, the classic problems appeared. The first is scalability: because the session sits in one instance's memory, a user who logs in on instance A is not recognized on instance B unless the load balancer pins them with sticky sessions, which hurts failover. The solution was externalizing sessions into Redis with Spring Session, which trades a little latency for shared state.
The second problem is CSRF. Because the cookie is sent automatically on every request, a malicious page can trick the browser into firing a transfer request that silently carries the victim's valid session cookie. The defenses are a CSRF token the attacker cannot read and the SameSite cookie attribute set to Strict, plus HttpOnly and Secure flags.
What senior engineers always add is the upside: session auth gives instant server-side revocation. If fraud is detected on Rajesh's account, deleting his session logs him out immediately, which stateless JWTs make much harder. So the choice is sessions for server-rendered banking portals and tokens for stateless microservices, depending on the architecture.
Remember one sentence: session-based auth stores identity on the server and gives the client only a claim check, which enables instant revocation but requires a shared store for horizontal scaling.
---