Design wechseln

Next.js-Routenschutz und Berechtigungskontrolle: Middleware und mehrschichtige Absicherung – vollständiger Leitfaden

Easton editorial illustration: cache waterfall instrument

Letzten Monat bei einem Sicherheitsaudit für einen Kunden sah der Prüfer meinen Berechtigungscode und fragte: „Das war’s? Nur eine Middleware-Abfrage?”

Ich war zunächst überzeugt. Middleware ist doch genau dafür da, oder? Nicht eingeloggte Benutzer werden zur Login-Seite umgeleitet, eingeloggte durchgelassen. Sieht wasserdicht aus.

Der Prüfer öffnete die Browser-Entwicklertools und zeigte es mir: Frontend-Routing umgehen, die Backend-API direkt mit Postman aufrufen – und die eigentlich geschützten Daten waren da.

Das war ein Weckruf.

Erst nach weiterer Recherche wurde klar: Next.js-Berechtigungskontrolle lässt sich nie allein mit Middleware lösen. Sie brauchen eine vollständige mehrschichtige Absicherungsarchitektur.

In diesem Artikel geht es um: Wie setzt man Middleware richtig ein? Was ist die Beziehung zu getServerSession? Wie gestaltet man ein RBAC-System für Admin-Backends? Und welche Code-Vorlagen können Sie direkt übernehmen?

Wenn Sie gerade Next.js-Berechtigungskontrolle implementieren oder Middleware und getServerSession kombinieren wollen, sollte dieser Artikel weiterhelfen.

Warum Berechtigungskontrolle nicht nur auf Middleware basieren darf

Was Middleware eigentlich macht

Zuerst die Positionierung von Middleware.

Sie läuft auf der Edge Runtime und ist die erste Schicht, die eingehende Requests erreicht. Hier können Sie Grobfilter setzen: Ist der Benutzer eingeloggt? Admin oder normaler Benutzer? Durchlassen oder umleiten?

Schnell und früh – klingt ideal für Berechtigungskontrolle, oder?

Stimmt. Aber allein reicht es nicht.

Ein realer Schwachstellenfall

Im Januar 2025 veröffentlichten Sicherheitsforscher CVE-2025-29927. Kurz gesagt: Angreifer können mit einem speziellen x-middleware-subrequest-Header in der Request die Middleware-Prüfung umgehen.

Alle Logik in der Middleware wird bei diesem Angriff wirkungslos.

Das ist kein Einzelfall. Als äußerste Verteidigungslinie kann Middleware umgangen, falsch konfiguriert oder durch Edge-Runtime-Einschränkungen bei komplexen Berechtigungsentscheidungen überfordert sein.

Die Next.js-Dokumentation betont ausdrücklich: „While Middleware can be useful for initial checks, it should not be your only line of defense.”

Übersetzt: Setzen Sie nicht alles auf Middleware.

Frontend blockiert – und das Backend?

Ich habe einmal ein Admin-Backend gebaut. In der Middleware stand eine Login-Prüfung: Nicht eingeloggte Benutzer, die /admin aufrufen, werden zur Login-Seite umgeleitet.

Sieht sicher aus. Menüklicks und Routenwechsel laufen durch die Middleware.

Wo lag das Problem? Die API war ungeschützt.

Ein Tester schaute im Network-Panel und fand: Der Endpoint zum Löschen von Benutzern war POST /api/users/delete. Er rief ihn direkt mit curl auf – mit beliebigen Parametern.

Gelöscht.

In der API Route gab es keine Berechtigungsprüfung. Ich hatte nur Frontend-Routen in der Middleware abgesichert und nicht bedacht, dass jemand APIs direkt aufrufen kann.

Das ist das Problem reiner Middleware-Abhängigkeit: Sie sichert die Frontend-Tür, aber nicht das Backend-Fenster.

Die offizielle Next.js-Empfehlung

In der Dokumentation steht das „Proximity Principle” – Berechtigungsprüfungen so nah wie möglich an den Daten platzieren.

Was heißt das?

Daten liegen in der Datenbank. Wer Daten schützen will, prüft Berechtigungen vor dem Datenzugriff – nicht auf Routing- oder Seitenebene, sondern auf Datenebene.

Das heißt nicht, dass Middleware nutzlos ist. Sie kann die erste Schicht sein: Umleitung nicht eingeloggter Benutzer, offensichtlich unberechtigte Rollen ablehnen. Aber Sie brauchen zusätzlich:

  • Prüfungen in Server Components: Vor dem Rendern validieren, ob der Benutzer die Seite sehen darf
  • Prüfungen in API Routes und Server Actions: Vor jeder Datenoperation Berechtigungen erneut prüfen
  • Prüfungen auf Datenbankebene: Row-Level Security oder Query-Filter, damit Benutzer nur autorisierte Daten sehen

Mehrschichtige Absicherung. Wird eine Schicht umgangen, greift die nächste.

Am Anfang wirkte mir das übertrieben. Nach ein paar Fehltritten wurde klar: Genau dort, wo Sicherheit „lästig” wirkt, sind Angreifer interessiert.

Die richtige Kombination aus Middleware und getServerSession

Warum getServerSession nicht in Middleware funktioniert

Als ich NextAuth zum ersten Mal nutzte, stolperte ich über genau diese Frage.

In Server Components holt man die Session mit getServerSession(authOptions). Logisch wollte ich dasselbe in der Middleware.

Fehler.

Nach längerer Recherche: Middleware läuft auf Edge Runtime, getServerSession braucht Node.js Runtime. Beides ist inkompatibel.

Edge Runtime ist Vercels leichtgewichtige Umgebung ohne vollständige Node.js-APIs – schnell und global verteilt. Middleware nutzt Edge Runtime für Performance, kann aber nicht alle Node.js-Funktionen verwenden.

Wie holt man die Session in der Middleware?

Der richtige Weg: getToken oder withAuth

NextAuth bietet zwei APIs speziell für Middleware:

Variante 1: getToken verwenden

// middleware.ts
import { getToken } from "next-auth/jwt"
import { NextResponse } from "next/server"

export async function middleware(req) {
  const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET })
  
  if (!token) {
    return NextResponse.redirect(new URL('/login', req.url))
  }
  
  // Rolle prüfen
  if (req.nextUrl.pathname.startsWith('/admin') && token.role !== 'admin') {
    return NextResponse.redirect(new URL('/403', req.url))
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: ['/admin/:path*', '/dashboard/:path*']
}

getToken parst den JWT aus dem Cookie und liefert Benutzerinformationen. Es unterstützt nur die JWT-Session-Strategie – bei Database Sessions funktioniert das nicht.

Variante 2: Higher-Order-Funktion withAuth

// middleware.ts
import { withAuth } from "next-auth/middleware"

export default withAuth({
  callbacks: {
    authorized: ({ token, req }) => {
      // Nicht eingeloggt
      if (!token) return false
      
      // /admin nur für admin-Rolle
      if (req.nextUrl.pathname.startsWith('/admin')) {
        return token.role === 'admin'
      }
      
      return true
    }
  }
})

export const config = {
  matcher: ['/admin/:path*', '/dashboard/:path*']
}

withAuth übernimmt die Redirect-Logik. Im authorized-Callback geben Sie true oder false zurück – bei false erfolgt die Umleitung zur Login-Seite.

Ich bevorzuge withAuth – der Code bleibt kompakter.

Wo wird getServerSession eingesetzt?

getServerSession gehört in Server Components, API Routes und Server Actions.

In Server Components:

// app/admin/page.tsx
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { redirect } from "next/navigation"

export default async function AdminPage() {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    redirect('/login')
  }
  
  if (session.user.role !== 'admin') {
    redirect('/403')
  }
  
  // Seite rendern
  return <UsersList />
}

Diese Schicht prüft, ob der Benutzer die Seite sehen darf. Keine Berechtigung – kein Zugriff.

In API Routes:

// app/api/users/route.ts
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { NextResponse } from "next/server"

export async function DELETE(req: Request) {
  const session = await getServerSession(authOptions)
  
  if (!session || session.user.role !== 'admin') {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
  }
  
  // Löschvorgang ausführen
  // ...
  
  return NextResponse.json({ success: true })
}

In Server Actions:

// app/actions.ts
'use server'

import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"

export async function deleteUser(userId: string) {
  const session = await getServerSession(authOptions)
  
  if (!session || session.user.role !== 'admin') {
    throw new Error('Unauthorized')
  }
  
  // Löschen ausführen
  // ...
}

Die Aufgabenteilung

Kurz gesagt:

  • Middleware (mit getToken): Grobe Routenabsicherung – Umleitung nicht eingeloggter Benutzer, grundlegende Rollenprüfung
  • getServerSession: Feingranulare Berechtigungskontrolle – finale Validierung vor Datenzugriff

