Module 7: Authentication, Authorization & Security

Authentication Strategies

Authentication verifies the identity of an incoming request. Secure production systems never store plain text passwords, prefer modern cryptographic key derivation functions, and pick stateful or stateless session models based on scalability and revocation needs.

Credential Verification:
Client Password ──▶ Argon2 / Bcrypt Hash Check ──▶ Issued Auth Artifact
                                                          │
          ┌───────────────────────────────────────────────┴───────────────────────────────┐
          ▼                                                                               ▼
Stateless (JWT Pair)                                                            Stateful (Session Cookie)
- Access Token (Short-lived: 15m)                                               - Opaque Session ID
- Refresh Token (Long-lived: 7d, rotated)                                       - In-memory/Redis lookup
- Encoded claims signed with private key                                        - Revocation is instant

1. Password Hashing: bcrypt vs. argon2

General-purpose hash algorithms (MD5, SHA-256, SHA-512) are built for throughput and are vulnerable to brute-force attacks on GPUs/ASICs. Password hashing functions must be deliberately slow and resource-heavy.

  • bcrypt: A Battle-tested key derivation function based on the Blowfish cipher. It is primarily CPU-hard (controlled via work factor rounds, typically 10 to 12).
  • argon2: The winner of the Password Hashing Competition (PHC). It is both CPU-hard and memory-hard, providing resistance against custom hardware and GPU-assisted cracking. Use the argon2id variant, which combines data-independent and data-dependent memory access.

Bash

npm install argon2 bcrypt

JavaScript

import argon2 from 'argon2';
import bcrypt from 'bcrypt';

// Argon2id Implementation (Recommended modern default)
export async function hashPasswordArgon(plainPassword) {
  return await argon2.hash(plainPassword, {
    type: argon2.argon2id,
    memoryCost: 2 ** 16, // 64 MB
    timeCost: 3,         // 3 iterations
    parallelism: 1
  });
}

export async function verifyPasswordArgon(hash, plainPassword) {
  // Constant-time comparison handled internally to prevent timing attacks
  return await argon2.verify(hash, plainPassword);
}

// bcrypt Alternative
const SALT_ROUNDS = 12;

export async function hashPasswordBcrypt(plainPassword) {
  return await bcrypt.hash(plainPassword, SALT_ROUNDS);
}

export async function verifyPasswordBcrypt(plainPassword, hash) {
  return await bcrypt.compare(plainPassword, hash);
}

2. Stateless Token Authentication: JWT (Access & Refresh Tokens)

A JSON Web Token (JWT) contains three base64url-encoded parts separated by dots: Header.Payload.Signature.

  • Signature: Created by hashing the header and payload with a private secret or asymmetric key (RS256 / EdDSA).
  • Storage Threat Model: Never store tokens in browser localStorage or sessionStorage (they are directly accessible via Cross-Site Scripting / XSS). Keep the Access Token in memory (or private module variable) and the Refresh Token in an httpOnly, Secure cookie.

Bash

npm install jsonwebtoken

JavaScript

// utils/jwt.js
import jwt from 'jsonwebtoken';

const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;

export function generateTokenPair(user) {
  const payload = {
    sub: user.id,
    role: user.role
  };

  const accessToken = jwt.sign(payload, ACCESS_SECRET, {
    expiresIn: '15m',
    issuer: 'api.domain.com',
    audience: 'app.domain.com'
  });

  const refreshToken = jwt.sign({ sub: user.id }, REFRESH_SECRET, {
    expiresIn: '7d',
    issuer: 'api.domain.com'
  });

  return { accessToken, refreshToken };
}

export function verifyAccessToken(token) {
  return jwt.verify(token, ACCESS_SECRET, {
    issuer: 'api.domain.com',
    audience: 'app.domain.com'
  });
}

export function verifyRefreshToken(token) {
  return jwt.verify(token, REFRESH_SECRET, {
    issuer: 'api.domain.com'
  });
}
Refresh Token Rotation & Invalidation Flow

To mitigate token theft, issue a new Refresh Token on every refresh call and track token families in Redis:

JavaScript

// controllers/auth.controller.js
export async function refresh(req, res) {
  const incomingRefreshToken = req.cookies.refreshToken;
  if (!incomingRefreshToken) {
    return res.status(401).json({ error: 'Refresh token missing' });
  }

  try {
    const decoded = verifyRefreshToken(incomingRefreshToken);
    
    // Check if token was already used or blacklisted in Redis
    const isRevoked = await redis.get(`bl:${incomingRefreshToken}`);
    if (isRevoked) {
      // Possible token theft: invalidate all sessions for this user family
      await redis.del(`user_session:${decoded.sub}`);
      return res.status(403).json({ error: 'Token reuse detected. Please log in again.' });
    }

    // Blacklist previous refresh token until its expiration
    await redis.set(`bl:${incomingRefreshToken}`, '1', 'EX', 7 * 86400);

    // Issue brand-new pair
    const user = await db.user.findUnique({ where: { id: decoded.sub } });
    const { accessToken, refreshToken: newRefreshToken } = generateTokenPair(user);

    // Send new refresh token in secured cookie
    res.cookie('refreshToken', newRefreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      path: '/api/v1/auth/refresh',
      maxAge: 7 * 24 * 60 * 60 * 1000
    });

    res.status(200).json({ accessToken });
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired refresh token' });
  }
}

3. Stateful Session-Based Auth with Hardened Cookies

Stateful auth uses an opaque session identifier stored in client cookies. The server stores user state in Redis or a database.

JavaScript

import session from 'express-session';
import { RedisStore } from 'connect-redis';
import { redis } from '../config/redis.js';

export function configureSession(app) {
  app.use(
    session({
      store: new RedisStore({ client: redis, prefix: 'sess:' }),
      name: '__Host-sid', // __Host- prefix guarantees Secure, path=/, and no subdomains
      secret: process.env.SESSION_SECRET,
      resave: false,
      saveUninitialized: false,
      cookie: {
        httpOnly: true, // Inaccessible to document.cookie (Defends against XSS exfiltration)
        secure: process.env.NODE_ENV === 'production', // Transmitted strictly over HTTPS
        sameSite: 'lax', // Defends against Cross-Site Request Forgery (CSRF)
        path: '/',
        maxAge: 1000 * 60 * 60 * 8 // 8 hours
      }
    })
  );
}
  • httpOnly: true: JavaScript cannot read the cookie string, preventing stolen sessions via injected XSS scripts.
  • SameSite: 'strict' | 'lax':
    • Strict: The cookie is never sent in cross-site requests (e.g., following a link from an external site).
    • Lax: Default safe option. The cookie is withheld on cross-site subrequests (images, iframes), but sent when a user navigates to the origin site directly (top-level GET).
  • Secure: true: The browser sends the cookie only when the connection uses TLS/HTTPS.

4. OAuth 2.0 with Passport.js (Google Strategy)

OAuth 2.0 delegates authentication to an Identity Provider (IdP) without exposing user passwords to the consuming application.

Bash

npm install passport passport-google-oauth20

JavaScript

// config/passport.js
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';

passport.use(
  new GoogleStrategy(
    {
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      callbackURL: 'https://api.domain.com/api/v1/auth/google/callback',
      scope: ['profile', 'email']
    },
    async (accessToken, refreshToken, profile, done) => {
      try {
        const email = profile.emails?.[0]?.value;
        const googleId = profile.id;

        // Upsert user into database
        let user = await db.user.findFirst({
          where: { OR: [{ googleId }, { email }] }
        });

        if (!user) {
          user = await db.user.create({
            data: {
              email,
              name: profile.displayName,
              googleId,
              avatar: profile.photos?.[0]?.value
            }
          });
        }

        return done(null, user);
      } catch (err) {
        return done(err, null);
      }
    }
  )
);

// Routes
// 1. Redirect to Google Consent Screen
app.get('/api/v1/auth/google', passport.authenticate('google', { session: false }));

// 2. Callback endpoint
app.get(
  '/api/v1/auth/google/callback',
  passport.authenticate('google', { session: false, failureRedirect: '/login' }),
  (req, res) => {
    // Generate internal JWTs or sessions for the authenticated user
    const { accessToken, refreshToken } = generateTokenPair(req.user);
    res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: true, sameSite: 'strict' });
    res.redirect(`https://app.domain.com/oauth/success?token=${accessToken}`);
  }
);

Authorization: Role-Based Access Control (RBAC)

Authentication proves who you are; Authorization determines what you are allowed to do.

Request ──▶ [ verifyToken ] ──▶ req.user attached ──▶ [ checkRole / checkPermission ] ──▶ Next()

Middleware Implementation

JavaScript

// middleware/auth.middleware.js
import { verifyAccessToken } from '../utils/jwt.js';

export function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ status: 'fail', message: 'Authentication required' });
  }

  const token = authHeader.split(' ')[1];
  try {
    const payload = verifyAccessToken(token);
    req.user = payload; // { sub: 'usr_123', role: 'editor' }
    next();
  } catch (err) {
    return res.status(401).json({ status: 'fail', message: 'Invalid or expired token' });
  }
}

// Hierarchical Role Authorization Middleware
export function authorizeRoles(...allowedRoles) {
  return (req, res, next) => {
    if (!req.user || !allowedRoles.includes(req.user.role)) {
      return res.status(403).json({
        status: 'fail',
        message: 'Forbidden: Insufficient privileges'
      });
    }
    next();
  };
}

// Granular Permission Matrix Middleware
const ROLE_PERMISSIONS = {
  viewer: ['read:articles'],
  editor: ['read:articles', 'create:articles', 'update:articles'],
  admin:  ['read:articles', 'create:articles', 'update:articles', 'delete:articles', 'manage:users']
};

export function requirePermission(permission) {
  return (req, res, next) => {
    const userRole = req.user?.role;
    const permissions = ROLE_PERMISSIONS[userRole] || [];

    if (!permissions.includes(permission)) {
      return res.status(403).json({
        status: 'fail',
        message: `Forbidden: Missing required permission [${permission}]`
      });
    }
    next();
  };
}

JavaScript

// routes/article.routes.js
import { Router } from 'express';
import { authenticate, requirePermission } from '../middleware/auth.middleware.js';

const router = Router();

router.delete(
  '/articles/:id',
  authenticate,
  requirePermission('delete:articles'),
  (req, res) => res.json({ message: 'Article deleted successfully' })
);

Backend Security Hardening

1. Setting HTTP Security Headers with Helmet

Helmet adds secure HTTP response headers to defend against clickjacking, sniffed MIME types, and cross-site injections.

Bash

npm install helmet

JavaScript

import helmet from 'helmet';

app.use(
  helmet({
    // Content Security Policy: Whitelist origins that can execute scripts / load styles
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", 'data:', 'https://cdn.domain.com'],
        connectSrc: ["'self'", 'https://api.domain.com'],
        frameAncestors: ["'none'"] // Blocks site from being embedded inside an iframe (Clickjacking defense)
      }
    },
    crossOriginEmbedderPolicy: true,
    crossOriginOpenerPolicy: { policy: 'same-origin' },
    crossOriginResourcePolicy: { policy: 'same-origin' },
    dnsPrefetchControl: { allow: false },
    frameguard: { action: 'deny' }, // Older X-Frame-Options equivalent
    hidePoweredBy: true,           // Strips 'X-Powered-By: Express' (prevents fingerprinting)
    hsts: {                        // Enforces HTTPS for 1 year including subdomains
      maxAge: 31536000,
      includeSubDomains: true,
      preload: true
    },
    noSniff: true,                 // Sets X-Content-Type-Options: nosniff
    xssFilter: true
  })
);

2. Defending Against Injection & Web Attacks

Attack Vector       Vulnerable Pattern                       Defense
───────────────────────────────────────────────────────────────────────────────────────────
SQL Injection       String concatenation in raw SQL          Parameterized queries ($1, ?), ORMs
NoSQL Injection     Passing unvalidated objects ($ne, $gt)   Input schema casting (Zod), mongo-sanitize
XSS                 Trusting untrusted HTML or reflections   Contextual encoding, Strict CSP, httpOnly cookies
CSRF                Unchecked cross-origin mutations         SameSite cookies, CSRF Double-Submit Tokens
Timing Attacks      Standard equality checks (===)           crypto.timingSafeEqual()
SQL Injection
  • Vulnerable: db.query("SELECT * FROM users WHERE email = '" + req.body.email + "'")
  • Defended: Use parameterized inputs where the SQL engine compiles the query template before binding values:

JavaScript

// Parameterized query using pg
await pool.query('SELECT id, name FROM users WHERE email = $1', [req.body.email]);
NoSQL Injection (MongoDB)
  • Vulnerable: Sending { "username": "admin", "password": { "$ne": null } } bypasses standard equality logic.
  • Defended: Enforce rigid input validation using Zod schemas and sanitize raw query keys:

Bash

npm install express-mongo-sanitize

JavaScript

import mongoSanitize from 'express-mongo-sanitize';

// Strips out any keys starting with '$' or containing '.'
app.use(mongoSanitize());
Cross-Site Scripting (XSS)

Never trust user inputs rendered directly back into HTML interfaces. Clean output using sanitizers:

Bash

npm install sanitize-html

JavaScript

import sanitizeHtml from 'sanitize-html';

const cleanBody = sanitizeHtml(req.body.untrustedContent, {
  allowedTags: ['b', 'i', 'em', 'strong', 'a'],
  allowedAttributes: { a: ['href'] }
});
Cross-Site Request Forgery (CSRF)

If using cookie-based auth, a rogue site can force an authenticated browser to fire requests to your backend (e.g., via an image tag or hidden form).

  • Use SameSite=Lax or SameSite=Strict cookies.
  • For legacy browsers or cross-origin setups, use the Double Submit Cookie pattern or custom headers (x-requested-with or custom API tokens). Since modern SPA/API architectures rely heavily on the Authorization: Bearer <token> header, CSRF is naturally mitigated because browsers do not automatically attach headers cross-origin without prior CORS approval.
Timing Attacks

Using standard string equality operators (=== or ==) checks character by character and exits on the first mismatch. Attackers can measure response time variations to infer secret tokens or signatures byte-by-byte.

JavaScript

import crypto from 'node:crypto';

export function timingSafeCheck(userInput, actualSecret) {
  const userBuffer = Buffer.from(userInput);
  const secretBuffer = Buffer.from(actualSecret);

  // If byte lengths do not match, comparison fails
  if (userBuffer.length !== secretBuffer.length) {
    return false;
  }

  // Executes in constant time regardless of where mismatches occur
  return crypto.timingSafeEqual(userBuffer, secretBuffer);
}

3. Rate-Limiting & DDoS Mitigation

Defend backend services against volumetric floods, denial-of-service, and brute-force credential stuffing by placing a rate-limiting tier in front of endpoints.

Bash

npm install express-rate-limit rate-limit-redis

JavaScript

// middleware/rateLimiter.js
import { rateLimit } from 'express-rate-limit';
import { RedisStore } from 'rate-limit-redis';
import { redis } from '../config/redis.js';

// Global API Rate Limiter
export const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  limit: 100,               // Limit each IP to 100 requests per window
  standardHeaders: 'draft-7', // Return standard RateLimit-* headers
  legacyHeaders: false,
  store: new RedisStore({
    sendCommand: (...args) => redis.call(...args)
  }),
  message: {
    status: 429,
    message: 'Too many requests from this IP, please try again after 15 minutes'
  }
});

// Strict Limiter for Sensitive Endpoints (Login, Password Reset)
export const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  limit: 5,                 // 5 failed attempts per hour
  skipSuccessfulRequests: true, // Only count 4xx/5xx failures
  store: new RedisStore({
    sendCommand: (...args) => redis.call(...args),
    prefix: 'rl:auth:'
  }),
  message: {
    status: 429,
    message: 'Too many failed login attempts. Account temporarily locked for 1 hour.'
  }
});

JavaScript

// Mount limiters across the Express stack
app.use('/api/', apiLimiter);
app.use('/api/v1/auth/login', authLimiter);