Routen schützen: Middleware & authorized

Zugriffsschutz hat in v5 zwei Ebenen: ein deklarativer authorized-Callback entscheidet zentral, wer welche Route sehen darf, und die Middleware setzt das vor dem Rendern durch. Login und Logout selbst löst du am elegantesten mit signIn/signOut als Server Action.

Drei Ebenen des Schutzes: (1) Middleware — blockt ganze Pfade, bevor überhaupt gerendert wird. (2) auth() in der Seite — feingranular pro Komponente. (3) authorized-Callback — die zentrale Regel, die Ebene 1 nutzt.

Der authorized-Callback

Neu in v5: Dieser Callback ist die eine Stelle, an der die Zugriffsregel steht. Die Middleware ruft ihn auf — gibt er false zurück, wird zum Login umgeleitet.

auth.ts
export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [GitHub],
  callbacks: {
    authorized({ request, auth }) {
      const eingeloggt = !!auth?.user;
      const aufDashboard =
        request.nextUrl.pathname.startsWith("/dashboard");

      if (aufDashboard) return eingeloggt; // sonst -> Login
      return true; // alles andere ist offen
    },
  },
});

Die Middleware aktivieren

Eine Datei an der Projekt-Wurzel genügt — sie re-exportiert auth als Middleware. Der matcher grenzt ein, auf welchen Pfaden sie überhaupt läuft.

middleware.ts (Next.js 15)
export { auth as middleware } from "@/auth";

export const config = {
  // statische Assets und _next ausnehmen
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
Next.js 16+: Die Datei heißt dort proxy.ts und der Export auth as proxy. Auf Next.js 15 (wie hier) bleibt es middleware.ts.

Login & Logout als Server Action

Kein Client-JavaScript nötig: Ein form mit einer inline Server Action ruft signIn/signOut direkt auf.

eine Server Component
import { signIn, signOut } from "@/auth";

export function LoginButton() {
  return (
    <form action={async () => {
      "use server";
      await signIn("github");
    }}>
      <button type="submit">Mit GitHub einloggen</button>
    </form>
  );
}

export function LogoutButton() {
  return (
    <form action={async () => {
      "use server";
      await signOut();
    }}>
      <button type="submit">Abmelden</button>
    </form>
  );
}
Warum eigentlich?Warum authorized statt if-Checks in jeder Seite?
Verstreust du if (!session) redirect(...) über zwanzig Seiten, vergisst du es irgendwann einmal — und genau diese Seite leakt. Der authorized-Callback bündelt die Regel an einer Stelle, und die Middleware setzt sie durch, bevor Server-Code läuft. Das ist Defense-in-Depth: ein zentrales Tor plus optionale Feincheck pro Seite, nicht zwanzig einzelne Türen.
Häufiger DenkfehlerMiddleware als einzige Verteidigung
Middleware ist bequem, aber sie ist kein vollständiger Ersatz für Checks am Datenzugriff. Verlasse dich bei sensiblen Aktionen zusätzlich auf auth() direkt in der Server Action bzw. dem Route Handler, der die Daten anfasst. Die offizielle Empfehlung lautet: Middleware für die grobe Umleitung, der Check nah an den Daten für die echte Autorisierung.
Tiefer reinsignIn/signOut: Action vs. Client
Es gibt zwei signIn: das aus @/auth (Server, für Server Actions / Programmatik) und das aus next-auth/react (Client-Hook-Variante für onClick). Im App Router ist die Server-Action-Variante der Default — sie funktioniert ohne Client-JS und leitet den OAuth-Flow serverseitig ein. Mit redirectTo steuerst du, wohin es nach erfolgreichem Login geht.