Full Stack
Advanced 15 min read Full Stack

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.

sessionJWThttpOnly cookierefresh tokenCSRFOAuth 2.0OIDCbcryptargon2middleware
Prerequisites: Real-Time Features
Introduction

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.

Motivation

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.
Think about it

Mental model

Imagine

A hotel key card

Think of

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.

Mechanics

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.

User submits credentials
Verify password
Issue session
Client stores cookie
Protected route middleware
Return data or 401
Reference

Key commands

npm install jsonwebtoken cookie-parser

JWT signing/verifying plus cookie parsing middleware.

added 9 packages ...
npm install argon2

Password hashing with Argon2id (OWASP-recommended).

npm install next-auth@beta

Auth.js for Next.js with session adapters and OAuth providers.

openssl rand -base64 32

Generate 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=/
Hands-on

Examples

Protected route middleware with a JWT in an httpOnly cookie

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 cookie
app.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.

Real world

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.
Guidelines

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.
Watch out

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.
Root cause analysis

Common issues

Error
401 Unauthorized after login on cross-origin setup
Root cause

Fetch didn't include credentials: 'include', so the cookie was never sent.

Fix

Set credentials: 'include' in client fetch/axios, and cors({ credentials: true, origin: exactOrigin }) on the server.

Error
Cookie not set (missing Set-Cookie in response)
Root cause

A proxy or middleware strips the header, or secure: true is set over plain HTTP.

Fix

Only send secure cookies over HTTPS (local dev usually needs secure: false), and check proxy header handling.

Error
jwt expired — the user is logged out every 15 minutes
Root cause

The access token expired and no refresh flow was implemented.

Fix

Add a refresh-token endpoint that issues a new access token, or use sliding sessions for cookie-based auth.

When things break

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.
Interviews

Interview checkpoint

Practice

Hands-on lab

Add Login, Guards and Logout
50 min · advanced
In progress
Objectives
  • 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

0/5 steps
0%
sandbox · Linux sandbox
$
Try these commands
Test yourself

Quick quiz

0/3
1

Which cookie attribute prevents JavaScript from ever reading the session token?

2

A cookie marked HttpOnly can still be read by JavaScript running on the same origin.

3

A database breach exposed your user password column. Which practice would have minimized the damage?

Quick reference

Cheat sheet

Session & token auth
res.cookie('token', jwt, { httpOnly: true, sameSite: 'lax', secure: true })Store tokens out of JS reach
jwt.verify(token, SECRET)Validate signature and expiry
expiresIn: '15m'Short access-token lifetime
argon2.hash / argon2.verifyHash and compare passwords
credentials: 'include'Send cookies on cross-origin fetch
SameSite + CSRF tokenBlock cross-site form POSTs
Go deeper

Official documentation