Menu

Authentifizierung

Das Verständnis von Authentifizierung ist entscheidend für den Schutz der Daten deiner Anwendung. Diese Seite führt dich durch die React- und Next.js-Funktionen, die für die Implementierung der Authentifizierung verwendet werden.

Bevor du beginnst, hilft es, den Prozess in drei Konzepte zu unterteilen:

  1. Authentifizierung: Überprüft, ob der Benutzer tatsächlich derjenige ist, der er vorgibt zu sein. Der Benutzer muss seine Identität mit etwas nachweisen, das er besitzt, wie zum Beispiel Benutzername und Passwort.
  2. Session-Management: Verfolgt den Authentifizierungsstatus des Benutzers über mehrere Anfragen hinweg.
  3. Autorisierung: Entscheidet, auf welche Routen und Daten der Benutzer zugreifen darf.

Dieses Diagramm zeigt den Authentifizierungsablauf mit React- und Next.js-Funktionen:

Diagramm zeigt den Authentifizierungsablauf mit React- und Next.js-Funktionen

Die Beispiele auf dieser Seite behandeln eine grundlegende Authentifizierung mit Benutzername und Passwort zu Bildungszwecken. Während du eine eigene Authentifizierungslösung implementieren kannst, empfehlen wir für mehr Sicherheit und Einfachheit die Verwendung einer Authentifizierungsbibliothek. Diese bieten integrierte Lösungen für Authentifizierung, Session-Management und Autorisierung sowie zusätzliche Funktionen wie Social Logins, Multi-Faktor-Authentifizierung und rollenbasierte Zugangskontrolle. Eine Liste findest du im Abschnitt Auth-Bibliotheken.

Authentifizierung

Registrierungs- und Login-Funktionalität

Du kannst das <form>-Element mit Reacts Server Actions und useFormState verwenden, um Benutzeranmeldedaten zu erfassen, Formularfelder zu validieren und die API deines Authentifizierungsanbieters oder die Datenbank aufzurufen.

Da Server Actions immer auf dem Server ausgeführt werden, bieten sie eine sichere Umgebung für die Handhabung der Authentifizierungslogik.

Hier sind die Schritte zur Implementierung der Registrierungs-/Login-Funktionalität:

1. Benutzeranmeldedaten erfassen

Um Benutzeranmeldedaten zu erfassen, erstelle ein Formular, das bei der Übermittlung eine Server Action aufruft. Zum Beispiel ein Registrierungsformular, das den Namen, die E-Mail-Adresse und das Passwort des Benutzers akzeptiert:

app/ui/signup-form.tsx
TypeScript
import { signup } from '@/app/actions/auth'
 
export function SignupForm() {
  return (
    <form action={signup}>
      <div>
        <label htmlFor="name">Name</label>
        <input id="name" name="name" placeholder="Name" />
      </div>
      <div>
        <label htmlFor="email">E-Mail</label>
        <input id="email" name="email" type="email" placeholder="E-Mail" />
      </div>
      <div>
        <label htmlFor="password">Passwort</label>
        <input id="password" name="password" type="password" />
      </div>
      <button type="submit">Registrieren</button>
    </form>
  )
}
app/actions/auth.tsx
TypeScript
export async function signup(formData: FormData) {}

2. Formularfelder auf dem Server validieren

Verwende die Server Action, um die Formularfelder auf dem Server zu validieren. Wenn dein Authentifizierungsanbieter keine Formularvalidierung bereitstellt, kannst du eine Schema-Validierungsbibliothek wie Zod oder Yup verwenden.

Mit Zod als Beispiel kannst du ein Formularschema mit passenden Fehlermeldungen definieren:

app/lib/definitions.ts
import { z } from 'zod'
 
export const SignupFormSchema = z.object({
  name: z
    .string()
    .min(2, { message: 'Name muss mindestens 2 Zeichen lang sein.' })
    .trim(),
  email: z.string().email({ message: 'Bitte gib eine gültige E-Mail-Adresse ein.' }).trim(),
  password: z
    .string()
    .min(8, { message: 'Mindestens 8 Zeichen lang' })
    .regex(/[a-zA-Z]/, { message: 'Mindestens einen Buchstaben enthalten.' })
    .regex(/[0-9]/, { message: 'Mindestens eine Zahl enthalten.' })
    .regex(/[^a-zA-Z0-9]/, {
      message: 'Mindestens ein Sonderzeichen enthalten.',
    })
    .trim(),
})
 
export type FormState =
  | {
      errors?: {
        name?: string[]
        email?: string[]
        password?: string[]
      }
      message?: string
    }
  | undefined
app/lib/definitions.js
import { z } from 'zod'
 
export const SignupFormSchema = z.object({
  name: z
    .string()
    .min(2, { message: 'Name muss mindestens 2 Zeichen lang sein.' })
    .trim(),
  email: z.string().email({ message: 'Bitte gib eine gültige E-Mail-Adresse ein.' }).trim(),
  password: z
    .string()
    .min(8, { message: 'Mindestens 8 Zeichen lang' })
    .regex(/[a-zA-Z]/, { message: 'Mindestens einen Buchstaben enthalten.' })
    .regex(/[0-9]/, { message: 'Mindestens eine Zahl enthalten.' })
    .regex(/[^a-zA-Z0-9]/, {
      message: 'Mindestens ein Sonderzeichen enthalten.',
    })
    .trim(),
})

Um unnötige Aufrufe der API deines Authentifizierungsanbieters oder der Datenbank zu vermeiden, kannst du in der Server Action frühzeitig return ausführen, wenn Formularfelder nicht dem definierten Schema entsprechen.

app/actions/auth.ts
import { SignupFormSchema, FormState } from '@/app/lib/definitions'
 
export async function signup(state: FormState, formData: FormData) {
  // Formularfelder validieren
  const validatedFields = SignupFormSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    password: formData.get('password'),
  })
 
  // Bei ungültigen Formularfeldern frühzeitig zurückkehren
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }
 
  // Provider oder DB aufrufen, um Benutzer zu erstellen...
}
app/actions/auth.js
import { SignupFormSchema } from '@/app/lib/definitions'
 
export async function signup(state, formData) {
  // Formularfelder validieren  
  const validatedFields = SignupFormSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    password: formData.get('password'),
  })
 
  // Bei ungültigen Formularfeldern frühzeitig zurückkehren
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }
 
  // Provider oder DB aufrufen, um Benutzer zu erstellen...
}

In deinem <SignupForm /> kannst du Reacts useFormState Hook verwenden, um Validierungsfehler während der Formularübermittlung anzuzeigen:

app/ui/signup-form.tsx
TypeScript
'use client'
 
import { useFormState, useFormStatus } from 'react-dom'
import { signup } from '@/app/actions/auth'
 
export function SignupForm() {
  const [state, action] = useFormState(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">E-Mail</label>
        <input id="email" name="email" placeholder="E-Mail" />
      </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>
      )}
      <SubmitButton />
    </form>
  )
}
 
function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button disabled={pending} type="submit">
      Registrieren
    </button>
  )
}

Hinweis:

  • Diese Beispiele verwenden Reacts useFormState Hook, der mit dem Next.js App Router gebündelt ist. Wenn du React 19 verwendest, nutze stattdessen useActionState. Weitere Informationen findest du in der React-Dokumentation.
  • In React 19 enthält useFormStatus zusätzliche Schlüssel im zurückgegebenen Objekt, wie data, method und action. Wenn du React 19 nicht verwendest, ist nur der pending-Schlüssel verfügbar.
  • In React 19 enthält useActionState auch einen pending-Schlüssel im zurückgegebenen Zustand.
  • Bevor du Daten änderst, solltest du immer sicherstellen, dass ein Benutzer auch autorisiert ist, die Aktion auszuführen. Siehe Authentifizierung und Autorisierung.

3. Benutzer erstellen oder Benutzeranmeldedaten prüfen

Nach der Validierung der Formularfelder kannst du durch Aufrufen der API deines Authentifizierungsanbieters oder der Datenbank ein neues Benutzerkonto erstellen oder prüfen, ob der Benutzer existiert.

Fortsetzung des vorherigen Beispiels:

