Full Stack Auth Flows
Auth is the biggest security surface in an app. Session cookies with CSRF protection, JWT access + refresh tokens, OAuth/OIDC providers, password hashing with argon2/bcrypt, and route guarding belong in one coherent flow you can reason about end to end.
What is this?
Full stack auth is the end-to-end flow that proves who a user is and keeps them signed in safely. The server hashes passwords with argon2/bcrypt, issues a credential (a session id stored server-side, or a signed JWT), and the client presents it on every request. Cookies marked httpOnly + SameSite keep tokens out of JavaScript; OAuth 2.0/OIDC delegate identity to providers like Google and GitHub. Every route that touches private data runs middleware that checks the credential and the user's role.
Why do you need it?
- Identity is the front door: one auth flaw unlocks every user's data, so it deserves the strongest patterns.
- httpOnly + SameSite cookies block the two classic attacks: XSS token theft and CSRF.
- Short-lived access tokens + rotating refresh tokens limit the damage if a token leaks.
- Centralizing auth in middleware means every protected route gets the same, audited checks instead of ad-hoc code.
- OAuth/OIDC remove the burden of storing other people's passwords and add MFA, SSO and account recovery for free.
Mental model
A hotel key card
An auth token
The key card (access token) opens your room for a short time; if you lose it, it expires soon anyway. The front desk keeps a paper trail (server session/refresh record) and re-issues a new card when you check back in. The card itself is useless without the hotel's lock reader trusting it.
How it works
- Registration: the server hashes the password with argon2 (or bcrypt) plus a per-user salt and stores only the hash.
- Login: the server re-hashes and compares; on success it issues a credential — a signed JWT, or a session id stored server-side with an httpOnly cookie.
- Every protected request passes through auth middleware that validates the credential, loads the user, and checks the required role.
- Short-lived access tokens are refreshed with a rotating refresh token; revoking the refresh token signs the user out everywhere.
- OAuth/OIDC: the user is redirected to the provider, which returns an authorization code the server exchanges for tokens — the app never sees the password.
The login and authorized-request flow.
Key commands
npm install jsonwebtoken cookie-parserJWT signing/verifying plus cookie parsing middleware.
added 9 packages ...
npm install argon2Password hashing with Argon2id (OWASP-recommended).
npm install next-auth@betaAuth.js for Next.js with session adapters and OAuth providers.
openssl rand -base64 32Generate a cryptographically random secret for JWT signing.
eR9tL3QxT8vZ0mN2... (random base64)
curl -i -X POST http://localhost:3001/api/login -H 'Content-Type: application/json' -d '{"email":"a@b.com","password":"secret"}'Inspect the Set-Cookie header returned by the login endpoint.
HTTP/1.1 200 OK\nSet-Cookie: token=eyJ...; HttpOnly; SameSite=lax; Path=/
Examples
Protected route middleware with a JWT in an httpOnly cookie
import jwt from "jsonwebtoken" export function requireAuth(req, res, next) { const token = req.cookies.token if (!token) return res.status(401).json({ error: "Missing token" }) try { req.user = jwt.verify(token, process.env.JWT_SECRET) next() } catch { res.status(401).json({ error: "Invalid or expired token" }) }} app.get("/api/me", requireAuth, (req, res) => { res.json({ user: req.user })}) // login sets the cookieapp.post("/api/login", (req, res) => { const token = jwt.sign({ sub: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: "15m", }) res.cookie("token", token, { httpOnly: true, sameSite: "lax", secure: true }) res.json({ ok: true })})The JWT is stored in an httpOnly cookie so JavaScript can't read it (XSS-resistant) and the browser attaches it automatically. requireAuth verifies the signature and expiry, attaches the user to the request, and short-circuits with 401 otherwise. The secret lives in an environment variable, never in the bundle.
In production
- Next.js apps use Auth.js sessions with a database adapter, httpOnly cookies and role-based middleware guarding /admin routes.
- APIs use short-lived JWTs (15 min) plus a rotating refresh token stored server-side; logout revokes the refresh token.
- Enterprise SSO integrates OIDC with the corporate IdP, so the platform inherits MFA and offboarding policies.
- Row-level access control pairs auth with per-user keyspaces (avatars/, /documents/:userId) so even valid sessions can't cross tenants.
Best practices
- Hash passwords with argon2id or bcrypt with a per-user salt; never store plaintext or unsalted hashes.
- Put session/JWT tokens in httpOnly, secure, SameSite cookies — not in localStorage where XSS can read them.
- Use short access-token lifetimes and rotate refresh tokens; revoke on logout and on password change.
- Add CSRF protection (SameSite=strict/lax, plus a CSRF token) for cookie-based auth.
- Rate-limit login and reset endpoints and add MFA for elevated actions.
Common mistakes
- Storing JWTs in localStorage — one XSS bug steals every session.
- Rolling your own password hashing or crypto instead of audited libraries.
- Forgetting CSRF protection and relying on SameSite alone for cross-site POST forms.
- Sending tokens in URLs (query strings) where they land in logs and referrer headers.
Common issues
401 Unauthorized after login on cross-origin setupFetch didn't include credentials: 'include', so the cookie was never sent.
Set credentials: 'include' in client fetch/axios, and cors({ credentials: true, origin: exactOrigin }) on the server.
Cookie not set (missing Set-Cookie in response)A proxy or middleware strips the header, or secure: true is set over plain HTTP.
Only send secure cookies over HTTPS (local dev usually needs secure: false), and check proxy header handling.
jwt expired — the user is logged out every 15 minutesThe access token expired and no refresh flow was implemented.
Add a refresh-token endpoint that issues a new access token, or use sliding sessions for cookie-based auth.
Debugging tips
- Inspect the Set-Cookie header on login: HttpOnly, Secure and SameSite must all be present in production.
- Decode the JWT payload (jwt.io) to check the exp claim and sub without leaking it.
- Test protected routes with curl: no cookie should yield 401; a forged token should yield 401, not 500.
- Watch for cookie scope problems: a token set on the API origin won't be sent to a frontend origin on a different port.
Interview checkpoint
Hands-on lab
- Hash passwords with argon2 at registration
- Issue a JWT in an httpOnly cookie at login
- Protect a route with requireAuth middleware
- Verify 401 without a cookie and 200 with a valid one
Add Login, Guards and Logout
50 min · advanced
Quick quiz
Which cookie attribute prevents JavaScript from ever reading the session token?
A cookie marked HttpOnly can still be read by JavaScript running on the same origin.
A database breach exposed your user password column. Which practice would have minimized the damage?
Cheat sheet
res.cookie('token', jwt, { httpOnly: true, sameSite: 'lax', secure: true })Store tokens out of JS reachjwt.verify(token, SECRET)Validate signature and expiryexpiresIn: '15m'Short access-token lifetimeargon2.hash / argon2.verifyHash and compare passwordscredentials: 'include'Send cookies on cross-origin fetchSameSite + CSRF tokenBlock cross-site form POSTs