Authentifizierung in Next.js implementieren

Das Verständnis von Authentifizierung ist entscheidend, um die Daten Ihrer Anwendung zu schützen. Diese Seite führt Sie durch die React- und Next.js-Funktionen, die Sie für die Implementierung von Authentifizierung verwenden können.

Bevor Sie beginnen, ist es hilfreich, den Prozess in drei Konzepte zu unterteilen:

  1. Authentifizierung: Überprüft, ob der Benutzer derjenige ist, für den er sich ausgibt. Der Benutzer muss seine Identität mit etwas nachweisen, das er besitzt, wie z.B. einem Benutzernamen und Passwort.
  2. Sitzungsverwaltung: Verfolgt den Authentifizierungsstatus des Benutzers über Anfragen hinweg.
  3. Autorisierung: Entscheidet, auf welche Routen und Daten der Benutzer zugreifen kann.

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

Diagramm des Authentifizierungsflusses mit React- und Next.js-Funktionen

Die Beispiele auf dieser Seite zeigen aus didaktischen Gründen eine grundlegende Authentifizierung mit Benutzername und Passwort. Obwohl Sie eine eigene Authentifizierungslösung implementieren können, empfehlen wir aus Sicherheits- und Einfachheitsgründen die Verwendung einer Authentifizierungsbibliothek. Diese bieten integrierte Lösungen für Authentifizierung, Sitzungsverwaltung und Autorisierung sowie zusätzliche Funktionen wie Social Logins, Multi-Faktor-Authentifizierung und rollenbasierte Zugriffskontrolle. Eine Liste finden Sie im Abschnitt Authentifizierungsbibliotheken.

Authentifizierung

Registrierungs- und Anmeldefunktionalität

Sie können das <form>-Element mit Server Actions von React und useActionState verwenden, um Benutzeranmeldedaten zu erfassen, Formularfelder zu validieren und die API oder Datenbank Ihres Authentifizierungsanbieters 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-/Anmeldefunktionalität:

1. Benutzeranmeldedaten erfassen

Um Benutzeranmeldedaten zu erfassen, erstellen Sie ein Formular, das beim Absenden eine Server Action aufruft. Beispielsweise ein Registrierungsformular, das den Namen, die E-Mail und das Passwort des Benutzers akzeptiert:

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">Email</label>
        <input id="email" name="email" type="email" placeholder="Email" />
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input id="password" name="password" type="password" />
      </div>
      <button type="submit">Sign Up</button>
    </form>
  )
}

2. Formularfelder auf dem Server validieren

Verwenden Sie die Server Action, um die Formularfelder auf dem Server zu validieren. Falls Ihr Authentifizierungsanbieter keine Formularvalidierung bietet, können Sie eine Schema-Validierungsbibliothek wie Zod oder Yup verwenden.

Am Beispiel von Zod können Sie ein Formularschema mit entsprechenden Fehlermeldungen definieren:

import { z } from 'zod'

export const SignupFormSchema = z.object({
  name: z
    .string()
    .min(2, { message: 'Name must be at least 2 characters long.' })
    .trim(),
  email: z.string().email({ message: 'Please enter a valid email.' }).trim(),
  password: z
    .string()
    .min(8, { message: 'Be at least 8 characters long' })
    .regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })
    .regex(/[0-9]/, { message: 'Contain at least one number.' })
    .regex(/[^a-zA-Z0-9]/, {
      message: 'Contain at least one special character.',
    })
    .trim(),
})

export type FormState =
  | {
      errors?: {
        name?: string[]
        email?: string[]
        password?: string[]
      }
      message?: string
    }
  | undefined

Um unnötige Aufrufe an die API oder Datenbank Ihres Authentifizierungsanbieters zu vermeiden, können Sie in der Server Action frühzeitig return aufrufen, falls Formularfelder nicht dem definierten Schema entsprechen.

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 Feldern frühzeitig zurückkehren
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }

  // Anbieter oder DB aufrufen, um Benutzer zu erstellen...
}

Zurück in Ihrer <SignupForm /> können Sie den useActionState-Hook von React verwenden, um Validierungsfehler während des Formularversands anzuzeigen:

'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">Password</label>
        <input id="password" name="password" type="password" />
      </div>
      {state?.errors?.password && (
        <div>
          <p>Password must:</p>
          <ul>
            {state.errors.password.map((error) => (
              <li key={error}>- {error}</li>
            ))}
          </ul>
        </div>
      )}
      <button disabled={pending} type="submit">
        Sign Up
      </button>
    </form>
  )
}

Wissenswert:

  • In React 19 enthält useFormStatus zusätzliche Schlüssel im zurückgegebenen Objekt, wie data, method und action. Falls Sie nicht React 19 verwenden, ist nur der pending-Schlüssel verfügbar.
  • Bevor Sie Daten mutieren, sollten Sie stets sicherstellen, dass ein Benutzer auch autorisiert ist, die Aktion durchzuführen. Siehe Authentifizierung und Autorisierung.

3. Benutzer erstellen oder Anmeldedaten prüfen

Nach der Validierung der Formularfelder können Sie ein neues Benutzerkonto erstellen oder überprüfen, ob der Benutzer existiert, indem Sie die API oder Datenbank Ihres Authentifizierungsanbieters aufrufen.

Fortsetzung des vorherigen Beispiels:

export async function signup(state: FormState, formData: FormData) {
  // 1. Formularfelder validieren
  // ...

  // 2. Daten für die Datenbankeinfügung vorbereiten
  const { name, email, password } = validatedFields.data
  // z.B. Passwort des Benutzers vor der Speicherung hashen
  const hashedPassword = await bcrypt.hash(password, 10)

  // 3. Benutzer in die Datenbank einfügen oder Auth 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: 'Beim Erstellen Ihres Kontos ist ein Fehler aufgetreten.',
    }
  }

  // TODO:
  // 4. Benutzersitzung erstellen
  // 5. Benutzer weiterleiten
}

Nach erfolgreicher Erstellung des Benutzerkontos oder Überprüfung der Anmeldedaten können Sie eine Sitzung erstellen, um den Authentifizierungsstatus des Benutzers zu verwalten. Abhängig von Ihrer Sitzungsverwaltungsstrategie kann die Sitzung in einem Cookie oder einer Datenbank oder beidem gespeichert werden. Fahren Sie mit dem Abschnitt Sitzungsverwaltung fort, um mehr zu erfahren.

Tipps:

  • Das obige Beispiel ist ausführlich, da es die Authentifizierungsschritte zu Schulungszwecken aufschlüsselt. Dies zeigt, dass die Implementierung einer eigenen sicheren Lösung schnell komplex werden kann. Erwägen Sie die Verwendung einer Auth Library, um den Prozess zu vereinfachen.
  • Um die Benutzererfahrung zu verbessern, können Sie nach doppelten E-Mails oder Benutzernamen früher im Registrierungsprozess suchen. Zum Beispiel während der Benutzer einen Benutzernamen eingibt oder das Eingabefeld den Fokus verliert. Dies kann unnötige Formularübermittlungen verhindern und dem Benutzer sofortiges Feedback geben. Sie können Anfragen mit Bibliotheken wie use-debounce entprellen, um die Häufigkeit dieser Prüfungen zu steuern.

Sitzungsverwaltung

Die Sitzungsverwaltung stellt sicher, dass der authentifizierte Zustand des Benutzers über Anfragen hinweg erhalten bleibt. Sie umfasst das Erstellen, Speichern, Aktualisieren und Löschen von Sitzungen oder Token.

Es gibt zwei Arten von Sitzungen:

  1. Zustandslos (Stateless): Sitzungsdaten (oder ein Token) werden in den Cookies des Browsers gespeichert. Der Cookie wird mit jeder Anfrage gesendet, was die Überprüfung der Sitzung auf dem Server ermöglicht. Diese Methode ist einfacher, kann aber weniger sicher sein, wenn sie nicht korrekt implementiert wird.
  2. Datenbank (Database): 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.

Gut zu wissen: Während Sie beide Methoden oder beide verwenden können, empfehlen wir die Verwendung einer Sitzungsverwaltungsbibliothek wie iron-session oder Jose.

Zustandslose Sitzungen (Stateless Sessions)

Um zustandslose Sitzungen zu erstellen und zu verwalten, müssen Sie einige Schritte befolgen:

  1. Generieren Sie einen geheimen Schlüssel, der zum Signieren Ihrer Sitzung verwendet wird, und speichern Sie ihn als Umgebungsvariable.
  2. Schreiben Sie Logik zum Verschlüsseln/Entschlüsseln von Sitzungsdaten mit einer Sitzungsverwaltungsbibliothek.
  3. Verwalten Sie Cookies mit der Next.js cookies API.

Zusätzlich zu den oben genannten Punkten sollten Sie Funktionen hinzufügen, um die Sitzung zu aktualisieren (oder zu erneuern), wenn der Benutzer zur Anwendung zurückkehrt, und die Sitzung zu löschen, wenn der Benutzer sich abmeldet.

Gut zu wissen: Überprüfen Sie, ob Ihre Auth Library Sitzungsverwaltung beinhaltet.

1. Generieren eines geheimen Schlüssels

Es gibt mehrere Möglichkeiten, einen geheimen Schlüssel zum Signieren Ihrer Sitzung zu generieren. Zum Beispiel können Sie den Befehl openssl in Ihrem Terminal verwenden:

terminal
openssl rand -base64 32

Dieser Befehl generiert eine 32-stellige zufällige Zeichenfolge, die Sie als geheimen Schlüssel verwenden und in Ihrer Umgebungsvariablendatei speichern können:

.env
SESSION_SECRET=your_secret_key

Sie können dann auf diesen Schlüssel in Ihrer Sitzungsverwaltungslogik verweisen:

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

2. Verschlüsseln und Entschlüsseln von Sitzungen

Als Nächstes können Sie Ihre bevorzugte Sitzungsverwaltungsbibliothek verwenden, um Sitzungen zu verschlüsseln und zu entschlüsseln. Fortsetzung des vorherigen Beispiels verwenden wir Jose (kompatibel mit der Edge Runtime) und Reacts server-only Paket, um sicherzustellen, dass Ihre Sitzungsverwaltungslogik nur auf dem Server ausgeführt wird.

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('Sitzungsüberprüfung fehlgeschlagen')
  }
}

Tipps:

  • Die Nutzdaten sollten die minimalen, eindeutigen Benutzerdaten enthalten, die in nachfolgenden Anfragen verwendet werden, wie z.B. die Benutzer-ID, Rolle usw. Sie sollten keine personenbezogenen 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, verwenden Sie die Next.js cookies API. Der Cookie sollte auf dem Server gesetzt werden und die empfohlenen Optionen enthalten:

  • HttpOnly: Verhindert den Zugriff auf den Cookie durch clientseitiges JavaScript.
  • Secure: Verwendet https zum Senden des Cookies.
  • SameSite: Gibt an, ob der Cookie mit cross-site-Anfragen gesendet werden kann.
  • Max-Age oder Expires: Löscht den Cookie nach einem bestimmten Zeitraum.
  • Path: Definiert den URL-Pfad für den Cookie.

Weitere Informationen zu diesen Optionen finden Sie unter MDN.

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 })
  const cookieStore = await cookies()

  cookieStore.set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: 'lax',
    path: '/',
  })
}

Zurück in Ihrer Server-Aktion können Sie die Funktion createSession() aufrufen und die redirect() API verwenden, um den Benutzer auf die entsprechende Seite weiterzuleiten:

import { createSession } from '@/app/lib/session'

export async function signup(state: FormState, formData: FormData) {
  // Vorherige Schritte:
  // 1. Formularfelder validieren
  // 2. Daten für die Datenbankeinfügung 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 Manipulationen zu verhindern.
  • 🎥 Ansehen: Erfahren Sie mehr über zustandslose Sitzungen und Authentifizierung mit Next.js → YouTube (11 Minuten).

Aktualisieren (oder Erneuern) von Sitzungen

Sie können auch die Ablaufzeit der Sitzung verlängern. Dies ist nützlich, um den Benutzer angemeldet zu halten, nachdem er wieder auf die Anwendung zugreift. Zum Beispiel:

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: '/',
  })
}

Tipp: Überprüfen Sie, ob Ihre Auth Library Refresh-Tokens unterstützt, die zur Verlängerung der Benutzersitzung verwendet werden können.

Löschen der Sitzung

Um die Sitzung zu löschen, können Sie das Cookie entfernen:

import 'server-only'
import { cookies } from 'next/headers'

export async function deleteSession() {
  const cookieStore = await cookies()
  cookieStore.delete('session')
}

Anschließend können Sie die Funktion deleteSession() in Ihrer Anwendung wiederverwenden, beispielsweise beim Abmelden:

import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'

export async function logout() {
  await deleteSession()
  redirect('/login')
}

Datenbank-Sitzungen

Um Datenbank-Sitzungen zu erstellen und zu verwalten, müssen Sie folgende Schritte durchführen:

  1. Erstellen Sie eine Tabelle in Ihrer Datenbank zur Speicherung von Sitzungen und Daten (oder prüfen Sie, ob Ihre Authentifizierungsbibliothek dies übernimmt).
  2. Implementieren Sie Funktionen zum Einfügen, Aktualisieren und Löschen von Sitzungen
  3. Verschlüsseln Sie die Sitzungs-ID, bevor Sie sie im Browser des Benutzers speichern, und stellen Sie sicher, dass Datenbank und Cookie synchron bleiben (dies ist optional, wird aber für optimistische Authentifizierungsprüfungen in Middleware empfohlen).

Beispiel:

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. 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 Zugriff können Sie Server-Caching für die Lebensdauer der Sitzung in Betracht ziehen. Sie können die Sitzungsdaten auch in Ihrer primären Datenbank belassen und Datenanfragen kombinieren, um die Anzahl der Abfragen zu reduzieren.
  • Sie können Datenbank-Sitzungen für erweiterte Anwendungsfälle verwenden, z.B. um den Zeitpunkt der letzten Anmeldung eines Benutzers zu verfolgen, die Anzahl aktiver Geräte oder um Benutzern die Möglichkeit zu geben, sich von allen Geräten abzumelden.

Nach der Implementierung der Sitzungsverwaltung müssen Sie Autorisierungslogik hinzufügen, um zu steuern, was Benutzer in Ihrer Anwendung aufrufen und tun können. Fahren Sie mit dem Abschnitt Autorisierung fort, um mehr zu erfahren.

Autorisierung

Sobald ein Benutzer authentifiziert ist und eine Sitzung erstellt wurde, können Sie die Autorisierung implementieren, um zu steuern, was der Benutzer in Ihrer Anwendung aufrufen und tun kann.

Es gibt zwei Haupttypen von Autorisierungsprüfungen:

  1. Optimistisch: Prüft, ob der Benutzer berechtigt ist, eine Route aufzurufen oder eine Aktion auszuführen, indem die im Cookie gespeicherten Sitzungsdaten verwendet werden. Diese Prüfungen sind nützlich für schnelle Operationen, wie das Anzeigen/Ausblenden von UI-Elementen oder das Umleiten von Benutzern basierend auf Berechtigungen oder Rollen.
  2. Sicher: Prüft, ob der Benutzer berechtigt ist, eine Route aufzurufen oder eine Aktion auszuführen, indem die in der Datenbank gespeicherten Sitzungsdaten verwendet werden. 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 Fälle, in denen Sie Middleware verwenden und Benutzer basierend auf Berechtigungen umleiten möchten:

  • Um optimistische Prüfungen durchzuführen. Da Middleware bei jeder Route ausgeführt wird, ist sie eine gute Möglichkeit, Umleitungslogik zu zentralisieren und nicht autorisierte Benutzer vorzufiltern.
  • Um statische Routen zu schützen, die Daten zwischen Benutzern teilen (z.B. Inhalte hinter einer Paywall).

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

Beispiel:

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$).*)'],
}

Während Middleware für erste Prüfungen nützlich sein kann, sollte sie nicht Ihre einzige Verteidigungslinie zum Schutz Ihrer Daten sein. Die meisten Sicherheitsprüfungen sollten so nah wie möglich an Ihrer Datenquelle durchgeführt werden, siehe Datenzugriffsschicht für weitere Informationen.

Tipps:

  • In Middleware können Sie Cookies auch mit req.cookies.get('session').value lesen.
  • Middleware verwendet die Edge Runtime, prüfen Sie, ob Ihre Authentifizierungsbibliothek und Ihre Sitzungsverwaltungsbibliothek kompatibel sind.
  • Sie können die Eigenschaft matcher in der Middleware verwenden, um festzulegen, auf welchen Routen Middleware ausgeführt werden soll. Für die Authentifizierung wird jedoch empfohlen, Middleware auf allen Routen auszuführen.

