TL;DR: OAuth 2.0 regelt die Autorisierung (wer darf was), OpenID Connect setzt die Authentifizierung (wer bist du) obendrauf. Zusammen bilden sie das Rückgrat moderner Login-Systeme wie "Anmelden mit Google". In diesem Artikel zeige ich dir, wie das alles zusammenhängt, welche Grant Types es gibt, was JWTs sind und wie du das Ganze in TypeScript/NestJS umsetzt.

🤯 Warum Authentifizierung so schwer ist

Hand aufs Herz: Hast du schon mal daran gedacht, Authentifizierung selbst zu bauen? Passwörter hashen, Sessions verwalten, Token generieren, Refresh-Logik implementieren, Brute-Force-Schutz, Rate Limiting, Password Reset Flows... Die Liste hört nicht auf.

Das Problem ist nicht, dass es technisch unmöglich wäre. Das Problem ist, dass du bei einem einzigen Fehler die Tür für Angreifer öffnest. Und genau deshalb gibt es Standards wie OAuth 2.0 und OpenID Connect. Sie nehmen dir die schwierigsten Teile ab, damit du dich auf deine eigentliche App konzentrieren kannst.

🔑 OAuth 2.0: Autorisierung, nicht Authentifizierung!

Das ist der häufigste Irrtum: OAuth 2.0 ist kein Authentifizierungsprotokoll. Es ist ein Autorisierungs-Framework. Der Unterschied?

  • Authentifizierung = "Wer bist du?" (Identität prüfen)
  • Autorisierung = "Was darfst du?" (Berechtigungen prüfen)

OAuth 2.0 löst folgendes Problem: Wie kann eine App auf Ressourcen eines Benutzers zugreifen, ohne dass der Benutzer sein Passwort teilen muss? Stell dir vor, eine Kalender-App will auf deine Google-Kontakte zugreifen. Statt dein Google-Passwort einzugeben, autorisierst du die App über einen kontrollierten Flow. Genial, oder?

🏗️ Die vier Rollen in OAuth 2.0

OAuth 2.0 definiert vier zentrale Rollen:

RolleBeschreibungBeispiel
Resource OwnerDer Benutzer, dem die Daten gehörenDu selbst
ClientDie App, die auf Daten zugreifen willDeine Kalender-App
Authorization ServerVergibt Tokens nach erfolgreicher AutorisierungGoogle OAuth Server
Resource ServerHält die geschützten RessourcenGoogle Contacts API

Diese vier Rollen interagieren in einem definierten Ablauf, den sogenannten Grant Types.

🔄 Grant Types: Der richtige Flow für den richtigen Anwendungsfall

Authorization Code Grant (+ PKCE) — Der Goldstandard

Das ist der Grant Type, den du in 90% der Fälle verwenden solltest. Er funktioniert so:

  1. Deine App leitet den Benutzer zum Authorization Server weiter
  2. Der Benutzer loggt sich ein und gibt seine Zustimmung
  3. Der Authorization Server leitet zurück mit einem Authorization Code
  4. Deine App tauscht den Code im Backend gegen ein Access Token

PKCE (Proof Key for Code Exchange, gesprochen "Pixie") fügt eine zusätzliche Sicherheitsebene hinzu. Deine App generiert einen zufälligen code_verifier und schickt dessen Hash (code_challenge) mit dem initialen Request. Beim Token-Austausch muss der originale code_verifier mitgeschickt werden. Das verhindert, dass ein Angreifer den Authorization Code abfangen und nutzen kann.

// PKCE: code_verifier und code_challenge generieren
import * as crypto from 'crypto';

function generateCodeVerifier(): string {
  return crypto.randomBytes(32).toString('base64url');
}

function generateCodeChallenge(verifier: string): string {
  return crypto
    .createHash('sha256')
    .update(verifier)
    .digest('base64url');
}

const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);

// Im Authorization Request:
// ?response_type=code&code_challenge={codeChallenge}&code_challenge_method=S256

Client Credentials Grant — Maschine-zu-Maschine

Kein Benutzer involviert. Dein Backend-Service authentifiziert sich direkt beim Authorization Server mit seiner Client ID und seinem Client Secret. Perfekt für Microservice-Kommunikation.

# Client Credentials Grant
curl -X POST https://auth.example.com/oauth/token   -d "grant_type=client_credentials"   -d "client_id=YOUR_CLIENT_ID"   -d "client_secret=YOUR_CLIENT_SECRET"   -d "scope=read:users"

Device Code Grant — Für Geräte ohne Browser

Smart TVs, CLI-Tools, IoT-Geräte — überall dort, wo man keinen Browser hat oder nur schwer tippen kann. Das Gerät zeigt einen Code an, du öffnest eine URL auf deinem Handy, gibst den Code ein und autorisierst. Kennst du bestimmt von Netflix oder GitHub CLI.

⚠️ Deprecated: Implicit Grant & Password Grant

Der Implicit Grant hat das Token direkt in der URL zurückgegeben. Problem: URLs landen in Browser-History, Logs und Referer-Headern. Sicherheitstechnisch ein Albtraum.

Der Resource Owner Password Credentials Grant hat den Benutzernamen und das Passwort direkt an die App übergeben. Das widerspricht dem gesamten Zweck von OAuth. Beide sind in der aktuellen Best Practice als veraltet markiert.

OAuth 2.0
OAuth 2.0 is the industry-standard protocol for authorization.

🪪 OpenID Connect: Authentifizierung on top

Okay, OAuth 2.0 sagt uns, was jemand darf. Aber wer ist dieser Jemand? Genau hier kommt OpenID Connect (OIDC) ins Spiel. Es ist eine dünne Schicht auf OAuth 2.0, die Authentifizierung hinzufügt.

Der entscheidende Unterschied: OIDC führt den ID Token ein. Während ein Access Token sagt "dieser Token hat Zugriff auf X", sagt ein ID Token "dieser Token gehört zu Benutzer Y mit E-Mail Z".

How OpenID Connect Works
OpenID Connect explained for developers.

🎫 ID Token vs Access Token vs Refresh Token

Drei Tokens, drei Aufgaben:

TokenZweckEmpfängerLebensdauer
ID TokenIdentität des Benutzers (Authentifizierung)Deine App (Client)Kurz (Minuten)
Access TokenZugriff auf geschützte Ressourcen (Autorisierung)Resource Server (API)Kurz (Minuten bis Stunden)
Refresh TokenNeues Access Token holen, ohne erneuten LoginAuthorization ServerLang (Tage bis Wochen)

Wichtig: Schick niemals ein ID Token an eine API! Das ID Token ist für deine App, um den Benutzer zu identifizieren. Für API-Zugriffe nutzt du das Access Token.

🧬 JWT: Anatomie eines Tokens

Die meisten ID Tokens (und viele Access Tokens) sind JWTs (JSON Web Tokens). Ein JWT besteht aus drei Teilen, getrennt durch Punkte:

eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature
# Header               . Payload                     . Signature

Jeder Teil ist Base64url-encoded:

// Header — Algorithmus und Token-Typ
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "my-key-id"    // Key ID zur Signaturprüfung
}

// Payload — Die Claims (Behauptungen)
{
  "iss": "https://auth.example.com",  // Issuer
  "sub": "user-123",                   // Subject (Benutzer-ID)
  "aud": "my-app",                     // Audience (für wen)
  "exp": 1700000000,                   // Expiration
  "iat": 1699999000,                   // Issued At
  "email": "[email protected]",         // OIDC Claim
  "name": "Max Mustermann"             // OIDC Claim
}

// Signature — HMAC oder RSA-Signatur über Header + Payload
// Wird mit dem Private Key des Authorization Servers erzeugt
// Kann mit dem Public Key verifiziert werden

Die Signatur stellt sicher, dass niemand den Inhalt manipuliert hat. Deine App kann den Public Key des Authorization Servers (via JWKS-Endpoint) abrufen und die Signatur prüfen, ohne den Authorization Server kontaktieren zu müssen.

🚀 Praxisbeispiel: "Login mit Google/GitHub"

So läuft ein typischer "Login mit Google"-Flow ab:

  1. Benutzer klickt "Mit Google anmelden" — Deine App leitet zum Google Authorization Endpoint weiter
  2. Google zeigt Login-Seite — Benutzer gibt Credentials ein
  3. Consent Screen — "App XY möchte auf dein Profil und deine E-Mail zugreifen"
  4. Redirect zurück — Mit Authorization Code in der URL
  5. Token-Austausch — Dein Backend tauscht den Code gegen ID Token + Access Token
  6. ID Token auswerten — Benutzer identifizieren, Session erstellen

🎯 Scopes und Claims

Scopes definieren, welche Berechtigungen angefragt werden. OIDC definiert Standard-Scopes:

  • openid — Pflicht für OIDC, liefert den ID Token
  • profile — Name, Bild, etc.
  • email — E-Mail-Adresse
  • offline_access — Refresh Token anfordern

Claims sind die einzelnen Datenpunkte im Token, die durch Scopes freigeschaltet werden. Der Scope email schaltet z.B. die Claims email und email_verified frei.

REST-APIs verstehen und nutzen
Alles was du über REST-APIs wissen musst.

🛠️ Implementation mit NestJS

Jetzt wird's praktisch. So setzt du JWT-basierte Authentifizierung mit NestJS um:

# Dependencies installieren
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwt
// auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { passportJwtSecret } from 'jwks-rsa';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      // JWKS-Endpoint deines OIDC Providers
      secretOrKeyProvider: passportJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        jwksUri: 'https://auth.example.com/.well-known/jwks.json',
      }),
      issuer: 'https://auth.example.com/',
      audience: 'my-api',
      algorithms: ['RS256'],
    });
  }

  async validate(payload: any) {
    return {
      userId: payload.sub,
      email: payload.email,
      roles: payload.roles || [],
    };
  }
}
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [PassportModule.register({ defaultStrategy: 'jwt' })],
  providers: [JwtStrategy],
  exports: [PassportModule],
})
export class AuthModule {}
// Geschützte Route
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('profile')
export class ProfileController {
  @Get()
  @UseGuards(AuthGuard('jwt'))
  getProfile(@Req() req) {
    // req.user enthält das validierte Token-Payload
    return {
      userId: req.user.userId,
      email: req.user.email,
    };
  }
}

⚠️ Häufige Fehler und Sicherheitsfallen

Hier die häufigsten Stolperfallen, die ich immer wieder sehe:

1. Tokens im localStorage speichern

Problem: Jedes XSS-Script kann den localStorage auslesen. Speichere Access Tokens stattdessen in httpOnly Cookies oder halte sie nur im Speicher (In-Memory).

2. Redirect URI nicht validieren

Problem: Ohne strenge Validierung der Redirect URI kann ein Angreifer den Authorization Code an seine eigene URL umleiten. Registriere exakte Redirect URIs, keine Wildcards.

3. CSRF-Schutz vergessen

Problem: Ohne state-Parameter kann ein Angreifer einen CSRF-Angriff durchführen. Generiere immer einen zufälligen state-Wert und prüfe ihn beim Callback.

4. Token-Signatur nicht prüfen

Problem: Wenn du den Token nur dekodierst, aber die Signatur nicht prüfst, kann jeder beliebige Tokens erstellen. Immer die Signatur gegen den JWKS-Endpoint verifizieren.

5. Zu lange Token-Lebensdauer

Problem: Access Tokens sollten kurzlebig sein (5-15 Minuten). Nutze Refresh Tokens für längere Sessions.

CORS: Cross-Origin Resource Sharing erklärt
Alles über CORS und warum es so wichtig für die Sicherheit deiner APIs ist.
DKIM, DMARC und SPF: Der Schutzschild deiner E-Mail-Kommunikation
Wie du deine E-Mail-Sicherheit mit DKIM, DMARC und SPF absicherst.

🏠 Self-Hosting: Auth0, Keycloak & Authentik

Du musst nicht zwingend einen Cloud-Provider nutzen. Es gibt großartige Self-Hosting-Optionen:

  • Keycloak — Der Klassiker. Java-basiert, extrem flexibel, riesige Community. Unterstützt SAML, OIDC, LDAP und mehr. Kann allerdings ein Ressourcen-Fresser sein.
  • Authentik — Modern, Python-basiert, schicke UI. Leichtgewichtiger als Keycloak und einfacher zu konfigurieren. Mein persönlicher Favorit für neue Projekte.
  • Auth0 — Eigentlich ein Cloud-Service, aber mit einem großzügigen Free Tier. Wenn du keine Lust auf Self-Hosting hast, ist Auth0 eine solide Wahl.
# Authentik mit Docker Compose starten
# docker-compose.yml
version: "3.4"
services:
  authentik-server:
    image: ghcr.io/goauthentik/server:latest
    command: server
    environment:
      AUTHENTIK_SECRET_KEY: "generate-a-long-random-string"
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__USER: authentik
      AUTHENTIK_POSTGRESQL__PASSWORD: authentik
      AUTHENTIK_POSTGRESQL__NAME: authentik
    ports:
      - "9000:9000"
      - "9443:9443"

💡 Fazit

OAuth 2.0 und OpenID Connect sind keine Raketenwissenschaft, aber sie haben viele bewegliche Teile. Die wichtigsten Takeaways:

  • OAuth 2.0 = Autorisierung, OpenID Connect = Authentifizierung obendrauf
  • Nutze Authorization Code + PKCE als Standard-Flow
  • Vergiss Implicit und Password Grant — die sind veraltet
  • Verstehe den Unterschied zwischen ID Token, Access Token und Refresh Token
  • Prüfe immer die Token-Signatur
  • Speichere Tokens niemals im localStorage
  • Nutze bestehende Lösungen wie Keycloak oder Authentik statt selbst zu bauen

Authentifizierung selbst zu bauen ist wie sein eigenes Krypto-Protokoll zu schreiben: Technisch machbar, aber fast immer eine schlechte Idee. Steh auf den Schultern von Giganten.

Artikel teilen:Share article: