Security audit of an Express.js JWT and PostgreSQL login demo detailing vulnerabilities, prioritized findings, remediation snippets, tests, and operational steps for authentication hardening.
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.
**Project:** Express Login Demo **Audit Date:** August 20, 2025 **Auditor:** Security Analysis Tool## Executive SummaryThis 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 ReviewConduct 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.
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.
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.
// tests/auth-refresh.spec.jsdescribe('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
Copy
// tests/brute-force.spec.jsdescribe('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.