Erstellen einer Datenzugriffsschicht (DAL)

Wir empfehlen die Erstellung einer DAL, um Ihre Datenanfragen und Autorisierungslogik zu zentralisieren.

Die DAL sollte eine Funktion enthalten, die die Sitzung des Benutzers während der Interaktion mit Ihrer Anwendung überprüft. Mindestens sollte die Funktion prüfen, ob die Sitzung gültig ist, und dann den Benutzer umleiten oder die für weitere Anfragen benötigten Benutzerinformationen zurückgeben.

Erstellen Sie beispielsweise eine separate Datei für Ihre DAL, die eine Funktion verifySession() enthält. Verwenden Sie dann die cache-API von React, um den Rückgabewert der Funktion während eines React-Renderdurchlaufs zwischenzuspeichern:

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 }
})

Sie können dann die Funktion verifySession() in Ihren Datenanfragen, Server-Aktionen und Route Handlern aufrufen:

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 Benutzerobjekt
      columns: {
        id: true,
        name: true,
        email: true,
      },
    })

    const user = data[0]

    return user
  } catch (error) {
    console.log('Failed to fetch user')
    return null
  }
})

Tipp:

  • Eine DAL kann zum Schutz von Daten verwendet werden, die zur Laufzeit abgerufen werden. Für statische Routen, die Daten zwischen Benutzern teilen, werden die Daten jedoch zum Build-Zeitpunkt und nicht zur Laufzeit abgerufen. Verwenden Sie Middleware, um statische Routen zu schützen.
  • Für sichere Prüfungen können Sie überprüfen, ob die Sitzung gültig ist, indem Sie die Sitzungs-ID mit Ihrer Datenbank vergleichen. Verwenden Sie die cache-Funktion von React, um unnötige doppelte Anfragen an die Datenbank während eines Renderdurchlaufs zu vermeiden.
  • Sie können verwandte Datenanfragen in einer JavaScript-Klasse konsolidieren, die verifySession() vor allen Methoden ausführt.

Verwendung von Data Transfer Objects (DTOs)

Beim Abrufen von Daten wird empfohlen, nur die notwendigen Daten zurückzugeben, die in Ihrer Anwendung verwendet werden, und nicht ganze Objekte. Wenn Sie beispielsweise Benutzerdaten abrufen, sollten Sie nur die Benutzer-ID und den Namen zurückgeben, anstatt das gesamte Benutzerobjekt, das Passwörter, Telefonnummern usw. enthalten könnte.

Falls Sie jedoch keine Kontrolle über die zurückgegebene Datenstruktur haben oder in einem Team arbeiten, in dem Sie vermeiden möchten, dass ganze Objekte an den Client übergeben werden, können Sie Strategien wie die Spezifizierung der Felder verwenden, die sicher für den Client freigegeben werden können.

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,
  }
}

Indem Sie Ihre Datenanfragen und Autorisierungslogik in einer DAL zentralisieren und DTOs verwenden, können Sie sicherstellen, dass alle Datenanfragen sicher und konsistent sind, was die Wartung, Überprüfung und Fehlerbehebung bei der Skalierung Ihrer Anwendung erleichtert.

Gut zu wissen:

  • Es gibt verschiedene Möglichkeiten, ein DTO zu definieren, von der Verwendung von toJSON() über individuelle Funktionen wie im obigen Beispiel bis hin zu JS-Klassen. Da dies JavaScript-Muster und keine React- oder Next.js-Funktionen sind, empfehlen wir, einige Recherchen durchzuführen, um das beste Muster für Ihre Anwendung zu finden.
  • Erfahren Sie mehr über Sicherheitsbest Practices in unserem Artikel zur Sicherheit in Next.js.

Server-Komponenten

Authentifizierungsprüfungen in Server-Komponenten sind nützlich für rollenbasierte Zugriffe. Beispielsweise, um Komponenten basierend auf der Rolle des Benutzers bedingt zu rendern:

import { verifySession } from '@/app/lib/dal'

export default 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 Funktion verifySession() aus unserer DAL, um nach 'admin', 'user' und nicht autorisierten Rollen zu prüfen. Dieses Muster stellt sicher, dass jeder Benutzer nur mit den für seine Rolle geeigneten Komponenten interagiert.

Layouts und Authentifizierungsprüfungen