Middleware ist der Pförtner am Wohnkomplex, getServerSession das Türschloss – selbst wenn der Pförtner durchwinkt, ohne Schlüssel kommen Sie nicht rein.

Mehrschichtige Absicherung – genau so.

Vollständiges RBAC-Design für Admin-Backends

Middleware und getServerSession sind wichtig, aber für ein echtes Admin-Backend brauchen Sie ein vollständiges RBAC-System (Role-Based Access Control).

Das RBAC-Modell verstehen

Kernidee: Benutzer → Rolle → Berechtigung.

  • Benutzer (User): Anna, Bernd
  • Rolle (Role): Admin, Editor, Viewer
  • Berechtigung (Permission): Benutzerliste ansehen, Artikel bearbeiten, Kommentare löschen

Ein Benutzer kann mehrere Rollen haben, eine Rolle mehrere Berechtigungen. Anna ist Admin mit allen Rechten; Bernd ist Editor und darf Artikel bearbeiten und Benutzerlisten ansehen.

Berechtigungen lassen sich in drei Granularitäten unterteilen:

  • Seitenebene: Darf /admin/users aufgerufen werden?
  • Funktionsebene: Darf der „Löschen”-Button geklickt werden?
  • Datenebene: Darf nur eigene Artikel gesehen werden?

Datenbankdesign (Prisma Schema):

model User {
  id    String @id @default(cuid())
  email String @unique
  roles Role[]
}

model Role {
  id          String       @id @default(cuid())
  name        String       @unique
  permissions Permission[]
  users       User[]
}

model Permission {
  id    String @id @default(cuid())
  name  String @unique // z. B. "user:view", "user:edit"
  roles Role[]
}

In vielen Projekten reicht ein einfacheres Modell. Bei wenigen Rollen (Admin, Editor, Viewer) können Sie Berechtigungen auch direkt im Code definieren.

Vier-Schichten-Absicherungsarchitektur

So setzen Sie Berechtigungsprüfungen auf jeder Ebene um.

Schicht 1: Middleware – grobe Absicherung

// middleware.ts
import { withAuth } from "next-auth/middleware"

export default withAuth({
  callbacks: {
    authorized: ({ token, req }) => {
      if (!token) return false
      
      const path = req.nextUrl.pathname
      
      // /admin nur für admin-Rolle
      if (path.startsWith('/admin')) {
        return token.role === 'admin'
      }
      
      // /dashboard erfordert Login
      if (path.startsWith('/dashboard')) {
        return true
      }
      
      return false
    }
  }
})

export const config = {
  matcher: ['/admin/:path*', '/dashboard/:path*']
}

Hier nur grundlegende Rollenprüfung – keine funktionsspezifischen Berechtigungen.

Schicht 2: Server Component – Seitenberechtigung

// app/admin/users/page.tsx
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { checkPermission } from "@/lib/permissions"
import { redirect } from "next/navigation"

export default async function UsersPage() {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    redirect('/login')
  }
  
  // Berechtigung für Benutzerliste prüfen
  const hasPermission = await checkPermission(session.user.id, 'user:view')
  
  if (!hasPermission) {
    redirect('/403')
  }
  
  // Seite rendern
  return <UsersList />
}

Schicht 3: UI-Bedingungsrendering – Funktionsberechtigung

// components/UsersList.tsx
'use client'

import { useSession } from "next-auth/react"
import { hasPermission } from "@/lib/permissions-client"

export function UsersList() {
  const { data: session } = useSession()
  
  const canEdit = hasPermission(session, 'user:edit')
  const canDelete = hasPermission(session, 'user:delete')
  
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>
          <span>{user.name}</span>
          {canEdit && <button>Bearbeiten</button>}
          {canDelete && <button>Löschen</button>}
        </div>
      ))}
    </div>
  )
}

Buttons werden nach Berechtigung ein- oder ausgeblendet. Das ist UX-Optimierung, keine Sicherheitsmaßnahme – technisch versierte Nutzer können Buttons im Browser wieder einblenden. Die echte Prüfung folgt in der nächsten Schicht.

Schicht 4: Server Action / API – Validierung vor Datenzugriff

// app/actions.ts
'use server'

import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { checkPermission } from "@/lib/permissions"

export async function deleteUser(userId: string) {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    throw new Error('Unauthorized')
  }
  
  // Löschberechtigung erforderlich
  const hasPermission = await checkPermission(session.user.id, 'user:delete')
  
  if (!hasPermission) {
    throw new Error('Forbidden')
  }
  
  // Löschen ausführen
  await db.user.delete({ where: { id: userId } })
  
  return { success: true }
}

Letzte Verteidigungslinie: Unabhängig von vorherigen Schichten muss vor jeder Datenoperation die Berechtigung erneut geprüft werden.

Berechtigungen zentral verwalten

Überall dieselbe Prüflogik zu schreiben, ist mühsam. Berechtigungsdefinitionen und Prüflogik gehören in eine zentrale Datei.

// lib/permissions.ts
export const PERMISSIONS = {
  USER_VIEW: 'user:view',
  USER_EDIT: 'user:edit',
  USER_DELETE: 'user:delete',
  POST_VIEW: 'post:view',
  POST_EDIT: 'post:edit',
  POST_DELETE: 'post:delete',
} as const

export const ROLES = {
  ADMIN: {
    name: 'admin',
    permissions: Object.values(PERMISSIONS) // Admin hat alle Berechtigungen
  },
  EDITOR: {
    name: 'editor',
    permissions: [
      PERMISSIONS.USER_VIEW,
      PERMISSIONS.POST_VIEW,
      PERMISSIONS.POST_EDIT,
    ]
  },
  VIEWER: {
    name: 'viewer',
    permissions: [
      PERMISSIONS.USER_VIEW,
      PERMISSIONS.POST_VIEW,
    ]
  }
} as const

// Serverseitige Berechtigungsprüfung
export async function checkPermission(userId: string, permission: string) {
  const user = await db.user.findUnique({
    where: { id: userId },
    include: { roles: true }
  })
  
  if (!user) return false
  
  // Prüfen, ob die Rolle des Benutzers die Berechtigung enthält
  const userRole = ROLES[user.role as keyof typeof ROLES]
  return userRole?.permissions.includes(permission) ?? false
}
// lib/permissions-client.ts (Client-Version)
import { Session } from "next-auth"

export function hasPermission(session: Session | null, permission: string) {
  if (!session?.user) return false
  
  const userRole = ROLES[session.user.role as keyof typeof ROLES]
  return userRole?.permissions.includes(permission) ?? false
}

Berechtigungen an einer Stelle pflegen – kein Suchen im gesamten Code.

Vorteile dieser Architektur

Mehr Code auf mehreren Ebenen – lohnt sich das?

Ja.

  • Sicherheit: Wird eine Schicht umgangen, greifen die anderen
  • Wartbarkeit: Berechtigungen zentral konfiguriert, Änderungen einfach
  • Benutzererfahrung: Unberechtigte Aktionen werden ausgeblendet, nicht erst nach Klick abgelehnt
  • Audit-freundlich: Jede Schicht hat klare Berechtigungsprüfungen – Sicherheitsaudits laufen leichter

Am Anfang kostet der Aufbau Zeit. Beim Hinzufügen neuer Features und Anpassen von Regeln sparen Sie später deutlich Aufwand.

Praxisbeispiele und Code-Vorlagen

Nach Theorie und Architektur folgen direkt nutzbare Vorlagen und Tipps zur Fehlersuche.

Vollständige Middleware-Konfiguration

Funktionsreiche Middleware mit Mehrrollen-Support und dynamischem Routing:

// middleware.ts
import { withAuth } from "next-auth/middleware"
import { NextResponse } from "next/server"

export default withAuth(
  function middleware(req) {
    const token = req.nextauth.token
    const path = req.nextUrl.pathname
    
    // Feingranulare Steuerung nach Pfad und Rolle
    if (path.startsWith('/admin') && token?.role !== 'admin') {
      return NextResponse.redirect(new URL('/403', req.url))
    }
    
    if (path.startsWith('/editor') && !['admin', 'editor'].includes(token?.role as string)) {
      return NextResponse.redirect(new URL('/403', req.url))
    }
    
    return NextResponse.next()
  },
  {
    callbacks: {
      authorized: ({ token }) => !!token
    }
  }
)

export const config = {
  matcher: [
    '/admin/:path*',
    '/editor/:path*',
    '/dashboard/:path*',
    '/api/admin/:path*',
    '/api/editor/:path*'
  ]
}

Wichtige Punkte:

  1. matcher definiert geschützte Pfade – Wildcard :path* unterstützt
  2. authorized-Callback für grundlegende Login-Prüfung
  3. In der middleware-Funktion feinere Rollenprüfung

Dynamisches Menü-Rendering

Menüs nach Berechtigungen zu filtern ist in Admin-Backends Standard:

// components/Sidebar.tsx
'use client'

import { useSession } from "next-auth/react"
import Link from "next/link"
import { hasPermission } from "@/lib/permissions-client"

const menuItems = [
  {
    label: 'Benutzerverwaltung',
    href: '/admin/users',
    permission: 'user:view'
  },
  {
    label: 'Artikelverwaltung',
    href: '/admin/posts',
    permission: 'post:view'
  },
  {
    label: 'Systemeinstellungen',
    href: '/admin/settings',
    permission: 'setting:manage'
  }
]

export function Sidebar() {
  const { data: session } = useSession()
  
  // Menüeinträge nach Berechtigung filtern
  const visibleItems = menuItems.filter(item => 
    hasPermission(session, item.permission)
  )
  
  return (
    <nav>
      {visibleItems.map(item => (
        <Link key={item.href} href={item.href}>
          {item.label}
        </Link>
      ))}
    </nav>
  )
}

Menükonfiguration und Berechtigungen an einem Ort – neue Einträge einfach ins menuItems-Array.

Wiederverwendbarer Berechtigungs-Hook

Ein React Hook für Client Components:

// hooks/usePermission.ts
import { useSession } from "next-auth/react"
import { hasPermission } from "@/lib/permissions-client"

export function usePermission(permission: string) {
  const { data: session, status } = useSession()
  
  const isLoading = status === 'loading'
  const isAllowed = hasPermission(session, permission)
  
  return { isAllowed, isLoading }
}

Verwendung:

// components/DeleteButton.tsx
'use client'

import { usePermission } from "@/hooks/usePermission"

export function DeleteButton({ userId }: { userId: string }) {
  const { isAllowed, isLoading } = usePermission('user:delete')
  
  if (isLoading) return <div>Loading...</div>
  if (!isAllowed) return null
  
  return (
    <button onClick={() => deleteUser(userId)}>
      Löschen
    </button>
  )
}

Häufige Probleme und Lösungen

Problem 1: Endlosschleife bei Middleware-Redirects

Symptom: Browser meldet „Zu viele Weiterleitungen”.

Ursache: Die Login-Seite liegt im matcher und wird ebenfalls abgefangen.

Lösung: Login-Seite und öffentliche Seiten vom matcher ausschließen.

export const config = {
  matcher: [
    /*
     * Alle Pfade außer:
     * - /login (Login-Seite)
     * - /api/auth (NextAuth API)
     * - /_next (Next.js intern)
     * - /favicon.ico, /robots.txt (statische Dateien)
     */
    '/((?!login|api/auth|_next|favicon.ico|robots.txt).*)',
  ]
}

Problem 2: getServerSession gibt null zurück

Symptom: Benutzer ist eingeloggt, aber getServerSession liefert null.

Ursachen:

  1. authOptions falsch konfiguriert oder nicht übergeben
  2. Cookie-Probleme (Cross-Domain, HTTPS)
  3. getServerSession fälschlich in Middleware verwendet

Lösung:

  • authOptions-Import prüfen
  • Nur in Server Components oder API Routes verwenden, nicht in Middleware
  • In der Entwicklung NEXTAUTH_URL prüfen
// Korrekte Verwendung
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/app/api/auth/[...nextauth]/route"

const session = await getServerSession(authOptions)

Problem 3: Performance bei Berechtigungsprüfungen

Symptom: Seiten laden langsam wegen Datenbankabfragen bei jeder Berechtigungsprüfung.

Ursache: Kein Caching – jeder Request fragt die Datenbank ab.

Lösung:

  1. Rolle und Berechtigungen im JWT-Token speichern
// app/api/auth/[...nextauth]/route.ts
export const authOptions: NextAuthOptions = {
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role
        token.permissions = user.permissions
      }
      return token
    },
    async session({ session, token }) {
      session.user.role = token.role
      session.user.permissions = token.permissions
      return session
    }
  }
}
  1. React Query oder SWR für Client-Caching
// hooks/usePermissions.ts
import useSWR from 'swr'

export function usePermissions() {
  const { data: permissions } = useSWR('/api/me/permissions', {
    revalidateOnFocus: false,
    dedupingInterval: 60000 // 1 Minute kein erneuter Request
  })
  
  return permissions
}

Schnell-Checkliste vor dem Go-Live

