OAuth: Wo Frontend-Entwickler scheitern
← Alle Notes

OAuth: Wo Frontend-Entwickler scheitern

15. Januar 20266 Min. Lesezeit
oauthsecurityfrontendbackend

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:

  1. Kleines Popup öffnen: window.open('/auth/google/popup', ...)
  2. Das Popup führt den OAuth-Flow serverseitig durch
  3. Bei Erfolg ruft das Popup window.opener.postMessage({ token, user }, origin) auf
  4. 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.