# Authentication Patterns für moderne Apps **Meta-Description:** Moderne Authentication Patterns. JWT, Session Auth, OAuth 2.0, Passkeys und Auth.js Integration für Next.js. **Keywords:** Authentication, JWT, OAuth, Passkeys, Session Auth, Auth.js, NextAuth, Security, OIDC --- ## Einführung Authentication ist die erste Verteidigungslinie jeder Anwendung. 2026 stehen mehrere Patterns zur Verfügung: **JWT, Sessions, OAuth, Passkeys** – jedes mit eigenen Trade-offs für Security und UX. --- ## Auth Patterns Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ AUTHENTICATION PATTERNS │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Session-Based (Stateful): │ │ ├── Server speichert Session │ │ ├── Cookie mit Session ID │ │ ├── Einfach zu invalidieren │ │ └── Best for: Traditional Web Apps │ │ │ │ JWT (Stateless): │ │ ├── Self-contained Token │ │ ├── Keine Server-Speicherung │ │ ├── Schwer zu invalidieren │ │ └── Best for: APIs, Microservices │ │ │ │ OAuth 2.0 / OIDC: │ │ ├── Delegierte Authentifizierung │ │ ├── Google, GitHub, etc. │ │ ├── Refresh Token Flow │ │ └── Best for: Social Login │ │ │ │ Passkeys (WebAuthn): │ │ ├── Passwordless │ │ ├── Biometric/Device-based │ │ ├── Phishing-resistant │ │ └── Best for: High Security, Modern UX │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Auth.js (NextAuth) v5 Setup ```bash npm install next-auth@beta ``` ```typescript // auth.ts 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 { z } from 'zod'; import bcrypt from 'bcryptjs'; export const { handlers, signIn, signOut, auth } = NextAuth({ adapter: PrismaAdapter(prisma), providers: [ GitHub({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET! }), Google({ clientId: process.env.GOOGLE_ID!, clientSecret: process.env.GOOGLE_SECRET! }), Credentials({ credentials: { email: { label: 'Email', type: 'email' }, password: { label: 'Password', 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; const user = await prisma.user.findUnique({ where: { email: parsed.data.email } }); if (!user?.password) return null; const valid = await bcrypt.compare( parsed.data.password, user.password ); if (!valid) return null; return { id: user.id, email: user.email, name: user.name }; } }) ], session: { strategy: 'jwt' // oder 'database' }, callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; token.role = user.role; } return token; }, async session({ session, token }) { session.user.id = token.id as string; session.user.role = token.role as string; return session; } }, pages: { signIn: '/login', error: '/login' } }); ``` ```typescript // app/api/auth/[...nextauth]/route.ts import { handlers } from '@/auth'; export const { GET, POST } = handlers; // middleware.ts import { auth } from '@/auth'; export default auth((req) => { if (!req.auth && req.nextUrl.pathname.startsWith('/dashboard')) { return Response.redirect(new URL('/login', req.nextUrl)); } }); export const config = { matcher: ['/dashboard/:path*', '/api/protected/:path*'] }; ``` --- ## JWT Implementation (Custom) ```typescript // lib/jwt.ts import { SignJWT, jwtVerify } from 'jose'; const secret = new TextEncoder().encode(process.env.JWT_SECRET!); interface TokenPayload { userId: string; email: string; role: string; } // Access Token (kurze Lebenszeit) export async function createAccessToken(payload: TokenPayload) { return new SignJWT(payload) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('15m') // 15 Minuten .sign(secret); } // Refresh Token (lange Lebenszeit) export async function createRefreshToken(userId: string) { return new SignJWT({ userId }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('7d') // 7 Tage .sign(secret); } export async function verifyToken(token: string) { try { const { payload } = await jwtVerify(token, secret); return payload as TokenPayload & { exp: number }; } catch { return null; } } // Token Rotation Pattern export async function refreshTokens(refreshToken: string) { const payload = await verifyToken(refreshToken); if (!payload) throw new Error('Invalid refresh token'); // Optional: Refresh Token im DB invalidieren (Rotation) await db.refreshToken.delete({ where: { token: refreshToken } }); const user = await db.user.findUnique({ where: { id: payload.userId } }); if (!user) throw new Error('User not found'); const newAccessToken = await createAccessToken({ userId: user.id, email: user.email, role: user.role }); const newRefreshToken = await createRefreshToken(user.id); // Neuen Refresh Token speichern await db.refreshToken.create({ data: { token: newRefreshToken, userId: user.id, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) } }); return { accessToken: newAccessToken, refreshToken: newRefreshToken }; } ``` --- ## Passkeys (WebAuthn) ```typescript // lib/passkeys.ts import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server'; const rpName = 'My App'; const rpID = 'example.com'; const origin = 'https://example.com'; // Registration export async function startPasskeyRegistration(userId: string, email: string) { const existingCredentials = await db.credential.findMany({ where: { userId } }); const options = await generateRegistrationOptions({ rpName, rpID, userID: userId, userName: email, attestationType: 'none', excludeCredentials: existingCredentials.map(c => ({ id: c.credentialId, type: 'public-key' })), authenticatorSelection: { residentKey: 'preferred', userVerification: 'preferred' } }); // Challenge speichern await db.user.update({ where: { id: userId }, data: { currentChallenge: options.challenge } }); return options; } export async function finishPasskeyRegistration( userId: string, response: RegistrationResponseJSON ) { const user = await db.user.findUnique({ where: { id: userId } }); if (!user?.currentChallenge) throw new Error('No challenge'); const verification = await verifyRegistrationResponse({ response, expectedChallenge: user.currentChallenge, expectedOrigin: origin, expectedRPID: rpID }); if (verification.verified && verification.registrationInfo) { await db.credential.create({ data: { userId, credentialId: verification.registrationInfo.credentialID, publicKey: Buffer.from(verification.registrationInfo.credentialPublicKey), counter: verification.registrationInfo.counter } }); } return verification.verified; } // Authentication export async function startPasskeyAuth(email: string) { const user = await db.user.findUnique({ where: { email }, include: { credentials: true } }); if (!user) throw new Error('User not found'); const options = await generateAuthenticationOptions({ rpID, allowCredentials: user.credentials.map(c => ({ id: c.credentialId, type: 'public-key' })), userVerification: 'preferred' }); await db.user.update({ where: { id: user.id }, data: { currentChallenge: options.challenge } }); return options; } export async function finishPasskeyAuth( email: string, response: AuthenticationResponseJSON ) { const user = await db.user.findUnique({ where: { email }, include: { credentials: true } }); if (!user?.currentChallenge) throw new Error('No challenge'); const credential = user.credentials.find( c => c.credentialId === response.id ); if (!credential) throw new Error('Unknown credential'); const verification = await verifyAuthenticationResponse({ response, expectedChallenge: user.currentChallenge, expectedOrigin: origin, expectedRPID: rpID, authenticator: { credentialID: credential.credentialId, credentialPublicKey: credential.publicKey, counter: credential.counter } }); if (verification.verified) { // Counter updaten await db.credential.update({ where: { id: credential.id }, data: { counter: verification.authenticationInfo.newCounter } }); return user; } throw new Error('Verification failed'); } ``` ```typescript // Client-Side (React) 'use client'; import { startRegistration, startAuthentication } from '@simplewebauthn/browser'; function PasskeyLogin() { const handleRegister = async () => { const options = await fetch('/api/passkey/register/start', { method: 'POST' }).then(r => r.json()); const result = await startRegistration(options); await fetch('/api/passkey/register/finish', { method: 'POST', body: JSON.stringify(result) }); }; const handleLogin = async () => { const options = await fetch('/api/passkey/login/start', { method: 'POST', body: JSON.stringify({ email }) }).then(r => r.json()); const result = await startAuthentication(options); await fetch('/api/passkey/login/finish', { method: 'POST', body: JSON.stringify(result) }); }; return (
); } ``` --- ## Magic Links ```typescript // Magic Link / Email Auth import { Resend } from 'resend'; import crypto from 'crypto'; const resend = new Resend(process.env.RESEND_API_KEY); export async function sendMagicLink(email: string) { const token = crypto.randomBytes(32).toString('hex'); const expires = new Date(Date.now() + 15 * 60 * 1000); // 15 min await db.verificationToken.create({ data: { identifier: email, token: await hash(token), expires } }); const url = `${process.env.NEXTAUTH_URL}/api/auth/verify?token=${token}&email=${email}`; await resend.emails.send({ from: 'noreply@example.com', to: email, subject: 'Login to My App', html: `

Click the link below to sign in:

Sign in to My App

This link expires in 15 minutes.

` }); } export async function verifyMagicLink(token: string, email: string) { const storedToken = await db.verificationToken.findFirst({ where: { identifier: email, expires: { gt: new Date() } } }); if (!storedToken || !await verify(token, storedToken.token)) { throw new Error('Invalid or expired token'); } await db.verificationToken.delete({ where: { id: storedToken.id } }); const user = await db.user.upsert({ where: { email }, update: { emailVerified: new Date() }, create: { email, emailVerified: new Date() } }); return user; } ``` --- ## Security Best Practices ```typescript // 1. Password Hashing import bcrypt from 'bcryptjs'; const SALT_ROUNDS = 12; export async function hashPassword(password: string) { return bcrypt.hash(password, SALT_ROUNDS); } // 2. CSRF Protection (automatisch bei Auth.js) // 3. Secure Cookie Settings const cookieOptions = { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const, path: '/', maxAge: 60 * 60 * 24 * 7 // 7 Tage }; // 4. Rate Limiting für Auth Endpoints // Siehe API Design Patterns Artikel // 5. Account Lockout async function checkLoginAttempts(email: string) { const attempts = await db.loginAttempt.count({ where: { email, success: false, createdAt: { gt: new Date(Date.now() - 15 * 60 * 1000) } } }); if (attempts >= 5) { throw new Error('Account temporarily locked. Try again later.'); } } ``` --- ## Fazit Authentication 2026: 1. **Auth.js v5**: Standard für Next.js 2. **Passkeys**: Die Zukunft des Logins 3. **JWT + Refresh**: Für APIs 4. **MFA**: Immer aktivieren wenn möglich Wähle basierend auf Security-Anforderungen und UX. --- ## Bildprompts 1. "Multiple authentication methods merging into single secure access, login concept" 2. "Passkey biometric authentication on device, passwordless future" 3. "Security layers protecting user identity, authentication shield" --- ## Quellen - [Auth.js Documentation](https://authjs.dev/) - [WebAuthn Guide](https://webauthn.guide/) - [SimpleWebAuthn](https://simplewebauthn.dev/) - [OWASP Authentication Cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)