Auth & Sicherheit

Auth.js mit Claude Code: Authentifizierung für Next.js 2026

Auth.js v5 (NextAuth): OAuth-Provider, Credentials-Login, JWT-Sessions, Prisma-Adapter, Middleware-Schutz und Role-Based Access Control — vollständig mit Claude Code implementiert.

📅 6. Mai 2026 ⏱ 11 min Lesezeit 🔨 Auth.js v5 · Next.js 15 · TypeScript

Authentifizierung ist eine der häufigsten Anforderungen moderner Web-Apps — und eine der fehleranfälligsten, wenn sie manuell implementiert wird. Auth.js v5 (ehemals NextAuth.js) ist die De-facto-Standardlösung für Next.js-Projekte und bietet OAuth, Credentials, Session-Management und Datenbank-Integration in einem einzigen Package.

In diesem Guide zeigt Claude Code, wie du Auth.js v5 mit dem App Router von Next.js 15 integrierst — von der ersten Konfiguration bis zu rollenbasiertem Zugriff und Middleware-Schutz für deine Routen.

Was du in diesem Artikel lernst

1. Auth.js v5 Setup — auth.ts Konfiguration

Auth.js v5 nutzt einen neuen universellen Ansatz: Eine einzige auth.ts-Datei im Projektstamm enthält die gesamte Konfiguration und exportiert alle benötigten Hilfsfunktionen. Das vereinfacht die Integration mit dem App Router erheblich.

Installation

Terminal
npm install next-auth@beta npm install @auth/prisma-adapter prisma @prisma/client npm install bcryptjs npm install -D @types/bcryptjs # AUTH_SECRET generieren npx auth secret

auth.ts — Zentrale Konfigurationsdatei

auth.ts (Projektstamm)
import NextAuth from "next-auth" import { PrismaAdapter } from "@auth/prisma-adapter" import { prisma } from "@/lib/prisma" import GitHub from "next-auth/providers/github" import Google from "next-auth/providers/google" import Credentials from "next-auth/providers/credentials" export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: PrismaAdapter(prisma), session: { strategy: "jwt" }, pages: { signIn: "/auth/login", error: "/auth/error", signOut: "/auth/logout", }, providers: [ GitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }), Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }), Credentials({ // Credentials-Konfiguration: siehe Abschnitt 3 name: "Email & Passwort", credentials: { email: { label: "E-Mail", type: "email" }, password: { label: "Passwort", type: "password" }, }, authorize: async (credentials) => { // Implementierung: siehe Abschnitt 3 return null }, }), ], callbacks: { // JWT & Session Callbacks: siehe Abschnitt 4 async jwt({ token, user }) { if (user) token.role = user.role return token }, async session({ session, token }) { session.user.role = token.role as string return session }, }, })

Route Handler — app/api/auth/[...nextauth]/route.ts

app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth" // Auth.js v5: handlers exportiert GET + POST für den App Router export const { GET, POST } = handlers

Umgebungsvariablen (.env.local)

.env.local
# Auth.js v5 Secret (npx auth secret generiert diesen Wert) AUTH_SECRET="your-generated-secret-here" # GitHub OAuth App GITHUB_CLIENT_ID="ghp_..." GITHUB_CLIENT_SECRET="github_client_secret..." # Google OAuth 2.0 GOOGLE_CLIENT_ID="123456789.apps.googleusercontent.com" GOOGLE_CLIENT_SECRET="GOCSPX-..." # Datenbank (Prisma) DATABASE_URL="postgresql://user:pass@localhost:5432/myapp"
Claude Code Tipp:

Auth.js v5 liest AUTH_SECRET automatisch aus der Umgebung — du musst den Wert nicht manuell übergeben. Claude Code erkennt fehlendes AUTH_SECRET und schlägt automatisch die Generierung via npx auth secret vor.

2. OAuth Provider — GitHub & Google

OAuth ist der einfachste Weg, sichere Authentifizierung zu implementieren: Der Nutzer loggt sich bei einem vertrauenswürdigen Anbieter ein, du erhältst ein Token. Kein Passwort-Hashing, keine eigene Passwort-Reset-Logik, keine Brute-Force-Gefahr.

GitHub OAuth App erstellen

GitHub Developer Settings

Navigiere zu: GitHub → Settings → Developer settings → OAuth Apps → New OAuth App

Produktion: Callback-URL auf https://deine-domain.com/api/auth/callback/github ändern.

Google OAuth 2.0 Credentials

Google Cloud Console

Navigiere zu: console.cloud.google.com → APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID

Provider-Konfiguration mit Custom Profil-Mapping

auth.ts — Erweitertes Provider-Setup
import GitHub from "next-auth/providers/github" import Google from "next-auth/providers/google" // GitHub: Profil-Mapping mit zusätzlichen Feldern GitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, profile(profile) { return { id: profile.id.toString(), name: profile.name ?? profile.login, email: profile.email, image: profile.avatar_url, role: "user", // Default-Rolle für neue Nutzer username: profile.login, // GitHub-Benutzername speichern } }, }), // Google: Scope explizit definieren Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, authorization: { params: { scope: "openid email profile", prompt: "consent", access_type: "offline", // Refresh Token erhalten }, }, profile(profile) { return { id: profile.sub, name: profile.name, email: profile.email, image: profile.picture, role: "user", } }, }),

Login-Button Komponente

components/auth/OAuthButtons.tsx
"use client" import { signIn } from "next-auth/react" export function OAuthButtons() { return ( <div className="flex flex-col gap-3"> <button onClick={() => signIn("github", { callbackUrl: "/dashboard" })} className="flex items-center justify-center gap-2 w-full py-2.5 px-4 bg-gray-900 text-white rounded-lg hover:bg-gray-800 transition-colors" > <GitHubIcon /> Mit GitHub anmelden </button> <button onClick={() => signIn("google", { callbackUrl: "/dashboard" })} className="flex items-center justify-center gap-2 w-full py-2.5 px-4 bg-white text-gray-900 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors" > <GoogleIcon /> Mit Google anmelden </button> </div> ) }

3. Credentials Provider — Email & Passwort Login

Für Apps, die keine OAuth-Abhängigkeit wollen, bietet Auth.js den Credentials Provider. Wichtig: Mit Credentials-Login wird kein Account in der Datenbank angelegt (kein Adapter-Flow). Das Session-Management bleibt rein JWT-basiert.

Sicherheitshinweis:

Credentials Provider deaktiviert automatisch CSRF-Schutz für den Login-Endpunkt. Verwende HTTPS in Produktion und implementiere Rate-Limiting (z.B. mit @upstash/ratelimit).

Passwort-Hashing mit bcrypt

lib/auth/password.ts
import bcrypt from "bcryptjs" const SALT_ROUNDS = 12 export async function hashPassword(password: string): Promise<string> { return bcrypt.hash(password, SALT_ROUNDS) } export async function verifyPassword( password: string, hashedPassword: string ): Promise<boolean> { return bcrypt.compare(password, hashedPassword) } // Registrierung: Passwort validieren bevor es gehasht wird export function validatePasswordStrength(password: string): { valid: boolean errors: string[] } { const errors: string[] = [] if (password.length < 8) errors.push("Mindestens 8 Zeichen erforderlich") if (!/[A-Z]/.test(password)) errors.push("Mindestens ein Großbuchstabe erforderlich") if (!/[0-9]/.test(password)) errors.push("Mindestens eine Ziffer erforderlich") return { valid: errors.length === 0, errors } }

authorize() Funktion — vollständige Implementierung

auth.ts — Credentials Provider authorize()
Credentials({ name: "Email & Passwort", credentials: { email: { label: "E-Mail", type: "email" }, password: { label: "Passwort", type: "password" }, }, async authorize(credentials) { // 1. Eingabe validieren if (!credentials?.email || !credentials?.password) { return null } const email = credentials.email as string const password = credentials.password as string // 2. Nutzer in Datenbank suchen const user = await prisma.user.findUnique({ where: { email: email.toLowerCase().trim() }, select: { id: true, email: true, name: true, image: true, role: true, hashedPassword: true, emailVerified: true, }, }) // 3. Nutzer nicht gefunden → null (KEIN detaillierter Fehler!) if (!user || !user.hashedPassword) { return null } // 4. Email-Verifikation prüfen (optional, aber empfohlen) if (!user.emailVerified) { throw new Error("EMAIL_NOT_VERIFIED") } // 5. Passwort prüfen const passwordMatch = await verifyPassword(password, user.hashedPassword) if (!passwordMatch) { return null // Kein "falsches Passwort" — Timing-Attack-Schutz } // 6. User-Objekt zurückgeben (wird im JWT gespeichert) return { id: user.id, email: user.email, name: user.name, image: user.image, role: user.role, } }, }),

Registrierungs-API Route

app/api/auth/register/route.ts
import { NextResponse } from "next/server" import { prisma } from "@/lib/prisma" import { hashPassword, validatePasswordStrength } from "@/lib/auth/password" export async function POST(req: Request) { try { const { name, email, password } = await req.json() // Validierung const { valid, errors } = validatePasswordStrength(password) if (!valid) { return NextResponse.json({ errors }, { status: 400 }) } // Duplikat-Check const existing = await prisma.user.findUnique({ where: { email } }) if (existing) { return NextResponse.json( { error: "Diese E-Mail-Adresse ist bereits registriert" }, { status: 409 } ) } // Nutzer anlegen const user = await prisma.user.create({ data: { name, email: email.toLowerCase().trim(), hashedPassword: await hashPassword(password), role: "user", }, }) return NextResponse.json({ id: user.id, email: user.email }, { status: 201 }) } catch (error) { return NextResponse.json({ error: "Interner Serverfehler" }, { status: 500 }) } }

4. Session & JWT — Token-Erweiterung und TypeScript

Auth.js v5 verwendet standardmäßig JWT-Sessions (kein Datenbank-Lookup bei jedem Request). Über Callbacks kannst du das Token und die Session mit eigenen Feldern wie role oder userId anreichern.

TypeScript Module Augmentation

types/next-auth.d.ts
import "next-auth" import "next-auth/jwt" // Session-Typ erweitern: session.user.role + session.user.id declare module "next-auth" { interface Session { user: { id: string role: string email: string name: string | null image: string | null } } interface User { role: string } } // JWT-Token-Typ erweitern declare module "next-auth/jwt" { interface JWT { role: string userId: string } }

JWT & Session Callbacks

auth.ts — vollständige Callbacks
callbacks: { // JWT Callback: wird bei Login + Token-Refresh aufgerufen async jwt({ token, user, account, trigger, session }) { // Beim ersten Login: User-Daten in Token schreiben if (user) { token.userId = user.id token.role = user.role ?? "user" } // trigger: "update" = signierte Session-Aktualisierung via update() if (trigger === "update" && session?.role) { token.role = session.role } // OAuth: Provider-Token für API-Calls speichern (optional) if (account?.provider === "github") { token.githubAccessToken = account.access_token } return token }, // Session Callback: wird bei jedem auth()-/useSession()-Aufruf aufgerufen async session({ session, token }) { // Token-Daten in Session übertragen (KEIN Datenbank-Lookup!) session.user.id = token.userId session.user.role = token.role return session }, },

Session in Server Components und API Routes

app/dashboard/page.tsx (Server Component)
import { auth } from "@/auth" import { redirect } from "next/navigation" export default async function DashboardPage() { const session = await auth() if (!session) { redirect("/auth/login") } return ( <main> <h1>Willkommen, {session.user.name}!</h1> <p>Deine Rolle: <strong>{session.user.role}</strong></p> </main> ) }
Client Component — useSession()
"use client" import { useSession, signOut } from "next-auth/react" export function UserNav() { const { data: session, status } = useSession() if (status === "loading") return <Skeleton /> if (status === "unauthenticated") return <LoginButton /> return ( <div> <img src={session!.user.image ?? ""} alt="Avatar" /> <span>{session!.user.name}</span> <button onClick={() => signOut({ callbackUrl: "/" })}> Abmelden </button> </div> ) }

5. Middleware & Routenschutz

Auth.js v5 integriert sich direkt in Next.js Middleware. Damit kannst du Routen schützen, noch bevor die Seite gerendert wird — ohne jeden einzelnen Server Component absichern zu müssen.

middleware.ts — Grundkonfiguration

middleware.ts (Projektstamm)
import { auth } from "@/auth" import { NextResponse } from "next/server" // Auth-Middleware: auth() als Middleware-Handler verwenden export default auth((req) => { const { pathname } = req.nextUrl const isLoggedIn = !!req.auth // Öffentliche Pfade: immer erlauben const publicPaths = ["/", "/auth/login", "/auth/register", "/auth/error"] const isPublicPath = publicPaths.some(p => pathname === p) || pathname.startsWith("/api/auth") || pathname.startsWith("/_next") || pathname.startsWith("/images") if (isPublicPath) return NextResponse.next() // Nicht eingeloggt → Login-Seite if (!isLoggedIn) { const loginUrl = new URL("/auth/login", req.url) loginUrl.searchParams.set("callbackUrl", pathname) return NextResponse.redirect(loginUrl) } // Admin-Bereich: Rolle prüfen if (pathname.startsWith("/admin") && req.auth?.user?.role !== "admin") { return NextResponse.redirect(new URL("/403", req.url)) } return NextResponse.next() }) // Matcher: welche Pfade Middleware verarbeitet export const config = { matcher: [ // Alle Pfade außer statische Dateien und Next.js-Internals "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", ], }

Authorized Callback — Alternative Methode

auth.ts — authorized Callback (Alternative zu manuellem Middleware-Check)
callbacks: { // authorized: wird in der Middleware aufgerufen authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user const isOnDashboard = nextUrl.pathname.startsWith("/dashboard") const isOnAdmin = nextUrl.pathname.startsWith("/admin") if (isOnAdmin) { return auth?.user?.role === "admin" } if (isOnDashboard) { if (isLoggedIn) return true return false // → automatischer Redirect zu /auth/login } return true // Alle anderen Pfade erlauben }, // ... jwt + session callbacks },
Performance-Tipp:

Middleware läuft auf dem Edge Runtime — kein Node.js-Datenbankzugriff möglich. Alle nötigen Daten (z.B. role) müssen im JWT-Token gespeichert sein. Das vermeidet teure Datenbank-Roundtrips bei jedem Request.

6. Datenbank-Adapter & RBAC mit Prisma

Der Prisma-Adapter synchronisiert Auth.js-Sessions mit deiner Datenbank: Accounts (OAuth), Sessions und Verification Tokens werden automatisch gespeichert. Du erweiterst das User-Schema um eigene Felder wie role oder hashedPassword.

Prisma Schema

prisma/schema.prisma
generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // Rollen-Enum enum Role { user moderator admin } model User { id String @id @default(cuid()) name String? email String @unique emailVerified DateTime? image String? hashedPassword String? // NULL für OAuth-Nutzer role Role @default(user) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt accounts Account[] sessions Session[] } model Account { id String @id @default(cuid()) userId String type String provider String providerAccountId String refresh_token String? @db.Text access_token String? @db.Text expires_at Int? token_type String? scope String? id_token String? @db.Text session_state String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) } model Session { id String @id @default(cuid()) sessionToken String @unique userId String expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model VerificationToken { identifier String token String @unique expires DateTime @@unique([identifier, token]) }

RBAC — Rollenbasierter Zugriff

lib/auth/rbac.ts
import { auth } from "@/auth" import { redirect } from "next/navigation" type Role = "user" | "moderator" | "admin" const roleHierarchy: Record<Role, number> = { user: 0, moderator: 1, admin: 2, } // Prüft ob aktuelle Rolle mindestens die Ziellevel erreicht export function hasRole(userRole: string, requiredRole: Role): boolean { return (roleHierarchy[userRole as Role] ?? -1) >= roleHierarchy[requiredRole] } // Server Component Guard: Session prüfen + Rollen validieren export async function requireRole(requiredRole: Role) { const session = await auth() if (!session) { redirect("/auth/login") } if (!hasRole(session.user.role, requiredRole)) { redirect("/403") } return session } // Convenience-Guards für häufige Rollen export const requireAdmin = () => requireRole("admin") export const requireModerator = () => requireRole("moderator")

Admin Guard in Server Components

app/admin/page.tsx
import { requireAdmin } from "@/lib/auth/rbac" import { prisma } from "@/lib/prisma" export default async function AdminPage() { // Wirft Redirect wenn kein Admin → kein manueller check nötig const session = await requireAdmin() const users = await prisma.user.findMany({ select: { id: true, email: true, role: true, createdAt: true }, orderBy: { createdAt: "desc" }, take: 50, }) return ( <div> <h1>Admin-Bereich</h1> <p>Eingeloggt als: {session.user.email}</p> <UserTable users={users} /> </div> ) }

Rolle per API Route ändern

app/api/admin/users/[id]/role/route.ts
import { auth } from "@/auth" import { prisma } from "@/lib/prisma" import { NextResponse } from "next/server" import { hasRole } from "@/lib/auth/rbac" export async function PATCH( req: Request, { params }: { params: { id: string } } ) { const session = await auth() // API-Route: Admin-Check if (!session || !hasRole(session.user.role, "admin")) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }) } const { role } = await req.json() const validRoles = ["user", "moderator", "admin"] if (!validRoles.includes(role)) { return NextResponse.json({ error: "Ungültige Rolle" }, { status: 400 }) } const updated = await prisma.user.update({ where: { id: params.id }, data: { role }, select: { id: true, email: true, role: true }, }) return NextResponse.json(updated) }

Zusammenfassung: Auth.js v5 Feature-Übersicht

Feature Implementierung Aufwand
OAuth GitHub / Google Provider in auth.ts, Callback URLs setzen ~15 min
Credentials Email/Passwort authorize() + bcrypt, eigene Register-Route ~45 min
JWT Session jwt() + session() Callback, TypeScript Augmentation ~20 min
Middleware Routenschutz middleware.ts mit auth(), matcher config ~15 min
RBAC Rollen Prisma Adapter, requireRole(), role hierarchy ~30 min

Fazit: Auth.js v5 mit Claude Code

Auth.js v5 ist der einfachste Weg, robuste Authentifizierung für Next.js App Router zu implementieren. Die neue auth.ts-Konfiguration vereinfacht das Setup erheblich: eine Datei, alle Provider, alle Callbacks, alle Exports.

Claude Code beschleunigt die Implementierung, indem es nicht nur Boilerplate generiert, sondern auch Sicherheits-Best-Practices (bcrypt-Rounds, Timing-Attack-Schutz, kein detaillierter Fehler beim Login) automatisch einbaut. Das Ergebnis: eine sichere, typsichere Authentifizierungslösung in unter zwei Stunden.

Checkliste vor dem Deploy

Auth.js mit Claude Code implementieren

Sichere Next.js-Authentifizierung in wenigen Stunden statt Tagen — mit KI-Unterstützung die Best Practices und Sicherheitspatterns automatisch einbaut.

Kostenlos testen — 14 Tage Trial