AI Authenticates Like a Tutorial from 2010
AI generates authentication with: plain-text password comparison (if (password === user.password)), no password hashing (stored as-is in the database), single long-lived tokens (JWT that never expires, never rotates), no session invalidation (logout does nothing server-side), and no MFA support (username + password only). Every one of these is a critical security vulnerability that modern auth libraries solve.
Modern authentication is: hashed (bcrypt/argon2 with salt), tokenized (short-lived access + long-lived refresh, rotated on use), session-aware (server-side session store with explicit invalidation), multi-factor (TOTP, WebAuthn, SMS as fallback), and provider-integrated (OAuth 2.0 / OIDC for Google, GitHub, etc.). AI generates none of these.
These rules cover: password hashing, JWT access/refresh token patterns, session lifecycle, MFA integration, and OAuth provider flows.
Rule 1: Password Hashing with bcrypt or Argon2
The rule: 'Never store or compare passwords in plain text. Hash with bcrypt (cost factor 12+) or Argon2id on registration: const hash = await bcrypt.hash(password, 12). Compare on login: const valid = await bcrypt.compare(password, user.passwordHash). The hash includes the salt — no separate salt column needed. bcrypt is deliberately slow (100ms+), making brute-force attacks impractical even if the database is compromised.'
For Argon2 vs bcrypt: 'Argon2id is the Password Hashing Competition winner — memory-hard, resistant to GPU attacks. bcrypt is battle-tested, widely supported, and sufficient for most applications. Choose Argon2id for new systems with high security requirements. Use bcrypt if your framework provides it (most do). Never use: MD5, SHA-256, or any fast hash for passwords — speed is the enemy of password security.'
AI generates: if (req.body.password === user.password) — plain-text comparison. One SQL injection or database leak exposes every password. bcrypt.compare takes the same time whether the password is right or wrong (constant-time comparison), preventing timing attacks. Two lines of code, exponentially more secure.
- bcrypt.hash(password, 12) — cost factor 12 = ~250ms per hash, brute-force impractical
- bcrypt.compare(password, hash) — constant-time, prevents timing attacks
- Argon2id for new high-security systems — memory-hard, GPU-resistant
- Never MD5/SHA-256 for passwords — fast hashes enable brute-force
- Salt is embedded in bcrypt hash — no separate salt column needed
bcrypt.hash(password, 12) + bcrypt.compare(password, hash). Two function calls replace plain-text comparison with 250ms-per-attempt brute-force resistance. The salt is embedded in the hash — no separate column, no manual salt generation.
Rule 2: JWT Access/Refresh Token Rotation
The rule: 'Use short-lived access tokens (15 minutes) + long-lived refresh tokens (7 days). Access token: carries claims, sent with every request, stored in memory (never localStorage). Refresh token: used only to get new access tokens, stored in httpOnly secure cookie, rotated on every use (old refresh token invalidated when new one is issued). This pattern limits the blast radius of a stolen access token to 15 minutes.'
For token rotation: 'On refresh: (1) validate the refresh token, (2) check it has not been revoked, (3) issue a new access token + new refresh token, (4) revoke the old refresh token. If someone presents an already-revoked refresh token, invalidate the entire token family — this means the refresh token was stolen and used by the attacker. Alert the user and force re-authentication.'
AI generates: jwt.sign({ userId }, secret) with no expiration — a token that works forever. If stolen, the attacker has permanent access. No refresh mechanism means the user must re-enter credentials when the token expires (if it ever does). Short-lived access + rotating refresh = security + usability.
AI generates jwt.sign({ userId }, secret) with no expiration. A stolen token grants permanent access. Short-lived access (15min) + rotating refresh (7d) limits blast radius to minutes, not forever.
Rule 3: Session Lifecycle Management
The rule: 'Sessions must have explicit lifecycle stages: creation (on login), validation (on every request), extension (on activity), and destruction (on logout). Server-side session store (Redis, database) is the source of truth — not the client token. On logout: destroy the session server-side, clear the client cookie, and if using JWTs, add the token to a denylist until its natural expiration.'
For session fixation prevention: 'Regenerate the session ID on every privilege escalation: login, role change, password change. The old session ID becomes invalid. This prevents session fixation attacks where an attacker sets a known session ID before the victim logs in. In Express: req.session.regenerate(). In Next.js with NextAuth: the session token is rotated automatically on sign-in.'
AI generates logout as: localStorage.removeItem('token') — client-side only. The token still works if intercepted. The server has no concept of 'logged out.' Server-side session destruction means the token is dead the moment the user clicks logout, regardless of what the client does.
- Server-side session store (Redis/DB) — source of truth, not client token
- Regenerate session ID on login and privilege changes — prevent fixation
- On logout: destroy server session + clear cookie + denylist JWT
- Absolute timeout (24h) + idle timeout (30min) — defense in depth
- Never localStorage for tokens — XSS can read it; httpOnly cookies cannot
Rule 4: Multi-Factor Authentication
The rule: 'Offer MFA for all accounts, require it for admin/privileged accounts. TOTP (Time-based One-Time Password) is the baseline — works with Google Authenticator, Authy, 1Password. WebAuthn/passkeys are the future — phishing-resistant, device-bound. SMS is the fallback of last resort — vulnerable to SIM-swapping but better than no second factor.'
For TOTP implementation: 'Generate a secret (otpauth://totp/...), display as QR code, verify the code before enabling. On login: (1) verify password, (2) if MFA enabled, prompt for TOTP code, (3) verify with speakeasy.totp.verify({ secret, token, window: 1 }). The window parameter allows for 30-second clock drift. Store recovery codes (8-10 single-use codes) hashed like passwords — they are passwords.'
AI generates login with username + password only. No MFA enrollment flow, no recovery codes, no backup methods. Adding MFA after launch is harder than building it in — the database schema, login flow, and UI all need changes. Build the MFA hooks from day one, even if you enable it only for admins initially.
Adding MFA after launch means changing the database schema, login flow, and UI simultaneously. Building the MFA hooks from the start (even if only enabled for admins) costs 30 minutes. Retrofitting it later costs days.
Rule 5: OAuth 2.0 and OIDC Provider Integration
The rule: 'Use OAuth 2.0 Authorization Code flow with PKCE for all provider integrations (Google, GitHub, etc.). Never use the Implicit flow — it exposes tokens in the URL fragment. PKCE (Proof Key for Code Exchange) protects against authorization code interception. Libraries like NextAuth, Passport.js, or Auth.js handle the flow correctly — never implement OAuth from scratch.'
For account linking: 'When a user signs in with Google and an account with that email already exists (from password signup), do not auto-merge — prompt the user to verify ownership of the existing account first. Auto-merging based on email is an account takeover vector: attacker creates a Google account with the victim email, signs in with OAuth, gains access to the victim account.'
AI generates OAuth with: client secret in frontend code (exposed to users), no state parameter (CSRF vulnerable), token stored in localStorage (XSS vulnerable), and auto-merge on email match (account takeover). Four security vulnerabilities in one flow. Use a library — it handles all of these correctly by default.
Complete Authentication Rules Template
Consolidated rules for authentication flows.
- bcrypt/Argon2id for passwords — cost factor 12+, never plain text, never fast hashes
- Short-lived access tokens (15min) + rotating refresh tokens (7d) — limit blast radius
- Server-side session store — destroy on logout, denylist JWTs, regenerate on privilege change
- TOTP MFA baseline + WebAuthn for phishing resistance — recovery codes hashed like passwords
- OAuth Authorization Code + PKCE — never Implicit flow, never auto-merge on email
- httpOnly secure cookies for tokens — never localStorage (XSS readable)
- Rate limit login attempts — 5 failures then exponential backoff or CAPTCHA
- Log all auth events — login, logout, MFA enroll, password change, failed attempts