Vor dem Deployment prüfen:

  • Macht Middleware nur Grobfilter?
  • Haben alle Server-Component-Seiten Berechtigungsprüfungen?
  • Haben alle API Routes Berechtigungsprüfungen?
  • Haben alle Server Actions Berechtigungsprüfungen?
  • Werden sensible Buttons nach Berechtigung ausgeblendet?
  • Sind Berechtigungen zentral verwaltet?
  • Enthält der JWT-Token Rolleninformationen?
  • Sind Login-Seite und öffentliche Seiten vom Middleware-matcher ausgeschlossen?

Wenn alles erfüllt ist, steht Ihre Berechtigungskontrolle auf solidem Fundament.

Fazit

Zurück zur Ausgangsfrage: Wie macht man Next.js-Berechtigungskontrolle richtig?

Antwort: Es gibt keine Ein-Lösung-für-alles.

Middleware ist wichtig – erste Verteidigungslinie, hält die meisten unautorisierten Zugriffe ab. Aber nicht alles. Sie brauchen Prüfungen in Server Components, Validierung in Server Actions und API Routes, und UX-Optimierung in der UI.

Mehrschichtige Absicherung wirkt aufwendig. Sicherheit hat aber selten eine Silver Bullet. Wird eine Schicht umgangen, greifen die anderen – das ist der zuverlässige Weg.

Nur Middleware vs. mehrschichtige Absicherung

VergleichsdimensionNur MiddlewareMehrschichtige Absicherung
SicherheitNiedrig, umgehbarHoch, mehrere Fallback-Schichten
EntwicklungsaufwandNiedrig, eine KonfigurationMittel, Prüfungen an mehreren Stellen
WartbarkeitSchlecht, Logik verstreutGut, zentrale Konfiguration
BenutzererfahrungMittel, Fehler erst nach KlickGut, unberechtigte Funktionen vorab ausgeblendet
Audit-freundlichSchlecht, ein PrüfpunktGut, jede Schicht dokumentiert

Mehrschichtige Absicherung kostet etwas mehr Aufwand – die Vorteile sind konkret.

Sofort handeln

Wenn Sie an einem Next.js-Projekt arbeiten, prüfen Sie jetzt:

  1. Basieren Berechtigungen nur auf Middleware?
  2. Haben API Routes und Server Actions eigene Berechtigungsprüfungen?
  3. Sind Berechtigungsregeln über viele Dateien verstreut?

Trifft eine Antwort zu, planen Sie den Umbau auf mehrschichtige Absicherung. Nicht alles auf einmal – beginnen Sie mit den kritischsten Datenzugriffen und erweitern Sie schrittweise.

Bei Sicherheit gilt: Früher reparieren ist besser als später.

Noch Fragen?

Dieser Artikel deckt Kernarchitektur und Praxis ab, aber nicht jeden Fall. Wenn Sie in Projekten auf andere Herausforderungen stoßen, zum Beispiel:

  • Datenebene-Berechtigungen (nur eigene Datensätze sehen)
  • Prisma Row-Level Security
  • Mandantenisolierung in Multi-Tenant-Systemen

hinterlassen Sie einen Kommentar – gemeinsam finden wir Lösungen.

Berechtigungskontrolle wird nie irrelevant. Hoffentlich ersparen Ihnen diese Hinweise ein paar typische Fehler.

Vollständiger Workflow für mehrschichtigen Next.js-Berechtigungsschutz

Von Middleware-Basisprüfung über getServerSession-Detailvalidierung bis zur Datenbank-Berechtigungsprüfung

