Auth.js v5 mit Next.js 16: Edge-sichere Authentifizierung im App Router (2026)

Auth.js v5 ist ein kompletter Rewrite – und Next.js 16 hat middleware.ts in proxy.ts umbenannt. Diese Anleitung zeigt die Edge-sichere Split-Config, Credentials- und OAuth-Setup, die Data-Access-Layer-Strategie nach CVE-2025-29927 sowie sechs Fehler, die jeden zweiten Auth-Flow brechen.

Auth.js v5 Next.js 16: Edge-Auth Setup 2026

Authentifizierung im Next.js App Router ist 2026 ein anderes Spiel als noch vor zwei Jahren. Auth.js v5 (vormals NextAuth) wurde komplett neu geschrieben, der bekannte middleware.ts-Pfad heisst in Next.js 16 jetzt proxy.ts, und die CVE-2025-29927-Lücke vom Frühjahr 2025 hat ziemlich brutal klargemacht, dass Perimeter-Schutz allein einfach nicht mehr ausreicht.

Also – ich führe dich hier durch ein Setup, das wirklich produktionsreif ist. Inklusive Split-Config, Credentials- und OAuth-Provider, einem sauberen Data Access Layer und den sechs Fehlern, an denen die meisten Tutorials krachend scheitern. Ehrlich gesagt habe ich diesen Stack inzwischen in drei Projekten ausgerollt, und jedes Mal sind genau dieselben Stolperfallen aufgetaucht.

Was ist neu in Auth.js v5?

Auth.js v5 ist kein Bugfix-Release. Es ist eher ein „setzt euch hin, das ist jetzt anders"-Release. Die wichtigsten Änderungen gegenüber NextAuth v4 betreffen sowohl die API als auch die Konfigurationsstruktur:

  • Universelle auth()-Funktion: Ersetzt getServerSession, withAuth und den alten Middleware-Helfer. Funktioniert identisch in Server Components, Route Handlern, Server Actions und im Proxy. (Endlich, muss man sagen.)
  • Kein authOptions-Prop-Drilling mehr: Die Konfiguration lebt in einer einzigen Datei im Repo-Root und exportiert auth, signIn, signOut sowie handlers.
  • Neue Umgebungsvariablen: Variablen mit Präfix AUTH_ werden automatisch erkannt. AUTH_GITHUB_ID und AUTH_GITHUB_SECRET ersetzen die alten NEXTAUTH_*-Namen.
  • Edge-First-Design: Vercel Edge Runtime und Cloudflare Workers sind erstklassige Ziele – allerdings nur, wenn die Konfiguration korrekt gesplittet ist. Wenn nicht, viel Spass beim Debuggen.
  • Minimum Next.js 14, empfohlen 16: Ab Next.js 16 läuft proxy.ts standardmässig auf dem Node.js-Runtime, was viele Edge-Workarounds überflüssig macht.

Next.js 16 ändert middleware.ts zu proxy.ts

Das ist die Falle Nummer eins. Wirklich. Jeder, der ein älteres Tutorial nachbaut, läuft genau hier rein: In Next.js 16 wurde middleware.ts in proxy.ts umbenannt. Die Datei muss entweder eine benannte proxy- oder eine Default-Export-Funktion bereitstellen. Die offizielle Auth.js-v5-Dokumentation verwendet noch middleware.ts – funktionell identisch, aber der Pfad muss angepasst werden, sonst passiert schlicht gar nichts.

Ein angenehmer Nebeneffekt: Da proxy.ts in Next.js 16 auf dem Node.js-Runtime läuft, fallen viele der Edge-Kompatibilitätsprobleme weg, die in Next.js 14 und 15 jeden zweiten Auth-Flow gebrochen haben. Trotzdem bleibt die Split-Config sinnvoll, weil sie sauberer testbar ist und die Build-Zeiten verkürzt.

Projekt-Setup in fünf Minuten

Voraussetzung ist ein Next.js-16-Projekt mit App Router. Installier die Beta-Version – die ist trotz Label längst stabil genug für Produktion:

npm install next-auth@beta
npm install @auth/prisma-adapter
npm install bcryptjs zod
npm install -D @types/bcryptjs

Leg eine .env.local-Datei mit den erforderlichen Variablen an:

AUTH_SECRET="generiere-mit-openssl-rand-base64-32"
AUTH_TRUST_HOST=true
AUTH_GITHUB_ID="dein-github-client-id"
AUTH_GITHUB_SECRET="dein-github-secret"
DATABASE_URL="postgresql://user:pass@host:5432/db"

Der AUTH_SECRET-Wert verschlüsselt die JWT-Sessions. Generier ihn mit openssl rand -base64 32 – und bitte nicht über irgendeinen Online-Generator, der den Wert mitprotokollieren könnte. Klingt offensichtlich, ist es leider nicht.

