OAuth: Wo Frontend-Entwickler scheitern
OAuth: Wo Frontend-Entwickler scheitern
Die meisten OAuth-Tutorials enden bei "User klickt Login, bekommt Token". Das ist der einfache Teil. Die echte Komplexität zeigt sich erst in der Produktion.
Die postMessage-Architektur
Als wir bei NeoXonline Google-, Facebook- und X-Login integrierten, war der naive Ansatz: User zum Provider redirecten und zurück. Das Problem: dabei verliert man React-State, laufende Uploads, Socket-Verbindungen. Alles wird zurückgesetzt.
Die Lösung ist eine postMessage-Architektur:
- Kleines Popup öffnen:
window.open('/auth/google/popup', ...) - Das Popup führt den OAuth-Flow serverseitig durch
- Bei Erfolg ruft das Popup
window.opener.postMessage({ token, user }, origin)auf - Das Hauptfenster empfängt die Nachricht, schließt das Popup, aktualisiert State
Der User bleibt auf der gleichen Seite. Kein Redirect, kein State-Verlust.
// Im Hauptfenster
window.addEventListener('message', (event) => {
if (event.origin !== window.location.origin) return; // Sicherheits-Check PFLICHT
if (event.data?.type === 'oauth-success') {
setUser(event.data.user);
setToken(event.data.token);
}
});
Der Origin-Check ist nicht optional. Ohne ihn kann jede Site ein gefälschtes User-Objekt senden.
Silent Refresh mit 5-Minuten-Buffer
Access-Tokens laufen ab. Die naive Lösung: warten, bis ein Request mit 401 fehlschlägt, dann refreshen. Problem: dabei entsteht kurz eine kaputte UI.
Das Pattern das funktioniert:
async function ensureValidToken(): Promise<string> {
const expiresAt = getTokenExpiry(); // im Memory, nicht localStorage
const BUFFER_MS = 5 * 60 * 1000; // 5 Minuten
if (Date.now() + BUFFER_MS < expiresAt) {
return getStoredToken(); // noch gültig mit Buffer
}
return await refreshToken(); // proaktiv refreshen
}
Vor jedem API-Call ensureValidToken() aufrufen — nicht erst bei 401.
Token-Rotation und Race Conditions
Keycloak (und die meisten modernen Provider) nutzen Token-Rotation: jeder Refresh liefert ein neues Refresh-Token und invalidiert das alte. Problem: wenn zwei gleichzeitige API-Calls beide einen Refresh auslösen, verwendet der zweite ein bereits invalidiertes Token — der User wird ausgeloggt.
Lösung: ein geteiltes Refresh-Promise mit Deduplication:
let refreshPromise: Promise<string> | null = null;
async function refreshToken(): Promise<string> {
if (refreshPromise) return refreshPromise;
refreshPromise = doActualRefresh()
.finally(() => { refreshPromise = null; });
return refreshPromise;
}
Eine Race Condition, ein Logout. Einmal falsch deployed — nie mehr vergessen.
Fazit
OAuth wirkt einfach, bis man die Edge Cases in der Produktion trifft: blockierte Popups, Concurrent-Refresh-Races, Token-Rotation-Sessions, Cross-Tab-Sync, Mobile-Browser die Hintergrund-Fenster schließen.
Nichts davon steht in der OAuth-Spec. Alles wird auf die harte Tour gelernt. In der Produktion. Um Mitternacht.