⏱️ Estimated time: 3 hr

  1. 1

    Step 1: Schicht 1: Middleware-Basisprüfung

    middleware.ts erstellen:
    ```ts
    import { withAuth } from 'next-auth/middleware'

    export default withAuth({
    pages: {
    signIn: '/login'
    }
    })

    export const config = {
    matcher: ['/dashboard/:path*', '/admin/:path*']
    }
    ```

    Funktion:
    • Prüft, ob der Benutzer eingeloggt ist
    • Nicht eingeloggte Benutzer werden zur Login-Seite umgeleitet
    • Schnell und früh im Request-Lebenszyklus

    Einschränkungen:
    • Kann umgangen werden (x-middleware-subrequest-Schwachstelle)
    • Nur Grobfilter möglich
    • Keine detaillierte Berechtigungsprüfung

    Wichtig: Middleware ist die erste Schutzschicht, aber nicht die einzige.
  2. 2

    Step 2: Schicht 2: getServerSession-Detailvalidierung

    In der Seite verwenden:
    ```tsx
    import { getServerSession } from 'next-auth'
    import { authOptions } from '@/app/api/auth/[...nextauth]/route'

    export default async function DashboardPage() {
    const session = await getServerSession(authOptions)

    if (!session) {
    redirect('/login')
    }

    // Benutzerrolle prüfen
    if (session.user.role !== 'admin') {
    redirect('/unauthorized')
    }

    return <div>Dashboard</div>
    }
    ```

    Funktion:
    • Detaillierte Identitätsvalidierung
    • Benutzerrollen prüfen
    • Feingranulare Berechtigungskontrolle

    Vorteile:
    • Sicherer als Middleware allein
    • Detaillierte Berechtigungsprüfung möglich
    • Nicht umgehbar

    Wichtig: getServerSession ist die zweite Schutzschicht für Detailvalidierung.
  3. 3

    Step 3: Schicht 3: Datenbank-Berechtigungsprüfung

    In API Route prüfen:
    ```ts
    import { getServerSession } from 'next-auth'
    import { db } from '@/lib/db'

    export async function GET(request: Request) {
    const session = await getServerSession(authOptions)

    if (!session) {
    return new Response('Unauthorized', { status: 401 })
    }

    // Datenbank-Berechtigungsprüfung
    const user = await db.user.findUnique({
    where: { id: session.user.id },
    include: { role: { include: { permissions: true } } }
    })

    const hasPermission = user.role.permissions.some(
    p => p.resource === 'users' && p.action === 'read'
    )

    if (!hasPermission) {
    return new Response('Forbidden', { status: 403 })
    }

    // Daten zurückgeben
    return Response.json({ users: [...] })
    }
    ```

    Funktion:
    • Finale Berechtigungsvalidierung
    • Berechtigungen in der Datenbank prüfen
    • Datensicherheit gewährleisten

    Wichtig: Die Datenbank ist die dritte Schutzschicht für finale Validierung.
  4. 4

    Step 4: RBAC-Berechtigungssystem implementieren

    Datenbank-Schema:
    ```prisma
    model User {
    id String @id @default(cuid())
    email String @unique
    role Role @relation(fields: [roleId], references: [id])
    roleId String
    }

    model Role {
    id String @id @default(cuid())
    name String @unique
    permissions Permission[]
    users User[]
    }

    model Permission {
    id String @id @default(cuid())
    resource String
    action String
    roles Role[]
    }
    ```

    Berechtigung prüfen:
    ```ts
    async function checkPermission(
    userId: string,
    resource: string,
    action: string
    ) {
    const user = await db.user.findUnique({
    where: { id: userId },
    include: { role: { include: { permissions: true } } }
    })

    return user.role.permissions.some(
    p => p.resource === resource && p.action === action
    )
    }
    ```

    Wichtig:
    • Benutzer haben Rollen
    • Rollen haben Berechtigungen
    • Berechtigungen steuern Ressourcenzugriff

FAQ

Warum darf Berechtigungskontrolle nicht nur auf Middleware basieren?
Grund: Middleware kann umgangen werden.

Schwachstellenfall:
• CVE-2025-29927: Angreifer können durch den Request-Header x-middleware-subrequest die Middleware-Prüfung umgehen
• Mit Postman direkt Backend-APIs aufrufen und geschützte Daten abrufen

Middleware-Positionierung:
• Läuft auf Edge Runtime, macht Grobfilter
• Prüft Login-Status und Benutzerrolle
• Schnell und früh, allein aber nicht ausreichend

Lösung: Mehrschichtige Absicherung
• Schicht 1: Middleware (Basisprüfung)
• Schicht 2: getServerSession (Detailvalidierung)
• Schicht 3: Datenbank-Berechtigungsprüfung (finale Validierung)

Wichtig: Drei Schichten für Sicherheit – keine darf fehlen.
Was ist der Unterschied zwischen Middleware und getServerSession?
Middleware:
• Läuft auf Edge Runtime
• Schnell und früh im Request-Lebenszyklus
• Nur Grobfilter (Login-Status, Benutzerrolle)
• Kann umgangen werden (x-middleware-subrequest-Schwachstelle)

getServerSession:
• Läuft auf Node.js Runtime
• Detaillierte Validierung möglich
• Benutzerrollen und Berechtigungen prüfen
• Nicht umgehbar

Gemeinsame Nutzung:
• Middleware für Basisprüfung (Schicht 1)
• getServerSession für Detailvalidierung (Schicht 2)
• Datenbank für finale Berechtigungsprüfung (Schicht 3)