app/actions/auth.tsx
TypeScript
export async function signup(state: FormState, formData: FormData) {
  // 1. Validate form fields
  // ...
 
  // 2. Prepare data for insertion into database
  const { name, email, password } = validatedFields.data
  // e.g. Hash the user's password before storing it
  const hashedPassword = await bcrypt.hash(password, 10)
 
  // 3. Insert the user into the database or call an Auth Library's API
  const data = await db
    .insert(users)
    .values({
      name,
      email,
      password: hashedPassword,
    })
    .returning({ id: users.id })
 
  const user = data[0]
 
  if (!user) {
    return {
      message: 'An error occurred while creating your account.',
    }
  }
 
  // TODO:
  // 4. Create user session
  // 5. Redirect user
}

Nach erfolgreicher Erstellung des Benutzerkontos oder Überprüfung der Benutzeranmeldedaten kannst du eine Sitzung erstellen, um den Authentifizierungsstatus des Benutzers zu verwalten. Je nach deiner Session-Management-Strategie kann die Sitzung in einem Cookie oder in einer Datenbank oder in beidem gespeichert werden. Im Abschnitt Session-Management erfährst du mehr.

Tipps:

  • Das obige Beispiel ist ausführlich, da es die Authentifizierungsschritte zu Bildungszwecken aufschlüsselt. Dies zeigt, dass die Implementierung einer eigenen sicheren Lösung schnell komplex werden kann. Erwäge die Verwendung einer Auth-Bibliothek, um den Prozess zu vereinfachen.
  • Um die Benutzererfahrung zu verbessern, solltest du doppelte E-Mail-Adressen oder Benutzernamen früher im Registrierungsprozess prüfen. Zum Beispiel während der Benutzer tippt oder das Eingabefeld den Fokus verliert. Dies kann unnötige Formularübermittlungen verhindern und dem Benutzer sofortiges Feedback geben. Du kannst Anfragen mit Bibliotheken wie use-debounce verzögern, um die Häufigkeit dieser Prüfungen zu steuern.

Session-Management

Session-Management stellt sicher, dass der authentifizierte Status des Benutzers über Anfragen hinweg erhalten bleibt. Es umfasst das Erstellen, Speichern, Aktualisieren und Löschen von Sitzungen oder Tokens.

Es gibt zwei Arten von Sitzungen:

  1. Zustandslos: Sitzungsdaten (oder ein Token) werden in den Cookies des Browsers gespeichert. Das Cookie wird mit jeder Anfrage gesendet, sodass die Sitzung auf dem Server überprüft werden kann. Diese Methode ist einfacher, kann aber weniger sicher sein, wenn sie nicht korrekt implementiert wird.
  2. Datenbank: Sitzungsdaten werden in einer Datenbank gespeichert, wobei der Browser des Benutzers nur die verschlüsselte Sitzungs-ID erhält. Diese Methode ist sicherer, kann aber komplex sein und mehr Serverressourcen verbrauchen.

Hinweis: Während du entweder die eine oder die andere Methode oder beide verwenden kannst, empfehlen wir die Verwendung einer Session-Management-Bibliothek wie iron-session oder Jose.

Zustandslose Sitzungen

Um zustandslose Sitzungen zu erstellen und zu verwalten, musst du einige Schritte befolgen:

  1. Generiere einen geheimen Schlüssel, der zum Signieren deiner Sitzung verwendet wird, und speichere ihn als Umgebungsvariable.
  2. Schreibe Logik zum Verschlüsseln/Entschlüsseln von Sitzungsdaten mit einer Session-Management-Bibliothek.
  3. Verwalte Cookies mit der Next.js cookies API.

Zusätzlich zu den oben genannten Punkten solltest du Funktionalität zum Aktualisieren (oder Erneuern) der Sitzung hinzufügen, wenn der Benutzer zur Anwendung zurückkehrt, und die Sitzung löschen, wenn sich der Benutzer abmeldet.

Hinweis: Prüfe, ob deine Auth-Bibliothek Session-Management enthält.

1. Geheimen Schlüssel generieren

Es gibt verschiedene Möglichkeiten, einen geheimen Schlüssel zum Signieren deiner Sitzung zu generieren. Zum Beispiel kannst du den openssl-Befehl in deinem Terminal verwenden:

terminal
openssl rand -base64 32

