Skip to Content
2026Next.js Auth

Authentifizierung in Next.js

Übersetzt aus dem Englischen mit Claude Opus 4.6.

Datum: Mai 2026
Lesedauer: 12 Minuten


Zusammenfassung von “How to implement authentication in Next.js” .


Überblick

Diese Dokumentation zeigt, wie man eine benutzerdefinierte Authentication mit Benutzername und Passwort implementiert.
Für erhöhte Sicherheit und Einfachheit wird empfohlen, Libraries zu verwenden. Diese bieten integrierte Lösungen für Authentication, Session Management und Authorization sowie zusätzliche Funktionen wie Social Logins, Multi-Faktor-Authentifizierung und rollenbasierte Zugriffskontrolle.

Authentication-Flow mit React und Next.js:

Diagramm, das den Authentication-Flow mit React- und Next.js-Features zeigt

1. Authentication

Formular mit useActionState client-side:

app/ui/signup-form.js
"use client"; import { signup } from "@/app/actions/auth"; import { useActionState } from "react"; export default function SignupForm() { const [state, action, pending] = useActionState(signup, undefined); return ( <form action={action}> <div> <label htmlFor="name">Name</label> <input id="name" name="name" placeholder="Name" /> </div> {state?.errors?.name && <p>{state.errors.name}</p>} <div> <label htmlFor="email">Email</label> <input id="email" name="email" placeholder="Email" /> </div> {state?.errors?.email && <p>{state.errors.email}</p>} <div> <label htmlFor="password">Passwort</label> <input id="password" name="password" type="password" /> </div> {state?.errors?.password && ( <div> <p>Passwort muss:</p> <ul> {state.errors.password.map((error) => ( <li key={error}>- {error}</li> ))} </ul> </div> )} <button disabled={pending} type="submit"> Registrieren </button> </form> ); }

Server Action behandelt die Authentication in einer sicheren Umgebung:

app/actions/auth.js
export async function signup(state, formData) { // 1. Formularfelder validieren (z.B. mit zod) // ... // 2. Daten für das Einfügen in die Datenbank vorbereiten const { name, email, password } = validatedFields.data; const hashedPassword = await bcrypt.hash(password, 10); // z.B. Passwort hashen // 3. Benutzer in die Datenbank einfügen oder eine Library-API aufrufen const data = await db .insert(users) .values({ name, email, password: hashedPassword }) .returning({ id: users.id }); const user = data[0]; if (!user) return { message: "Fehler beim Erstellen des Kontos" }; // 4. Benutzer-Session erstellen // 5. Benutzer weiterleiten }

2. Session Management

Stellt sicher, dass der authentifizierte Zustand des Benutzers über Requests hinweg erhalten bleibt.

Zwei Arten von Sessions:

  1. Stateless: Session-Daten (oder ein Token) werden in den Cookies des Browsers gespeichert. Der Cookie wird bei jedem Request mitgesendet, sodass die Session auf dem Server verifiziert werden kann. Diese Methode ist einfacher, kann aber weniger sicher sein, wenn sie nicht korrekt implementiert wird.
  2. Database: Session-Daten werden in einer Datenbank gespeichert, wobei der Browser des Benutzers nur die verschlüsselte Session-ID erhält. Diese Methode ist sicherer, kann aber komplexer sein und mehr Server-Ressourcen beanspruchen.

2.1. Einen Secret Key generieren

Zum Beispiel kann man openssl verwenden, um einen Secret Key zum Signieren der Session zu generieren:

terminal
openssl rand -base64 32
.env
SESSION_SECRET=your_secret_key
app/lib/session.js
const secretKey = process.env.SESSION_SECRET;

2.2. Sessions verschlüsseln und entschlüsseln

Beispiel mit der Jose -Library:

app/lib/session.ts
import "server-only"; import { SignJWT, jwtVerify } from "jose"; import { SessionPayload } from "@/app/lib/definitions"; const secretKey = process.env.SESSION_SECRET; const encodedKey = new TextEncoder().encode(secretKey); export async function encrypt(payload: SessionPayload) { return new SignJWT(payload) .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() .setExpirationTime("7d") .sign(encodedKey); } export async function decrypt(session: string | undefined = "") { try { const { payload } = await jwtVerify(session, encodedKey, { algorithms: ["HS256"], }); return payload; } catch (error) { console.log("Session-Verifizierung fehlgeschlagen"); } }

Der payload sollte die minimalen, eindeutigen Benutzerdaten enthalten, die in nachfolgenden Requests verwendet werden. Er sollte keine personenbezogenen Daten wie Telefonnummer, E-Mail-Adresse, Kreditkarteninformationen usw. oder sensible Daten wie Passwörter enthalten.

2.3. Cookies

Cookie auf dem Server setzen:

app/lib/session.ts
import "server-only"; import { cookies } from "next/headers"; import { encrypt } from "@/app/lib/session"; export async function createSession(userId: string) { const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); const session = await encrypt({ userId, expiresAt }); const cookieStore = await cookies(); cookieStore.set("session", session, { httpOnly: true, // Verhindert clientseitigen JS-Zugriff secure: true, // Nur über HTTPS senden expires: expiresAt, // Nach Ablauf löschen sameSite: "lax", // Bestimmt, ob Cookie bei Cross-Site-Requests gesendet wird path: "/", // URL-Pfad }); // Weitere Optionen: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies } export async function updateSession() { // Session verlängern durch Entschlüsselung und Aktualisierung der Ablaufzeit } export async function deleteSession() { const cookieStore = await cookies(); cookieStore.delete("session"); }

createSession() innerhalb der Server Action aufrufen und den Benutzer weiterleiten:

app/actions/auth.ts
import { createSession, deleteSession } from "@/app/lib/session"; export async function signup(state: FormState, formData: FormData) { // Vorherige Schritte: // 1. Formularfelder validieren (z.B. mit zod) // 2. Daten für das Einfügen in die Datenbank vorbereiten // 3. Benutzer in die Datenbank einfügen oder eine Library-API aufrufen // Aktuelle Schritte: // 4. Benutzer-Session erstellen await createSession(user.id); // 5. Benutzer weiterleiten redirect("/profile"); } export async function logout() { await deleteSession(); redirect("/login"); }

3. Authorization

Steuert, worauf der Benutzer innerhalb der Anwendung zugreifen und was er tun kann.

Zwei Hauptarten von Authorization Checks:

  1. Optimistic: Prüft, ob der Benutzer berechtigt ist, auf eine Route zuzugreifen oder eine Aktion auszuführen, anhand der im Cookie gespeicherten Session-Daten. Diese Prüfungen sind nützlich für schnelle Operationen, wie das Ein-/Ausblenden von UI-Elementen oder das Weiterleiten von Benutzern basierend auf Berechtigungen oder Rollen.
  2. Secure: Prüft, ob der Benutzer berechtigt ist, auf eine Route zuzugreifen oder eine Aktion auszuführen, anhand der in der Datenbank gespeicherten Session-Daten. Diese Prüfungen sind sicherer und werden für Operationen verwendet, die Zugriff auf sensible Daten oder Aktionen erfordern.

Optimistic Checks mit Proxy (Optional)

Anwendungsfälle für Proxy , um Benutzer basierend auf Berechtigungen weiterzuleiten:

  • Zum Durchführen von Optimistic Checks. Da Proxy bei jeder Route ausgeführt wird, ist es eine gute Möglichkeit, die Redirect-Logik zu zentralisieren und nicht autorisierte Benutzer vorab zu filtern.
  • Zum Schützen statischer Routen, die Daten zwischen Benutzern teilen (z.B. Inhalte hinter einer Paywall).

Da Proxy jedoch bei jeder Route ausgeführt wird, einschliesslich prefetched  Routen, ist es wichtig, nur die Session aus dem Cookie zu lesen (Optimistic Checks) und Datenbankabfragen zu vermeiden, um Performance-Probleme zu verhindern.

proxy.ts
import { NextRequest, NextResponse } from "next/server"; import { decrypt } from "@/app/lib/session"; import { cookies } from "next/headers"; // 1. Geschützte und öffentliche Routen definieren const protectedRoutes = ["/dashboard"]; const publicRoutes = ["/login", "/signup", "/"]; export default async function proxy(req: NextRequest) { // 2. Prüfen, ob die aktuelle Route geschützt oder öffentlich ist const path = req.nextUrl.pathname; const isProtectedRoute = protectedRoutes.includes(path); const isPublicRoute = publicRoutes.includes(path); // 3. Session aus dem Cookie entschlüsseln const cookie = (await cookies()).get("session")?.value; const session = await decrypt(cookie); // 4. Zu /login weiterleiten, wenn der Benutzer nicht authentifiziert ist if (isProtectedRoute && !session?.userId) { return NextResponse.redirect(new URL("/login", req.nextUrl)); } // 5. Zu /dashboard weiterleiten, wenn der Benutzer authentifiziert ist if ( isPublicRoute && session?.userId && !req.nextUrl.pathname.startsWith("/dashboard") ) { return NextResponse.redirect(new URL("/dashboard", req.nextUrl)); } return NextResponse.next(); } // Routen, bei denen Proxy nicht ausgeführt werden soll export const config = { matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"], };

Obwohl Proxy für erste Prüfungen nützlich sein kann, sollte es nicht die einzige Verteidigungslinie zum Schutz der Daten sein. Die Mehrheit der Sicherheitsprüfungen sollte so nah wie möglich an der Datenquelle durchgeführt werden, siehe Data Access Layer für weitere Informationen.

Data Access Layer (DAL)

Ein DAL wird empfohlen, um Datenanfragen und Authorization-Logik zu zentralisieren.

Der DAL sollte eine Funktion enthalten, die die Session des Benutzers verifiziert, während er mit der Anwendung interagiert. Mindestens sollte die Funktion prüfen, ob die Session gültig ist, und dann weiterleiten oder die Benutzerinformationen zurückgeben, die für weitere Requests benötigt werden.

Erstelle zum Beispiel eine separate Datei für deinen DAL, die eine verifySession()-Funktion enthält. Verwende dann Reacts cache -API, um den Rückgabewert der Funktion während eines React Render Pass zu memoizen:

app/lib/dal.ts
import "server-only"; import { cookies } from "next/headers"; import { decrypt } from "@/app/lib/session"; export const verifySession = cache(async () => { const cookie = (await cookies()).get("session")?.value; const session = await decrypt(cookie); if (!session?.userId) redirect("/login"); return { isAuth: true, userId: session.userId }; });

Anschliessend kann die verifySession()-Funktion in Datenanfragen, Server Actions und Route Handlers aufgerufen werden:

app/lib/dal.ts
export const getUser = cache(async () => { const session = await verifySession(); if (!session) return null; try { const data = await db.query.users.findMany({ where: eq(users.id, session.userId), // Explizit nur die benötigten Spalten zurückgeben, nicht das gesamte User-Objekt columns: { id: true, name: true, email: true }, }); const user = data[0]; return user; } catch (error) { console.log("Benutzer konnte nicht abgerufen werden"); return null; } });

Tipp:

  • Ein DAL kann verwendet werden, um Daten zu schützen, die zur Request-Zeit abgerufen werden. Bei statischen Routen, die Daten zwischen Benutzern teilen, werden die Daten jedoch zur Build-Zeit und nicht zur Request-Zeit abgerufen. Verwende Proxy, um statische Routen zu schützen.
  • Für sichere Prüfungen kann die Gültigkeit der Session durch Vergleich der Session-ID mit der Datenbank überprüft werden. Verwende Reacts cache -Funktion, um unnötige doppelte Anfragen an die Datenbank während eines Render Pass zu vermeiden.
  • Es kann sinnvoll sein, zusammengehörige Datenanfragen in einer JavaScript-Klasse zu konsolidieren, die verifySession() vor allen Methoden ausführt.

Data Transfer Objects (DTO)

Beim Abrufen von Daten wird empfohlen, nur die notwendigen Daten zurückzugeben, die in der Anwendung verwendet werden, und nicht ganze Objekte. Wenn man zum Beispiel Benutzerdaten abruft, sollte man möglicherweise nur die ID und den Namen des Benutzers zurückgeben, anstatt das gesamte Benutzerobjekt, das Passwörter, Telefonnummern usw. enthalten könnte.

Wenn man jedoch keine Kontrolle über die zurückgegebene Datenstruktur hat oder in einem Team arbeitet, in dem man vermeiden möchte, dass ganze Objekte an den Client übergeben werden, können Strategien wie die Festlegung, welche Felder sicher dem Client zugänglich gemacht werden können, verwendet werden.

app/lib/dto.ts
import "server-only"; import { getUser } from "@/app/lib/dal"; function canSeeUsername(viewer: User) { return true; } function canSeePhoneNumber(viewer: User, team: string) { return viewer.isAdmin || team === viewer.team; } export async function getProfileDTO(slug: string) { const data = await db.query.users.findMany({ where: eq(users.slug, slug), // Hier spezifische Spalten zurückgeben }); const user = data[0]; const currentUser = await getUser(user.id); // Oder hier nur das zurückgeben, was für die Abfrage relevant ist return { username: canSeeUsername(currentUser) ? user.username : null, phonenumber: canSeePhoneNumber(currentUser, user.team) ? user.phonenumber : null, }; }

Durch die Zentralisierung von Datenanfragen und Authorization-Logik in einem DAL und die Verwendung von DTOs kann sichergestellt werden, dass alle Datenanfragen sicher und konsistent sind, was die Wartung, Prüfung und das Debugging bei wachsender Anwendung erleichtert.

Gut zu wissen:

  • Es gibt verschiedene Möglichkeiten, ein DTO zu definieren – von der Verwendung von toJSON() über einzelne Funktionen wie im obigen Beispiel bis hin zu JS-Klassen. Da es sich um JavaScript-Patterns handelt und nicht um ein React- oder Next.js-Feature, empfehlen wir, etwas Recherche zu betreiben, um das beste Pattern für die eigene Anwendung zu finden.
  • Mehr über Security Best Practices im Security in Next.js-Artikel .

Server Components

Auth Checks in Server Components  sind nützlich für rollenbasierten Zugriff. Zum Beispiel, um Komponenten basierend auf der Rolle des Benutzers bedingt zu rendern:

app/dashboard/page.tsx
import { verifySession } from "@/app/lib/dal"; export default async function Dashboard() { const session = await verifySession(); const userRole = session?.user?.role; // Angenommen, 'role' ist Teil des Session-Objekts if (userRole === "admin") { return <AdminDashboard />; } else if (userRole === "user") { return <UserDashboard />; } else { redirect("/login"); } }

Im Beispiel verwenden wir die verifySession()-Funktion aus unserem DAL, um die Rollen ‘admin’, ‘user’ und nicht autorisiert zu prüfen. Dieses Pattern stellt sicher, dass jeder Benutzer nur mit den für seine Rolle passenden Komponenten interagiert.

Layouts und Auth Checks

Aufgrund von Partial Rendering  ist bei Prüfungen in Layouts  Vorsicht geboten, da diese bei der Navigation nicht neu gerendert werden, was bedeutet, dass die Benutzer-Session nicht bei jedem Routenwechsel geprüft wird.

Stattdessen sollten die Prüfungen nahe an der Datenquelle oder der Komponente durchgeführt werden, die bedingt gerendert wird.

Betrachte zum Beispiel ein gemeinsames Layout, das Benutzerdaten abruft und das Benutzerbild in einer Navigation anzeigt. Anstatt den Auth Check im Layout durchzuführen, sollten die Benutzerdaten (getUser()) im Layout abgerufen und der Auth Check im DAL durchgeführt werden.

Dies garantiert, dass überall dort, wo getUser() in der Anwendung aufgerufen wird, der Auth Check durchgeführt wird, und verhindert, dass Entwickler vergessen zu prüfen, ob der Benutzer zum Zugriff auf die Daten berechtigt ist.

Auth Checks in Page Components

Zum Beispiel kann auf einer Dashboard-Seite die Benutzer-Session verifiziert und die Benutzerdaten abgerufen werden:

app/dashboard/page.tsx
import { verifySession } from "@/app/lib/dal"; export default async function DashboardPage() { const session = await verifySession(); // Benutzerspezifische Daten aus der Datenbank oder Datenquelle abrufen const user = await getUserData(session.userId); return ( <div> <h1>Willkommen, {user.name}</h1> {/* Dashboard-Inhalt */} </div> ); }

Auth Checks in Leaf Components

Auth Checks können auch in Leaf Components durchgeführt werden, die UI-Elemente basierend auf Benutzerberechtigungen bedingt rendern. Zum Beispiel eine Komponente, die nur Admin-Aktionen anzeigt:

app/ui/admin-actions.tsx
import { verifySession } from "@/app/lib/dal"; export default async function AdminActions() { const session = await verifySession(); const userRole = session?.user?.role; if (userRole !== "admin") return null; return ( <div> <button>Benutzer löschen</button> <button>Einstellungen bearbeiten</button> </div> ); }

Dieses Pattern ermöglicht es, UI-Elemente basierend auf Benutzerberechtigungen ein- oder auszublenden, während sichergestellt wird, dass der Auth Check zur Render-Zeit in jeder Komponente stattfindet.

Gut zu wissen:

  • Ein gängiges Pattern in SPAs ist es, in einem Layout oder einer übergeordneten Komponente return null zu verwenden, wenn ein Benutzer nicht autorisiert ist. Dieses Pattern wird nicht empfohlen, da Next.js-Anwendungen mehrere Einstiegspunkte haben, was den Zugriff auf verschachtelte Route Segments und Server Actions nicht verhindert.
  • Stelle sicher, dass alle Server Actions, die von diesen Komponenten aufgerufen werden, auch eigene Authorization Checks durchführen, da clientseitige UI-Einschränkungen allein für die Sicherheit nicht ausreichend sind.

Server Actions

Server Actions  sollten mit den gleichen Sicherheitsüberlegungen wie öffentlich zugängliche API-Endpunkte behandelt werden, und es sollte geprüft werden, ob der Benutzer eine Mutation durchführen darf.

Im folgenden Beispiel prüfen wir die Rolle des Benutzers, bevor die Aktion fortgesetzt wird:

app/lib/actions.ts
"use server"; import { verifySession } from "@/app/lib/dal"; export async function serverAction(formData: FormData) { const session = await verifySession(); const userRole = session?.user?.role; // Frühzeitig abbrechen, wenn der Benutzer nicht autorisiert ist if (userRole !== "admin") { return null; } // Mit der Aktion für autorisierte Benutzer fortfahren }

Route Handlers

Route Handlers sollten mit den gleichen Sicherheitsüberlegungen wie öffentlich zugängliche API-Endpunkte behandelt werden, und es sollte geprüft werden, ob der Benutzer auf den Route Handler zugreifen darf.

Zum Beispiel:

app/api/route.ts
import { verifySession } from "@/app/lib/dal"; export async function GET() { // Benutzer-Authentication und Rollenverifizierung const session = await verifySession(); // Prüfen, ob der Benutzer authentifiziert ist if (!session) { // Benutzer ist nicht authentifiziert return new Response(null, { status: 401 }); } // Prüfen, ob der Benutzer die Rolle 'admin' hat if (session.user.role !== "admin") { // Benutzer ist authentifiziert, hat aber nicht die richtigen Berechtigungen return new Response(null, { status: 403 }); } // Für autorisierte Benutzer fortfahren }

Das obige Beispiel zeigt einen Route Handler mit einer zweistufigen Sicherheitsprüfung. Zuerst wird auf eine aktive Session geprüft und anschliessend verifiziert, ob der eingeloggte Benutzer ein ‘admin’ ist.

Context Providers

Die Verwendung von Context Providers für Auth funktioniert dank Interleaving. Allerdings wird React context in Server Components nicht unterstützt, wodurch sie nur für Client Components anwendbar sind.

Dies funktioniert, aber alle untergeordneten Server Components werden zuerst auf dem Server gerendert und haben keinen Zugriff auf die Session-Daten des Context Providers:

app/layout.ts
import { ContextProvider } from "auth-lib"; export default function RootLayout({ children }) { return ( <html lang="en"> <body> <ContextProvider>{children}</ContextProvider> </body> </html> ); }
'use client'; import { useSession } from "auth-lib"; export default function Profile() { const { userId } = useSession(); const { data } = useSWR(`/api/user/${userId}`, fetcher) return ( // ... ); }

Wenn Session-Daten in Client Components benötigt werden (z.B. für clientseitiges Data Fetching), kann Reacts taintUniqueValue-API verwendet werden, um zu verhindern, dass sensible Session-Daten dem Client zugänglich gemacht werden.

Libraries

Auth Libraries

Session Management Libraries