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.