Aufgrund des Partiellen Renderings ist Vorsicht geboten, wenn Prüfungen in Layouts durchgeführt werden, da diese bei der Navigation nicht neu gerendert werden, was bedeutet, dass die Benutzersitzung nicht bei jedem Routenwechsel überprüft wird.

Stattdessen sollten Sie die Prüfungen in der Nähe Ihrer Datenquelle oder der Komponente durchführen, die bedingt gerendert wird.

Betrachten Sie beispielsweise ein gemeinsames Layout, das die Benutzerdaten abruft und das Benutzerbild in einer Navigation anzeigt. Anstatt die Authentifizierungsprüfung im Layout durchzuführen, sollten Sie die Benutzerdaten (getUser()) im Layout abrufen und die Authentifizierungsprüfung in Ihrer DAL durchführen.

Dies stellt sicher, dass die Authentifizierungsprüfung überall in Ihrer Anwendung durchgeführt wird, wo getUser() aufgerufen wird, und verhindert, dass Entwickler vergessen, zu überprüfen, ob der Benutzer berechtigt ist, auf die Daten zuzugreifen.

export default async function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  const user = await getUser();

  return (
    // ...
  )
}

Gut zu wissen:

  • Ein gängiges Muster in SPAs ist, return null in einem Layout oder einer Top-Level-Komponente zurückzugeben, wenn ein Benutzer nicht autorisiert ist. Dieses Muster wird nicht empfohlen, da Next.js-Anwendungen mehrere Einstiegspunkte haben, die nicht verhindern, dass verschachtelte Routensegmente und Server-Aktionen aufgerufen werden können.

Server-Aktionen

Behandeln Sie Server-Aktionen mit den gleichen Sicherheitsüberlegungen wie öffentlich zugängliche API-Endpunkte und überprüfen Sie, ob der Benutzer eine Mutation durchführen darf.

Im folgenden Beispiel überprü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 zurückkehren, wenn der Benutzer nicht berechtigt ist, die Aktion durchzuführen
  if (userRole !== 'admin') {
    return null
  }

  // Für autorisierte Benutzer mit der Aktion fortfahren
}

Route-Handler

Behandeln Sie Route-Handler mit den gleichen Sicherheitsüberlegungen wie öffentlich zugängliche API-Endpunkte und überprüfen Sie, ob der Benutzer berechtigt ist, auf den Route-Handler zuzugreifen.

Beispiel:

import { verifySession } from '@/app/lib/dal'

export async function GET() {
  // Benutzerauthentifizierung und Rollenüberprüfung
  const session = await verifySession()

  // Überprüfen, ob der Benutzer authentifiziert ist
  if (!session) {
    // Benutzer ist nicht authentifiziert
    return new Response(null, { status: 401 })
  }

  // Überprüfen, ob der Benutzer die 'admin'-Rolle hat
  if (session.user.role !== 'admin') {
    // Benutzer ist authentifiziert, hat aber nicht die erforderlichen 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 Sitzung geprüft, und dann wird überprüft, ob der angemeldete Benutzer ein 'admin' ist.

Kontext-Provider

Die Verwendung von Kontext-Providern für die Authentifizierung funktioniert aufgrund der Verschachtelung. Allerdings wird React context in Server-Komponenten nicht unterstützt, was sie nur für Client-Komponenten anwendbar macht.

Dies funktioniert, aber alle untergeordneten Server-Komponenten werden zuerst auf dem Server gerendert und haben keinen Zugriff auf die Sitzungsdaten des Kontext-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 (
    // ...
  );
}
'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-Komponenten benötigt werden (z.B. für clientseitiges Datenabrufen), verwenden Sie Reacts taintUniqueValue API, um zu verhindern, dass sensible Sitzungsdaten dem Client ausgesetzt werden.

Ressourcen

Nachdem Sie nun etwas über Authentifizierung in Next.js gelernt haben, finden Sie hier Next.js-kompatible Bibliotheken und Ressourcen, die Ihnen bei der Implementierung einer sicheren Authentifizierung und Sitzungsverwaltung helfen:

Authentifizierungsbibliotheken

Sitzungsverwaltungsbibliotheken

Weiterführende Literatur

Um weiter über Authentifizierung und Sicherheit zu lernen, lesen Sie die folgenden Ressourcen: