Przejdź do treści
Sicherheit

OAuth2 und OpenID Connect - Moderne Authentifizierung

Veröffentlicht am:
·5 Min. Lesezeit·Autor: MDS Software Solutions Group

OAuth2 und OpenID

bezpieczenstwo

OAuth2 und OpenID Connect - Moderne Authentifizierung

Die Sicherheit von Webanwendungen beginnt mit der korrekten Authentifizierung und Autorisierung von Benutzern. OAuth 2.0 und OpenID Connect sind zwei sich ergaenzende Standards, die zum Fundament moderner Authentifizierungssysteme geworden sind. In diesem Artikel werden wir ihre Funktionsweise, Unterschiede, Implementierungsmuster und Best Practices fuer die Sicherheit ausfuehrlich besprechen.

Was ist OAuth 2.0?#

OAuth 2.0 ist ein Autorisierungsprotokoll, das Anwendungen erlaubt, begrenzten Zugriff auf Benutzerkonten bei externen Diensten zu erhalten. Es ist entscheidend zu verstehen, dass OAuth 2.0 allein kein Authentifizierungsprotokoll ist - es befasst sich ausschliesslich mit der Autorisierung, also der Festlegung, auf welche Ressourcen eine Anwendung zugreifen kann.

Hauptrollen in OAuth 2.0#

  • Resource Owner - der Benutzer, dem die geschuetzten Ressourcen gehoeren
  • Client - die Anwendung, die Zugriff auf Ressourcen anfordert
  • Authorization Server - der Server, der Zugriffstokens ausstellt
  • Resource Server - der Server, der die geschuetzten Ressourcen hostet
+--------+                               +---------------+
|        |--(A)- Authorization Request ->|   Resource    |
|        |                               |     Owner     |
|        |<-(B)-- Authorization Grant ---|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(C)-- Authorization Grant -->| Authorization |
| Client |                               |     Server    |
|        |<-(D)----- Access Token -------|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(E)----- Access Token ------>|    Resource   |
|        |                               |     Server    |
|        |<-(F)--- Protected Resource ---|               |
+--------+                               +---------------+

OAuth 2.0 Flows (Grant Types)#

OAuth 2.0 definiert mehrere Flows, die jeweils fuer unterschiedliche Szenarien konzipiert sind. Die Wahl des richtigen Flows ist entscheidend fuer die Anwendungssicherheit.

1. Authorization Code Flow#

Dies ist der sicherste und am haeufigsten verwendete Flow, der fuer serverseitige Anwendungen konzipiert ist. Der Autorisierungscode wird serverseitig gegen ein Token getauscht, sodass das Token niemals im Browser exponiert wird.

// Schritt 1: Benutzer zum Authorization Server weiterleiten
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', process.env.OAUTH_CLIENT_ID!);
authUrl.searchParams.set('redirect_uri', 'https://myapp.com/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', generateRandomState());

// Schritt 2: Callback verarbeiten - Code gegen Token tauschen
// /api/auth/callback.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const code = request.nextUrl.searchParams.get('code');
  const state = request.nextUrl.searchParams.get('state');

  // State validieren (CSRF-Schutz)
  if (!validateState(state)) {
    return NextResponse.json({ error: 'Invalid state' }, { status: 400 });
  }

  // Code gegen Token tauschen
  const tokenResponse = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: code!,
      redirect_uri: 'https://myapp.com/callback',
      client_id: process.env.OAUTH_CLIENT_ID!,
      client_secret: process.env.OAUTH_CLIENT_SECRET!,
    }),
  });

  const tokens = await tokenResponse.json();
  // tokens = { access_token, refresh_token, id_token, expires_in }

  // Tokens in sicherer Session speichern
  await createSession(tokens);

  return NextResponse.redirect('/dashboard');
}

2. Authorization Code Flow mit PKCE#