Codebeispiel:
```ts
// middleware.ts (Schicht 1)
export default withAuth({
pages: { signIn: '/login' }
})

// page.tsx (Schicht 2)
const session = await getServerSession(authOptions)
if (session.user.role !== 'admin') {
redirect('/unauthorized')
}

// api/route.ts (Schicht 3)
const hasPermission = await checkPermission(userId, 'users', 'read')
if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}
```
Wie implementiert man ein RBAC-Berechtigungssystem?
RBAC = Role-Based Access Control (rollenbasierte Zugriffskontrolle)

Datenbank-Schema:
```prisma
model User {
id String @id
email String @unique
role Role @relation(fields: [roleId], references: [id])
roleId String
}

model Role {
id String @id
name String @unique
permissions Permission[]
users User[]
}

model Permission {
id String @id
resource String // Ressource: users, posts usw.
action String // Aktion: read, write, delete
roles Role[]
}
```

Berechtigung prüfen:
```ts
async function checkPermission(
userId: string,
resource: string,
action: string
) {
const user = await db.user.findUnique({
where: { id: userId },
include: { role: { include: { permissions: true } } }
})

return user.role.permissions.some(
p => p.resource === resource && p.action === action
)
}
```

Wichtig:
• Benutzer haben Rollen
• Rollen haben Berechtigungen
• Berechtigungen steuern Ressourcenzugriff
Wie implementiert man eine mehrschichtige Absicherungsarchitektur?
Drei Schutzschichten:

Schicht 1: Middleware (Basisprüfung)
```ts
// middleware.ts
export default withAuth({
pages: { signIn: '/login' }
})
```

Schicht 2: getServerSession (Detailvalidierung)
```tsx
// page.tsx
const session = await getServerSession(authOptions)
if (session.user.role !== 'admin') {
redirect('/unauthorized')
}
```

Schicht 3: Datenbank-Berechtigungsprüfung (finale Validierung)
```ts
// api/route.ts
const hasPermission = await checkPermission(userId, 'users', 'read')
if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}
```

Wichtig:
• Drei Schichten für Sicherheit
• Keine Schicht darf fehlen
• Middleware macht Grobfilter, getServerSession Feinfilter, Datenbank finale Validierung
Wie schützt man sich vor der x-middleware-subrequest-Schwachstelle?
Schwachstelle: Angreifer können durch den Request-Header x-middleware-subrequest die Middleware-Prüfung umgehen.

Schutzmaßnahmen:
• Nicht nur auf Middleware verlassen
• Berechtigungen auch in API Routes prüfen
• getServerSession zur Validierung nutzen
• Datenbank für finale Berechtigungsprüfung

Codebeispiel:
```ts
// api/route.ts
export async function GET(request: Request) {
// Auch wenn Middleware umgangen wird, wird hier geprüft
const session = await getServerSession(authOptions)

if (!session) {
return new Response('Unauthorized', { status: 401 })
}

// Datenbank-Berechtigungsprüfung
const hasPermission = await checkPermission(session.user.id, 'users', 'read')

if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}

return Response.json({ users: [...] })
}
```

Wichtig:
• Mehrschichtige Absicherung
• API Routes müssen Berechtigungen prüfen
• Datenbank für finale Validierung

Empfehlung: Niemals nur auf Middleware verlassen – Berechtigungen auch in API Routes prüfen.
Was sind Best Practices für Berechtigungskontrolle?
Drei Schutzschichten:
• Middleware für Basisprüfung (Schicht 1)
• getServerSession für Detailvalidierung (Schicht 2)
• Datenbank für finale Berechtigungsprüfung (Schicht 3)

RBAC-System:
• Benutzer haben Rollen
• Rollen haben Berechtigungen
• Berechtigungen steuern Ressourcenzugriff

Code-Organisation:
• Hilfsfunktionen für Berechtigungsprüfung erstellen
• Einheitlich in API Routes verwenden
• Code-Duplikation vermeiden

Sicherheitsempfehlungen:
• Niemals nur auf Middleware verlassen
• Berechtigungen auch in API Routes prüfen
• Datenbank für finale Validierung
• Berechtigungskonfiguration regelmäßig prüfen

Wichtig:
• Mehrschichtige Absicherung für Sicherheit
• Keine Schicht darf fehlen
• Berechtigungssystem kontinuierlich verbessern

Merken: Berechtigungskontrolle ist die Basis der Sicherheit – nicht unterschätzen.

13 Min. Lesezeit · Veröffentlicht am: 19. Dez. 2025 · Aktualisiert am: 14. Juli 2026

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog