Authentication and Authorization Patterns

Advanced
15 min

Authentication and Authorization Patterns

Authentication answers "who is this?"; authorization answers "what may they do?". In the App Router these questions are asked in three places, and the division matters. After this lesson you will be able to issue and verify session cookies, protect data in the right layer, enforce roles, and decide when a library is the better choice.

Three Layers of Protection

| Layer | Role | Strength | |---|---|---| | Proxy (proxy.ts) | Redirect anonymous users early | Optimistic; can be bypassed | | Data Access Layer | Verify the session before every query or mutation | Authoritative | | UI | Hide controls the user cannot use | Cosmetic |

The DAL is the source of truth; the proxy and the UI improve experience but never replace a check next to the data.

Creating a Session

A stateless session is a signed token in an httpOnly cookie; the jose library signs and verifies it:

typescript
// lib/session.ts import "server-only"; import { SignJWT, jwtVerify } from "jose"; import { cookies } from "next/headers"; const key = new TextEncoder().encode(process.env.SESSION_SECRET); export async function encrypt(payload: { userId: string; role: string }) { return new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setExpirationTime("7d").sign(key); } export async function decrypt(token: string) { try { return (await jwtVerify(token, key, { algorithms: ["HS256"] })).payload; } catch { return null; } } export async function createSession(userId: string, role: string) { const token = await encrypt({ userId, role }); (await cookies()).set("session", token, { httpOnly: true, secure: true, sameSite: "lax", path: "/", maxAge: 604800 }); }

A login Server Action verifies the password with a slow hash such as bcrypt or Argon2, then creates the session:

typescript
"use server"; export async function login(formData: FormData) { const user = await findUserByEmail(String(formData.get("email"))); if (!user || !(await verifyPassword(String(formData.get("password")), user.hash))) { return { error: "Invalid credentials" }; } await createSession(user.id, user.role); redirect("/dashboard"); }

Logging out deletes the cookie. For server-side revocation, store a session id in the cookie and the record in the database.

Verifying in the Data Access Layer

The verifySession function in the sample at the top of this lesson reads the cookie, verifies it, and redirects if invalid. Wrapping it in React's cache means many components calling it in one request verify once. Every DAL function starts with it, and every query is scoped to the caller, so a user can never fetch another user's orders by guessing an id.

Authorization: Roles and DTOs

Enforce roles where the action happens:

typescript
// lib/dal.ts export async function deleteUser(targetId: string) { const { role } = await verifySession(); if (role !== "admin") throw new Error("Forbidden"); await db.user.delete({ where: { id: targetId } }); }

Return data transfer objects rather than raw records: map the row to { id, name, avatarUrl } and drop the password hash. Do not rely on a layout calling verifySession() to protect its children; layouts do not re-run on every navigation, so sensitive pages call the DAL themselves.

Using a Library

Production systems usually adopt a library for OAuth, email verification and passkeys:

| Library | Style | |---|---| | Auth.js (NextAuth) | Open source, provider-driven, auth() helper | | Better Auth | Open source, TypeScript-first, plugin system | | Clerk, Kinde, WorkOS | Hosted services with prebuilt UI |

Whichever you choose, the DAL pattern stays: one function returns the verified session and every query goes through it.

Common mistakes

  • Treating a proxy redirect as proof of authentication and skipping checks in Server Actions.
  • Storing the session token in localStorage, exposing it to any script on the page.
Quick Quiz
Question 1 of 3

Which layer is the authoritative place to verify a session?

Key Takeaways

  • Authenticate and authorise in the data access layer; the proxy and UI are supporting layers.
  • Sessions are signed tokens or ids in httpOnly, secure, sameSite cookies; jose signs and verifies them.
  • Wrap verifySession in cache so each request verifies once, and scope every query to the caller.
  • Enforce roles inside the functions that act, and return DTOs rather than raw records.
  • Libraries such as Auth.js, Better Auth or Clerk add providers and UI; the DAL pattern remains.

Next lesson: State Management in the App Router — decide what belongs in the URL, on the server and in client stores.

Authentication and Authorization Patterns - Next.js | CodeYourCraft | CodeYourCraft