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 Denkfehler — Middleware 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 rein — signIn/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.