Dieser Befehl generiert eine 32-Zeichen lange Zufallszeichenfolge, die du als geheimen Schlüssel verwenden und in deiner Umgebungsvariablen-Datei speichern kannst:

.env
SESSION_SECRET=dein_geheimer_schluessel

Dann kannst du diesen Schlüssel in deiner Session-Management-Logik referenzieren:

app/lib/session.js
const secretKey = process.env.SESSION_SECRET

2. Sitzungen verschlüsseln und entschlüsseln

Als nächstes kannst du deine bevorzugte Session-Management-Bibliothek verwenden, um Sitzungen zu verschlüsseln und zu entschlüsseln. In Fortsetzung des vorherigen Beispiels verwenden wir Jose (kompatibel mit der Edge Runtime) und Reacts server-only Paket, um sicherzustellen, dass deine Session-Management-Logik nur auf dem Server ausgeführt wird.

app/lib/session.ts
TypeScript
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('Sitzung konnte nicht verifiziert werden')
  }
}

Tipps:

  • Die Payload sollte die minimalen, eindeutigen Benutzerdaten enthalten, die in nachfolgenden Anfragen verwendet werden, wie die Benutzer-ID, Rolle usw. Sie sollte keine persönlich identifizierbaren Informationen wie Telefonnummer, E-Mail-Adresse, Kreditkarteninformationen usw. oder sensible Daten wie Passwörter enthalten.

3. Cookies setzen (empfohlene Optionen)

Um die Sitzung in einem Cookie zu speichern, verwende die Next.js cookies API. Das Cookie sollte auf dem Server gesetzt werden und die empfohlenen Optionen enthalten:

  • HttpOnly: Verhindert, dass clientseitiges JavaScript auf das Cookie zugreift.
  • Secure: Verwendet https zum Senden des Cookies.
  • SameSite: Legt fest, ob das Cookie mit domänenübergreifenden Anfragen gesendet werden kann.
  • Max-Age oder Expires: Löscht das Cookie nach einer bestimmten Zeit.
  • Path: Definiert den URL-Pfad für das Cookie.

Weitere Informationen zu diesen Optionen findest du auf MDN.

app/lib/session.ts
import 'server-only'
import { cookies } from 'next/headers'
 
export async function createSession(userId: string) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
  const session = await encrypt({ userId, expiresAt })(await cookies()).set(
    'session', 
    session,
    {
      httpOnly: true,
      secure: true,
      expires: expiresAt,
      sameSite: 'lax',
      path: '/',
    }
  )
}
app/lib/session.js
import 'server-only'
import { cookies } from 'next/headers'
 
export async function createSession(userId) {
  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,
    secure: true, 
    expires: expiresAt,
    sameSite: 'lax',
    path: '/',
  })
}

In deiner Server Action kannst du die createSession()-Funktion aufrufen und die redirect()-API verwenden, um den Benutzer zur entsprechenden Seite weiterzuleiten:

app/actions/auth.ts
import { createSession } from '@/app/lib/session'
 
export async function signup(state: FormState, formData: FormData) {
  // Vorherige Schritte:
  // 1. Formularfelder validieren
  // 2. Daten für die Datenbank vorbereiten
  // 3. Benutzer in die Datenbank einfügen oder Library-API aufrufen
 
  // Aktuelle Schritte:
  // 4. Benutzersitzung erstellen
  await createSession(user.id)
  // 5. Benutzer weiterleiten
  redirect('/profile')
}
app/actions/auth.js
import { createSession } from '@/app/lib/session'
 
export async function signup(state, formData) {
  // Vorherige Schritte:
  // 1. Formularfelder validieren
  // 2. Daten für die Datenbank vorbereiten
  // 3. Benutzer in die Datenbank einfügen oder Library-API aufrufen
 
  // Aktuelle Schritte:
  // 4. Benutzersitzung erstellen
  await createSession(user.id)
  // 5. Benutzer weiterleiten
  redirect('/profile')
}

Tipps:

  • Cookies sollten auf dem Server gesetzt werden, um clientseitige Manipulation zu verhindern.
  • 🎥 Erfahre mehr über zustandslose Sitzungen und Authentifizierung mit Next.js → YouTube (11 Minuten).

