Skip to main content
โšก Calmops

Understanding Web Authentication: Sessions, Cookies, JWT Tokens & OAuth2 Introduction

Table of Contents

Understanding Web Authentication: Sessions, Cookies, JWT Tokens & OAuth2 Introduction

Authentication is the cornerstone of web security. Every time you log into your email, social media account, or banking website, authentication mechanisms are working behind the scenes to verify your identity. Understanding how these mechanisms work is essential for building secure, scalable web applications.

This comprehensive guide covers four fundamental authentication approaches: sessions, cookies, JWT tokens, and OAuth2. We’ll explore how each works, their strengths and weaknesses, security implications, and when to use each approach.


Table of Contents

  1. Why Authentication Matters
  2. Sessions: Server-Side Authentication
  3. Cookies: The Storage Mechanism
  4. JWT Tokens: Stateless Authentication
  5. OAuth2: Delegated Authorization
  6. Comparing Authentication Approaches
  7. Security Considerations
  8. Best Practices & Recommendations
  9. Conclusion

Why Authentication Matters

Before diving into specific mechanisms, let’s understand the problem authentication solves.

The Core Challenge

When a user submits their username and password to your web application, you need to:

  1. Verify their identity - Confirm they are who they claim to be
  2. Maintain that identity - Remember they’re logged in across subsequent requests
  3. Protect against impersonation - Prevent other users from pretending to be them

HTTP is a stateless protocol, meaning each request is independent. Your server doesn’t inherently know that the person making request #2 is the same person who made request #1. Authentication mechanisms solve this by creating a persistent identity across stateless HTTP requests.

Authentication vs Authorization

These terms are often confused:

  • Authentication: Verifying who you are (identity)
  • Authorization: Determining what you’re allowed to do (permissions)

This guide focuses on authentication. Authorization typically builds on top of whatever authentication mechanism you choose.


Sessions: Server-Side Authentication

Sessions are the traditional, battle-tested approach to maintaining user identity on the web. A session represents an authenticated user’s interaction with your application over time.

How Sessions Work

User submits credentials (username/password)
           โ†“
Server validates credentials against database
           โ†“
Server creates a session: generates unique session ID
           โ†“
Server stores session data (user ID, login time, etc.) in memory/database
           โ†“
Server returns session ID to client (typically via cookie)
           โ†“
Client stores session ID and sends it with every subsequent request
           โ†“
Server looks up session ID to retrieve user information

Session Architecture Diagram

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Client Browser                   โ”‚
โ”‚  Session ID (stored in cookie or localStorage)      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                     โ”‚
                     โ”‚ Sends session ID with each request
                     โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   Web Server                        โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚  Session Store (In-Memory / Redis / DB)      โ”‚   โ”‚
โ”‚  โ”‚  Session ID โ†’ {user_id, login_time, data}    โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Practical Example: Node.js with Express

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

const app = express();

// Create Redis client for session storage
const redisClient = createClient();
redisClient.connect();

// Configure session middleware
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,        // Prevent JavaScript access
    secure: true,          // HTTPS only
    sameSite: 'Strict',    // CSRF protection
    maxAge: 1000 * 60 * 60 * 24  // 24 hours
  }
}));

// Login endpoint
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // Verify credentials (bcrypt recommended for production)
  const user = await User.findOne({ username });
  if (!user || !bcrypt.compareSync(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // Create session - data is automatically saved to session store
  req.session.userId = user.id;
  req.session.username = user.username;
  
  res.json({ message: 'Logged in successfully' });
});

// Protected endpoint - check if session exists
app.get('/profile', (req, res) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  
  res.json({
    userId: req.session.userId,
    username: req.session.username
  });
});

// Logout endpoint
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).json({ error: 'Logout failed' });
    res.json({ message: 'Logged out successfully' });
  });
});

Advantages of Sessions

  • Server control: Server can invalidate sessions immediately (logout, security issues)
  • Scalable security: Server-side logic can enforce complex authorization rules
  • Easy revocation: Simply delete the session to log out a user
  • Flexible data storage: Can store any data related to the session
  • Proven approach: Decades of real-world production use

Disadvantages of Sessions

  • Server-side storage: Requires memory/database space for each active session
  • Scaling challenges: Multi-server deployments need shared session storage (Redis, database)
  • Cookie dependency: Relies on cookies for transport (can be disabled by users)
  • Reduced mobility: Harder to share authentication across multiple domains/applications

When to Use Sessions

  • Traditional monolithic web applications
  • Applications where immediate logout/revocation is critical
  • Systems where server has complete control over authorization
  • Single-server deployments or applications with Redis/memcached

Cookies: The Storage Mechanism

Cookies are not an authentication mechanism themselvesโ€”they’re a transport mechanism for authentication data. However, understanding cookies is crucial because they’re the primary way browsers store and transmit session IDs and other authentication tokens.

What Are Cookies?

A cookie is a small piece of data (max 4KB) that the server instructs the browser to store. The browser automatically includes cookies with every request to the domain that set them.

How Cookies Work

Server sends HTTP response header: Set-Cookie: sessionId=abc123
             โ†“
Browser stores the cookie
             โ†“
Browser automatically includes in next request: Cookie: sessionId=abc123
             โ†“
Server reads the cookie from the request

When setting a cookie, the server specifies important attributes:

// Example: Setting a cookie in Node.js/Express
res.cookie('sessionId', 'abc123def456', {
  httpOnly: true,     // JavaScript cannot access (prevents XSS theft)
  secure: true,       // HTTPS only (prevents network interception)
  sameSite: 'Strict', // Not sent in cross-site requests (prevents CSRF)
  maxAge: 86400000,   // Expires in 24 hours (milliseconds)
  path: '/',          // Sent with all requests to this domain
  domain: 'example.com' // Sent only to example.com, not subdomains
});

Developers sometimes confuse cookies with other client-side storage options:

Storage Size Scope Expiration Sent with HTTP?
Cookies 4KB Per domain User-controlled Yes (automatic)
localStorage 5-10MB Per domain Persistent No (manual)
sessionStorage 5-10MB Per tab Tab closed No (manual)
IndexedDB 50MB+ Per domain Persistent No (manual)

For authentication, cookies are preferred because:

  1. Automatically sent with every request
  2. Can be marked HttpOnly to prevent JavaScript access
  3. Can be marked Secure to enforce HTTPS
  4. Can be marked SameSite to prevent CSRF attacks

Security Best Practices for Cookies

// Production-ready cookie configuration
const secureCookie = {
  httpOnly: true,      // Essential: prevents XSS attacks
  secure: true,        // Essential: HTTPS only
  sameSite: 'Lax',     // Prevents CSRF in most cases ('Strict' for banking)
  maxAge: 1000 * 60 * 60 * 24 * 7,  // 7 days
  path: '/',
  // Note: Do NOT set domain if you want subdomain isolation
};

// For development without HTTPS
const devCookie = {
  httpOnly: true,
  secure: false,       // Allow HTTP in development
  sameSite: 'Lax',
  maxAge: 1000 * 60 * 60 * 24 * 7
};
Attack How It Works Prevention
XSS JavaScript steals cookie httpOnly: true
Network Interception Man-in-the-middle reads cookie secure: true (HTTPS)
CSRF Cross-site form submission sameSite: 'Strict'/'Lax'
Session Fixation Attacker sets known session ID Regenerate session ID after login

JWT Tokens: Stateless Authentication

JWT (JSON Web Token) is a modern, stateless authentication approach that’s particularly popular in mobile apps and APIs. Unlike sessions, JWTs are self-contained and verified using cryptographic signatures.

What Is a JWT?

A JWT is a digitally signed JSON object that contains encoded user information. It has three parts, separated by periods:

Header.Payload.Signature

Example JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Breaking it down:

// Header (base64url encoded)
{
  "alg": "HS256",  // Algorithm: HMAC SHA-256
  "typ": "JWT"
}

// Payload (base64url encoded)
{
  "sub": "1234567890",      // Subject (typically user ID)
  "name": "John Doe",
  "iat": 1516239022,        // Issued At (timestamp)
  "exp": 1516325422        // Expiration time
}

// Signature (created by server)
HMACSHA256(base64urlEncode(header) + "." + base64urlEncode(payload), secret)

How JWT Authentication Works

1. User submits credentials (username/password)
           โ†“
2. Server validates credentials against database
           โ†“
3. Server creates JWT:
   - Encodes user data in payload
   - Signs with server's secret key
           โ†“
4. Server returns JWT to client
           โ†“
5. Client stores JWT (localStorage, sessionStorage, or cookie)
           โ†“
6. Client sends JWT in HTTP header: Authorization: Bearer <token>
           โ†“
7. Server verifies JWT signature with its secret key
   - If signature is valid, token wasn't tampered with
   - If signature is valid, user data in token can be trusted
           โ†“
8. Server grants access to protected resource

Practical Example: Node.js with JWT

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

const app = express();
const JWT_SECRET = process.env.JWT_SECRET; // Keep this secure!

// Login endpoint - issue JWT
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // Find user in database
  const user = await User.findOne({ username });
  if (!user || !bcrypt.compareSync(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // Create JWT token
  const token = jwt.sign(
    {
      userId: user.id,
      username: user.username,
      email: user.email,
      role: user.role
    },
    JWT_SECRET,
    {
      expiresIn: '24h',      // Token expires in 24 hours
      issuer: 'myapp',       // Who issued this token
      audience: 'myapp-users' // Who can use this token
    }
  );
  
  // Return token to client
  res.json({
    token,
    expiresIn: '24h',
    user: {
      id: user.id,
      username: user.username,
      email: user.email
    }
  });
});

// Middleware to verify JWT
const verifyToken = (req, res, next) => {
  // Token comes in Authorization header: "Bearer <token>"
  const authHeader = req.headers.authorization;
  const token = authHeader && authHeader.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  try {
    // Verify signature and expiration
    const decoded = jwt.verify(token, JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(403).json({ error: 'Invalid token' });
  }
};

// Protected endpoint
app.get('/profile', verifyToken, (req, res) => {
  res.json({
    message: 'Access granted',
    user: req.user
  });
});

// Logout (client-side)
app.post('/logout', verifyToken, (req, res) => {
  // In JWT, you don't "log out" on the server
  // Client simply deletes the token
  // For additional security, server can maintain a token blacklist
  res.json({ message: 'Logout successful (delete token on client)' });
});

JWT in HTTP Requests

When using JWT, the token is typically sent in the Authorization header:

GET /api/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

JWT vs Opaque Sessions

Aspect JWT Opaque Session ID
Storage Server-side (optional) Server-side (required)
Verification Signature validation Database lookup
Scalability Excellent (no DB lookup) Requires shared store (Redis)
Revocation Difficult (use blacklist) Immediate (delete session)
Data Self-contained Requires server lookup
Cross-domain Easy (no cookies needed) Difficult (cookie restrictions)
Mobile Excellent Good (with CORS)

Advantages of JWT

  • Stateless: Server doesn’t need to store sessions; reduces server memory
  • Scalable: No shared session storage needed; each server can verify tokens independently
  • Mobile-friendly: Doesn’t rely on cookies; works great for mobile apps and SPAs
  • Cross-domain: Can be used across multiple domains/APIs
  • Self-contained: Token contains user data; no need for database lookup after verification

Disadvantages of JWT

  • Token size: Larger than session IDs (affects bandwidth for high-traffic APIs)
  • Revocation difficulty: Can’t immediately invalidate a token (token remains valid until expiration)
  • Data exposure: Payload is base64-encoded, not encrypted (don’t put secrets in JWT)
  • Refresh token complexity: Handling token expiration requires refresh token pattern
  • Logout complexity: Requires token blacklist or other mechanisms

When to Use JWT

  • Mobile apps and native applications
  • REST APIs and microservices
  • Single-Page Applications (SPAs) with client-side routing
  • Cross-domain authentication
  • Scenarios where server scalability is critical

OAuth2: Delegated Authorization

OAuth2 is a standard for delegated authorization, allowing users to grant third-party applications access to their data without sharing passwords. It’s fundamentally different from the previous three approachesโ€”it’s not primarily about authentication, but about authorization.

OAuth2 vs Authentication

Important distinction:

  • OAuth2 is an authorization protocol (what you can do)
  • Authentication is about verifying identity (who you are)

However, OAuth2 can be used with an authentication protocol (OpenID Connect, discussed briefly below) to achieve both authentication and authorization.

The OAuth2 Scenario

You visit a photo printing website (example.com).
The site says: "Log in with Google" or "Log in with Facebook"
            โ†“
You click the button
            โ†“
You're redirected to Google's login page
            โ†“
You enter your Google credentials to Google (not to example.com)
            โ†“
Google asks: "example.com wants access to your profile. Allow?"
            โ†“
You click "Allow"
            โ†“
Google redirects you back to example.com with an authorization code
            โ†“
example.com exchanges the code for an access token
            โ†“
example.com uses the token to fetch your profile from Google
            โ†“
example.com logs you in

OAuth2 Components

Resource Owner: You (the user)

Client: The application requesting access (example.com)

Authorization Server: Issues tokens (Google, Facebook, GitHub)

Resource Server: Hosts protected resources (your Google Drive files, GitHub repos)

In many cases, the Authorization Server and Resource Server are the same service (e.g., Google), but they can be separate.

OAuth2 Authorization Code Flow (Most Secure)

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Browser   โ”‚          โ”‚   Web App    โ”‚         โ”‚    OAuth2    โ”‚
โ”‚  (User)     โ”‚          โ”‚  (Client)    โ”‚         โ”‚   Provider   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ”‚   (Google)   โ”‚
      โ”‚                         โ”‚                  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
      โ”‚ Click "Login with Google"                       โ”‚
      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚
      โ”‚ Redirects to Google login                       โ”‚
      โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
      โ”‚                                                  โ”‚
      โ”‚ Enters credentials at Google                    โ”‚
      โ”‚                                                  โ”‚
      โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
      โ”‚ Asks for permission (scope)                     โ”‚
      โ”‚                                                  โ”‚
      โ”‚ Grants permission, Google redirects back        โ”‚
      โ”‚ with authorization code                         โ”‚
      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚
      โ”‚                                                  โ”‚
      โ”‚                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚
      โ”‚                    โ”‚ POST /token with code        โ”‚
      โ”‚                    โ”‚ (backend-to-backend)         โ”‚
      โ”‚                    โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
      โ”‚                    โ”‚ Returns access_token         โ”‚
      โ”‚
      โ”‚ Logged in! Session established
      โ”‚

Practical Example: OAuth2 with Google (Node.js)

const express = require('express');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const session = require('express-session');

const app = express();

// Passport configuration for Google OAuth2
passport.use(new GoogleStrategy({
  clientID: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  callbackURL: 'http://localhost:3000/auth/google/callback'
},
// Verification callback - called after successful Google authentication
async (accessToken, refreshToken, profile, done) => {
  try {
    // Find or create user in your database
    let user = await User.findOne({ googleId: profile.id });
    
    if (!user) {
      user = await User.create({
        googleId: profile.id,
        displayName: profile.displayName,
        email: profile.emails[0].value,
        profilePicture: profile.photos[0].value
      });
    }
    
    return done(null, user);
  } catch (error) {
    return done(error);
  }
}
));

// Serialize user for session
passport.serializeUser((user, done) => {
  done(null, user.id);
});

// Deserialize user from session
passport.deserializeUser(async (id, done) => {
  const user = await User.findById(id);
  done(null, user);
});

// Session configuration
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false
}));

app.use(passport.initialize());
app.use(passport.session());

// OAuth2 routes
// Redirects user to Google login
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

// Callback after Google authentication
app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) => {
    // Successful authentication, user is logged in via session
    res.redirect('/dashboard');
  }
);

// Protected route
app.get('/dashboard', (req, res) => {
  if (!req.isAuthenticated()) {
    return res.redirect('/login');
  }
  
  res.json({
    message: 'Welcome to dashboard',
    user: req.user
  });
});

// Logout
app.get('/logout', (req, res) => {
  req.logout((err) => {
    if (err) return res.status(500).json({ error: 'Logout failed' });
    res.redirect('/');
  });
});

OAuth2 Scopes

When requesting access, the client specifies scopesโ€”permissions the user is granting:

// Request specific scopes from Google
passport.authenticate('google', {
  scope: [
    'profile',           // Access basic profile info
    'email',             // Access email address
    'https://www.googleapis.com/auth/calendar.readonly'  // Google Calendar
  ]
})

The user sees these requested permissions and can accept or deny. The app only receives the access token if the user grants the requested scopes.

Advantages of OAuth2

  • User privacy: Users don’t share passwords with third-party apps
  • Selective permissions: Users grant only the scopes they’re comfortable with
  • Revocable: Users can revoke app access at any time in their provider settings
  • Reduced security burden: Let identity providers handle authentication
  • Rich user data: Access to user’s data from the provider
  • Social login: Familiar login experience for users

Disadvantages of OAuth2

  • Dependency: Relies on external provider (service outage = users can’t log in)
  • Complexity: More moving parts than simple sessions
  • Vendor control: Limited by provider’s policies and features
  • Data sharing: Your app receives information from the provider
  • Learning curve: More complex to implement from scratch
  • Limited to specific providers: Only works with services that support OAuth2

When to Use OAuth2

  • SaaS applications where social login is a feature
  • Third-party integrations (accessing user’s Google Drive, GitHub repos, etc.)
  • When you want to reduce password management burden
  • Applications serving global audiences
  • When you need selective permissions/scopes

OpenID Connect: OAuth2 + Authentication

OpenID Connect (OIDC) builds on top of OAuth2 to add an authentication layer. It introduces an id_token (a JWT) that proves the user’s identity, in addition to the access_token used for authorization.

// With OIDC, you also get an ID token that contains user claims
{
  "iss": "https://accounts.google.com",
  "azp": "client_id.apps.googleusercontent.com",
  "aud": "client_id.apps.googleusercontent.com",
  "sub": "110169547472869293633",  // User ID
  "email": "[email protected]",
  "email_verified": true,
  "at_hash": "...",
  "iat": 1516239022,
  "exp": 1516325422
}

Many modern OAuth2 providers support OIDC, making OAuth2 suitable for both authorization and authentication.


Comparing Authentication Approaches

Decision Matrix

Factor Sessions Cookies JWT OAuth2
Stateless No No Yes Yes (with ID token)
Server Storage Required No Optional No
Immediate Logout Yes No No No
Scalability Medium N/A High High
Mobile Apps Poor No Excellent Excellent
Cross-domain Difficult No Easy Easy
Password sharing No No No No
Third-party access No No No Yes
Learning curve Low Low Medium High
Production ready Yes Yes Yes Yes

Flowchart: Choosing an Authentication Mechanism

Start: Choosing Authentication for Your App
       โ”‚
       โ”œโ”€ Single-Page App (SPA) or Mobile App?
       โ”‚  โ””โ”€ YES โ†’ Use JWT or OAuth2
       โ”‚
       โ”œโ”€ Traditional Server-Rendered App?
       โ”‚  โ””โ”€ YES โ†’ Use Sessions + Cookies
       โ”‚
       โ”œโ”€ Microservices / API Gateway?
       โ”‚  โ””โ”€ YES โ†’ Use JWT
       โ”‚
       โ”œโ”€ Need Third-Party Access (Google, GitHub login)?
       โ”‚  โ””โ”€ YES โ†’ Use OAuth2 (with OpenID Connect for auth)
       โ”‚
       โ”œโ”€ Need Immediate Token Revocation?
       โ”‚  โ””โ”€ YES โ†’ Sessions or maintain token blacklist
       โ”‚
       โ””โ”€ Global Scaling with Minimal Server Resources?
          โ””โ”€ YES โ†’ Use JWT or OAuth2

Architecture Comparison

Traditional Session Architecture:

Client โ”€ HTTP โ”€> Server โ”€ DB/Redis โ”€> Session Store
         (with session cookie)

JWT Architecture:

Client โ”€ HTTP โ”€> Server
(with JWT in header)

OAuth2 Architecture:

Client โ”€> OAuth2 Provider (Login)
         โ”‚
         โ””โ”€> Client App โ”€> OAuth2 Provider (Token Exchange)
                 โ”‚
                 โ””โ”€> Resource Server (Access Resources)

Real-World Examples

Best fit for Sessions:

  • GitHub-style issue tracking website
  • Internal company tool
  • Content management system
  • Applications with complex authorization rules

Best fit for JWT:

  • Mobile app backend API
  • Microservices
  • Real-time applications (WebSocket APIs)
  • Stateless serverless functions

Best fit for OAuth2:

  • Third-party integrations
  • “Log in with Google” features
  • Applications needing access to user’s cloud storage
  • Cross-platform authentication

Security Considerations

Attack Vectors & Prevention

1. Cross-Site Scripting (XSS)

Attack: Malicious JavaScript steals authentication tokens from localStorage.

Prevention:

  • For sessions: Use httpOnly cookies
  • For JWT: Store in httpOnly cookies instead of localStorage
  • Validate and sanitize all user inputs
  • Use Content Security Policy (CSP) headers
// Vulnerable: JWT in localStorage can be stolen by XSS
localStorage.setItem('token', jwt);

// Better: JWT in httpOnly cookie (from server)
res.cookie('token', jwt, { httpOnly: true, secure: true });

2. Cross-Site Request Forgery (CSRF)

Attack: Attacker tricks your browser into making unwanted requests to a website where you’re authenticated.

Prevention:

  • Use SameSite cookie attribute
  • Implement CSRF tokens for state-changing operations
  • Require authentication for sensitive operations
// Prevention: SameSite attribute
res.cookie('sessionId', id, {
  sameSite: 'Strict',  // Not sent in cross-site requests
  secure: true,
  httpOnly: true
});

// Additional: CSRF token for forms
app.post('/transfer-money', (req, res) => {
  // Verify CSRF token in addition to session
  if (!verifyCsrfToken(req)) {
    return res.status(403).json({ error: 'CSRF token invalid' });
  }
  // Process transfer
});

3. Session Fixation

Attack: Attacker tricks user into using a known session ID.

Prevention:

  • Generate new session ID after successful login
  • Use cryptographically secure random generators
// Regenerate session after login (express-session does this)
req.session.regenerate((err) => {
  if (err) return res.status(500).json({ error: 'Login failed' });
  
  req.session.userId = user.id;
  res.json({ message: 'Logged in' });
});

4. Token Leakage

Attack: Tokens exposed in logs, error messages, or transmitted over HTTP.

Prevention:

  • Use HTTPS for all authentication traffic
  • Don’t log sensitive tokens
  • Implement proper error handling (don’t expose token details)
  • Use token expiration (short-lived tokens)
// Never log tokens
console.log(`Token: ${token}`); // โŒ WRONG

// Rotate tokens frequently
const token = jwt.sign(data, secret, { expiresIn: '15m' });

// Use HTTPS in production
app.use((req, res, next) => {
  if (process.env.NODE_ENV === 'production' && !req.secure) {
    return res.redirect('https://' + req.get('host') + req.url);
  }
  next();
});

5. Password Storage

Attack: Weak password hashing allows attackers to crack user passwords.

Prevention:

  • Use strong, modern hashing algorithms (bcrypt, Argon2)
  • Never store plaintext passwords
  • Use salt to prevent rainbow table attacks
const bcrypt = require('bcrypt');

// Hash password during registration
const saltRounds = 10;  // Higher = slower but more secure
const passwordHash = await bcrypt.hash(password, saltRounds);

// Verify password during login
const isValid = await bcrypt.compare(inputPassword, passwordHash);

// Never do this:
// db.save({ username, password });  // โŒ WRONG

HTTPS: Non-Negotiable

All authentication must occur over HTTPS. HTTP exposes credentials and tokens to network interception.

// Enforce HTTPS
app.use((req, res, next) => {
  if (!req.secure && process.env.NODE_ENV === 'production') {
    return res.redirect('https://' + req.get('host') + req.url);
  }
  next();
});

// Use secure headers
const helmet = require('helmet');
app.use(helmet());  // Sets various security headers

Best Practices & Recommendations

General Principles

  1. Defense in Depth: Use multiple layers of security
  2. Least Privilege: Give users only the permissions they need
  3. Fail Secure: When in doubt, deny access
  4. Regular Updates: Keep dependencies patched
  5. Monitoring: Log and alert on suspicious authentication activity

Implementation Checklist

  • Use HTTPS everywhere (enforce with HSTS headers)
  • Hash passwords with bcrypt or Argon2
  • Use httpOnly and Secure flags for authentication cookies
  • Implement SameSite cookie attribute for CSRF protection
  • Add appropriate Content-Security-Policy headers
  • Implement rate limiting on login endpoints
  • Log authentication events (without logging sensitive data)
  • Use environment variables for secrets (never commit to git)
  • Implement token expiration
  • Provide users with logout/session management options
  • Require strong passwords or use passkeys/biometric auth
  • Implement optional multi-factor authentication (MFA)

Session Management Best Practices

// Complete session configuration
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,  // Use env variable
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,           // Prevent XSS
    secure: true,             // HTTPS only
    sameSite: 'Strict',       // Prevent CSRF
    maxAge: 1000 * 60 * 60 * 24,  // 24 hours
    domain: 'example.com'     // Restrict to domain
  }
}));

// Regenerate session after login
app.post('/login', async (req, res) => {
  // ... validate credentials ...
  
  req.session.regenerate((err) => {
    if (err) return res.status(500).json({ error: 'Login failed' });
    
    req.session.userId = user.id;
    req.session.lastActivity = Date.now();
    
    res.json({ message: 'Logged in' });
  });
});

// Destroy session on logout
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).json({ error: 'Logout failed' });
    res.clearCookie('connect.sid');
    res.json({ message: 'Logged out' });
  });
});

JWT Best Practices

// Complete JWT implementation
const jwt = require('jsonwebtoken');

// Issue token with reasonable expiration
const issueToken = (userId) => {
  return jwt.sign(
    { userId, iat: Math.floor(Date.now() / 1000) },  // Include issued-at time
    process.env.JWT_SECRET,
    {
      expiresIn: '15m',  // Short-lived access token
      issuer: 'myapp',
      audience: 'myapp-users'
    }
  );
};

// Issue refresh token for long-lived authentication
const issueRefreshToken = (userId) => {
  return jwt.sign(
    { userId, type: 'refresh' },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: '7d' }
  );
};

// Verify token with all checks
const verifyToken = (token) => {
  try {
    return jwt.verify(token, process.env.JWT_SECRET, {
      issuer: 'myapp',
      audience: 'myapp-users'
    });
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      throw new Error('Token expired');
    }
    throw new Error('Invalid token');
  }
};

// Handle token refresh
app.post('/refresh', (req, res) => {
  const { refreshToken } = req.body;
  
  try {
    const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
    
    // Issue new access token
    const newAccessToken = issueToken(decoded.userId);
    res.json({ accessToken: newAccessToken });
  } catch (error) {
    res.status(401).json({ error: 'Refresh failed' });
  }
});

Multi-Factor Authentication (MFA)

While not covered in detail here, MFA is increasingly important:

// After password authentication succeeds
app.post('/login', async (req, res) => {
  // ... validate password ...
  
  if (user.mfaEnabled) {
    // Send MFA code via email/SMS
    const mfaCode = generateSecureCode();
    await sendMfaCode(user.email, mfaCode);
    
    // Store temporary session without userId
    req.session.pendingMfa = true;
    req.session.pendingUserId = user.id;
    
    return res.json({ message: 'MFA code sent' });
  }
  
  // ... create authenticated session ...
});

// Verify MFA code
app.post('/verify-mfa', (req, res) => {
  if (!req.session.pendingMfa) {
    return res.status(401).json({ error: 'No pending MFA' });
  }
  
  const { mfaCode } = req.body;
  if (!verifyMfaCode(req.session.pendingUserId, mfaCode)) {
    return res.status(401).json({ error: 'Invalid MFA code' });
  }
  
  // Complete authentication
  req.session.userId = req.session.pendingUserId;
  delete req.session.pendingMfa;
  
  res.json({ message: 'Authentication complete' });
});

Conclusion

Each authentication mechanism serves different purposes and works best in different contexts:

  • Sessions remain the gold standard for traditional web applications, offering simplicity, immediate revocation, and server-side control.

  • Cookies are the primary transport mechanism for sessions, and understanding their security attributes is crucial for any web developer.

  • JWT tokens excel in modern architectures: mobile apps, single-page applications, microservices, and APIs. They provide stateless authentication at scale.

  • OAuth2 solves a different problemโ€”it allows users to grant third-party applications access to their data without sharing passwords. When combined with OpenID Connect, it also provides authentication.

Choosing the Right Approach

Use Sessions if:

  • Building a traditional server-rendered web application
  • You need immediate logout/token revocation
  • Your server infrastructure supports shared session storage
  • Your application has complex authorization logic

Use JWT if:

  • Building a mobile app, SPA, or API
  • You need to scale horizontally without shared session storage
  • Your application will be accessed from multiple domains
  • You can tolerate tokens being valid until expiration

Use OAuth2 if:

  • You want to offer “Login with Google/GitHub/Facebook”
  • You need to access user data from external providers
  • You’re building an API that third parties will integrate with
  • You want to reduce password management burden

Real-world recommendation: Many modern applications use a hybrid approach:

  • OAuth2 or sessions for initial authentication
  • JWT tokens for API access and mobile clients
  • Sessions for traditional web page access
  • Refresh tokens to handle token expiration securely

Security should always be your top priority. When in doubt, refer to OWASP guidelines, keep dependencies updated, use HTTPS, and implement defense-in-depth with multiple security layers.


Further Reading & Resources

Comments