tva
← Artigos

Quando a autenticação no Docker impede o login móvel: um bug entre plataformas

O ticket chegou em uma tarde de quinta-feira: “Login com o Google não está funcionando em dispositivos móveis.” A conversa de suporte era curta – três usuários, todos iOS, todos relatando o mesmo indicador de carregamento do OAuth que não levava a lugar nenhum. O aplicativo web funcionava. O desktop funcionava. Apenas o acesso em dispositivos móveis falhava, e apenas para login via OAuth. A autenticação por e-mail e senha continuava sem problemas.

Este é o tipo de relatório de bug que promete uma correção rápida e entrega uma tarde de confusão. Os sintomas superficiais – um fluxo de autenticação quebrado em uma plataforma – apontam em uma dúzia de direções simultaneamente. URIs de redirecionamento. CORS. Troca de token. Armazenamento de sessão. Configuração do lado do cliente. Cada um é plausível. Nenhum deles era a causa.

A stack

O aplicativo é executado em uma instância Supabase auto-hospedada – a stack completa do Docker Compose, implantada em um servidor dedicado. A arquitetura do Supabase separa a autenticação em seu próprio microsserviço: GoTrue, que lida com fluxos OAuth, emissão de tokens e gerenciamento de usuários. O GoTrue se comunica com o PostgreSQL via uma conexão direta ao banco de dados, lendo e escrevendo no esquema auth. O frontend é um app React Native que se comunica com o Supabase via a biblioteca cliente JavaScript padrão.

Esta é uma configuração relativamente comum para equipes que preferem controlar seus dados. O Supabase auto-hospedado fornece o banco de dados PostgreSQL completo, a camada de autenticação, assinaturas em tempo real e o serviço de armazenamento, todos executados em contêineres que você gerencia. A contrapartida – e este incidente é um exemplo preciso disso – é que os serviços gerenciados lidam silenciosamente com detalhes de configuração que se tornam falhas visíveis quando você os gerencia sozinho.

A primeira hipótese: incompatibilidade de URI de redirecionamento

Erros OAuth em dispositivos móveis quase sempre começam com URIs de redirecionamento. Aplicativos nativos usam esquemas de URL personalizados (myapp://auth/callback) em vez de URLs HTTPS, e uma incompatibilidade entre o callback registrado e o que o provedor OAuth espera produz exatamente o tipo de falha silenciosa que estávamos vendo: o fluxo OAuth inicia, o navegador abre, o usuário autentica e o callback nunca é concluído.

Verificamos a configuração do URI de redirecionamento no Google Cloud Console. Verificamos as configurações do projeto Supabase. Verificamos a configuração de deep-link do aplicativo React Native. Tudo correspondia. Os URIs estavam corretamente registrados em ambos os lados. A tela de consentimento OAuth foi concluída sem erro – o Google estava aceitando o login. A falha estava acontecendo após o callback, em algum lugar na troca de token.

A segunda hipótese: CORS

Os cabeçalhos de compartilhamento de recursos entre origens (CORS) são o segundo ponto normalmente verificado ao depurar OAuth em dispositivos móveis. Quando um app móvel faz requisições para um backend em execução em um domínio personalizado, a configuração CORS determina se o modelo de segurança do navegador permite a requisição. Uma política CORS mal configurada produz uma classe de erros que pode parecer idêntica a uma falha de autenticação.

Mas na realidade, o CORS é aplicado pelos navegadores; as requisições que um app nativo envia por meio de seu próprio cliente HTTP não estão sujeitas a essas restrições. E o sintoma – que a autenticação por e-mail funcionava enquanto o OAuth não funcionava – era inconsistente com uma falha CORS. Ambos usam o mesmo endpoint GoTrue. Se o CORS estivesse rejeitando requisições, muito provavelmente afetaria ambos, não apenas o OAuth. Adicionamos registro detalhado ao cliente móvel. As requisições estavam chegando ao serviço GoTrue. O serviço estava respondendo. Os códigos de status eram 500.

Lendo os logs reais

Os logs de contêiner são onde a depuração auto-hospedada diverge mais nitidamente dos serviços gerenciados. Com o Supabase Cloud, o painel expõe logs estruturados de cada serviço. Com o Docker auto-hospedado, você consulta os logs de contêineres individuais diretamente:

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

O contêiner GoTrue estava registrando erros continuamente. A linha relevante:

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

Isso não era um erro de redirecionamento OAuth. Era uma falha de resolução de função PostgreSQL. Nas versões do PostgreSQL anteriores à 13, gen_random_uuid() é fornecida apenas pela extensão pgcrypto – uma extensão PostgreSQL padrão instalada pelo Supabase no esquema extensions em vez do esquema padrão public. A partir do PostgreSQL 13, ela também está integrada ao núcleo e sempre pode ser resolvida, portanto esse erro específico aponta para uma versão principal mais antiga. O GoTrue estava chamando gen_random_uuid() sem qualificador de esquema, esperando que o PostgreSQL a encontrasse via o search_path. Não estava encontrando.

O que o search_path faz

O PostgreSQL resolve nomes de funções e tabelas não qualificados pesquisando uma sequência de esquemas em ordem. O padrão é "$user", public: primeiro procura em um esquema com o nome do usuário atual do banco de dados, depois em public. Se uma função está em qualquer outro esquema e não está explicitamente qualificada com seu nome de esquema – extensions.gen_random_uuid() em vez de gen_random_uuid() – o PostgreSQL retorna um erro “does not exist” independentemente de a função realmente existir.

A implantação gerenciada do Supabase configura o search_path do PostgreSQL para incluir extensions, auth e public. Essa configuração está documentada na base de código do Supabase, mas não é destacada de forma clara no guia de auto-hospedagem. No Supabase gerenciado, ela é aplicada automaticamente. No Docker auto-hospedado, depende de quais variáveis de ambiente e scripts de inicialização foram aplicados. Na nossa instalação, nenhuma dessas duas formas de configuração havia sido aplicada.

Por que em dispositivos móveis e não na web

Uma pergunta que os logs não respondem de imediato: por que a autenticação web funcionava enquanto o acesso em dispositivos móveis falhava? A resposta está em como os fluxos OAuth diferem entre ambientes.

Os aplicativos web que usam OAuth do Supabase têm como padrão o fluxo implícito: o servidor de autorização retorna tokens diretamente no fragmento de URL após o callback OAuth. O cliente os extrai da URL, os armazena e a sessão é estabelecida sem uma etapa separada de troca de token – um caminho de código que por acaso não chamava gen_random_uuid().

Os apps móveis que seguem as melhores práticas de segurança atuais usam PKCE (Proof Key for Code Exchange, ou prova de chave para troca de código). No PKCE, o cliente gera um verificador de código aleatório, calcula seu hash para produzir um desafio de código e envia o desafio ao servidor de autorização. Após o callback, o cliente envia o verificador de código original ao endpoint de token, que o verifica antes de emitir tokens. Esse fluxo PKCE era o caminho de código que estava falhando. O GoTrue estava chamando gen_random_uuid() ao gerar o estado OAuth para o fluxo PKCE, e o esquema extensions não estava no search_path.

O fluxo implícito na web contornou esse caminho de código específico. O PKCE em dispositivos móveis não. Uma diferença sutil na implementação OAuth expôs um erro de configuração que existia na stack Docker desde a implantação – simplesmente não havia sido percorrido até que um cliente móvel tentasse um fluxo PKCE.

A correção

A correção é garantir que o search_path do PostgreSQL inclua todos os esquemas dos quais o GoTrue e o PostgREST dependem. Em uma configuração do Docker Compose, a forma mais clara de fazer isso é aplicar a configuração no nível do banco de dados para que persista independentemente de qual contêiner se conecta:

-- Execute uma vez no banco de dados
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;

Uma alternativa é passar o parâmetro pela configuração do serviço PostgreSQL no Docker Compose:

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

Aplicamos as duas configurações. Após reiniciar o contêiner GoTrue, o OAuth em dispositivos móveis foi concluído sem erro. O login com a Apple – que havíamos evitado testar durante o processo de depuração – também funcionou imediatamente, confirmando que o problema era específico ao fluxo e não ao provedor.

O que este incidente revela sobre depuração auto-hospedada

O caminho de depuração – de “falha de autenticação em dispositivos móveis” a “search_path errado no PostgreSQL” – levou mais tempo do que deveria. Vale a pena extrair várias lições.

Os logs de contêiner são a fonte de referência. Ambas as hipóteses incorretas foram investigadas antes de ler os logs do GoTrue. Os logs continham o erro exato na primeira entrada relevante. Em uma stack Docker auto-hospedada, ler os logs é o primeiro passo, não um último recurso após esgotar outras teorias.

Falhas específicas de uma plataforma em um backend compartilhado quase sempre indicam uma diferença no caminho de código do cliente, não uma diferença em como o backend trata aquele cliente específico. O backend não sabe que está atendendo a um app móvel ou a um navegador. Ele segue o caminho de código determinado pelos parâmetros da requisição. Entender qual caminho de código o PKCE ativa – em oposição ao fluxo implícito – teria apontado para o estado do fluxo PKCE imediatamente.

Os serviços gerenciados e os serviços auto-hospedados não são configurações equivalentes. O Supabase Cloud aplica detalhes de configuração que estão documentados, mas não são automaticamente aplicados na configuração do Docker Compose. Quando algo funciona no Supabase Cloud, mas falha na versão auto-hospedada, a lacuna está quase sempre em uma dessas configurações. O sistema de acompanhamento de problemas do repositório oficial auto-hospedado é frequentemente a maneira mais rápida de localizar a configuração específica que está faltando.

Um bug que “só afeta uma plataforma” é frequentemente um bug que sempre esteve presente, exposto apenas pelo primeiro cliente a percorrer um caminho de código específico. Neste caso, o erro de search_path existia desde o dia em que a stack Docker foi implantada. Levou meses e uma implementação de OAuth em dispositivos móveis para torná-lo visível.

Artigos relacionados

Leitura complementar