Sitzungen aktualisieren (oder erneuern)

Du kannst auch die Ablaufzeit der Sitzung verlängern. Dies ist nützlich, um den Benutzer eingeloggt zu halten, nachdem er wieder auf die Anwendung zugreift. Zum Beispiel:

app/lib/session.ts
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
 
export async function updateSession() {
  const session = (await cookies()).get('session')?.value
  const payload = await decrypt(session)
 
  if (!session || !payload) {
    return null
  }
 
  const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
 
  const cookieStore = await cookies()
  cookieStore().set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expires,
    sameSite: 'lax',
    path: '/',
  })
}
app/lib/session.js
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
 
export async function updateSession() {
  const session = (await cookies()).get('session')?.value
  const payload = await decrypt(session)
 
  if (!session || !payload) {
    return null
  }
 
  const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)(await cookies()).set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expires,
    sameSite: 'lax',
    path: '/',
  })
}

Tipp: Prüfe, ob deine Auth-Bibliothek Refresh-Tokens unterstützt, die verwendet werden können, um die Sitzung des Benutzers zu verlängern.

Sitzung löschen

Um die Sitzung zu löschen, kannst du das Cookie löschen:

app/lib/session.ts
import 'server-only'
import { cookies } from 'next/headers'
 
export async function deleteSession() {
  const cookieStore = await cookies()
  cookieStore().delete('session')
}
app/lib/session.js
import 'server-only'
import { cookies } from 'next/headers'
 
export async function deleteSession() {
  const cookieStore = await cookies()
  cookieStore().delete('session')
}

Dann kannst du die deleteSession()-Funktion in deiner Anwendung wiederverwenden, zum Beispiel beim Abmelden:

app/actions/auth.ts
import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'
 
export async function logout() {
  deleteSession()
  redirect('/login')
}
app/actions/auth.js
import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'
 
export async function logout() {
  deleteSession()
  redirect('/login')
}

Datenbank-Sitzungen

Um Datenbank-Sitzungen zu erstellen und zu verwalten, musst du folgende Schritte ausführen:

  1. Erstelle eine Tabelle in deiner Datenbank zur Speicherung von Sitzungen und Daten (oder prüfe, ob deine Auth-Bibliothek dies übernimmt).
  2. Implementiere Funktionalität zum Einfügen, Aktualisieren und Löschen von Sitzungen
  3. Verschlüssele die Sitzungs-ID, bevor du sie im Browser des Benutzers speicherst, und stelle sicher, dass die Datenbank und das Cookie synchron bleiben (dies ist optional, aber empfohlen für optimistische Auth-Prüfungen in Middleware).

Zum Beispiel:

app/lib/session.ts
import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'
 
export async function createSession(id: number) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
 
  // 1. Eine Sitzung in der Datenbank erstellen
  const data = await db
    .insert(sessions)
    .values({
      userId: id,
      expiresAt,
    })
    // Sitzungs-ID zurückgeben
    .returning({ id: sessions.id })
 
  const sessionId = data[0].id
 
  // 2. Sitzungs-ID verschlüsseln
  const session = await encrypt({ sessionId, expiresAt })
 
  // 3. Sitzung in Cookies für optimistische Auth-Prüfungen speichern
  const cookieStore = await cookies()
  cookieStore().set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: 'lax',
    path: '/',
  })
}
app/lib/session.js
import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'
 
export async function createSession(id) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
 
  // 1. Eine Sitzung in der Datenbank erstellen
  const data = await db
    .insert(sessions)
    .values({
      userId: id,
      expiresAt,
    })
    // Sitzungs-ID zurückgeben
    .returning({ id: sessions.id })
 
  const sessionId = data[0].id
 
  // 2. Sitzungs-ID verschlüsseln
  const session = await encrypt({ sessionId, expiresAt })
 
  // 3. Sitzung in Cookies für optimistische Auth-Prüfungen speichern
  const cookieStore = await cookies()
  cookieStore().set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: 'lax',
    path: '/',
  })
}

