Skip to main content
This lesson summarizes a comprehensive authentication security review of an Express.js login demo (Postgres + JWT). The review was guided by a structured audit prompt that enumerates common pitfalls, remediation guidance, and test recommendations. Below you’ll find the audit prompt used to drive the analysis, an executive summary, prioritized findings, concrete remediation snippets, recommended tests, and operational next steps.
Note: the audit focused on authentication hardening patterns relevant to most Express-based demos: password hashing, JWT handling, refresh-token rotation, session invalidation, brute-force protection, input validation, and secure cookie/CSRF practices.

Structured audit prompt (representative)

**Project:** Express Login Demo  
**Audit Date:** August 20, 2025  
**Auditor:** Security Analysis Tool

## Executive Summary

This security audit analyzed the Express.js login demo application with JWT authentication and PostgreSQL database integration. The audit examined 5 core files and identified several **CRITICAL** and **HIGH** risk vulnerabilities that require immediate attention.

> # Authentication Flow Review

Conduct a comprehensive authentication security review.

Check for:

1. Password hashing
  * Uses `bcrypt.hash()` with salt rounds ≥ 10 (ideally 12); async, not sync; no double-hashing on updates.
  * `bcrypt.compare()` used for login.

2. JWT secret/key strength & storage
  * Secrets/keys not hardcoded; loaded from env or secret manager; separate keys for access vs refresh.
  * Strong entropy (≥256-bit for HS256) or asymmetric (RS256/ES256) with rotation plan.

3. Token settings
  * Access token TTL 5–15 minutes; refresh token TTL 7–30 days; sliding sessions bounded by a max session age.
  * Verify `algorithms`, `issuer (iss)`, `audience (aud)`, `subject (sub)`, `jti`, `iat`, `exp`, and optional `nbf`.

4. Refresh token implementation
  * Rotation on every use and reuse detection (if old RT is presented, revoke the whole session family).
  * RTs stored in HttpOnly, Secure, SameSite cookie; hashed at rest if persisted; per-device tracking.

5. Session invalidation on password change
  * Previously issued tokens rejected after password reset.

6. Brute force protection
  * Rate-limit login/reset/verify endpoints; progressive backoff per username+IP in fast store (e.g., Redis); optional CAPTCHA after threshold.

7. Account enumeration defenses
  * Generic errors and identical timing for user-not-found vs bad-password; optional jitter.

8. Password reset flow security
  * One-time, short-TTL verification tokens; hashed at rest; invalidated after use; no secrets in logs.

9. SQL/NoSQL injection in auth paths
  * Parameterized SQL; no user-controlled operators in filters; query sanitization enabled.

10. AuthZ integrity
  * Roles/permissions loaded server-side; deny-by-default; do not trust JWT claims alone.

11. Cookie & CSRF configuration (if cookies used)
  * `HttpOnly`, `Secure`, `SameSite=Lax|Strict`; narrow `path`/`domain`; explicit `Max-Age`. CSRF protection on state-changing endpoints and refresh route.

12. Input validation & normalization
  * Email/username normalization; length & charset checks; password policy; strong schema validation (zod/joi/celebrate).

13. Mass assignment risks
  * Whitelist allowed fields for updates; cannot set `role`, `emailVerified`, `passwordResetToken`, etc., from `req.body`.

14. JWT misuse
  * Do not use `jwt.decode()` for authorization; always `jwt.verify()` with explicit `algorithms`.

15. Logging & telemetry
  * No logging of passwords, tokens, reset links, or PII; structured logs with redaction.

16. Dependency & crypto hygiene
  * Keep `jsonwebtoken` and `bcrypt` up-to-date; use vetted crypto primitives; do not use MD5/SHA1 for password hashing.

17. Transport & CORS
  * HTTPS enforced; CORS locked to trusted origins; no wildcard credentials.

18. Open redirect / `next` param
  * Restrict post-login redirects to vetted paths/origins.

19. Operational controls
  * Secret rotation procedure; monitoring for suspicious auth patterns; alerting on RT reuse.

Provide:
* Structured finding report (title, severity, CWE, evidence, explanation, exploitability notes, minimal PoC where safe, remediation with precise code/config changes).
* Checklist diff; concrete code locations and minimal drop-in fixes.
* Tests for validation (e.g., RT reuse detection, password-changed invalidation, NoSQLi payload).
* Write final report into audits/ folder.
The audit prompt above guided automated analysis and subsequent manual review. The rest of this article summarizes key findings, rationale, and minimal remediation snippets produced during the review.
This article focuses on actionable fixes for Express.js authentication flows: secure password storage, robust JWT handling, refresh-token rotation, brute-force defenses, safe cookie/CSRF configuration, and input validation. Use the snippets below as minimal drop-in improvements and adapt them to your project structure.

Executive summary (high level)

  • Primary focus: authentication flow hardening for an Express demo with PostgreSQL and JWTs.
  • Main risks found (representative):
    • Hard-coded or weak JWT secret — Critical
    • Plaintext password storage or missing hashing — Critical
    • Missing brute-force/rate-limiting on login/reset — High
    • Long-lived access tokens / no refresh-token rotation — High
    • Insufficient input validation / potential account enumeration — Medium-High
  • Recommended priorities:
    1. Replace weak/hard-coded secrets; use environment or secret manager and rotate keys.
    2. Ensure passwords are hashed (bcrypt with async salts ≥12) before persistence.
    3. Implement RT rotation + reuse detection, shorten access TTL.
    4. Add rate-limiting and consistent, generic error messaging to prevent enumeration.
    5. Harden cookies, CSRF protections, and input validation.

Key checks — what to inspect and why

AreaWhy it mattersQuick check
Password hashingPrevents exposure of plaintext credentials on breachEnsure bcrypt async hash on register and update, salt rounds ≥ 10 (12 recommended)
JWT managementWeak secrets enable token forgery; alg/config misuse leads to bypassNo hard-coded secrets; env-managed keys; explicit verify options (alg, iss, aud)
Token TTLs & rotationLimits window of compromise; rotation reduces risk of leaked RTsAccess 5–15m; RT 7–30d with rotation on use, hashed RTs at rest
Refresh token strategyProper rotation + reuse detection prevents session hijackingTrack RT per device, rotate on use, revoke family on reuse
Session invalidationUsers must be logged out on password reset/changeMaintain session version or token revocation list invalidated on password change
Brute force & enumerationPrevents automated account takeover and username discoveryPer-user+IP rate limits (Redis), generic errors, timing stability
Password reset securityPrevents token leakage and reuseOne-time tokens, short TTL, hashed at rest, do not log tokens
Injection & input validationPrevents data exfiltration and privilege elevationParameterized queries, schema validation (zod/joi/express-validator)
Cookie & CSRFProtects session tokens from XSS/CSRFHttpOnly, Secure, SameSite, CSRF on state-changing endpoints
Logging & telemetryRedaction prevents credential leakage in logsNo logging of passwords/tokens/PII; structured logs and alerting
Relevant references:

Example findings (representative)

  • Weak JWT Secret Management
    • Severity: Critical | CWE-798 (Hard-coded Credentials)
    • Evidence: .env included JWT_SECRET=your_jwt_secret_key_here.
    • Remediation: Replace with a cryptographically strong secret stored in environment or secret manager; rotate keys and use asymmetric keys (RS256/ES256) if feasible.
  • No password hashing on registration
    • Severity: Critical
    • Evidence: Registration flow persisted plaintext password.
    • Remediation: Use bcrypt.hash() (async) with salt rounds ≥ 12 before persisting.
  • Missing brute force protection on login
    • Severity: High
    • Evidence: No rate limiting or lockouts on login/reset endpoints.
    • Remediation: Add express-rate-limit with Redis backing; block after threshold and present generic error messages.
  • Excessive token lifetimes / no RT rotation
    • Severity: High
    • Evidence: Long-lived access tokens; refresh tokens not rotated.
    • Remediation: Shorten access TTL, implement RT rotation with reuse detection and per-device tracking.

Concrete remediation snippets

Place these snippets into appropriate files (e.g., routes/auth.js, middleware/, or a config file). They are minimal and intended to be adapted to your project style and error handling.

1) Register with bcrypt hashing (async)

// routes/auth.js
const bcrypt = require('bcrypt');
const { body, validationResult } = require('express-validator');

router.post(
  '/register',
  [
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 8, max: 128 })
      .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).+$/)
      .withMessage('Password must contain uppercase, lowercase, number, and special character'),
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });

    const { email, password, name } = req.body;
    const hashedPassword = await bcrypt.hash(password, 12); // async, salt rounds = 12
    await pool.query(
      'INSERT INTO users (email, name, password) VALUES ($1, $2, $3)',
      [email, name || null, hashedPassword]
    );
    res.status(201).json({ success: true });
  }
);

2) Rate limit login attempts (express-rate-limit + Redis)

// middleware/rate-limit.js
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis'); // optional backed store
const Redis = require('ioredis');

const redisClient = new Redis(process.env.REDIS_URL);

const loginLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redisClient.call(...args) }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // after 5 failed attempts
  message: { error: 'Too many login attempts, please try again later' },
  standardHeaders: true,
  legacyHeaders: false,
});

