Menu
Zurück zum Blog
1 min read
Technologie

Authentication 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

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:

  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