Tipps:

  • Für schnelleren Datenzugriff solltest du eine Datenbank wie Vercel Redis in Betracht ziehen. Du kannst die Sitzungsdaten aber auch in deiner primären Datenbank behalten und Datenanfragen kombinieren, um die Anzahl der Abfragen zu reduzieren.
  • Du kannst dich für Datenbank-Sitzungen bei fortgeschritteneren Anwendungsfällen entscheiden, wie zum Beispiel der Verfolgung der letzten Anmeldung eines Benutzers oder der Anzahl aktiver Geräte, oder um Benutzern die Möglichkeit zu geben, sich von allen Geräten abzumelden.

Nach der Implementierung des Session-Managements musst du Autorisierungslogik hinzufügen, um zu steuern, auf was Benutzer in deiner Anwendung zugreifen und tun können. Fahre mit dem Abschnitt Autorisierung fort, um mehr zu erfahren.

Autorisierung

Sobald ein Benutzer authentifiziert ist und eine Sitzung erstellt wurde, kannst du Autorisierung implementieren, um zu steuern, worauf der Benutzer in deiner Anwendung zugreifen und was er tun kann.

Es gibt zwei Hauptarten von Autorisierungsprüfungen:

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

Für beide Fälle empfehlen wir:

Optimistische Prüfungen mit Middleware (Optional)

Es gibt einige Fälle, in denen du Middleware verwenden und Benutzer basierend auf Berechtigungen umleiten möchtest:

  • Für optimistische Prüfungen. Da Middleware auf jeder Route ausgeführt wird, ist sie gut geeignet, um Umleitungslogik zu zentralisieren und nicht autorisierte Benutzer vorzufiltern.
  • Zum Schutz statischer Routen, die Daten zwischen Benutzern teilen (z.B. Inhalte hinter einer Bezahlschranke).

Da Middleware jedoch auf jeder Route ausgeführt wird, einschließlich vorgeholter Routen, ist es wichtig, nur die Sitzung aus dem Cookie zu lesen (optimistische Prüfungen) und Datenbankprüfungen zu vermeiden, um Leistungsprobleme zu verhindern.

Zum Beispiel:

middleware.ts
TypeScript
import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
 
// 1. Geschützte und öffentliche Routen festlegen
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
 
export default async function middleware(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. Sitzung aus dem Cookie entschlüsseln
  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)
 
  // 4. Zu /login umleiten, wenn der Benutzer nicht authentifiziert ist
  if (isProtectedRoute && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }
 
  // 5. Zu /dashboard umleiten, 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, auf denen Middleware nicht ausgeführt werden soll
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
middleware.js
import { NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
 
// 1. Geschützte und öffentliche Routen festlegen
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
 
export default async function middleware(req) {
  // 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. Sitzung aus dem Cookie entschlüsseln
  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)
 
  // 4. Zu /login umleiten, wenn der Benutzer nicht authentifiziert ist
  if (isProtectedRoute && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }
 
  // 5. Zu /dashboard umleiten, 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, auf denen Middleware nicht ausgeführt werden soll
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

Während Middleware für erste Prüfungen nützlich sein kann, sollte sie nicht deine einzige Verteidigungslinie beim Schutz deiner Daten sein. Die Mehrheit der Sicherheitsprüfungen sollte so nah wie möglich an deiner Datenquelle durchgeführt werden, siehe Data Access Layer für weitere Informationen.

Tipps:

  • In Middleware kannst du auch Cookies mit req.cookies.get('session').value lesen.
  • Middleware verwendet die Edge Runtime, prüfe ob deine Auth-Bibliothek und Session-Management-Bibliothek kompatibel sind.
  • Du kannst die matcher-Eigenschaft in der Middleware verwenden, um festzulegen, auf welchen Routen Middleware ausgeführt werden soll. Für Auth wird jedoch empfohlen, dass Middleware auf allen Routen ausgeführt wird.

Erstellen eines Data Access Layer (DAL)

Wir empfehlen, einen DAL zu erstellen, um deine Datenanfragen und Autorisierungslogik zu zentralisieren.

Der DAL sollte eine Funktion enthalten, die die Sitzung des Benutzers überprüft, während er mit deiner Anwendung interagiert. Die Funktion sollte mindestens prüfen, ob die Sitzung gültig ist, und dann die Benutzerinformationen zurückgeben oder umleiten, die für weitere Anfragen 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-Durchlaufs zu speichern:

app/lib/dal.ts
TypeScript
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 }
})
app/lib/dal.js
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 }
})

