Skip to content
C.W.K.
Stream
Lesson 06 of 09 · published

Protected Routes

~20 min · defense in depth, auth

Level 0Curious
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

Defense in depth

Don't rely on one auth check. Layer them — each catches what the others miss.

LayerCatches
ProxyBots and casual users hitting protected URLs unauthenticated
LayoutServer-rendered pages that someone reaches without going through the proxy (e.g. preview deployments)
Server Action / Route HandlerProgrammatic POSTs from anywhere on the internet

What each layer is for

Proxy is the redirect/UX gate. Layout is the "rendering this page would leak data" gate. Action/handler is the "writing data could happen even without rendering a page" gate. The last one is the security gate — the only one you cannot skip.

Layer access checks by responsibility. Proxy may redirect early, a layout may shape the signed-in experience, but every Server Action, Route Handler, and data-access function must enforce the permission required for that operation. Put the decisive check at the deepest trusted boundary. Defense in depth is not copying the same boolean into three files. Each layer answers a different question, and duplicated policy drifts. Centralize the domain authorization rule, then call it from every mutation and sensitive read that needs the rule.

Open the protected URL normally, call its handler directly, invoke its action with another user's ID, and attempt the underlying data path from a test. All routes should converge on the same deny decision while redirects remain a UX convenience.

Code

Layer 1 — proxy·ts
// proxy.ts
export function proxy(request: NextRequest) {
  const session = request.cookies.get('session');
  if (!session && request.nextUrl.pathname.startsWith('/app')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}
Layer 2 — layout·tsx
// app/(app)/layout.tsx
import { auth } from '@/auth';
import { redirect } from 'next/navigation';

export default async function AppLayout({ children }: { children: React.ReactNode }) {
  const session = await auth();
  if (!session) redirect('/login');
  return (
    <>
      <AppNav user={session.user} />
      {children}
    </>
  );
}
Layer 3 — every action verifies·ts
'use server';
import { auth } from '@/auth';

export async function deleteAccount() {
  const session = await auth();
  if (!session) throw new Error('Unauthorized');
  await db.user.delete({ where: { id: session.user.id } });
}

External links

Exercise

Pick a sensitive action in your app (delete account, transfer money, etc.). Add explicit auth checks at proxy, layout, and action level. Try to bypass each layer and confirm the deepest one stops you.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.