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:
| Rolle | Beschreibung | Beispiel |
|---|---|---|
| Resource Owner | Der Benutzer, dem die Daten gehören | Du selbst |
| Client | Die App, die auf Daten zugreifen will | Deine Kalender-App |
| Authorization Server | Vergibt Tokens nach erfolgreicher Autorisierung | Google OAuth Server |
| Resource Server | Hält die geschützten Ressourcen | Google 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:
- Deine App leitet den Benutzer zum Authorization Server weiter
- Der Benutzer loggt sich ein und gibt seine Zustimmung
- Der Authorization Server leitet zurück mit einem Authorization Code
- 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.
🪪 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".
🎫 ID Token vs Access Token vs Refresh Token
Drei Tokens, drei Aufgaben:
| Token | Zweck | Empfänger | Lebensdauer |
|---|---|---|---|
| ID Token | Identität des Benutzers (Authentifizierung) | Deine App (Client) | Kurz (Minuten) |
| Access Token | Zugriff auf geschützte Ressourcen (Autorisierung) | Resource Server (API) | Kurz (Minuten bis Stunden) |
| Refresh Token | Neues Access Token holen, ohne erneuten Login | Authorization Server | Lang (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:
- Benutzer klickt "Mit Google anmelden" — Deine App leitet zum Google Authorization Endpoint weiter
- Google zeigt Login-Seite — Benutzer gibt Credentials ein
- Consent Screen — "App XY möchte auf dein Profil und deine E-Mail zugreifen"
- Redirect zurück — Mit Authorization Code in der URL
- Token-Austausch — Dein Backend tauscht den Code gegen ID Token + Access Token
- 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 Tokenprofile— Name, Bild, etc.email— E-Mail-Adresseoffline_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.
🛠️ 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.
🏠 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.
Mehr Artikel entdecken
Angular Signals: Reactive State Management Without RxJS 🚀
Angular Signals bring reactive state management without RxJS complexity. Learn signal(), computed(), and effect() for cleaner Angular code.
OAuth 2.0 & OpenID Connect: Finally Understanding Authentication 🔐
TL;DR: OAuth 2.0 handles authorization (who can do what), OpenID Connect adds authentication (who are you) on top. Together they form the backbone of modern login systems like "Sign in with Google". This article breaks down how it all fits together, which grant types exist, what