Du kannst dann die verifySession()-Funktion in deinen Datenanfragen, Server Actions, Route Handlers aufrufen:

app/lib/dal.ts
TypeScript
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 statt des ganzen Benutzerobjekts
      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 zum Anfragezeitpunkt abgerufen werden. Für statische Routen, die Daten zwischen Benutzern teilen, werden die Daten jedoch zur Build-Zeit und nicht zur Anfrage-Zeit abgerufen. Verwende Middleware, um statische Routen zu schützen.
  • Für sichere Prüfungen kannst du überprüfen, ob die Sitzung gültig ist, indem du die Sitzungs-ID mit deiner Datenbank vergleichst. Verwende Reacts cache-Funktion, um unnötige doppelte Anfragen an die Datenbank während eines Render-Durchlaufs zu vermeiden.
  • Du möchtest möglicherweise verwandte Datenanfragen in einer JavaScript-Klasse zusammenfassen, die verifySession() vor allen Methoden ausführt.

Using Data Transfer Objects (DTO)

Beim Abrufen von Daten wird empfohlen, nur die notwendigen Daten zurückzugeben, die in deiner Anwendung verwendet werden, und nicht ganze Objekte. Wenn du beispielsweise Benutzerdaten abrufst, solltest du möglicherweise nur die Benutzer-ID und den Namen zurückgeben, anstatt des gesamten Benutzerobjekts, das Passwörter, Telefonnummern usw. enthalten könnte.

Wenn du jedoch keine Kontrolle über die zurückgegebene Datenstruktur hast oder in einem Team arbeitest, in dem du vermeiden möchtest, dass ganze Objekte an den Client übergeben werden, kannst du Strategien wie das Festlegen von Feldern verwenden, die sicher an den Client weitergegeben werden können.

app/lib/dto.ts
TypeScript
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 spezifisch ist
  return {
    username: canSeeUsername(currentUser) ? user.username : null,
    phonenumber: canSeePhoneNumber(currentUser, user.team)
      ? user.phonenumber
      : null,
  }
}
app/lib/dto.js
import 'server-only'
import { getUser } from '@/app/lib/dal'
 
function canSeeUsername(viewer) {
  return true
}
 
function canSeePhoneNumber(viewer, team) {
  return viewer.isAdmin || team === viewer.team
}
 
export async function getProfileDTO(slug) {
  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 spezifisch ist
  return {
    username: canSeeUsername(currentUser) ? user.username : null,
    phonenumber: canSeePhoneNumber(currentUser, user.team)
      ? user.phonenumber
      : null,
  }
}

Durch die Zentralisierung deiner Datenanfragen und Autorisierungslogik in einem DAL und die Verwendung von DTOs kannst du sicherstellen, dass alle Datenanfragen sicher und konsistent sind, was die Wartung, Prüfung und Fehlersuche erleichtert, wenn deine Anwendung wächst.

Hinweis:

  • 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 dies JavaScript-Muster und keine React- oder Next.js-Funktionen sind, empfehlen wir dir, nach dem besten Muster für deine Anwendung zu suchen.
  • Erfahre mehr über Sicherheitsbestpraktiken in unserem Artikel Security in Next.js.

Server Components

Auth-Prüfungen in Server Components sind nützlich für rollenbasierte Zugriffssteuerung. Zum Beispiel, um Komponenten basierend auf der Benutzerrolle bedingt zu rendern:

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

In diesem Beispiel verwenden wir die verifySession()-Funktion aus unserem DAL, um 'admin', 'user' und nicht autorisierte Rollen zu prüfen. Dieses Muster stellt sicher, dass jeder Benutzer nur mit Komponenten interagiert, die seiner Rolle entsprechen.

Layouts und Auth-Prüfungen

Aufgrund des Partial Rendering sei vorsichtig bei Prüfungen in Layouts, da diese bei der Navigation nicht neu gerendert werden, was bedeutet, dass die Benutzersitzung nicht bei jedem Routenwechsel überprüft wird.