PKCE (Proof Key for Code Exchange) ist eine Erweiterung des Authorization Code Flow, die eine zusaetzliche Sicherheitsebene hinzufuegt. Es ist obligatorisch fuer SPA- und mobile Anwendungen, bei denen das client_secret nicht sicher gespeichert werden kann.

import crypto from 'crypto';

// code_verifier und code_challenge generieren
function generatePKCE() {
  const codeVerifier = crypto.randomBytes(32).toString('base64url');
  const codeChallenge = crypto
    .createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');

  return { codeVerifier, codeChallenge };
}

// Schritt 1: Weiterleitung mit PKCE Challenge
const { codeVerifier, codeChallenge } = generatePKCE();

// codeVerifier in der Session speichern (httpOnly Cookie oder serverseitiger Speicher)
cookies().set('pkce_verifier', codeVerifier, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  maxAge: 600, // 10 Minuten
});

const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', process.env.OAUTH_CLIENT_ID!);
authUrl.searchParams.set('redirect_uri', 'https://myapp.com/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', generateRandomState());

// Schritt 2: Code mit code_verifier tauschen
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: 'https://myapp.com/callback',
    client_id: process.env.OAUTH_CLIENT_ID!,
    code_verifier: codeVerifier, // Anstelle von client_secret
  }),
});

3. Client Credentials Flow#

Dieser Flow ist fuer die Server-zu-Server-Kommunikation (M2M) konzipiert, bei der keine Benutzerinteraktion stattfindet. Die Anwendung autorisiert sich direkt mit ihren eigenen Anmeldedaten.

// Server-zu-Server-Kommunikation
async function getM2MAccessToken(): Promise<string> {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: process.env.SERVICE_CLIENT_ID!,
      client_secret: process.env.SERVICE_CLIENT_SECRET!,
      scope: 'api:read api:write',
    }),
  });

  const data = await response.json();
  return data.access_token;
}

// Verwendung in einem Microservice
async function callInternalAPI() {
  const token = await getM2MAccessToken();

  const response = await fetch('https://internal-api.example.com/data', {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  return response.json();
}

4. Implicit Flow (veraltet)#

Der Implicit Flow wurde urspruenglich fuer SPA-Anwendungen entwickelt, gilt aber aufgrund von Sicherheitsproblemen als veraltet. Das Zugriffstoken wird direkt im URL-Fragment zurueckgegeben, wodurch es einem Leak ausgesetzt ist. Stattdessen sollte der Authorization Code Flow mit PKCE verwendet werden.

// VERALTET - nicht in neuen Projekten verwenden!
// Token ist in der URL sichtbar:
https://myapp.com/callback#access_token=eyJhbGc...&token_type=bearer

OpenID Connect (OIDC) - Die Identitaetsschicht#

OpenID Connect ist eine Identitaetsschicht, die auf OAuth 2.0 aufbaut. Waehrend OAuth 2.0 die Frage "worauf haben Sie Zugriff?" beantwortet, beantwortet OIDC die Frage "wer sind Sie?".

Wichtige OIDC-Erweiterungen#

  • ID Token - ein JWT mit Benutzeridentitaetsinformationen
  • UserInfo Endpoint - ein Endpunkt zum Abrufen von Profildaten
  • Standard-Scopes - openid, profile, email, address, phone
  • Discovery - automatische Erkennung der Provider-Konfiguration (.well-known/openid-configuration)
// OIDC Discovery-Konfiguration abrufen
async function getOIDCConfig(issuer: string) {
  const response = await fetch(
    `${issuer}/.well-known/openid-configuration`
  );
  return response.json();
}

// Beispiel einer Discovery-Antwort
// {
//   "issuer": "https://auth.example.com",
//   "authorization_endpoint": "https://auth.example.com/authorize",
//   "token_endpoint": "https://auth.example.com/token",
//   "userinfo_endpoint": "https://auth.example.com/userinfo",
//   "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
//   "scopes_supported": ["openid", "profile", "email"],
//   "response_types_supported": ["code", "id_token", "token id_token"]
// }

JWT-Tokens - Access, Refresh und ID Token#

JSON Web Tokens (JWT) sind das Standard-Token-Format im OAuth 2.0 / OIDC-Oekosystem. Sie bestehen aus drei durch Punkte getrennten Teilen: Header, Payload und Signature.

Access Token#

Das Access Token autorisiert den Zugriff auf geschuetzte Ressourcen. Es sollte eine kurze Lebensdauer haben (typischerweise 5-60 Minuten).

// Beispiel eines Access Token Payloads
{
  "iss": "https://auth.example.com",
  "sub": "user_123",
  "aud": "https://api.example.com",
  "exp": 1708300800,
  "iat": 1708297200,
  "scope": "openid profile email api:read",
  "client_id": "my-app"
}

Refresh Token#

Das Refresh Token dient zur Erneuerung von Access Tokens ohne erneute Benutzerinteraktion. Es sollte sicher gespeichert werden (z.B. in einem httpOnly Cookie auf der Serverseite).

// Token-Erneuerung implementieren
async function refreshAccessToken(refreshToken: string) {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: process.env.OAUTH_CLIENT_ID!,
      client_secret: process.env.OAUTH_CLIENT_SECRET!,
    }),
  });

  if (!response.ok) {
    throw new Error('Token-Erneuerung fehlgeschlagen - Benutzer muss sich erneut authentifizieren');
  }

  const tokens = await response.json();
  return {
    accessToken: tokens.access_token,
    refreshToken: tokens.refresh_token, // Rotation - neues Refresh Token
    expiresAt: Date.now() + tokens.expires_in * 1000,
  };
}

