Implementierung von Authentifizierung in Next.js

Das Verständnis von Authentifizierung ist entscheidend für den Schutz der Daten Ihrer Anwendung. Diese Seite führt Sie durch die React- und Next.js-Funktionen, die für die Implementierung von Authentifizierung verwendet werden 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 Authentifizierungsablauf mit React- und Next.js-Funktionen:

Diagramm des Authentifizierungsablaufs 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

Hier sind die Schritte zur Implementierung eines Anmelde- und/oder Registrierungsformulars:

  1. Der Benutzer übermittelt seine Anmeldedaten über ein Formular.
  2. Das Formular sendet eine Anfrage, die von einer API-Route verarbeitet wird.
  3. Nach erfolgreicher Überprüfung wird der Prozess abgeschlossen, was die erfolgreiche Authentifizierung des Benutzers anzeigt.
  4. Wenn die Überprüfung fehlschlägt, wird eine Fehlermeldung angezeigt.

Betrachten Sie ein Anmeldeformular, in dem Benutzer ihre Anmeldedaten eingeben können:

import { FormEvent } from 'react'
import { useRouter } from 'next/router'

export default function LoginPage() {
  const router = useRouter()

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()

    const formData = new FormData(event.currentTarget)
    const email = formData.get('email')
    const password = formData.get('password')

    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    })

    if (response.ok) {
      router.push('/profile')
    } else {
      // Fehler behandeln
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Passwort" required />
      <button type="submit">Anmelden</button>
    </form>
  )
}
import { FormEvent } from 'react'
import { useRouter } from 'next/router'

export default function LoginPage() {
  const router = useRouter()

  async function handleSubmit(event) {
    event.preventDefault()

    const formData = new FormData(event.currentTarget)
    const email = formData.get('email')
    const password = formData.get('password')

    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    })

    if (response.ok) {
      router.push('/profile')
    } else {
      // Fehler behandeln
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Passwort" required />
      <button type="submit">Anmelden</button>
    </form>
  )
}

Das obige Formular hat zwei Eingabefelder für die Erfassung der E-Mail und des Passworts des Benutzers. Bei der Übermittlung wird eine Funktion ausgelöst, die eine POST-Anfrage an eine API-Route (/api/auth/login) sendet.

Sie können dann die API Ihres Authentifizierungsanbieters in der API-Route aufrufen, um die Authentifizierung zu behandeln:

import type { NextApiRequest, NextApiResponse } from 'next'
import { signIn } from '@/auth'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const { email, password } = req.body
    await signIn('credentials', { email, password })

    res.status(200).json({ success: true })
  } catch (error) {
    if (error.type === 'CredentialsSignin') {
      res.status(401).json({ error: 'Ungültige Anmeldedaten.' })
    } else {
      res.status(500).json({ error: 'Etwas ist schiefgelaufen.' })
    }
  }
}
import { signIn } from '@/auth'

export default async function handler(req, res) {
  try {
    const { email, password } = req.body
    await signIn('credentials', { email, password })

    res.status(200).json({ success: true })
  } catch (error) {
    if (error.type === 'CredentialsSignin') {
      res.status(401).json({ error: 'Ungültige Anmeldedaten.' })
    } else {
      res.status(500).json({ error: 'Etwas ist schiefgelaufen.' })
    }
  }
}

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 Tokens.

Es gibt zwei Arten von Sitzungen:

  1. Zustandslos (Stateless): Die 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): Die 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 komplexer sein und mehr Serverressourcen verbrauchen.

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

Zustandslose Sitzungen (Stateless Sessions)

Setzen und Löschen von Cookies

Sie können API-Routen verwenden, um die Sitzung als Cookie auf dem Server zu setzen:

import { serialize } from 'cookie'
import type { NextApiRequest, NextApiResponse } from 'next'
import { encrypt } from '@/app/lib/session'

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const sessionData = req.body
  const encryptedSessionData = encrypt(sessionData)

  const cookie = serialize('session', encryptedSessionData, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 24 * 7, // Eine Woche
    path: '/',
  })
  res.setHeader('Set-Cookie', cookie)
  res.status(200).json({ message: 'Successfully set cookie!' })
}
import { serialize } from 'cookie'
import { encrypt } from '@/app/lib/session'

export default function handler(req, res) {
  const sessionData = req.body
  const encryptedSessionData = encrypt(sessionData)

  const cookie = serialize('session', encryptedSessionData, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 24 * 7, // Eine Woche
    path: '/',
  })
  res.setHeader('Set-Cookie', cookie)
  res.status(200).json({ message: 'Successfully set cookie!' })
}

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

Erstellen einer Sitzung auf dem Server:

import db from '../../lib/db'
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const user = req.body
    const sessionId = generateSessionId()
    await db.insertSession({
      sessionId,
      userId: user.id,
      createdAt: new Date(),
    })

    res.status(200).json({ sessionId })
  } catch (error) {
    res.status(500).json({ error: 'Internal Server Error' })
  }
}
import db from '../../lib/db'

export default async function handler(req, res) {
  try {
    const user = req.body
    const sessionId = generateSessionId()
    await db.insertSession({
      sessionId,
      userId: user.id,
      createdAt: new Date(),
    })

    res.status(200).json({ sessionId })
  } catch (error) {
    res.status(500).json({ error: 'Internal Server Error' })
  }
}

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: Überprüft, ob der Benutzer berechtigt ist, auf eine Route zuzugreifen 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: Überprüft, ob der Benutzer berechtigt ist, auf eine Route zuzugreifen 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:

  • Zur Durchführung optimistischer Prüfungen. Da Middleware auf jeder Route läuft, ist sie eine gute Möglichkeit, Umleitungslogik zu zentralisieren und nicht autorisierte Benutzer vorzufiltern.
  • Zum Schutz statischer Routen, die Daten zwischen Benutzern teilen (z.B. Inhalte hinter einer Paywall).

Da Middleware jedoch auf jeder Route läuft, einschließlich vorab geladener Routen, ist es wichtig, nur die Sitzung 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 angeben
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 laufen sollte
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
import { NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'

// 1. Geschützte und öffentliche Routen angeben
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)

  // 5. Zu /login umleiten, wenn der Benutzer nicht authentifiziert ist
  if (isProtectedRoute && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }

  // 6. 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 laufen sollte
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 Authentifizierungs- und Sitzungsverwaltungsbibliothek kompatibel sind.
  • Sie können die matcher-Eigenschaft in der Middleware verwenden, um anzugeben, auf welchen Routen Middleware laufen soll. Für die Authentifizierung wird jedoch empfohlen, dass Middleware auf allen Routen läuft.

Erstellung einer Data Access Layer (DAL)

Schutz von API-Routen

API-Routen in Next.js sind essenziell für die Handhabung serverseitiger Logik und Datenverwaltung. Es ist entscheidend, diese Routen zu sichern, um sicherzustellen, dass nur autorisierte Benutzer auf bestimmte Funktionalitäten zugreifen können. Dies beinhaltet typischerweise die Überprüfung des Authentifizierungsstatus des Benutzers und seiner rollenbasierten Berechtigungen.

Hier ist ein Beispiel für die Sicherung einer API-Route:

import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const session = await getSession(req)

  // Prüfen, ob der Benutzer authentifiziert ist
  if (!session) {
    res.status(401).json({
      error: 'Benutzer ist nicht authentifiziert',
    })
    return
  }

  // Prüfen, ob der Benutzer die 'admin'-Rolle hat
  if (session.user.role !== 'admin') {
    res.status(401).json({
      error: 'Unautorisierter Zugriff: Benutzer hat keine Admin-Berechtigungen.',
    })
    return
  }

  // Für autorisierte Benutzer mit der Route fortfahren
  // ... Implementierung der API-Route
}
export default async function handler(req, res) {
  const session = await getSession(req)

  // Prüfen, ob der Benutzer authentifiziert ist
  if (!session) {
    res.status(401).json({
      error: 'Benutzer ist nicht authentifiziert',
    })
    return
  }

  // Prüfen, ob der Benutzer die 'admin'-Rolle hat
  if (session.user.role !== 'admin') {
    res.status(401).json({
      error: 'Unautorisierter Zugriff: Benutzer hat keine Admin-Berechtigungen.',
    })
    return
  }

  // Für autorisierte Benutzer mit der Route fortfahren
  // ... Implementierung der API-Route
}

Dieses Beispiel zeigt eine API-Route mit einer zweistufigen Sicherheitsprüfung für Authentifizierung und Autorisierung. Zuerst wird auf eine aktive Sitzung geprüft, und dann wird verifiziert, ob der angemeldete Benutzer ein 'admin' ist. Dieser Ansatz gewährleistet sicheren Zugriff, beschränkt auf authentifizierte und autorisierte Benutzer, und hält eine robuste Sicherheit für die Anfrageverarbeitung aufrecht.

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 sicherer Authentifizierung und Sitzungsverwaltung helfen:

Authentifizierungsbibliotheken

Sitzungsverwaltungsbibliotheken

Weiterführende Literatur

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