1 min read
TechnologieAuthentication Patterns für moderne Apps
Moderne Authentication Patterns. JWT, Session Auth, OAuth 2.0, Passkeys und Auth.js Integration für Next.js.
AuthenticationJWTOAuthPasskeysSession AuthAuth.js

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
npm install next-auth@beta// 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'
}
});// 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)
// 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)
// 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');
}// 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 (
<div>
<button onClick={handleRegister}>Register Passkey</button>
<button onClick={handleLogin}>Login with Passkey</button>
</div>
);
}Magic Links
// 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: `
<p>Click the link below to sign in:</p>
<a href="${url}">Sign in to My App</a>
<p>This link expires in 15 minutes.</p>
`
});
}
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
// 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:
- Auth.js v5: Standard für Next.js
- Passkeys: Die Zukunft des Logins
- JWT + Refresh: Für APIs
- MFA: Immer aktivieren wenn möglich
Wähle basierend auf Security-Anforderungen und UX.
Bildprompts
- "Multiple authentication methods merging into single secure access, login concept"
- "Passkey biometric authentication on device, passwordless future"
- "Security layers protecting user identity, authentication shield"