Understanding Stateful Authentication

Table of contents
- Introduction
- Stateful vs Stateless Authentication
- Stateful Authentication
- Stateless Authentication
- Diving deep into Stateful Authentication
- How will we implement Stateful Authentication
- Cookies - A crucial element of authentication
- Simple Stateful authentication using cookies and session IDs
- Recap
- Additional Learning
- What is Next ?

Introduction
In this article we will explore mainly different authentication techniques out there.
Additionally we will dive deep into how to achieve stateful authentication, all about cookies, all the related stuffs.
All the concepts are discussed theoretically as well as practically via express code snippets.
Concepts all universal and applicable to every languages.
Stateful vs Stateless Authentication
When learning about Authentication, understanding stateful and stateless authentication is crucial. Both approaches have their use cases, pros, and cons. Let's break them down.
Stateful Authentication
In stateful authentication, the server remembers the user's state between requests. Not clear ? Let’s dive into how it works —
How it Works:
The user logs in by providing a username and password.
The server authenticates the user and creates a session, which is stored in a session store (like in-memory, Redis, or a database).
A session ID is sent back to the client and is typically stored in a cookie in the browser.
For each subsequent request, the client sends this session ID.
The server verifies the session ID against its stored session data.
Note: The cookie contains only the session ID, not any user data.
Key Facts:
Requires server-side storage, which can lead to scalability issues.
It's difficult to share sessions across multiple servers without centralized session storage.
Stateless Authentication
In stateless authentication, the server does not store any user session data between requests.
How it Works:
The user logs in with their credentials (username and password).
The server authenticates the user and generates a token (typically a JWT – JSON Web Token).
The token is sent to the client and stored in
localStorage
or in acookie
.With each request, the client sends the token (usually in the
Authorization
header).The server verifies the token using a secret key, but it does not store any session information.
Key Facts:
Scales better than stateful authentication.
Ideal for distributed systems, micro-services, or server-less architectures.
Tokens can carry extra user data (e.g., roles, permissions) and support stateless authorization.
Diving deep into Stateful Authentication
From our earlier discussion, the concept of stateful authentication should now be clearer—especially in terms of its step-by-step flow.
When a user logs in, there are five main tasks we need to perform to implement stateful authentication:
How will we implement Stateful Authentication
1. Generate a Session ID
2. Store the Session ID in a Session Store
3. Send the Session ID to the Client
4. Verify the Session ID on Every Request
5. Handle Logout
Cookies - A crucial element of authentication
Cookie is a small piece of text data stored in user’s browser by the server.
It is used to remember stateful information like login status, user preferences, session IDs.
It is included in every HTTPRequest to the same domain.
There are different types of cookies -
Origin Based Cookies :
Cookies can be first party or 3rd party.
1st party cookies
are set by the domain itself and used in works like login, authentication etc.3rd party cookies
are set by other domains like trackers, ads etc and mainly restricted in modern browsers
Duration Based Cookies :
Session cookies,
exist until the browser is closed and is not stored in disk.Persistent cookies
, exist till a certain expiration date, mainly used for remember me functionality.
Security Based Cookies :
Secure cookies
, are the cookies which are sent over HTTP only, they protect against network sniffing.HTTPOnly cookies
are not accessible byJavascript
and help to preventXSS attacks
.Samesite cookies,
restricts when cookie is sent incross-site requests
.
Simple Stateful authentication using cookies and session IDs
We will implement a simple session based authentication from scratch using cookies and in memory store.
Lets first see the directory structure:
project-root/
│
├── src/
│ ├── map.js // our in-memory session store
│ ├── middleware.js // a simple middleware to check if a user is loggedin or not
│ ├── server.js // express server
│ ├── utils.js // some utility functions
│ └── userData.txt // user data is stored here a simple file based storage
│
├── package.json
└── package-lock.json
For each user we have his/her name, email, password. For each user a random id is generated when he/she signs up and it is stored in userData.txt file.
Also when required we will fetch the user data form the same file.
Below is how it is done :
/*==== utils.js ====*/ const fs = require("fs"); //function to add an user to the file storage function addUser(name, email, password) { const user = { id: Math.random().toString(36).substr(2, 9), //generate random id name, email, password, }; fs.appendFileSync("userData.txt", JSON.stringify(user) + "\n"); console.log("User saved!"); } //function to fetch users from file storage function getUsers() { if (!fs.existsSync("userData.txt")) { return []; //if there is no such file then return } //read user data as a string const data = fs.readFileSync("userData.txt", "utf8").trim(); //if file is empty and there is no user data if (!data) { return []; } // split the string on basis of new line and parse the stringified JSON return data.split("\n").map((line) => JSON.parse(line)); } module.exports = { addUser, getUsers, };
The userData.txt file will contain all the user data and looks something like this
{"id":"v7ysj7bpa","name":"apurba","email":"apurba@gmail.com","password":"12345"} {"id":"rpamtrjc6","name":"shubham","email":"shubham@gmail.com","password":"13579"}
We will have our in-memory session store to map sessionID with corresponding userID.
We will use Javascript Map for that purpose.
Below is how it is implemented:
/*==== map.js ====*/ let map = new Map(); //initialise a new map class Store { getID(sessionID) { return map.get(sessionID); //get the userID for a corresponding sessionID } setID(sessionID, userID) { map.set(sessionID, userID); //map and store the sessionID and userID } deleteID(sessionID) { map.delete(sessionID); //delete the sessionID entry on request like logout } } module.exports = new Store();
Now will dive into the main server code where we will understand how the code flow is done and where the session is created and and how it is checked for authenticated user.
Ensure that everything below goes into the server.js file.
Let’s start by adding all the dependencies and file imports.
const express = require("express"); const app = express(); const store = require("./map"); //store is our in-memory map we import it from map.js file const { addUser, getUsers } = require("./utils"); //these two functions are used to add and get user from userData.txt file const { v4: uuidv4 } = require("uuid"); //this a npm package which generated random 128-bit UUID(Universally Unique Identifier) const cookieParser = require("cookie-parser"); //we are using cookies so we need to parse them when we need app.use(cookieParser()); //cookie parser for our need app.use(express.json()); // It is a inbuilt middleware that parses incoming requests with JSON payloads. app.use(express.urlencoded({ extended: true })); // middleware for parsing form data (HTML form submissions).
Below is a
POST
endpoint on/signup
route:app.post("/signup", (req, res) => { try { // Extract name, email, and password from request body const { name, email, password } = req.body; // Validate that none of the fields are missing if (!name || !email || !password) { return res.status(400).json({ msg: "Every field is mandatory" }); } // Fetch all existing users from storage const users = getUsers(); // Check if a user with the same email already exists const existingUser = users.find((user) => user.email === email); if (existingUser) { return res .status(400) .json({ msg: "You are already registered.. Kindly Login" }); } // Add the new user to the storage addUser(name, email, password); // Send a success response return res.status(201).json({ msg: "Signup successful!" }); } catch (e) { // If any server error occurs, log it and respond with 500 console.error(e); return res.status(500).json({ msg: "Signup error" }); } });
Below is a
POST
endpoint on the/login
route.This is the time where we create a sessionId and store it in the client’s cookie.
At the same time, the sessionId is stored on the server side inside our in-memory map (which acts as server storage).
This approach is called stateful authentication because the server holds the authentication state (session info).
For each subsequent request, we can verify the session via a custom middleware by checking the sessionId from the cookie.
// POST endpoint for user login app.post("/login", (req, res) => { try { // Extract email and password from request body const { email, password } = req.body; // Validate that both fields are provided if (!email || !password) { return res.status(400).json({ msg: "Every field is mandatory" }); } // Fetch all registered users from storage const users = getUsers(); // Check if the user with the given email exists const existingUser = users.find((user) => user.email === email); if (!existingUser) { return res .status(400) .json({ msg: "You are not registered.. Kindly Signup" }); } // Validate the password if (existingUser.password !== password) { return res.status(400).json({ msg: "Invalid credentials" }); } // If email and password are valid, generate a new session ID const sessionID = uuidv4(); // Store the sessionID mapped to the userID in server's in-memory map (stateful authentication) store.setID(sessionID, existingUser.id); // Set the sessionID in client's cookie res.cookie("sessionID", sessionID, { httpOnly: true, // Cookie cannot be accessed via client-side JS (security) sameSite: "Strict", // Prevents the browser from sending this cookie along with cross-site requests expires: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // Cookie expires in 3 days }); // Respond back with a success message return res.status(200).json({ msg: "Login successful!" }); } catch (e) { // Handle any server-side errors console.error(e); return res.status(500).json({ msg: "Login error" }); } });
Let’s now explore the custom middleware which will provide authentication restrictions.
Means using the middleware to protect our route will ensure that all the users are first loggedin in order to access that endpoint data.
Below is how it is implemented:
/*==== middleware.js ====*/ // Import server-side in-memory session store const store = require("./map"); // Import utility function to fetch all users const { getUsers } = require("./utils"); // Custom middleware to allow only authenticated users const authenticatedUsersOnly = (req, res, next) => { // Check if cookies exist and if sessionID is present if (!req.cookies || !req.cookies.sessionID) { return res.status(401).json("You are not Loggedin"); } // Fetch the userID mapped to the sessionID from server's in-memory storage const userID = store.getID(req.cookies.sessionID); // If sessionID is invalid or expired if (!userID) { return res .status(401) .json({ msg: "Invalid session. Please login again." }); } // Fetch all users from storage const users = getUsers(); // Check if a valid user exists with the fetched userID const existingUser = users.find((user) => user.id === userID); // If user not found in database/storage if (!existingUser) { return res.status(401).json({ msg: "User not found. Please login." }); } // Attach the user object to the request so that downstream routes can use it req.user = existingUser; // Allow the request to proceed to the next middleware or route handler next(); }; // Export the middleware function module.exports = authenticatedUsersOnly;
This middleware checks if a
sessionID cookie
exists and is valid by comparing it with server-side storage.If the session is valid and matches a registered user, the request proceeds.
Otherwise, it blocks access and forces the user to login again.
This is how authentication is enforced for protected routes.We import the authentication middleware and use it to protect certain routes that require the user to be logged in.
For example, we create a GET endpoint at
/data
, and POST/logout
endpoint which can only be accessed by authenticated users.Below is how it is implemented :
// POST endpoint to handle user logout const authenticatedUsersOnly = require("./middleware"); // import the middleware app.post("/logout", authenticatedUsersOnly, (req, res) => { try { // Extract the sessionID from cookies const sessionID = req.cookies.sessionID; // Clear the sessionID cookie from the client side res.clearCookie("sessionID", { httpOnly: true, sameSite: "Strict", }); // Remove the sessionID from the server-side in-memory storage store.deleteID(sessionID); // Send success response return res .status(200) .json({ msg: "You have been logged out successfully." }); } catch (e) { console.error(e); // Send server error response if something goes wrong return res.status(500).json({ msg: "Something went wrong during logout." }); } });
This
/logout
route removes the sessionID from both client-side cookies and server-side memory (store
).It ensures that the user is fully logged out and any further protected requests will require re-authentication.
This is a key step to complete the stateful authentication lifecycle.
// GET endpoint to return authenticated user's data const authenticatedUsersOnly = require("./middleware"); // import the middleware app.get("/data", authenticatedUsersOnly, (req, res) => { try { // Retrieve the authenticated user's information from the request object const user = req.user; // Send a simple message including the user's name and email return res .status(200) .send(`Hello ${user.name}, your email is ${user.email}`); } catch (e) { // Handle server errors res.status(500).send(`Something wrong happened at Server`); } });
This
/data
route is a protected endpoint that only authenticated users can access.After passing through the
authenticatedUsersOnly
middleware, the server fetches the user’s details fromreq.user
and responds with a personalized message.This shows how session-based authentication enables secure access to user-specific data.
Finally we start our server on PORT=8081 like this:
const PORT = 8081; app.listen(PORT, () => { console.log(`Listening on port ${PORT}`); });
Recap
In this article, we implemented a simple stateful authentication system using Express.js.
We created a login flow where users authenticate using their email and password, after which a session ID is generated, stored both on the client side (as a cookie) and on the server side (in an in-memory map).
We built a custom authentication middleware to protect certain routes that should only be accessible by logged-in users.
This setup ensures that sensitive routes like
/data
and/logout
are secured, while still keeping the overall system simple and easy to understand.
Although this example uses basic in-memory storage, the same concepts can be extended to use databases or production-grade session stores for scalability and security.
Additional Learning
Before we move further, I also encourage you to explore how session middleware provided by Express (like
express-session
) works under the hood.It automatically handles session creation, management, and cookie setting for you, making authentication workflows much easier.
Try implementing a simple application using
express-session
where the server automatically creates a session and stores session data for each user.
What is Next ?
In the next article, we will explore Stateless Authentication using Token-Based Authentication (such as JWT – JSON Web Tokens).
In token-based authentication, the server does not store any session information.
Instead, after a successful login, the server issues a signed token to the client.
The client sends this token in the headers of subsequent requests, and the server simply verifies the token's validity, making the system completely stateless.
Subscribe to my newsletter
Read articles from S. Apurba directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