Die Split-Config: auth.config.ts und auth.ts

Die Split-Config ist nicht optional, sobald ein Datenbank-Adapter im Spiel ist. Der Adapter zieht Node.js-spezifische Abhängigkeiten ins Bundle – und die laufen im Edge-Runtime des Proxy einfach nicht. Der Split sorgt dafür, dass der Proxy nur das lädt, was edge-kompatibel ist.

auth.config.ts – Edge-sicherer Teil

// auth.config.ts
import type { NextAuthConfig } from "next-auth";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
import { z } from "zod";

export const authConfig = {
  pages: {
    signIn: "/login",
    error: "/login",
  },
  providers: [
    GitHub,
    Credentials({
      credentials: {
        email: { label: "E-Mail", type: "email" },
        password: { label: "Passwort", type: "password" },
      },
      async authorize(credentials) {
        const parsed = z
          .object({
            email: z.string().email(),
            password: z.string().min(8),
          })
          .safeParse(credentials);

        if (!parsed.success) return null;

        // Wichtig: Im Proxy nicht hier validieren –
        // die echte DB-Abfrage findet in auth.ts statt.
        return null;
      },
    }),
  ],
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnDashboard = nextUrl.pathname.startsWith("/dashboard");

      if (isOnDashboard) return isLoggedIn;
      if (isLoggedIn && nextUrl.pathname.startsWith("/login")) {
        return Response.redirect(new URL("/dashboard", nextUrl));
      }
      return true;
    },
  },
} satisfies NextAuthConfig;

auth.ts – Vollständige Konfiguration mit Adapter

// auth.ts
import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import bcrypt from "bcryptjs";
import { prisma } from "@/lib/prisma";
import { authConfig } from "./auth.config";
import Credentials from "next-auth/providers/credentials";

export const { handlers, signIn, signOut, auth } = NextAuth({
  ...authConfig,
  adapter: PrismaAdapter(prisma),
  session: { strategy: "jwt" },
  providers: [
    ...authConfig.providers.filter((p) => p.id !== "credentials"),
    Credentials({
      async authorize(credentials) {
        const user = await prisma.user.findUnique({
          where: { email: credentials?.email as string },
        });
        if (!user?.passwordHash) return null;

        const valid = await bcrypt.compare(
          credentials?.password as string,
          user.passwordHash
        );
        if (!valid) return null;

        return { id: user.id, email: user.email, name: user.name };
      },
    }),
  ],
});

app/api/auth/[...nextauth]/route.ts

// app/api/auth/[...nextauth]/route.ts
export { GET, POST } from "@/auth";

proxy.ts – Der entscheidende Schritt für Next.js 16

// proxy.ts (vormals middleware.ts in Next.js 15)
import NextAuth from "next-auth";
import { authConfig } from "./auth.config";

export const { auth: proxy } = NextAuth(authConfig);

export const config = {
  matcher: [
    "/dashboard/:path*",
    "/account/:path*",
    "/api/protected/:path*",
  ],
};

Zwei Details, die ich nicht oft genug betonen kann: Erstens wird im Proxy authConfig aus auth.config.ts importiert – nicht aus auth.ts. Sonst landet der Prisma-Adapter im Edge-Bundle und der Build bricht ab. Zweitens: verwende explizite Matcher. Das gängige Negativ-Lookahead-Pattern /((?!api|_next/static|_next/image|.*\.png$).*) kann /api/auth/signin trotz api-Ausschluss erfassen und Endlos-Redirects (ERR_TOO_MANY_REDIRECTS) auslösen. Da habe ich mal eine ganze Stunde verbrannt, bis der Groschen gefallen ist.

Session-Strategie: JWT oder Datenbank?

Auth.js v5 unterstützt beides. Die Wahl beeinflusst Performance, Revokierbarkeit und Edge-Kompatibilität:

  • JWT-Strategie (session: { strategy: "jwt" }): Token wird signiert in einem Cookie gespeichert. Der Proxy validiert die Signatur ohne Datenbankabfrage – ideal fürs Edge. Nachteil: Sessions sind nicht ohne weiteres widerrufbar, bis sie ablaufen.
  • Datenbank-Strategie: Jeder Request prüft die Session-ID in der DB. Volle Kontrolle, sofortige Revokation – aber langsamer und im Edge-Runtime nur mit Adaptern wie Neon HTTP oder Upstash möglich.

Mein pragmatischer Mittelweg für 2026: kurze JWT-Access-Tokens (15 Minuten) plus langlebige Refresh-Tokens in der Datenbank (7–30 Tage). So bleibt der Hot Path schnell, und ein kompromittiertes Konto lässt sich beim nächsten Refresh stoppen. Funktioniert in der Praxis erstaunlich gut.

