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
- OAuth GitHub & Google Provider einrichten
- Credentials Email/Passwort-Login mit bcrypt absichern
- JWT Session-Callbacks und Token-Erweiterung
- RBAC Rollenbasierter Zugriff mit Prisma-Adapter
- Middleware Routen automatisch schützen
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
- Application name: Dein App-Name
- Homepage URL:
http://localhost:3000 (Entwicklung)
- Authorization callback URL:
http://localhost:3000/api/auth/callback/github
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
- Application type: Web application
- Authorized redirect URIs:
http://localhost:3000/api/auth/callback/google
- Scopes:
email, profile (Standard)
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_SECRET gesetzt und mindestens 32 Zeichen lang
- HTTPS in Produktion (Auth.js-Cookies sind Secure by Default)
- Callback-URLs in GitHub/Google OAuth Apps auf Produktions-Domain gesetzt
- Prisma Migrations ausgeführt (
npx prisma migrate deploy)
- Rate-Limiting für
/api/auth/register und Login implementiert
- E-Mail-Verifikation aktiviert (falls Credentials Provider genutzt)
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