Understanding Stateful Authentication

S. ApurbaS. Apurba
12 min read

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:

  1. The user logs in by providing a username and password.

  2. The server authenticates the user and creates a session, which is stored in a session store (like in-memory, Redis, or a database).

  3. A session ID is sent back to the client and is typically stored in a cookie in the browser.

  4. For each subsequent request, the client sends this session ID.

  5. 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:

  1. The user logs in with their credentials (username and password).

  2. The server authenticates the user and generates a token (typically a JWT – JSON Web Token).

  3. The token is sent to the client and stored in localStorage or in a cookie.

  4. With each request, the client sends the token (usually in the Authorization header).

  5. 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 -

  1. 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

  2. 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.

  3. Security Based Cookies :

    • Secure cookies, are the cookies which are sent over HTTP only, they protect against network sniffing.

    • HTTPOnly cookies are not accessible by Javascript and help to prevent XSS attacks.

    • Samesite cookies, restricts when cookie is sent in cross-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 from req.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.

0
Subscribe to my newsletter

Read articles from S. Apurba directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

S. Apurba
S. Apurba