Auth is where my "ship it" energy usually crashes. I asked for login and got a flow that stored a JWT in localStorage, had no CSRF thought, and logged the user out on every hard refresh because the session check ran client-side only.
The prompt below pins the session to httpOnly cookies, does the auth check in middleware so protected routes are actually protected on the server, and handles the OAuth callback and account-linking edge cases. It's the first generated auth I didn't immediately rip out.
How do you get the model to stop reaching for localStorage tokens by reflex?
Implement authentication for a Next.js 14 App Router app with both email/password and one OAuth provider ({PROVIDER}). Security is the priority; do not optimize for brevity.
NON-NEGOTIABLES:
- Sessions live in httpOnly, Secure, SameSite=Lax cookies. NEVER store tokens in localStorage or sessionStorage.
- Passwords hashed with a slow KDF (argon2id or bcrypt with cost >= 12). Never store or log plaintext.
- Route protection happens in middleware.ts on the server, so a protected page never renders for an unauthenticated request. Client checks are UX only, not the gate.
- CSRF protection on all state-changing POSTs.
- OAuth: handle the callback, verify state, and handle account linking when the OAuth email matches an existing email account (don't create a duplicate user).
EDGE CASES to handle explicitly: expired session, revoked/rotated refresh, email already registered, OAuth denied by user, and concurrent logins.
DELIVER: middleware.ts, the auth lib (session create/verify/destroy), the login/register route handlers, and the OAuth callback handler. Strict TypeScript, no any.
End with a threat-model note: for each of session theft, CSRF, and password leak, name the specific line/mechanism that mitigates it.