Stattdessen solltest du die Prüfungen nahe an deiner Datenquelle oder der Komponente durchführen, die bedingt gerendert wird.

Betrachte zum Beispiel ein gemeinsames Layout, das die Benutzerdaten abruft und das Benutzerbild in einer Navigation anzeigt. Anstatt die Auth-Prüfung im Layout durchzuführen, solltest du die Benutzerdaten (getUser()) im Layout abrufen und die Auth-Prüfung in deinem DAL durchführen.

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

app/layout.tsx
TypeScript
export default async function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  const user = await getUser();
 
  return (
    // ...
  )
}
app/lib/dal.ts
export const getUser = cache(async () => {
  const session = await verifySession()
  if (!session) return null
 
  // Benutzer-ID aus Sitzung holen und Daten abrufen
})
app/lib/dal.js
export const getUser = cache(async () => {
  const session = await verifySession()
  if (!session) return null
 
  // Benutzer-ID aus Sitzung holen und Daten abrufen
})

Hinweis:

  • Ein häufiges Muster in SPAs ist es, in einem Layout oder einer Top-Level-Komponente return null zurückzugeben, wenn ein Benutzer nicht autorisiert ist. Dieses Muster wird nicht empfohlen, da Next.js-Anwendungen mehrere Einstiegspunkte haben, was nicht verhindert, dass auf verschachtelte Routensegmente und Server Actions zugegriffen wird.

Server Actions

Server Actions sollten mit den gleichen Sicherheitsüberlegungen wie öffentlich zugängliche API-Endpunkte behandelt werden. Überprüfe, ob der Benutzer berechtigt ist, eine Mutation durchzuführen.

Im folgenden Beispiel prüfen wir die Benutzerrolle, bevor wir die Aktion fortsetzen lassen:

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 zurückkehren, wenn der Benutzer nicht zur Durchführung der Aktion berechtigt ist
  if (userRole !== 'admin') {
    return null
  }
 
  // Mit der Aktion für autorisierte Benutzer fortfahren
}
app/lib/actions.js
'use server'
import { verifySession } from '@/app/lib/dal'
 
export async function serverAction() {
  const session = await verifySession()
  const userRole = session.user.role
 
  // Frühzeitig zurückkehren, wenn der Benutzer nicht zur Durchführung der Aktion berechtigt ist
  if (userRole !== 'admin') {
    return null
  }
 
  // Mit der Aktion für autorisierte Benutzer fortfahren
}

Route Handlers

Route Handler sollten mit den gleichen Sicherheitsüberlegungen wie öffentlich zugängliche API-Endpunkte behandelt werden. Überprüfe, ob der Benutzer berechtigt ist, auf den Route Handler zuzugreifen.

Zum Beispiel:

app/api/route.ts
import { verifySession } from '@/app/lib/dal'
 
export async function GET() {
  // Benutzerauthentifizierung und Rollenüberprüfung
  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 'admin'-Rolle 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
}
app/api/route.js
import { verifySession } from '@/app/lib/dal'
 
export async function GET() {
  // Benutzerauthentifizierung und Rollenüberprüfung
  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 'admin'-Rolle 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 demonstriert einen Route Handler mit einer zweistufigen Sicherheitsprüfung. Es wird zuerst auf eine aktive Sitzung geprüft und dann verifiziert, ob der angemeldete Benutzer ein 'admin' ist.

Context Provider

Die Verwendung von Context Providern für Auth funktioniert aufgrund von 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 Sitzungsdaten des Context Providers:

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

Wenn Sitzungsdaten in Client Components benötigt werden (z.B. für clientseitiges Datenabrufen), verwende Reacts taintUniqueValue API, um zu verhindern, dass sensible Sitzungsdaten dem Client zugänglich gemacht werden.

Ressourcen

Nachdem du über Authentifizierung in Next.js gelernt hast, hier sind Next.js-kompatible Bibliotheken und Ressourcen, die dir bei der Implementierung sicherer Authentifizierung und Session-Management helfen:

Auth-Bibliotheken

Session-Management-Bibliotheken

Weiterführende Lektüre

Um mehr über Authentifizierung und Sicherheit zu lernen, sieh dir die folgenden Ressourcen an: