Skip to main content
A presentation slide titled "Cookie and Session Security" with a large dark-blue curved shape on the right containing the word "Demo." The bottom-left shows a small "© Copyright KodeKloud" text.
This lesson demonstrates how to audit an Express application for cookie- and session-related weaknesses and provides concrete remediation patterns. The example focuses on JWT-based authentication (no server-side cookie sessions), highlights the security implications, and maps findings to prioritized fixes you can implement quickly. We ran an interactive analysis session (local assistant/REPL) to explore the repository and guide the audit. The session output looked like this:
/help for help, /status for your current setup

cwd: /Users/jeremy/Repos/Claude Code Course/Express-login-demo

> Try "refactor <filepath>"

? for shortcuts
A shortened sample of the structured audit prompt used to direct the analysis:
• Update Todos
  ☐ Examine session configuration in server.js
  ☐ Analyze cookie security settings
  ☐ Check for CSRF protection implementation
  ☐ Review session storage configuration
  ☐ Examine route handlers for session security
  ☐ Create structured security findings report

• Let me examine the codebase structure and analyze the session/cookie security implementation.

• Read(server.js)
  └ Read 19 lines (ctrl+r to expand)

• Read(package.json)
  └ Read 25 lines (ctrl+r to expand)

• Search(pattern: "routes/**/*.js")
  └ Found 1 file (ctrl+r to expand)
Large or reusable prompts used for these audits are stored in a repository to avoid retyping long instructions each time.

What the audit looked for

The audit checks focused on session and cookie hygiene, CSRF controls, and session storage practices. Key control areas included:
  • Session configuration (secure/httpOnly/sameSite, regeneration, timeouts)
  • Cookie security (flags, scope, no sensitive data in cookies, encryption)
  • CSRF protection (tokens, double-submit cookies, origin/referer checks)
  • Session storage (avoid default memory store, prefer Redis/DB, cleanup)
Detailed checklist mapping:
Control AreaSpecific ChecksWhy it matters
Session configurationsecure, httpOnly, sameSite, maxAge, resave/saveUninitializedPrevents token theft and session fixation
Cookie securityFlags, domain/path scoping, encryption for sensitive valuesLimits client-side access and cross-site leakage
CSRF protectioncsrf middleware, double-submit cookie, header validationMitigates cross-site request forgery when cookies are used
Session storageNo in-memory store in prod, use Redis or DB, TTL cleanupDurable and scalable revocation/invalidations
JWT-specificRefresh tokens, blacklisting, secret strengthJWTs can be valid even after logout without revocation
The audit also produced a structured findings report with fields like Title, Severity, CWE, Evidence (file/line), Exploitability notes, and remediation snippets.

High-level summary of findings

  • The app uses JWT-based authentication (no server-side cookie sessions), so many cookie-specific checks were N/A. However, JWTs introduce other risks that require operational controls.
  • Key issues discovered:
    • No server-side token revocation (no blacklisting).
    • No refresh token pattern — access tokens are long-lived or not rotated safely.
    • JWT secret management is weak or not validated (risk of brute-force or leaked secrets).
    • No CSRF middleware detected (relevant if cookies are introduced later).
    • No explicit guidance for secure client-side token storage (storing JWTs in localStorage is risky).
Example diagnostic notes:
  • No cookie usage found — Secure/HttpOnly/SameSite checks were marked N/A.
  • No CSRF middleware in server.js (server.js:1-19).
  • No refresh token implementation and no logout endpoint that invalidates tokens.
  • JWTs remain valid until expiry — no revocation mechanism in place.
Final (example) risk score: 8.5 / 10 (high). Immediate priorities: rotate JWT secret and add token blacklisting.

Representative audit excerpts

The report explicitly noted that many cookie checks were skipped due to JWT-only usage, but it stressed that any future introduction of cookies must include Secure, HttpOnly, and SameSite flags and CSRF protections. It also flagged missing logout/revocation endpoints and insecure secret handling.

Remediation snippets and patterns

Below are concrete, copy-paste-friendly code patterns you can adapt to your codebase. Keep your app structure and error handling in mind when integrating these.
  1. Server-side session (cookie) configuration (only if you switch to cookie sessions)
const session = require('express-session');

app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: {
    secure: process.env.NODE_ENV === 'production', // HTTPS-only in prod
    httpOnly: true, // prevent JS access
    sameSite: 'strict', // mitigate CSRF for sensitive ops
    maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
  },
  resave: false,
  saveUninitialized: false
}));
  1. CSRF protection (when cookies are used)
const csrf = require('csurf');

app.use(csrf({
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    sameSite: 'strict'
  }
}));

// Example: expose token in responses where needed
app.get('/form', (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});
  1. Strong JWT secret validation (startup-time checks)
const crypto = require('crypto');

if (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'your_jwt_secret_key_here') {
  throw new Error('JWT_SECRET must be set to a strong, random value');
}

if (process.env.JWT_SECRET.length < 32) {
  throw new Error('JWT_SECRET must be at least 32 characters long');
}
  1. Implement refresh token pattern (access + refresh token)
const crypto = require('crypto');
const jwt = require('jsonwebtoken');

// Example login handler
router.post('/login', async (req, res) => {
  // Authenticate user here...
  const payload = { sub: user.id };
  const accessToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m' });
  const refreshToken = crypto.randomBytes(40).toString('hex');

  // Store refreshToken in DB with user id and expiration
  await db.insertRefreshToken({ token: refreshToken, userId: user.id, expiresAt });

  // Return access token and set refresh token in an HttpOnly, Secure cookie if desired
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 1000 * 60 * 60 * 24 * 30 // 30 days
  });

  res.json({ accessToken });
});
  1. Token blacklisting (Redis-backed revocation) — middleware + logout
// Redis client (example using node-redis v4)
const redis = require('redis');
const redisClient = redis.createClient();
redisClient.connect().catch(console.error);
const jwt = require('jsonwebtoken');

// Middleware to check blacklisted tokens and verify token validity
const checkBlacklist = async (req, res, next) => {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith('Bearer ')) return res.status(401).json({ error: 'Missing token' });

  const token = auth.split(' ')[1];

  // Verify token signature and expiry before trusting claims
  try {
    jwt.verify(token, process.env.JWT_SECRET);
  } catch (err) {
    return res.status(401).json({ error: 'Invalid token' });
  }

  const blacklisted = await redisClient.get(`blacklist:${token}`);
  if (blacklisted) return res.status(401).json({ error: 'Token revoked' });

  next();
};
Logout handler to add token to blacklist with TTL equal to the remaining lifetime:
// Example logout handler
router.post('/logout', async (req, res) => {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith('Bearer ')) return res.status(400).end();

  const token = auth.split(' ')[1];

  // Verify token to get expiry (verify throws if invalid)
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const now = Math.floor(Date.now() / 1000);
    const ttl = (decoded.exp || now) - now;

    if (ttl > 0) {
      await redisClient.set(`blacklist:${token}`, '1', { EX: ttl });
    }
  } catch (err) {
    // If token is invalid, we can still respond success — it is effectively logged out
  }

  res.status(200).json({ success: true });
});
  1. Sliding session timeout / short-lived access tokens example
const jwt = require('jsonwebtoken');

const checkTokenFreshness = (req, res, next) => {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith('Bearer ')) return res.status(401).end();

  const token = auth.split(' ')[1];

  // Verify token to ensure claims (iat/exp) are trustworthy
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const tokenAge = Math.floor(Date.now() / 1000) - (decoded.iat || 0);

    // If token is older than 1 hour, require re-authentication
    if (tokenAge > 3600) return res.status(401).json({ error: 'Session expired, re-authenticate' });

    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid token' });
  }
};
Avoid storing access or refresh tokens in localStorage or any storage accessible to JavaScript. Prefer HttpOnly cookies for refresh tokens and keep access tokens short-lived and in memory. Storing tokens in localStorage increases your attack surface for XSS.
  1. Guidance summary — secure token handling
  • Use HttpOnly, Secure, SameSite cookies for refresh tokens.
  • Keep access tokens short-lived (minutes) and refresh them via a secure refresh flow.
  • Store refresh tokens server-side (DB/Redis) or as HttpOnly cookies with rotation.
  • Validate JWT_SECRET at startup and rotate secrets on a regular schedule.

Proof-of-concept checks (curl examples)

Use these commands to reproduce the lack of revocation or logout behavior during testing:
# 1. Login and capture JWT
curl -s -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"password123"}'

# 2. Attempt logout (if no endpoint exists)
curl -i -X POST http://localhost:3000/api/auth/logout \
  -H "Authorization: Bearer eyJ..."
# 3. Verify token remains valid until expiry
curl -i -X GET http://localhost:3000/api/protected \
  -H "Authorization: Bearer eyJ..."
# If valid, protected route responds 200 OK — indicates no server-side invalidation

Prioritization & remediation roadmap

Top fixes to reduce risk fast:
PriorityActionDuration
P0Replace weak/hard-coded JWT secrets with a strong random secret and enforce min lengthImmediate
P0Implement token blacklisting (Redis) for revocationImmediate
P1Add refresh token pattern with HttpOnly secure cookiesWeek 1
P1Add logout that revokes access and refresh tokensWeek 1
P2Add CSRF protection if cookies are usedWeek 2
P2Move any session state to Redis or a DB-backed store (no memory store in prod)Week 2–4
Suggested timeline:
  • Immediate: JWT secret rotation and token blacklisting.
  • Week 1: Refresh tokens + secure cookie patterns and logout.
  • Week 2: CSRF controls and session store adoption.
  • Month 1: Session monitoring, adaptive timeouts, and device tracking.

Compliance impact

The audit mapped findings to common frameworks:
  • OWASP Top 10: A07:2021 (Identification and Authentication Failures) — applicable due to lack of revocation and long-lived tokens.
  • PCI DSS / SOC 2 / NIST: Insufficient session invalidation and access management controls may impact compliance posture.

Closing notes

  • JWT-only architectures reduce some cookie risks but require operational controls: revocation, refresh, and secure storage.
  • If you introduce cookies later, ensure Secure, HttpOnly, and SameSite flags and add CSRF protection.
  • Break audits into focused checks (sessions, cookies, CSRF, storage) to produce actionable, prioritized findings and reduce missed items.
Upcoming topics will cover file handling and business logic audits.

Watch Video