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:

1. Authentication
Formular mit useActionState client-side:
"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:
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:
- 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.
- 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:
openssl rand -base64 32SESSION_SECRET=your_secret_keyconst secretKey = process.env.SESSION_SECRET;2.2. Sessions verschlüsseln und entschlüsseln
Beispiel mit der Jose -Library:
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:
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:
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:
- 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.
- 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.
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:
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:
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.
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:
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:
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:
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 nullzu 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:
"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:
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:
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.