🔐
OAuth Providers
GitHub, Google, Discord & 60+ weitere sofort einsatzbereit
⚡
Edge Runtime
Auth.js v5 läuft nativ auf Vercel Edge & Cloudflare Workers
🗄️
Database Adapter
Prisma, Drizzle, MongoDB — persistente Sessions out-of-the-box
🛡️
Middleware Guard
Route-Protection auf Edge-Level, bevor der Server antwortet
⚠️ Breaking Changes v4 → v5: Auth.js v5 ist KEINE Drop-in-Ersetzung für NextAuth v4. Die Konfiguration wurde komplett überarbeitet. Claude Code kennt alle Unterschiede und migriert deinen Code sicher.
Warum Auth.js v5 + Claude Code so gut harmonieren
Authentication ist einer der Bereiche wo Fehler am teuersten sind — eine falsch konfigurierte Session kann tausende User-Accounts kompromittieren. Auth.js hat sich als Standard für Next.js etabliert, weil es sichere Defaults mitbringt. Claude Code kennt die gesamte Dokumentation, alle Provider-Eigenheiten und die häufigsten Konfigurationsfehler.
| Feature | NextAuth v4 | Auth.js v5 NEU |
| Konfigurationsdatei | [...nextauth].ts | auth.ts + middleware.ts |
| Edge Runtime | ❌ Nicht unterstützt | ✅ Nativ |
| Server Components | Eingeschränkt | auth() direkt nutzbar |
| Framework-Support | Nur Next.js | Next.js, SvelteKit, Express, Qwik |
| Callbacks | callbacks.session | callbacks.jwt + callbacks.session vereinfacht |
| TypeScript | Eingeschränkt | Vollständig typisiert |
1. Setup und Konfiguration
Der Einstieg beginnt mit dem AUTH_SECRET und der zentralen auth.ts-Datei. Claude Code erledigt die komplette Boilerplate — du beschreibst nur welche Provider und welches Datenbankschema du brauchst.
Initialisierung mit Claude Code
Setup
AUTH_SECRET
# Prompt für Claude Code:
# "Richte Auth.js v5 in meinem Next.js 14 App Router Projekt ein.
# Ich brauche GitHub + Google OAuth und einen Credentials Provider.
# Nutze Prisma als Database Adapter. Erstelle alle nötigen Dateien."
# Schritt 1: Installation
npm install next-auth@beta @auth/prisma-adapter
# AUTH_SECRET generieren (NIEMALS hardcoden!)
npx auth secret
# → Fügt automatisch AUTH_SECRET=... in .env.local ein
# .env.local (von Claude Code generiert):
AUTH_SECRET="dein-generierter-secret-32-bytes-minimum"
AUTH_GITHUB_ID="deine-github-client-id"
AUTH_GITHUB_SECRET="dein-github-client-secret"
AUTH_GOOGLE_ID="deine-google-client-id"
AUTH_GOOGLE_SECRET="dein-google-client-secret"
DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"
auth.ts — Die zentrale Konfigurationsdatei
Konfiguration
Session
// auth.ts — Projektroot (NICHT in /app oder /pages!)
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
import Google from "next-auth/providers/google"
import Credentials from "next-auth/providers/credentials"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"
import bcrypt from "bcryptjs"
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
// Session-Strategie: "database" für persistente Sessions mit Prisma
session: { strategy: "database" },
providers: [
GitHub(), // liest AUTH_GITHUB_ID + AUTH_GITHUB_SECRET automatisch
Google(), // liest AUTH_GOOGLE_ID + AUTH_GOOGLE_SECRET automatisch
Credentials({
// Credentials Provider — siehe Abschnitt 3
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Passwort", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null
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
)
return valid ? user : null
},
}),
],
callbacks: {
// Session mit zusätzlichen User-Feldern anreichern
async session({ session, user }) {
session.user.id = user.id
session.user.role = user.role // Custom-Feld aus DB
return session
},
},
pages: {
signIn: "/auth/login", // Custom Login-Seite
error: "/auth/error", // Fehlerseite
},
})
// Route Handler für /api/auth/[...nextauth]
// app/api/auth/[...nextauth]/route.ts:
// export { handlers as GET, handlers as POST } from "@/auth"
Claude Code Tipp: Sage Claude Code "Erstelle den Route Handler und typisiere die Session so dass session.user.id und session.user.role TypeScript-sicher sind." — es erweitert automatisch next-auth.d.ts mit den richtigen Interface-Erweiterungen.
2. OAuth Providers: GitHub und Google
Auth.js v5 liefert über 60 vorkonfigurierte Provider. Die meisten brauchen nur Client ID und Secret — Claude Code konfiguriert Profile-Callbacks, Avatar-Mapping und Custom-Claims ohne dass du die Provider-Dokumentation lesen musst.
GitHub Provider mit Profile-Customization
OAuth
GitHub
// Erweiterter GitHub Provider mit Scope + Profile-Mapping
// Prompt: "GitHub OAuth mit Zugriff auf Email (auch private) und
// Organisations-Membership prüfen. User-Role auf 'admin' setzen
// wenn GitHub-Username in ADMIN_USERS env-Variable ist."
import GitHub from "next-auth/providers/github"
GitHub({
// Zusätzliche Scopes für private Email-Adressen
authorization: {
params: {
scope: "read:user user:email read:org",
},
},
// Profile-Callback: GitHub-Antwort → Auth.js User-Objekt
async profile(githubProfile) {
const adminUsers = (process.env.ADMIN_USERS ?? "").split(",")
return {
id: githubProfile.id.toString(),
name: githubProfile.name ?? githubProfile.login,
email: githubProfile.email,
image: githubProfile.avatar_url,
username: githubProfile.login,
// Custom-Feld: automatisch Admin wenn in Whitelist
role: adminUsers.includes(githubProfile.login) ? "admin" : "user",
}
},
})
Google Provider mit HD-Domain-Restriction
OAuth
Google
// Google OAuth — nur für bestimmte Workspace-Domain (B2B-Usecase)
// Prompt: "Google OAuth, nur Accounts mit @meinefirma.de erlaubt.
// Andere Domains → Redirect auf /auth/error?error=EmailDomain"
import Google from "next-auth/providers/google"
Google({
authorization: {
params: {
prompt: "consent",
access_type: "offline", // Refresh Token erhalten
response_type: "code",
// hd = Hosted Domain: zeigt nur Accounts der Domain
hd: "meinefirma.de",
},
},
async profile(googleProfile) {
return {
id: googleProfile.sub,
name: googleProfile.name,
email: googleProfile.email,
image: googleProfile.picture,
emailVerified: googleProfile.email_verified,
}
},
})
// signIn-Callback für serverseitige Domain-Validierung:
callbacks: {
async signIn({ user, account, profile }) {
if (account?.provider === "google") {
const allowed = user.email?.endsWith("@meinefirma.de")
if (!allowed) return "/auth/error?error=EmailDomain"
}
return true
},
}
Claude Code Tipp: "Erkläre mir den Unterschied zwischen hd-Parameter und signIn-Callback für Domain-Restriction." — Claude erklärt warum hd nur das Login-UI einschränkt, der Callback aber serverseitig validiert und damit sicherheitskritisch ist.
Account-Linking und Duplicate-Email-Handling
OAuth
Account Linking
// Problem: User loggt sich mit GitHub ein (email@test.de)
// Danach versucht er Google mit derselben Email → Fehler!
// Prompt: "Implementiere Account-Linking: gleiche Email über
// verschiedene Provider zusammenführen."
callbacks: {
async signIn({ user, account }) {
if (!account || !user.email) return true
// Prüfe ob Email bereits existiert mit anderem Provider
const existingUser = await prisma.user.findUnique({
where: { email: user.email },
include: { accounts: true },
})
if (existingUser) {
const hasThisProvider = existingUser.accounts.some(
(a) => a.provider === account.provider
)
if (!hasThisProvider) {
// Account verknüpfen statt neuen User anlegen
await prisma.account.create({
data: {
userId: existingUser.id,
type: account.type,
provider: account.provider,
providerAccountId: account.providerAccountId,
access_token: account.access_token,
refresh_token: account.refresh_token,
},
})
return true
}
}
return true
},
}
3. Credentials Provider: Email + Passwort
Der Credentials Provider ermöglicht klassische Email/Passwort-Authentifizierung. Auth.js empfiehlt OAuth für die meisten Fälle — aber wenn du eigene Passwörter brauchst, setzt Claude Code bcrypt, Timing-Attack-Schutz und eine saubere Login-Form auf.
⚠️ Wichtig: Credentials Provider erzwingt session: { strategy: "jwt" } wenn du keinen Database Adapter nutzt. Mit Prisma Adapter kannst du auch "database" nutzen — aber dann musst du User-IDs manuell in den JWT mappen.
Credentials Provider mit bcrypt
Credentials
bcrypt
// Prompt: "Credentials Provider mit bcrypt, Timing-Attack-Schutz
// und Zod-Validation für die Eingaben."
import { z } from "zod"
import bcrypt from "bcryptjs"
const credentialsSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Passwort", type: "password" },
},
async authorize(credentials) {
// Zod-Validation — niemals raw user input vertrauen
const parsed = credentialsSchema.safeParse(credentials)
if (!parsed.success) return null
const { email, password } = parsed.data
const user = await prisma.user.findUnique({
where: { email },
})
// Timing-Attack-Schutz: auch wenn User nicht existiert vergleichen
const dummyHash = "$2b$12$invalid.hash.for.timing.safety.xxx"
const passwordMatch = await bcrypt.compare(
password,
user?.passwordHash ?? dummyHash
)
if (!user || !passwordMatch) {
// IMMER denselben Fehler — keine Info über "Email nicht gefunden"
throw new Error("CredentialsSignin")
}
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
role: user.role,
}
},
})
Custom Login Form mit Server Actions
Credentials
Server Actions
// app/auth/login/page.tsx — Login-Formular mit signIn()
// Prompt: "Erstelle eine Login-Seite mit Server Action, die signIn()
// aus auth.ts aufruft und Fehler schön anzeigt."
"use client"
import { signIn } from "next-auth/react"
import { useState } from "react"
import { useRouter } from "next/navigation"
export default function LoginPage() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const router = useRouter()
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setLoading(true)
setError(null)
const form = new FormData(e.currentTarget)
const result = await signIn("credentials", {
email: form.get("email"),
password: form.get("password"),
redirect: false, // Kein automatischer Redirect
})
if (result?.error) {
setError("E-Mail oder Passwort falsch.")
// NIEMALS den originalen error weitergeben — Info-Leak!
} else {
router.push("/dashboard")
}
setLoading(false)
}
return (
<form onSubmit={handleSubmit}>
{error && <p className="error">{error}</p>}
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit" disabled={loading}>
{loading ? "Anmelden..." : "Anmelden"}
</button>
// OAuth-Buttons zusätzlich
<button type="button" onClick={() => signIn("github")}>
Mit GitHub anmelden
</button>
<button type="button" onClick={() => signIn("google")}>
Mit Google anmelden
</button>
</form>
)
}
4. Session Management
Auth.js v5 revolutioniert das Session-Handling in Next.js. Die neue auth()-Funktion funktioniert in Server Components, API Routes und Middleware — kein getServerSession mehr nötig. Claude Code wählt automatisch den richtigen Ansatz je nach Context.
useSession in Client Components
Client
useSession
// Client Component — Session für UI-Rendering
// Prompt: "Zeige Avatar und Name wenn eingeloggt, Login-Button sonst.
// Lade-State während Session-Check abgedeckt."
"use client"
import { useSession, signIn, signOut } from "next-auth/react"
export function UserNav() {
const { data: session, status } = useSession()
if (status === "loading") {
return <div className="animate-pulse w-8 h-8 rounded-full bg-gray-200" />
}
if (status === "unauthenticated") {
return <button onClick={() => signIn()}>Anmelden</button>
}
return (
<div className="flex items-center gap-3">
<img
src={session.user.image ?? "/default-avatar.png"}
alt={session.user.name ?? "User"}
className="w-8 h-8 rounded-full"
/>
<span>{session.user.name}</span>
<button onClick={() => signOut({ callbackUrl: "/" })}>
Abmelden
</button>
</div>
)
}
// SessionProvider in layout.tsx:
// <SessionProvider session={session}>
// {children}
// </SessionProvider>
auth() in Server Components — die neue Art
Server Component
auth()
// Server Component — direkte auth() ohne getServerSession
// Das ist die Auth.js v5 Hauptneuerung!
// Prompt: "Dashboard-Page nur für eingeloggte User. Zeige
// personalisierten Content aus DB basierend auf user.id."
import { auth } from "@/auth"
import { redirect } from "next/navigation"
import { prisma } from "@/lib/prisma"
export default async function DashboardPage() {
// auth() = kein Hook, kein Context — direkt in async Server Component
const session = await auth()
if (!session) {
redirect("/auth/login")
}
// User-Daten direkt aus DB laden (session.user.id dank Callback-Setup)
const userData = await prisma.user.findUnique({
where: { id: session.user.id },
include: { subscriptions: true, projects: true },
})
return (
<main>
<h1>Willkommen, {session.user.name}!</h1>
<p>Role: {session.user.role}</p>
<p>Projekte: {userData?.projects.length}</p>
</main>
)
}
// In API Routes (app/api/user/route.ts):
export async function GET() {
const session = await auth()
if (!session) return new Response("Unauthorized", { status: 401 })
return Response.json({ user: session.user })
}
Performance-Tipp: auth() in Server Components macht automatisch Session-Caching pro Request. Du kannst es mehrfach in einer Page-Komponente und ihren Children aufrufen — Auth.js dedupliziert die DB-Anfrage. Claude Code erklärt dir das wenn du fragst "Ist es teuer auth() mehrfach aufzurufen?"
5. Middleware Route Protection
Die Middleware in Auth.js v5 läuft auf Edge Runtime — sie schützt Routen bevor der Next.js Server überhaupt antwortet. Das ist performanter und sicherer als serverseitiger Redirect in jeder Page-Komponente.
middleware.ts — Routen absichern
Middleware
Matcher
// middleware.ts — im Projektroot
// Prompt: "Schütze /dashboard/*, /api/protected/* und /admin/*.
// /admin nur für role='admin'. API-Routen geben 401 JSON zurück,
// Pages machen Redirect auf /auth/login."
import { auth } from "@/auth"
import { NextResponse } from "next/server"
export default auth(function middleware(req) {
const { nextUrl, auth: session } = req
const isLoggedIn = !!session
const pathname = nextUrl.pathname
// API-Routen: JSON 401 statt Redirect
if (pathname.startsWith("/api/protected")) {
if (!isLoggedIn) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
)
}
return NextResponse.next()
}
// Admin-Bereich: zusätzliche Role-Prüfung
if (pathname.startsWith("/admin")) {
if (!isLoggedIn || session.user.role !== "admin") {
return NextResponse.redirect(new URL("/auth/unauthorized", req.url))
}
}
// Dashboard: nur Auth prüfen
if (pathname.startsWith("/dashboard") && !isLoggedIn) {
const loginUrl = new URL("/auth/login", req.url)
loginUrl.searchParams.set("callbackUrl", pathname)
return NextResponse.redirect(loginUrl)
}
return NextResponse.next()
})
// Welche Routen durchlaufen die Middleware?
export const config = {
matcher: [
// Dashboard und Admin absichern
"/dashboard/:path*",
"/admin/:path*",
// API-Routen absichern (nicht /api/auth/!)
"/api/protected/:path*",
// Statische Dateien und next internals überspringen
"/((?!_next/static|_next/image|favicon.ico).*)",
],
}
Callback-URL und Post-Login-Redirect
Middleware
Redirect
// Auth-Seiten (login/register) für eingeloggte User umleiten
// Prompt: "Wenn eingeloggter User /auth/login aufruft → Redirect
// zu /dashboard. Callback-URL aus Query-Parameter respektieren."
export default auth(function middleware(req) {
const { nextUrl, auth: session } = req
const isLoggedIn = !!session
const isAuthPage = nextUrl.pathname.startsWith("/auth")
if (isAuthPage) {
if (isLoggedIn) {
// Eingeloggt auf Auth-Seite → weiterleiten
const callbackUrl = nextUrl.searchParams.get("callbackUrl")
return NextResponse.redirect(
new URL(callbackUrl ?? "/dashboard", req.url)
)
}
return NextResponse.next()
}
// Geschützte Routen...
if (!isLoggedIn && nextUrl.pathname.startsWith("/dashboard")) {
const loginUrl = new URL("/auth/login", req.url)
// Original-URL als callbackUrl mitgeben
loginUrl.searchParams.set("callbackUrl", nextUrl.pathname)
return NextResponse.redirect(loginUrl)
}
})
Sicherheitswarnung: Validiere callbackUrl immer gegen eine Allowlist! Ein Open-Redirect-Angriff kann den User nach dem Login auf eine Phishing-Seite leiten. Claude Code fügt diese Prüfung automatisch ein wenn du "sichere callbackUrl" in deinem Prompt erwähnst.
6. Database Adapter mit Prisma
Ohne Adapter speichert Auth.js Sessions nur im JWT-Cookie — bei Logout oder Token-Rotation sind sie nicht invalidierbar. Der Prisma Adapter speichert User, Accounts, Sessions und Verification Tokens in deiner Datenbank. Claude Code generiert das komplette Schema.
Prisma Schema für Auth.js v5
Database
Prisma
// schema.prisma — von Claude Code generiert
// Prompt: "Erstelle das Prisma-Schema für Auth.js v5 Adapter.
// Ergänze User um: role (String), passwordHash (String optional),
// createdAt, updatedAt. Nutze PostgreSQL."
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
// Pflicht-Model für Auth.js — NIEMALS umbenennen!
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
image String?
// Custom-Felder (Auth.js kompatibel)
role String @default("user")
passwordHash String? // Nur für Credentials-Provider
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])
}
Prisma Adapter einbinden + TypeScript-Typen
Database
TypeScript
// lib/prisma.ts — Singleton Pattern (PFLICHT für Next.js!)
import { PrismaClient } from "@prisma/client"
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["query"] : [],
})
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma
}
// next-auth.d.ts — TypeScript-Erweiterung für Custom-Felder
// Prompt: "Typisiere Session so dass user.id, user.role
// und user.username überall TypeScript-sicher sind."
import NextAuth from "next-auth"
declare module "next-auth" {
interface User {
role?: string
username?: string
passwordHash?: string
}
interface Session {
user: User & {
id: string
role: string
}
}
}
declare module "next-auth/jwt" {
interface JWT {
id?: string
role?: string
}
}
User-Registrierung + Passwort-Hashing
Database
bcrypt
Server Action
// app/auth/register/actions.ts — Server Action für Registrierung
// Prompt: "Server Action für Registrierung: Email-Validierung,
// Passwort bcrypt-hashen, User in DB anlegen, direkt einloggen."
"use server"
import { z } from "zod"
import bcrypt from "bcryptjs"
import { prisma } from "@/lib/prisma"
import { signIn } from "@/auth"
const registerSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
password: z.string().min(8).regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
"Mindestens 1 Großbuchstabe, 1 Kleinbuchstabe, 1 Zahl"
),
})
export async function registerUser(formData: FormData) {
const data = Object.fromEntries(formData)
const parsed = registerSchema.safeParse(data)
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors }
}
const { name, email, password } = parsed.data
// Prüfe ob Email bereits existiert
const existing = await prisma.user.findUnique({ where: { email } })
if (existing) return { error: { email: ["Email bereits registriert"] } }
// bcrypt mit cost factor 12 (empfohlen für 2026)
const passwordHash = await bcrypt.hash(password, 12)
await prisma.user.create({
data: { name, email, passwordHash },
})
// Direkt einloggen nach Registrierung
await signIn("credentials", {
email, password,
redirectTo: "/dashboard",
})
}
Migrations-Tipp: Nach Schema-Änderungen einfach Claude Code sagen: "Erstelle und führe die Prisma-Migration aus." Es generiert prisma migrate dev --name add-auth-tables und erklärt was jede Tabelle macht.
Claude Code für dein nächstes Next.js-Projekt?
Authentication, API-Routen, Datenbankschemas — Claude Code kennt alle Next.js 14 Best Practices und implementiert sauber, sicher und typsicher.
14 Tage kostenlos — kein Kreditkarte nötig →
Auth.js v5 + Claude Code: Das Fazit
Authentication ist der Teil eines Projekts der am häufigsten falsch gemacht wird — und wo Fehler am teuersten sind. Auth.js v5 liefert sichere Defaults, Claude Code kennt alle Konfigurationsoptionen.
- ✓ Setup in Minuten statt Stunden —
auth.ts, Prisma-Schema, Environment-Variablen in einem Prompt
- ✓ OAuth ohne Provider-Dokumentation — GitHub, Google, Discord und 60+ weitere vorkonfiguriert
- ✓ Sichere Credentials-Implementierung — bcrypt, Timing-Attack-Schutz, Zod-Validation automatisch
- ✓ TypeScript-sicher von Anfang an — Session-Typen, JWT-Typen, Custom-Felder korrekt deklariert
- ✓ Edge-kompatible Middleware — Route-Protection auf Edge-Level, kein Server-Round-Trip
- ✓ Proaktive Sicherheitswarnungen — Claude Code warnt bevor du unsichere Patterns implementierst
Nächster Schritt: Hast du bereits ein Next.js-Projekt mit Auth.js v4? Sage Claude Code: "Migriere meine NextAuth v4 Konfiguration auf Auth.js v5. Zeige mir alle Breaking Changes." — es erstellt einen vollständigen Migrations-Plan.