Defense-in-Depth: Der Data Access Layer

CVE-2025-29927 hat im März 2025 ziemlich deutlich gezeigt, dass Middleware-basierte Authentifizierung allein nicht reicht. Angreifer konnten den Header x-middleware-subrequest spoofen und so geschützte Routen erreichen. Next.js 14.2.25 und 15.2.3 haben den Bug gepatcht – aber die richtige Lehre daraus ist eine andere: Authentifizierung gehört an drei Schichten, nicht an eine.

Schicht 1: Proxy (Routing-Schutz)

Schnelle JWT-Prüfung am Edge. Blockt Bots und entfernt sichtbare Angriffsfläche – aber ist eben kein verlässlicher Schutz für sensible Daten.

Schicht 2: Data Access Layer (zentrale Autorisierung)

Statt in jedem Server Component auth() aufzurufen, kapsel den Zugriff in einer einzigen Modulgrenze:

// lib/dal.ts
import "server-only";
import { cache } from "react";
import { redirect } from "next/navigation";
import { auth } from "@/auth";

export const verifySession = cache(async () => {
  const session = await auth();
  if (!session?.user?.id) redirect("/login");
  return { userId: session.user.id, role: session.user.role };
});

export const getUserOrders = cache(async () => {
  const { userId } = await verifySession();
  return prisma.order.findMany({ where: { userId } });
});

Der "server-only"-Import löst einen Build-Fehler aus, falls ein Client Component dieses Modul versehentlich importiert. cache() von React dedupliziert die Session-Abfrage innerhalb desselben Renders. Kleiner Aufwand, riesiger Effekt.

Schicht 3: Server Actions (Mutations-Schutz)

Jede Mutation prüft die Session erneut – unabhängig davon, was die UI gerade anzeigt:

// app/dashboard/actions.ts
"use server";
import { verifySession } from "@/lib/dal";
import { revalidatePath } from "next/cache";

export async function deletePost(formData: FormData) {
  const { userId } = await verifySession();
  const postId = formData.get("postId") as string;

  await prisma.post.deleteMany({
    where: { id: postId, authorId: userId },
  });

  revalidatePath("/dashboard");
}

Der Trick liegt im deleteMany mit kombiniertem where: Selbst wenn jemand die postId manipuliert, wird nur gelöscht, was dem User wirklich gehört. Defense-in-Depth in einer Zeile.

Sechs typische Fehler – und wie man sie vermeidet

  1. auth.ts im Proxy importieren: Bringt den DB-Adapter ins Edge-Bundle. Lösung: immer auth.config.ts im Proxy verwenden.
  2. Datenbank-Sessions am Edge ohne HTTP-Adapter: Standard-Postgres-Treiber öffnen TCP-Sockets, die im Edge-Runtime schlicht nicht existieren. Lösung: JWT-Strategie oder Neon HTTP / Upstash.
  3. Vergessener Session-Check in Server Actions: Server Actions sind öffentlich erreichbare POST-Endpoints. Ohne explizite Prüfung kann sie jeder aufrufen. Wirklich, jeder.
  4. Negativ-Lookahead-Matcher: Führt zu Redirect-Loops auf /api/auth/signin. Lösung: explizite Matcher-Liste.
  5. Passwörter mit SHA-256 oder MD5 hashen: Diese sind zu schnell. Nur bcrypt (Cost 12+) oder argon2id.
  6. Fehlende Cookie-Flags: Auth-Cookies müssen httpOnly: true, in Produktion secure: true und sameSite: "lax" oder "strict" haben. Auth.js v5 setzt diese Defaults korrekt – aber bei Custom-Cookie-Konfigurationen schnell vergessen.

Session in Server Components abfragen

Die auth()-Funktion ist die einzige API, die man dafür kennen muss:

// app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
  const session = await auth();
  if (!session?.user) redirect("/login");

  return (
    <main>
      <h1>Hallo, {session.user.name}</h1>
    </main>
  );
}

In Client Components ist useSession() aus next-auth/react verfügbar – setzt aber einen <SessionProvider> im Root-Layout voraus und verursacht ein Client-Bundle. Mein Tipp: wenn möglich, lieber Server-seitig prüfen und nur die nötigen Felder als Props weiterreichen.

Login- und Logout-Flow

// app/login/actions.ts
"use server";
import { signIn } from "@/auth";
import { AuthError } from "next-auth";

export async function authenticate(_prev: unknown, formData: FormData) {
  try {
    await signIn("credentials", formData);
  } catch (error) {
    if (error instanceof AuthError) {
      switch (error.type) {
        case "CredentialsSignin":
          return "Ungültige Anmeldedaten.";
        default:
          return "Etwas ist schiefgelaufen.";
      }
    }
    throw error;
  }
}

Beim Logout-Button reicht eine simple Server Action:

<form action={async () => { "use server"; await signOut(); }}>
  <button type="submit">Abmelden</button>
</form>

OAuth-Provider hinzufügen

Auth.js v5 unterstützt über 80 OAuth-Provider. Für GitHub reicht ein Eintrag in auth.config.ts – wenn die Umgebungsvariablen AUTH_GITHUB_ID und AUTH_GITHUB_SECRET gesetzt sind, übernimmt Auth.js den Rest automatisch:

import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";

// In authConfig.providers:
providers: [
  GitHub,
  Google({ allowDangerousEmailAccountLinking: true }),
  // ... Credentials
]

Die allowDangerousEmailAccountLinking-Option verknüpft mehrere Provider mit derselben E-Mail. Klingt gefährlich – ist es auch, wenn ein Provider unverifizierte E-Mails ausgibt. Bei Google und GitHub sind die Adressen verifiziert, sodass das Risiko überschaubar bleibt. Bei eigenen Credentials-Flows mit unverifizierten Mails: Finger weg.

Auth.js v5 vs. Alternativen 2026

LibraryStärkenTrade-off
Auth.js v5Open Source, volle Kontrolle, kein Vendor-Lock-inSetup-Aufwand, Sicherheit liegt beim Team
ClerkPre-built UI, MFA out-of-the-boxKostenpflichtig ab 10k MAU
WorkOS / Supabase AuthEnterprise-SSO, SCIM, integrierte DBAnbieter-Bindung
Eigenes JWTMaximale FlexibilitätVolle Verantwortung für jede Sicherheitslücke

Für Projekte ohne Compliance-Sonderwünsche und mit etwas Engineering-Kapazität ist Auth.js v5 in den meisten Fällen die wirtschaftlich vernünftigste Wahl. Clerk ist klasse für Startups, die in der ersten Woche live gehen wollen – aber ab einer gewissen Grösse zahlt man sich dort dumm und dämlich.

FAQ – Häufige Fragen zu Auth.js v5 und Next.js 16

Heisst die Datei in Next.js 16 jetzt middleware.ts oder proxy.ts?

Ab Next.js 16 ist es proxy.ts. Die alte middleware.ts-Datei funktioniert übergangsweise noch, ist aber deprecated. Migration: einfach umbenennen und den Export von middleware in proxy ändern. Inhaltlich identisch.

Kann ich Auth.js v5 ohne Datenbank verwenden?

Ja. Mit session: { strategy: "jwt" } und ohne Adapter bleibt die Konfiguration komplett stateless. Sinnvoll für OAuth-only-Flows oder Prototypen. Sobald du E-Mail-Verifikation, MFA oder Account-Linking willst, ist ein Adapter Pflicht.

Warum bekomme ich Edge-Runtime-Fehler mit Prisma?

Weil auth.ts direkt in proxy.ts importiert wurde. Der Proxy läuft (in älteren Versionen) am Edge, wo Prismas Node-APIs fehlen. Die Lösung ist die Split-Config: auth.config.ts im Proxy, auth.ts nur in Server Components und Route Handlern.

Wie schütze ich Server Actions vor unauthorisierten Aufrufen?

Jede Server Action muss am Anfang verifySession() oder direkt auth() aufrufen. Server Actions sind im Auslieferungszustand öffentlich erreichbare POST-Endpoints – der Schutz im UI (Button ausblenden) reicht definitiv nicht.

Ist Auth.js v5 in Produktion stabil genug?

Ja. Trotz Beta-Label setzen es Hunderte von Produktionsanwendungen seit Mitte 2024 ein. Die Breaking Changes seit Beta 18 sind minimal, und die Maintainer veröffentlichen monatlich Patches. Für Greenfield-Projekte in 2026 ist v5 die richtige Wahl – v4 sollte nur noch für Wartungs-Migrationen relevant sein.

Fazit

Auth.js v5 mit Next.js 16 ist deutlich konsistenter als alles, was es davor gab – aber die Konfiguration verzeiht keine Abkürzungen. Wer die Split-Config respektiert, explizite Matcher verwendet und die Data-Access-Layer-Schicht wirklich ernst nimmt, bekommt ein Auth-System, das edge-schnell ist, sich sauber lokal entwickeln lässt und CVE-2025-29927-resistent bleibt.

Die zwei häufigsten Stolperfallen – falscher Import im Proxy und fehlende Auth-Checks in Server Actions – kosten ein paar Minuten Disziplin, sparen aber Wochen an Incident-Response. Und ehrlich: das ist ein Tausch, den man jederzeit machen sollte.

Über den Autor Editorial Team

Our team of expert writers and editors.