Skip to content

Session Management in MPAs β€” Cookies, Sessions, and User State

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Session Management in MPAs. We cover key concepts, practical examples, and best practices to help you master this topic.

Session management in MPAs uses server-side sessions with cookies to store user state across HTTP requests, enabling authentication persistence, user preferences, and shopping cart data.

What You'll Learn

By the end of this tutorial, you will understand how sessions work in MPAs, the difference between cookie-based and server-side sessions, session storage options (in-memory, Redis, database), session security best practices, and how to manage user login state across page requests.

Why It Matters

HTTP is stateless β€” each request is independent. Without sessions, every page load would require the user to log in again. Sessions are the foundation of all authenticated experiences on the web, from e-commerce checkouts to personalized dashboards.

Real-World Use

Amazon uses server-side sessions to remember your shopping cart, browsing history, and recommendations across requests. When you add an item to your cart and navigate to a different page, the session cookie tells the server who you are, and the server retrieves your cart data from its session store.

Session Flow in MPAs
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Browser  β”‚        β”‚  Server  β”‚        β”‚  Session  β”‚
    β”‚          β”‚        β”‚          β”‚        β”‚   Store   β”‚
    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
         β”‚                   β”‚                    β”‚
         β”‚  POST /login      β”‚                    β”‚
         β”‚  (email, password)β”‚                    β”‚
         │──────────────────>β”‚                    β”‚
         β”‚                   β”‚  Verify credentialsβ”‚
         β”‚                   β”‚                    β”‚
         β”‚                   β”‚  Create session    β”‚
         β”‚                   │───────────────────>β”‚
         β”‚                   β”‚  Session ID: abc123β”‚
         β”‚                   β”‚<───────────────────│
         β”‚                   β”‚                    β”‚
         β”‚  Set-Cookie:      β”‚                    β”‚
         β”‚  session=abc123   β”‚                    β”‚
         β”‚<──────────────────│                    β”‚
         β”‚                   β”‚                    β”‚
         β”‚  GET /dashboard   β”‚                    β”‚
         β”‚  Cookie: session= β”‚                    β”‚
         β”‚  abc123           β”‚                    β”‚
         │──────────────────>β”‚                    β”‚
         β”‚                   β”‚  Lookup session    β”‚
         β”‚                   │───────────────────>β”‚
         β”‚                   β”‚  User ID: 42       β”‚
         β”‚                   β”‚<───────────────────│
         β”‚                   β”‚                    β”‚
         β”‚  Dashboard HTML   β”‚                    β”‚
         β”‚  with user data   β”‚                    β”‚
         β”‚<──────────────────│                    β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Think of sessions like a locker room at a gym. When you arrive (log in), you get a key (session cookie) with a number on it. The server has a matching locker (session store) with your belongings (user data, cart items). Every time you return to the counter (make a request), you show your key, and the server retrieves your locker contents.

Session Implementation

const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');

const redisClient = redis.createClient({
    url: process.env.REDIS_URL || 'redis://localhost:6379'
});

redisClient.connect().catch(console.error);

const app = express();

// Session configuration
app.use(session({
    store: new RedisStore({ client: redisClient }),
    secret: process.env.SESSION_SECRET || 'your-secret-key',
    name: 'myapp.sid',  // Custom cookie name (not default 'connect.sid')
    resave: false,
    saveUninitialized: false,
    cookie: {
        httpOnly: true,      // Not accessible via JavaScript
        secure: true,        // HTTPS only
        sameSite: 'strict',  // CSRF protection
        maxAge: 24 * 60 * 60 * 1000  // 24 hours
    }
}));

// Login route
app.post('/login', async (req, res) => {
    const { email, password } = req.body;
    const user = await db.users.authenticate(email, password);

    if (!user) {
        return res.status(401).render('login', {
            error: 'Invalid email or password'
        });
    }

    // Store user data in session
    req.session.userId = user.id;
    req.session.userRole = user.role;
    req.session.createdAt = Date.now();

    // Regenerate session ID to prevent session fixation
    req.session.regenerate((err) => {
        if (err) return next(err);
        res.redirect('/dashboard');
    });
});

// Middleware to check authentication
function requireAuth(req, res, next) {
    if (!req.session || !req.session.userId) {
        req.session.returnTo = req.originalUrl;
        return res.redirect('/login');
    }
    next();
}

// Protected route
app.get('/dashboard', requireAuth, async (req, res) => {
    const user = await db.users.findById(req.session.userId);
    res.render('dashboard', {
        title: 'Dashboard',
        user: user
    });
});

// Logout
app.post('/logout', (req, res) => {
    req.session.destroy((err) => {
        if (err) return next(err);
        res.clearCookie('myapp.sid');
        res.redirect('/');
    });
});

Redis Session Store

// Redis session operations
const sessionUtils = {
    // Get active session count
    async getActiveSessionCount() {
        const keys = await redisClient.keys('sess:*');
        return keys.length;
    },

    // Get all sessions for a user
    async getUserSessions(userId) {
        const keys = await redisClient.keys('sess:*');
        const sessions = [];

        for (const key of keys) {
            const data = await redisClient.get(key);
            if (data) {
                const session = JSON.parse(data);
                if (session.userId === userId) {
                    sessions.push({
                        id: key.replace('sess:', ''),
                        createdAt: session.createdAt,
                        userAgent: session.userAgent
                    });
                }
            }
        }
        return sessions;
    },

    // Force logout all sessions for a user (password change)
    async invalidateUserSessions(userId) {
        const keys = await redisClient.keys('sess:*');
        for (const key of keys) {
            const data = await redisClient.get(key);
            if (data) {
                const session = JSON.parse(data);
                if (session.userId === userId) {
                    await redisClient.del(key);
                }
            }
        }
    },

    // Extend session TTL on activity
    async touchSession(sessionId) {
        await redisClient.expire(`sess:${sessionId}`, 86400); // 24 hours
    }
};

// Expected output:
// Active sessions: 1,234
// User 42 has 3 active sessions
// Invalidated 3 sessions for user 42

Session Security Best Practices

// Session security configuration
const sessionSecurity = {
    // Regenerate session ID after login
    regenerateOnLogin: true,

    // Rotate session ID periodically
    rotateInterval: 15 * 60 * 1000, // 15 minutes

    // Maximum sessions per user
    maxConcurrentSessions: 5,

    // Absolute session timeout (force re-login)
    absoluteTimeout: 7 * 24 * 60 * 60 * 1000, // 7 days

    // Idle session timeout
    idleTimeout: 30 * 60 * 1000, // 30 minutes

    // Check for session hijacking
    checkUserAgent: true,
    checkIP: false // IP changes are common on mobile
};

// Middleware for session rotation
app.use((req, res, next) => {
    if (req.session && req.session.userId) {
        // Check idle timeout
        const idle = Date.now() - req.session.lastActivity;
        if (idle > sessionSecurity.idleTimeout) {
            req.session.destroy();
            return res.redirect('/login?expired=1');
        }

        // Check absolute timeout
        const elapsed = Date.now() - req.session.createdAt;
        if (elapsed > sessionSecurity.absoluteTimeout) {
            req.session.destroy();
            return res.redirect('/login?expired=1');
        }

        // Rotate session ID periodically
        const rotationElapsed = Date.now() - req.session.lastRotation;
        if (rotationElapsed > sessionSecurity.rotateInterval) {
            req.session.lastRotation = Date.now();
            req.session.regenerate((err) => {
                if (err) return next(err);
                next();
            });
            return;
        }

        // Update last activity
        req.session.lastActivity = Date.now();
    }
    next();
});

Common Mistakes

  1. Storing too much data in session. Session data is loaded on every request. Store only the user ID and essential state. Fetch detailed data from the database when needed.
  2. Not using secure cookies in production. Sessions without Secure and HttpOnly flags are vulnerable to XSS and man-in-the-middle attacks. Always enable these flags in production.
  3. In-memory sessions in production. The default MemoryStore leaks memory and does not scale across multiple servers. Use Redis, Memcached, or a database for production sessions.
  4. Not invalidating sessions on password change. When a user changes their password, all existing sessions should be invalidated to prevent the old password from still working.
  5. Session fixation vulnerability. Not regenerating the session ID after login allows attackers to fixate a session ID. Always call regenerate() after authentication.

Practice Questions

  1. How does a server-side session work with cookies?
  2. What is the difference between cookie-based and server-side sessions?
  3. Why should you use Redis for session storage in production?
  4. What security measures protect sessions from hijacking?
  5. Why should you regenerate the session ID after login?

Challenge: Implement a complete session management system for an MPA with Express.js"Express" >}}.js and Redis. Include: login with session creation and ID regeneration, protected routes that check session, session timeout (idle and absolute), session display page showing active sessions with device info, force logout of individual sessions, and session invalidation on password change.

FAQ

What is the difference between sessions and JWT tokens?

Sessions are server-side state with a cookie identifier. JWTs are self-contained tokens with all data encoded. Sessions require server storage; JWTs are stateless.

How long should a session last?

24 hours is standard for web apps. Bank and healthcare apps use shorter sessions (15-30 minutes). Use idle timeout (30 min) to automatically log out inactive users.

Can sessions work across subdomains?

Yes. Set the cookie domain to '.example.com' to share sessions across subdomains. Ensure all subdomains use the same session secret and store.

What happens when Redis goes down?

Sessions become unavailable. All users are logged out. Use Redis Sentinel or cluster for high availability, and implement a fallback that shows an error page.

How do I handle concurrent sessions?

Limit sessions per user (e.g., 5 max). When a user logs in from a 6th device, invalidate the oldest session. Display active sessions in the user's profile.

Mini Project

Build a session-managed MPA with Express.js and Redis: login/logout with session regeneration, protected dashboard page showing user-specific content, session display page listing all active sessions with last activity time, ability to revoke individual sessions from the profile page, idle timeout that logs out inactive users, and absolute timeout that forces re-login after 7 days.

What's Next

You understand session management. Now learn about CSRF Protection to secure your forms against cross-site request forgery attacks.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro