# 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)
