Files
Portfolio/blog-posts/49-authentication-patterns.md
T
damjan_savicandClaude Opus 4.5 43484c5023 Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics
- Add .env.example for environment configuration
- Add accessibility and lighthouse audit scripts
- Remove obsolete SEO reports and temporary files
- Remove dev-dist build artifacts and backup files
- Remove unused portrait images (moved/consolidated elsewhere)
- Update contact form and component improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 11:42:11 +01:00

560 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (
<div>
<button onClick={handleRegister}>Register Passkey</button>
<button onClick={handleLogin}>Login with Passkey</button>
</div>
);
}
```
---
## 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: `
<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
```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)