tva
← Approfondimenti

Quando l’autenticazione in Docker blocca l’accesso mobile: storia di un bug tra piattaforme

Il ticket è arrivato un giovedì pomeriggio: «Google Sign-In non funziona sui dispositivi mobili». La conversazione con l’assistenza era breve: tre utenti, tutti su iOS, segnalavano lo stesso indicatore di caricamento OAuth che non portava da nessuna parte. L’app web funzionava. Su desktop funzionava. Il problema si verificava solo sui dispositivi mobili e solo con l’accesso OAuth. L’autenticazione con email e password continuava a funzionare senza problemi.

È il tipo di segnalazione che promette una correzione rapida e si trasforma in un pomeriggio di confusione. I sintomi visibili, un flusso di autenticazione bloccato su una sola piattaforma, suggeriscono una dozzina di cause contemporaneamente. URI di reindirizzamento. CORS. Scambio dei token. Archiviazione della sessione. Configurazione del client. Tutte ipotesi plausibili. Nessuna si è rivelata la causa.

Lo stack

L’applicazione gira su un’istanza di Supabase gestita sulla nostra infrastruttura: l’intero stack Docker Compose, distribuito su un server dedicato. L’architettura di Supabase separa l’autenticazione in un microservizio dedicato, GoTrue, che gestisce i flussi OAuth, l’emissione dei token e gli utenti. GoTrue comunica con PostgreSQL tramite una connessione diretta al database, leggendo e scrivendo nello schema auth. Il frontend è un’applicazione React Native che comunica con Supabase tramite la libreria client JavaScript standard.

È una configurazione relativamente comune per i team che preferiscono mantenere il controllo dei propri dati. Supabase sulla propria infrastruttura offre l’intero database PostgreSQL, il livello di autenticazione, le sottoscrizioni in tempo reale e il servizio di archiviazione, tutti eseguiti in container che gestisci tu. Il compromesso, di cui questo incidente è un esempio preciso, è che i servizi gestiti si occupano senza clamore di dettagli di configurazione che diventano errori visibili quando sei tu a gestirli.

La prima ipotesi: URI di reindirizzamento non corrispondenti

Il debug degli errori OAuth sui dispositivi mobili parte quasi sempre dagli URI di reindirizzamento. Le app native usano schemi URL personalizzati (myapp://auth/callback) anziché URL HTTPS. Una discrepanza tra il callback registrato e quello atteso dal provider OAuth produce proprio il tipo di errore silenzioso che osservavamo: il flusso OAuth parte, il browser si apre, l’utente si autentica e il callback non viene mai completato.

Abbiamo verificato la configurazione degli URI di reindirizzamento nella Google Cloud Console, le impostazioni del progetto Supabase e la configurazione dei deep link dell’app React Native. Tutto corrispondeva. Gli URI erano registrati correttamente su entrambi i lati. La schermata di consenso OAuth si completava senza errori: Google accettava l’accesso. L’errore si verificava dopo il callback, in qualche punto dello scambio dei token.

La seconda ipotesi: CORS

Gli header Cross-Origin Resource Sharing sono il secondo controllo istintivo nel debug di OAuth sui dispositivi mobili. Quando un’app mobile invia richieste a un backend su un dominio personalizzato, la configurazione CORS determina se il modello di sicurezza del browser consente la richiesta. Una policy CORS configurata male produce errori che possono sembrare identici a un errore di autenticazione.

In realtà, però, CORS è applicato dai browser; le richieste che un’app nativa invia tramite il proprio client HTTP non sono soggette a queste restrizioni. E il sintomo, l’autenticazione con email funzionante mentre OAuth falliva, non era compatibile con un errore CORS. Entrambe usano lo stesso endpoint GoTrue. Se CORS avesse respinto le richieste, molto probabilmente avrebbe interessato entrambe, non solo OAuth. Abbiamo aggiunto log dettagliati al client mobile. Le richieste arrivavano al servizio GoTrue. Il servizio rispondeva. I codici di stato erano 500.

Leggere i log effettivi

I log dei container sono il punto in cui il debug sulla propria infrastruttura si distingue più nettamente da quello dei servizi gestiti. Con Supabase Cloud, la dashboard mostra log strutturati di ogni servizio. Con Docker sulla propria infrastruttura, recuperi direttamente i log dei singoli container:

docker logs supabase-auth --since 1h 2>&1 | grep -i error

Il container GoTrue registrava errori di continuo. La riga rilevante era:

level=error msg="Error creating oauth state" error="ERROR: function gen_random_uuid() does not exist (SQLSTATE 42883)"

Non era un errore di reindirizzamento OAuth, ma un errore di risoluzione di una funzione in PostgreSQL. Nelle versioni di PostgreSQL precedenti alla 13, gen_random_uuid() è fornita esclusivamente dall’estensione pgcrypto, un’estensione standard di PostgreSQL che Supabase installa nello schema extensions anziché nello schema predefinito public. Da PostgreSQL 13 in poi è anche integrata nel nucleo del sistema ed è sempre risolvibile, quindi questo specifico errore indica una versione principale precedente. GoTrue chiamava gen_random_uuid() senza specificare lo schema, aspettandosi che PostgreSQL la trovasse tramite il percorso di ricerca. Non la trovava.

A cosa serve search_path

PostgreSQL risolve i nomi di funzioni e tabelle privi di qualificazione cercando in una sequenza ordinata di schemi. Il valore predefinito è "$user", public: prima cerca in uno schema con il nome dell’utente corrente del database, poi in public. Se una funzione si trova in un altro schema e il suo nome non è esplicitamente qualificato con quello dello schema, come extensions.gen_random_uuid() invece di gen_random_uuid(), PostgreSQL restituisce un errore «does not exist», indipendentemente dal fatto che la funzione esista davvero.

La distribuzione gestita di Supabase configura il search_path di PostgreSQL per includere extensions, auth e public. Questa configurazione è documentata nel codice di Supabase, ma non è messa in evidenza nella guida al self-hosting. Nel servizio gestito di Supabase viene applicata automaticamente. Con Docker sulla propria infrastruttura dipende dalle variabili d’ambiente e dagli script di inizializzazione applicati. Nella nostra configurazione mancavano entrambi.

Perché sui dispositivi mobili e non sul web

La spiegazione dei log non risponde subito a una domanda: perché l’autenticazione web funzionava e quella mobile no? La risposta sta nelle differenze tra i flussi OAuth nei due ambienti.

Le applicazioni web che usano OAuth di Supabase adottano per impostazione predefinita il flusso implicito: il server di autorizzazione restituisce i token direttamente nel frammento dell’URL dopo il callback OAuth. Il client li estrae dall’URL, li memorizza e stabilisce la sessione senza un passaggio separato di scambio dei token. Quel percorso di esecuzione, in questo caso, non chiamava gen_random_uuid().

Le applicazioni mobili che seguono le attuali buone pratiche di sicurezza usano PKCE, Proof Key for Code Exchange. Con PKCE, il client genera un code verifier casuale, ne calcola l’hash per ottenere un code challenge e invia il challenge al server di autorizzazione. Dopo il callback, il client invia il code verifier originale all’endpoint dei token, che lo verifica prima di emettere i token. Questo flusso PKCE era il percorso di esecuzione che falliva. GoTrue chiamava gen_random_uuid() quando generava lo stato OAuth per il flusso PKCE e lo schema extensions non era nel percorso di ricerca.

Il flusso implicito sul web evitava questo specifico percorso di esecuzione. PKCE sui dispositivi mobili lo attraversava. Una sottile differenza nell’implementazione di OAuth ha fatto emergere un errore di configurazione presente nello stack Docker fin dalla distribuzione: quel percorso non era mai stato eseguito prima che un client mobile tentasse un flusso PKCE.

La correzione

La correzione consiste nell’assicurarsi che il search_path di PostgreSQL includa tutti gli schemi da cui dipendono GoTrue e PostgREST. In una configurazione Docker Compose, il modo più pulito è applicarla a livello di database, affinché persista indipendentemente dal container che si connette:

-- 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;

Un’alternativa è passare l’impostazione tramite la configurazione del servizio PostgreSQL in Docker Compose:

services:
  db:
    image: supabase/postgres:15.1.0.117
    command:
      - postgres
      - -c
      - search_path=extensions,auth,public,storage

Abbiamo applicato entrambe le soluzioni. Dopo il riavvio del container GoTrue, OAuth sui dispositivi mobili si completava senza errori. Anche Apple Sign-In, che avevamo esitato a provare durante il debug, ha funzionato immediatamente, confermando che il problema dipendeva dal flusso e non dal provider.

Cosa rivela questo incidente sul debug nella propria infrastruttura

Il percorso di debug, da «autenticazione bloccata sui dispositivi mobili» a «search_path errato in PostgreSQL», ha richiesto più tempo del necessario. Se ne possono ricavare diverse lezioni.

I log dei container sono la fonte di verità. Abbiamo seguito entrambe le ipotesi errate prima di leggere i log di GoTrue. La prima voce rilevante conteneva già l’errore esatto. In uno stack Docker gestito sulla propria infrastruttura, leggere i log è il primo passo, non l’ultima risorsa dopo aver esaurito le altre ipotesi.

Gli errori limitati a una piattaforma in un backend condiviso indicano quasi sempre una differenza nel percorso di esecuzione del client, non nel trattamento di quel client da parte del backend. Il backend non sa se sta servendo un’app mobile o un browser. Segue il percorso determinato dai parametri della richiesta. Capire quale percorso attiva PKCE rispetto al flusso implicito ci avrebbe indirizzati subito allo stato del flusso PKCE.

I servizi gestiti e quelli sulla propria infrastruttura non hanno configurazioni equivalenti. Supabase Cloud applica dettagli di configurazione documentati che non vengono applicati automaticamente nella configurazione Docker Compose. Quando qualcosa funziona su Supabase Cloud ma fallisce nella propria infrastruttura, la differenza risiede quasi sempre in una di queste configurazioni. Il sistema di segnalazione dei problemi del repository ufficiale per il self-hosting è spesso il modo più rapido per individuare l’impostazione mancante.

Un bug che «colpisce solo una piattaforma» è spesso un bug sempre presente, emerso soltanto quando il primo client ha eseguito uno specifico percorso. In questo caso, l’errore nel search_path esisteva dal giorno in cui lo stack Docker era stato distribuito. Sono serviti mesi e un’implementazione di OAuth per dispositivi mobili per renderlo visibile.

Approfondimenti correlati

Per approfondire