router.post('/login', loginLimiter, async (req, res) => {
  // login logic
});

3) HTTPS redirect middleware (enforce TLS in production)

// app.js or a middleware module
app.use((req, res, next) => {
  const proto = req.header('x-forwarded-proto') || req.protocol;
  if (process.env.NODE_ENV === 'production' && proto !== 'https') {
    return res.redirect(`https://${req.header('host')}${req.url}`);
  }
  next();
});

4) Generic server-side error logging and client response

// Inside a try/catch in routes
try {
  // login logic
} catch (error) {
  console.error('Login error:', error); // server-side only
  res.status(500).json({ error: 'Authentication service temporarily unavailable' });
}

5) Strong JWT verification (explicit algorithms and claim checks)

const jwt = require('jsonwebtoken');

function verifyAccessToken(token) {
  return jwt.verify(token, process.env.JWT_ACCESS_PUBLIC_OR_SECRET, {
    algorithms: ['HS256'], // or ['RS256'] for asymmetric
    issuer: 'your-service',
    audience: 'your-client',
  });
}

6) Password validation rules (express-validator)

// validation/auth.js
const { body } = require('express-validator');

const registerValidation = [
  body('email').isEmail().isLength({ max: 254 }).normalizeEmail({ gmail_remove_dots: false }),
  body('password')
    .isLength({ min: 8, max: 128 })
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).+$/)
    .withMessage('Password must contain uppercase, lowercase, number, and special character'),
];

module.exports = { registerValidation };
Immediate actions (next 48 hours): replace weak/hard-coded JWT secrets, ensure passwords are hashed before persistence, add login rate-limiting, shorten access token TTL, and implement refresh-token rotation. These address the highest-impact findings.

Test recommendations (examples)

  • Refresh token reuse detection (pseudo-test)
// tests/auth-refresh.spec.js
describe('Refresh Token Security', () => {
  it('invalidates session family on token reuse', async () => {
    // 1) obtain refresh token A for device/session 1
    // 2) use refresh token A to get new access + refresh token B (rotation)
    // 3) attempt to use old refresh token A again
    // 4) expect reuse detection: both tokens invalidated, session revoked
  });
});
  • Brute force protection test
// tests/brute-force.spec.js
describe('Brute Force Protection', () => {
  it('rate limits after 5 failed attempts', async () => {
    // make 6 login attempts with invalid password
    // expect 6th request returns 429
  });
});
  • Password reset flow test
    • Create a short-TTL reset token, assert it’s hashed at rest, use once, and verify it’s invalid afterwards.

Report generation & operational checklist

The audit output should be committed as a structured markdown report (example filename: audits/AUTHENTICATION_FLOW_REPORT.md) and include:
  • Executive summary and risk score
  • Critical & high severity findings with CWE references
  • Evidence (file/line references where available)
  • Minimal remediation snippets and suggested timelines:
    • Immediate (48 hours): replace JWT secret with strong key; add bcrypt hashing on registration and updates; add brute-force protection; shorten access token TTL; add refresh-token rotation.
    • Within 1 week: implement session invalidation on password change; comprehensive input validation; JWT middleware and claim verification.
    • Within 1 month: logging redaction; monitoring/alerting for RT reuse; penetration testing and key rotation procedures.
  • Tests to validate fixes (RT reuse, invalidation after password change, bruteforce detection).
  • Checklist diff showing before/after and file-level changes.
A VS Code window displaying an "Authentication Security Audit Report" Markdown file with sections like "Compliance Impact" and "Next Steps" listing PCI DSS, OWASP, and SOC issues. The project explorer, file tree, terminal and minimap are visible in a dark-themed editor.

Where to find prompts and next steps

All prompts used for these reviews were kept in the repository for reuse and iteration. Recommended operational next steps:
  • Replace placeholder secrets with production-grade secrets (secrets manager or KMS).
  • Implement the code fixes above and run the tests suggested.
  • Add monitoring and alerting for refresh-token reuse and anomalous auth patterns.
  • Schedule a secondary audit after fixes are merged and tests pass.
A desktop screenshot showing a browser window open to a GitHub repository called "Claude-Code-Reviewing-Prompts" with a list of Markdown files and commit info. A code editor with project files is visible in the background.

Final notes

  • Not every check applies to every application. Mark items “Unable to Verify” where required context or files are missing.
  • Prioritize concrete, minimal fixes that can be validated with tests and rolled out progressively.
  • Preserve the practice of never exposing secrets or tokens in logs and enable key rotation and monitoring.

Watch Video