Wenn Docker-Auth die mobile Anmeldung verhindert: Ein plattformübergreifender Fehler
Das Ticket kam an einem Donnerstagnachmittag: „Die Anmeldung mit Google funktioniert auf Mobilgeräten nicht.“ Der Support-Thread war kurz – drei Nutzer, alle auf iOS, alle mit derselben endlosen OAuth-Ladeanzeige. Die Webanwendung funktionierte. Auf dem Desktop funktionierte alles. Nur auf Mobilgeräten schlug die Anmeldung fehl, und dort ausschließlich die OAuth-Anmeldung. Die Authentifizierung per E-Mail und Passwort funktionierte weiterhin problemlos.
Das ist die Art von Fehlermeldung, die eine schnelle Lösung erwarten lässt und stattdessen einen Nachmittag voller Unklarheiten beschert. Die sichtbaren Symptome – ein defekter Authentifizierungsablauf auf einer Plattform – legen gleichzeitig ein Dutzend mögliche Ursachen nahe. Redirect-URIs. CORS. Token-Austausch. Sitzungsspeicherung. Client-Konfiguration. Jede davon ist plausibel. Keine war die Ursache.
Der Stack
Die Anwendung läuft auf einer selbst betriebenen Supabase-Instanz – dem vollständigen Docker-Compose-Stack auf einem dedizierten Server. Supabase lagert die Authentifizierung in einen eigenen Microservice aus: GoTrue übernimmt OAuth-Abläufe, die Ausstellung von Tokens und die Benutzerverwaltung. GoTrue kommuniziert über eine direkte Datenbankverbindung mit PostgreSQL und liest und schreibt im Schema auth. Das Frontend ist eine React-Native-Anwendung, die über die standardmäßige JavaScript-Clientbibliothek mit Supabase kommuniziert.
Diese Konfiguration ist bei Teams, die die Kontrolle über ihre Daten behalten möchten, relativ verbreitet. Selbst betriebenes Supabase stellt die vollständige PostgreSQL-Datenbank, die Authentifizierungsschicht, Echtzeit-Abonnements und den Speicherdienst bereit. Alles läuft in Containern, die Sie selbst verwalten. Der Nachteil – und dieser Vorfall zeigt ihn sehr genau – ist, dass verwaltete Dienste Konfigurationsdetails im Hintergrund erledigen, die beim eigenen Betrieb zu sichtbaren Fehlern führen können.
Die erste Hypothese: Abweichende Redirect-URIs
Bei OAuth-Fehlern auf Mobilgeräten prüft man fast immer zuerst die Redirect-URIs. Native Apps verwenden eigene URL-Schemata (myapp://auth/callback) statt HTTPS-URLs. Eine Abweichung zwischen dem registrierten Callback und den Erwartungen des OAuth-Anbieters führt genau zu dem stillen Fehler, den wir beobachteten: Der OAuth-Ablauf startet, der Browser öffnet sich, der Nutzer authentifiziert sich, und der Callback wird nie abgeschlossen.
Wir überprüften die Redirect-URI-Konfiguration in der Google Cloud Console, die Supabase-Projekteinstellungen und die Deep-Link-Konfiguration der React-Native-App. Alles stimmte überein. Die URIs waren auf beiden Seiten korrekt registriert. Der OAuth-Zustimmungsdialog wurde ohne Fehler abgeschlossen – Google akzeptierte die Anmeldung. Der Fehler trat nach dem Callback auf, irgendwo beim Token-Austausch.
Die zweite Hypothese: CORS
Als Nächstes prüft man beim Debugging von mobilem OAuth meist die Header für Cross-Origin Resource Sharing. Wenn eine mobile App Anfragen an ein Backend unter einer eigenen Domain sendet, bestimmt die CORS-Konfiguration, ob das Sicherheitsmodell des Browsers die Anfrage zulässt. Eine falsch konfigurierte CORS-Richtlinie kann Fehler hervorrufen, die wie ein Authentifizierungsfehler aussehen.
Tatsächlich wird CORS jedoch von Browsern durchgesetzt; Anfragen, die eine native App über ihren eigenen HTTP-Client sendet, unterliegen diesen Beschränkungen nicht. Und das Symptom – die Anmeldung per E-Mail funktionierte, OAuth dagegen nicht – passte nicht zu einem CORS-Fehler. Beide verwenden denselben GoTrue-Endpunkt. Würde CORS Anfragen ablehnen, beträfe das höchstwahrscheinlich beide und nicht nur OAuth. Wir ergänzten den mobilen Client um ausführliche Protokollierung. Die Anfragen erreichten den GoTrue-Dienst. Der Dienst antwortete. Die Statuscodes waren 500.
Die Logs lesen
Bei Container-Logs unterscheidet sich das Debugging selbst betriebener Systeme besonders deutlich von dem verwalteter Dienste. In Supabase Cloud zeigt das Dashboard strukturierte Logs aller Dienste an. Bei selbst betriebenem Docker lesen Sie die Logs direkt aus den einzelnen Containern:
docker logs supabase-auth --since 1h 2>&1 | grep -i error
Der GoTrue-Container hatte fortlaufend Fehler protokolliert. Die relevante Zeile:
level=error msg="Error creating oauth state" error="ERROR: function gen_random_uuid() does not exist (SQLSTATE 42883)"
Das war kein OAuth-Redirect-Fehler. PostgreSQL konnte eine Funktion nicht auflösen. In PostgreSQL-Versionen vor 13 wird gen_random_uuid() ausschließlich von der Erweiterung pgcrypto bereitgestellt – einer Standarderweiterung von PostgreSQL, die Supabase im Schema extensions statt im Standardschema public installiert. Ab PostgreSQL 13 ist die Funktion auch im Kern integriert und immer auflösbar; dieser konkrete Fehler deutet daher auf eine ältere Hauptversion hin. GoTrue rief gen_random_uuid() ohne Schema-Präfix auf und erwartete, dass PostgreSQL die Funktion über den Suchpfad findet. Das geschah nicht.
Was search_path bewirkt
PostgreSQL löst Funktions- und Tabellennamen ohne Schema-Präfix auf, indem es eine Folge von Schemas der Reihe nach durchsucht. Der Standard ist "$user", public: zuerst ein Schema mit dem Namen des aktuellen Datenbankbenutzers, dann public. Liegt eine Funktion in einem anderen Schema und wird nicht ausdrücklich mit dessen Namen aufgerufen – extensions.gen_random_uuid() statt gen_random_uuid() –, meldet PostgreSQL „does not exist“, unabhängig davon, ob die Funktion tatsächlich existiert.
In der verwalteten Supabase-Umgebung enthält der PostgreSQL-search_path die Schemas extensions, auth und public. Diese Konfiguration ist im Supabase-Code dokumentiert, wird im Einrichtungsleitfaden für Self-Hosting aber nicht besonders hervorgehoben. Bei verwaltetem Supabase wird sie automatisch angewendet. Beim eigenen Docker-Betrieb hängt sie davon ab, welche Umgebungsvariablen und Initialisierungsskripte angewendet wurden. Bei uns war beides nicht der Fall.
Warum Mobilgeräte betroffen waren und das Web nicht
Eine Frage beantwortet die Erklärung aus den Logs nicht sofort: Warum funktionierte die Authentifizierung im Web, während sie auf Mobilgeräten fehlschlug? Die Antwort liegt in den unterschiedlichen OAuth-Abläufen dieser Umgebungen.
Webanwendungen mit Supabase OAuth verwenden standardmäßig den Implicit Flow: Der Autorisierungsserver gibt Tokens nach dem OAuth-Callback direkt im URL-Fragment zurück. Der Client liest sie aus der URL, speichert sie, und die Sitzung wird ohne separaten Token-Austausch aufgebaut – ein Codepfad, der in diesem Fall gen_random_uuid() nicht aufrief.
Mobile Anwendungen, die aktuellen Sicherheitsempfehlungen folgen, verwenden PKCE: Proof Key for Code Exchange. Bei PKCE erzeugt der Client einen zufälligen Code Verifier, bildet daraus per Hash eine Code Challenge und sendet diese an den Autorisierungsserver. Nach dem Callback sendet der Client den ursprünglichen Code Verifier an den Token-Endpunkt, der ihn vor der Ausstellung von Tokens überprüft. Dieser PKCE-Ablauf war der fehlerhafte Codepfad. GoTrue rief beim Erzeugen des OAuth-Status für den PKCE-Ablauf gen_random_uuid() auf, und das Schema extensions fehlte im Suchpfad.
Der Implicit Flow im Web umging diesen speziellen Codepfad. PKCE auf Mobilgeräten tat das nicht. Ein subtiler Unterschied in der OAuth-Implementierung legte einen Konfigurationsfehler offen, der seit dem Deployment im Docker-Stack bestanden hatte – er war nur nie ausgelöst worden, bis ein mobiler Client einen PKCE-Ablauf versuchte.
Die Lösung
Die richtige Lösung besteht darin, sicherzustellen, dass der PostgreSQL-search_path alle Schemas enthält, von denen GoTrue und PostgREST abhängen. Bei Docker Compose lässt sich das am saubersten auf Datenbankebene festlegen, damit die Einstellung unabhängig vom verbundenen Container bestehen bleibt:
-- Run once against the database
ALTER DATABASE postgres SET search_path TO extensions, auth, public, storage;
ALTER ROLE authenticator SET search_path TO extensions, auth, public, storage;
ALTER ROLE supabase_auth_admin SET search_path TO extensions, auth, public, storage;
Alternativ lässt sich die Einstellung über die Konfiguration des PostgreSQL-Dienstes in Docker Compose übergeben:
services:
db:
image: supabase/postgres:15.1.0.117
command:
- postgres
- -c
- search_path=extensions,auth,public,storage
Wir wendeten beides an. Nach dem Neustart des GoTrue-Containers wurde der mobile OAuth-Ablauf ohne Fehler abgeschlossen. Auch die Anmeldung mit Apple – deren Test wir während der Fehlersuche zunächst gescheut hatten – funktionierte sofort. Das bestätigte, dass das Problem am Ablauf und nicht am Anbieter lag.
Was dieser Vorfall über das Debugging selbst betriebener Systeme zeigt
Die Fehlersuche – von „Authentifizierung auf Mobilgeräten defekt“ zu „falscher search_path in PostgreSQL“ – dauerte länger als nötig. Daraus lassen sich mehrere Lehren ziehen.
Container-Logs liefern die maßgeblichen Hinweise. Wir verfolgten beide falschen Hypothesen, bevor wir die GoTrue-Logs lasen. Bereits der erste relevante Eintrag enthielt den exakten Fehler. In einem selbst betriebenen Docker-Stack ist das Lesen der Logs der erste Schritt, keine letzte Option nach dem Ausschöpfen anderer Theorien.
Plattformspezifische Fehler in einem gemeinsamen Backend weisen fast immer auf einen Unterschied im Codepfad des Clients hin, nicht darauf, dass das Backend diesen Client anders behandelt. Das Backend weiß nicht, ob es eine mobile App oder einen Browser bedient. Es folgt dem Codepfad, den die Anfrageparameter bestimmen. Hätten wir verstanden, welchen Codepfad PKCE im Gegensatz zum Implicit Flow aktiviert, hätten wir sofort den Zustand des PKCE-Ablaufs untersucht.
Verwaltete und selbst betriebene Dienste haben keine gleichwertigen Konfigurationen. Supabase Cloud wendet Konfigurationsdetails an, die zwar dokumentiert sind, im Docker-Compose-Setup aber nicht automatisch übernommen werden. Wenn etwas in Supabase Cloud funktioniert, beim eigenen Betrieb aber fehlschlägt, liegt der Unterschied fast immer in einer dieser Einstellungen. Der Issue-Tracker des offiziellen Self-Hosting-Repositorys ist oft der schnellste Weg, die fehlende Einstellung zu finden.
Ein Fehler, der „nur eine Plattform betrifft“, ist oft ein Fehler, der schon immer vorhanden war und erst durch den ersten Client sichtbar wird, der einen bestimmten Codepfad ausführt. In diesem Fall bestand der search_path-Fehler seit dem Tag, an dem der Docker-Stack bereitgestellt wurde. Es brauchte Monate und eine mobile OAuth-Implementierung, um ihn sichtbar zu machen.