ID Token#

Das ID Token ist ein JWT, das spezifisch fuer OpenID Connect ist und Benutzeridentitaetsinformationen (Claims) enthaelt.

// Beispiel eines ID Token Payloads
{
  "iss": "https://auth.example.com",
  "sub": "user_123",
  "aud": "my-client-id",
  "exp": 1708300800,
  "iat": 1708297200,
  "auth_time": 1708297100,
  "nonce": "abc123",
  "name": "Max Mustermann",
  "email": "max@example.com",
  "email_verified": true,
  "picture": "https://example.com/avatar.jpg"
}

Scopes und Claims#

Scopes definieren, welche Art von Zugriff angefordert wird. Claims sind konkrete Informationen, die in Tokens enthalten sind.

// Standard-OIDC-Scopes und zugehoerige Claims
const oidcScopes = {
  openid: ['sub'],                    // Erforderlich - Benutzerkennung
  profile: ['name', 'family_name', 'given_name', 'picture', 'locale'],
  email: ['email', 'email_verified'],
  address: ['address'],
  phone: ['phone_number', 'phone_number_verified'],
};

// Benutzerdefinierte Scopes (im Authorization Server definiert)
// scope: "api:read api:write admin"

// Bestimmte Claims in einer OIDC-Anfrage anfordern
const claimsRequest = {
  id_token: {
    email: { essential: true },
    email_verified: { essential: true },
    given_name: null,     // angefordert, aber nicht erforderlich
  },
  userinfo: {
    picture: null,
    locale: null,
  },
};

JWT-Token-Validierung#

Die korrekte Token-Validierung ist sicherheitskritisch. Vertrauen Sie niemals einem Token ohne vollstaendige Ueberpruefung.

import { jwtVerify, createRemoteJWKSet } from 'jose';

// JWKS-Client erstellen (cached oeffentliche Schluessel)
const JWKS = createRemoteJWKSet(
  new URL('https://auth.example.com/.well-known/jwks.json')
);

interface TokenPayload {
  sub: string;
  email: string;
  scope: string;
  iss: string;
  aud: string;
  exp: number;
}

async function validateAccessToken(token: string): Promise<TokenPayload> {
  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: 'https://auth.example.com',      // Issuer ueberpruefen
      audience: 'https://api.example.com',       // Audience ueberpruefen
      clockTolerance: 30,                         // 30s Uhrentoleranz
    });

    return payload as unknown as TokenPayload;
  } catch (error) {
    if (error instanceof Error) {
      if (error.message.includes('expired')) {
        throw new Error('TOKEN_EXPIRED');
      }
      if (error.message.includes('signature')) {
        throw new Error('INVALID_SIGNATURE');
      }
    }
    throw new Error('TOKEN_VALIDATION_FAILED');
  }
}

// Validierungs-Middleware fuer Next.js API Routes
import { NextRequest, NextResponse } from 'next/server';

export async function authMiddleware(request: NextRequest) {
  const authHeader = request.headers.get('authorization');

  if (!authHeader?.startsWith('Bearer ')) {
    return NextResponse.json(
      { error: 'Fehlender Authorization-Header' },
      { status: 401 }
    );
  }

  const token = authHeader.slice(7);

  try {
    const payload = await validateAccessToken(token);

    // Erforderliche Scopes pruefen
    const requiredScopes = ['api:read'];
    const tokenScopes = payload.scope.split(' ');
    const hasRequiredScopes = requiredScopes.every(s =>
      tokenScopes.includes(s)
    );

    if (!hasRequiredScopes) {
      return NextResponse.json(
        { error: 'Unzureichende Berechtigungen' },
        { status: 403 }
      );
    }

    // Token ist gueltig - fortfahren
    const headers = new Headers(request.headers);
    headers.set('x-user-id', payload.sub);
    headers.set('x-user-email', payload.email);

    return NextResponse.next({ headers });
  } catch (error) {
    return NextResponse.json(
      { error: 'Ungueltiges Token' },
      { status: 401 }
    );
  }
}

Identitaetsanbieter (Identity Providers)#

Auth0#

Auth0 ist einer der beliebtesten IDaaS-Anbieter (Identity as a Service). Es bietet fertige SDKs und schnelle Integration.

// Auth0-Konfiguration mit Next.js
// npm install @auth0/nextjs-auth0

// app/api/auth/[auth0]/route.ts
import { handleAuth, handleLogin, handleLogout } from '@auth0/nextjs-auth0';

export const GET = handleAuth({
  login: handleLogin({
    authorizationParams: {
      scope: 'openid profile email',
      audience: process.env.AUTH0_AUDIENCE,
    },
  }),
  logout: handleLogout({
    returnTo: process.env.AUTH0_BASE_URL,
  }),
});

// Verwendung in einer Server-Komponente
import { getSession } from '@auth0/nextjs-auth0';

export default async function DashboardPage() {
  const session = await getSession();

  if (!session) {
    redirect('/api/auth/login');
  }

  return (
    <div>
      <h1>Willkommen, {session.user.name}</h1>
      <p>E-Mail: {session.user.email}</p>
    </div>
  );
}

Keycloak#

Keycloak ist eine Open-Source-Identitaetsmanagement-Loesung, die auf der eigenen Infrastruktur gehostet werden kann (Self-Hosted).

// Keycloak-Konfiguration mit next-auth
import NextAuth from 'next-auth';
import KeycloakProvider from 'next-auth/providers/keycloak';

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    KeycloakProvider({
      clientId: process.env.KEYCLOAK_CLIENT_ID!,
      clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
      issuer: process.env.KEYCLOAK_ISSUER, // z.B. https://keycloak.example.com/realms/my-realm
    }),
  ],
  callbacks: {
    async jwt({ token, account }) {
      if (account) {
        token.accessToken = account.access_token;
        token.refreshToken = account.refresh_token;
        token.expiresAt = account.expires_at;
      }

      // Automatische Token-Erneuerung
      if (Date.now() < (token.expiresAt as number) * 1000) {
        return token;
      }

      return refreshKeycloakToken(token);
    },
    async session({ session, token }) {
      session.accessToken = token.accessToken as string;
      return session;
    },
  },
});

Azure AD (Microsoft Entra ID)#

Azure AD ist Microsofts Identitaetsmanagement-Loesung, die in Unternehmensumgebungen weit verbreitet ist.

// Azure AD-Konfiguration mit MSAL
import { ConfidentialClientApplication } from '@azure/msal-node';

const msalConfig = {
  auth: {
    clientId: process.env.AZURE_AD_CLIENT_ID!,
    authority: `https://login.microsoftonline.com/${process.env.AZURE_AD_TENANT_ID}`,
    clientSecret: process.env.AZURE_AD_CLIENT_SECRET!,
  },
};

const msalClient = new ConfidentialClientApplication(msalConfig);

// Autorisierungs-URL generieren
async function getAuthUrl() {
  return msalClient.getAuthCodeUrl({
    scopes: ['openid', 'profile', 'email', 'User.Read'],
    redirectUri: 'https://myapp.com/api/auth/callback',
    state: generateRandomState(),
  });
}

// Code gegen Token tauschen
async function handleCallback(code: string) {
  const result = await msalClient.acquireTokenByCode({
    code,
    scopes: ['openid', 'profile', 'email', 'User.Read'],
    redirectUri: 'https://myapp.com/api/auth/callback',
  });

  return {
    accessToken: result.accessToken,
    account: result.account,
    idTokenClaims: result.idTokenClaims,
  };
}

Implementierung in Next.js mit NextAuth.js#

NextAuth.js (Auth.js) ist die beliebteste Bibliothek fuer die Authentifizierungsbehandlung in Next.js-Anwendungen. Sie unterstuetzt mehrere Provider und vereinfacht den gesamten Prozess.

// auth.ts - zentrale Konfiguration
import NextAuth from 'next-auth';
import Google from 'next-auth/providers/google';
import GitHub from 'next-auth/providers/github';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: { strategy: 'jwt' },
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      authorization: {
        params: {
          scope: 'openid email profile',
          access_type: 'offline',     // Fuer Refresh Token
          prompt: 'consent',
        },
      },
    }),
    GitHub({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
  ],
  callbacks: {
    async jwt({ token, user, account }) {
      if (user) {
        token.role = user.role;
        token.id = user.id;
      }
      if (account) {
        token.accessToken = account.access_token;
      }
      return token;
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.role = token.role as string;
        session.user.id = token.id as string;
      }
      return session;
    },
    async authorized({ auth, request }) {
      const isLoggedIn = !!auth?.user;
      const isProtected = request.nextUrl.pathname.startsWith('/dashboard');

      if (isProtected && !isLoggedIn) {
        return Response.redirect(new URL('/login', request.nextUrl));
      }

      return true;
    },
  },
  pages: {
    signIn: '/login',
    error: '/auth/error',
  },
});

// middleware.ts
export { auth as middleware } from '@/auth';

export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};

Sitzungsverwaltung (Session Management)#

Eine ordnungsgemaesse Sitzungsverwaltung ist genauso wichtig wie die Authentifizierung selbst. Hier sind bewaehrte Ansaetze.

// Sichere Sitzungsspeicherung mit verschluesselten Cookies
import { SignJWT, jwtVerify } from 'jose';
import { cookies } from 'next/headers';

const secretKey = new TextEncoder().encode(process.env.SESSION_SECRET!);

interface SessionData {
  userId: string;
  email: string;
  role: string;
  accessToken: string;
  refreshToken: string;
  expiresAt: number;
}

// Sitzung erstellen
async function createSession(data: SessionData) {
  const session = await new SignJWT(data as unknown as Record<string, unknown>)
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime('7d')
    .setIssuedAt()
    .sign(secretKey);

  cookies().set('session', session, {
    httpOnly: true,           // Nicht aus JavaScript zugreifbar
    secure: true,             // Nur HTTPS
    sameSite: 'lax',          // CSRF-Schutz
    maxAge: 60 * 60 * 24 * 7, // 7 Tage
    path: '/',
  });
}

// Sitzung lesen
async function getSession(): Promise<SessionData | null> {
  const sessionCookie = cookies().get('session')?.value;
  if (!sessionCookie) return null;

  try {
    const { payload } = await jwtVerify(sessionCookie, secretKey);
    return payload as unknown as SessionData;
  } catch {
    return null;
  }
}

// Sitzung mit automatischer Token-Erneuerung aktualisieren
async function refreshSession(): Promise<SessionData | null> {
  const session = await getSession();
  if (!session) return null;

  // Pruefen, ob Access Token innerhalb von 5 Minuten ablaeuft
  if (session.expiresAt - Date.now() < 5 * 60 * 1000) {
    try {
      const newTokens = await refreshAccessToken(session.refreshToken);
      const updatedSession = {
        ...session,
        accessToken: newTokens.accessToken,
        refreshToken: newTokens.refreshToken,
        expiresAt: newTokens.expiresAt,
      };
      await createSession(updatedSession);
      return updatedSession;
    } catch {
      // Refresh Token abgelaufen - Benutzer abmelden
      await destroySession();
      return null;
    }
  }

  return session;
}

// Sitzung zerstoeren
async function destroySession() {
  cookies().delete('session');
}

CSRF-Schutz#

Cross-Site Request Forgery (CSRF) ist ein Angriff, bei dem eine boesartige Webseite nicht autorisierte Aktionen im Namen eines angemeldeten Benutzers ausfuehrt. Im Kontext von OAuth 2.0 ist der CSRF-Schutz kritisch.

// CSRF-Schutz mit State-Parameter im OAuth-Flow
import crypto from 'crypto';
import { cookies } from 'next/headers';

function generateState(): string {
  const state = crypto.randomBytes(32).toString('hex');

  cookies().set('oauth_state', state, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 600, // 10 Minuten
  });

  return state;
}

function validateState(receivedState: string | null): boolean {
  const storedState = cookies().get('oauth_state')?.value;
  cookies().delete('oauth_state'); // Einmalige Verwendung

  if (!storedState || !receivedState) return false;

  // Vergleich resistent gegen Timing-Angriffe
  return crypto.timingSafeEqual(
    Buffer.from(storedState),
    Buffer.from(receivedState)
  );
}

// Zusaetzlicher CSRF-Schutz mit Double Submit Cookie
function generateCSRFToken(): string {
  const token = crypto.randomBytes(32).toString('hex');

  cookies().set('csrf_token', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 3600,
  });

  return token; // Wird als Meta-Tag oder Hidden Field an den Client zurueckgegeben
}

// Middleware zur Validierung des CSRF-Tokens
export function validateCSRF(request: NextRequest): boolean {
  const cookieToken = request.cookies.get('csrf_token')?.value;
  const headerToken = request.headers.get('x-csrf-token');

  if (!cookieToken || !headerToken) return false;

  return crypto.timingSafeEqual(
    Buffer.from(cookieToken),
    Buffer.from(headerToken)
  );
}

Best Practices fuer die Sicherheit#

Bei der Implementierung von OAuth 2.0 und OIDC sollten die folgenden Sicherheitsrichtlinien beachtet werden:

1. Token-Speicherung#

// GUT - Tokens in httpOnly Cookies (serverseitig)
cookies().set('access_token', token, {
  httpOnly: true,    // Kein JS-Zugriff
  secure: true,      // Nur HTTPS
  sameSite: 'strict', // CSRF-Schutz
  maxAge: 900,       // 15 Minuten
});

// SCHLECHT - Tokens im localStorage (anfaellig fuer XSS)
// localStorage.setItem('access_token', token);

// SCHLECHT - Tokens in normalen Cookies (aus JS zugreifbar)
// document.cookie = `access_token=${token}`;

2. Minimale Scopes#

// GUT - minimale Berechtigungen anfordern
const scopes = 'openid profile email';

// SCHLECHT - alles "fuer alle Faelle" anfordern
// const scopes = 'openid profile email admin api:full';

3. Token Rotation#

// Refresh Token Rotation implementieren
async function rotateTokens(refreshToken: string) {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: process.env.OAUTH_CLIENT_ID!,
    }),
  });

  const tokens = await response.json();

  // Das alte Refresh Token wird automatisch ungueltig gemacht
  // Wenn das alte Token erneut verwendet wird,
  // wird die gesamte Token-Familie ungueltig gemacht (Diebstahlerkennung)
  return tokens;
}

4. Redirect-URI-Validierung#

// Whitelist der erlaubten redirect_uris
const ALLOWED_REDIRECTS = [
  'https://myapp.com/callback',
  'https://myapp.com/auth/callback',
];

function validateRedirectUri(uri: string): boolean {
  return ALLOWED_REDIRECTS.includes(uri);
}

5. Sicheres Abmelden#

// Vollstaendige Abmeldung - lokal + Identity Provider-seitig
async function logout() {
  // 1. Lokale Sitzung entfernen
  await destroySession();

  // 2. Refresh Token beim Authorization Server widerrufen
  await fetch('https://auth.example.com/revoke', {
    method: 'POST',
    body: new URLSearchParams({
      token: refreshToken,
      token_type_hint: 'refresh_token',
      client_id: process.env.OAUTH_CLIENT_ID!,
    }),
  });

  // 3. Weiterleitung zum RP-Initiated Logout (OIDC)
  const logoutUrl = new URL('https://auth.example.com/logout');
  logoutUrl.searchParams.set('id_token_hint', idToken);
  logoutUrl.searchParams.set(
    'post_logout_redirect_uri',
    'https://myapp.com'
  );

  redirect(logoutUrl.toString());
}

Zusammenfassung#

OAuth 2.0 und OpenID Connect sind leistungsstarke Werkzeuge zum Aufbau sicherer Authentifizierungssysteme. Die wichtigsten Erkenntnisse:

  • Verwenden Sie den Authorization Code Flow mit PKCE fuer Web- und mobile Anwendungen
  • Verwenden Sie niemals den Implicit Flow in neuen Projekten
  • Speichern Sie Tokens sicher - httpOnly Cookies, niemals localStorage
  • Validieren Sie Tokens vollstaendig - Issuer, Audience, Signatur, Ablaufzeit
  • Implementieren Sie Refresh Token Rotation mit Diebstahlerkennung
  • Schuetzen Sie vor CSRF - State-Parameter, SameSite Cookies
  • Wenden Sie das Prinzip der minimalen Rechte an - minimale Scopes
  • Waehlen Sie den richtigen Anbieter - Auth0 fuer einen schnellen Start, Keycloak fuer volle Kontrolle, Azure AD fuer das Microsoft-Oekosystem

Die korrekte Implementierung der Authentifizierung ist das Fundament der Sicherheit jeder Anwendung. Unabhaengig von der gewaehlten Loesung lohnt es sich immer, Zeit in das Verstaendnis der Protokolle und ihre korrekte Umsetzung zu investieren.


Benoetigen Sie Hilfe bei der Implementierung sicherer Authentifizierung in Ihrer Anwendung? Das Team von MDS Software Solutions Group ist spezialisiert auf die Konzeption und Implementierung von Authentifizierungssystemen auf Basis von OAuth 2.0 und OpenID Connect. Ob Sie eine neue Anwendung entwickeln oder ein bestehendes System modernisieren, kontaktieren Sie uns - wir helfen Ihnen, die optimale Loesung fuer Ihr Projekt auszuwaehlen und umzusetzen.

Autor
MDS Software Solutions Group

Team von Programmierexperten, die sich auf moderne Webtechnologien spezialisiert haben.

OAuth2 und OpenID Connect - Moderne Authentifizierung | MDS Software Solutions Group | MDS Software Solutions Group