Pular para o conteúdo

Noções de segurança

Supabase "Auth session missing!": para onde foi a sessão

"Auth session missing!" é o supabase-js sem achar nenhuma sessão logada e sem chegar a perguntar ao Supabase. Para onde foi a sessão, e a correção.

Vlad Tkachenko11 min de leitura
Um porta-crachá vazio pendurado num cordão, com o espaço do crachá contornado em âmbar, na frente de dois crachás com rosto.

Em resumo

  • "Auth session missing!" (AuthSessionMissingError, status 400) vem do supabase-js, dentro do seu app. A biblioteca procurou uma sessão logada, não achou nenhuma e nunca enviou a requisição ao Supabase.
  • Numa página em que ninguém fez login, essa é a resposta certa, e o seu código só precisa tratá-la. Numa página de redefinição de senha, quer dizer que o link não fez login de ninguém aqui. Numa Edge Function ou num servidor, a sessão do visitante nunca foi repassada.
  • Deixe o getUser() onde está. Trocar por getSession() num servidor faz o erro sumir acreditando no que quer que o cookie diga.

O seu app do Lovable ou do Bolt faz o login das pessoas com o Supabase. Numa manhã você abre o console do navegador e encontra isto em vermelho, numa página em que nada parece errado:

AuthSessionMissingError: Auth session missing!

Ou uma cliente escreve dizendo que a página "Defina uma nova senha" recusa ela com essas mesmas palavras, toda vez.

Pesquise "auth session missing supabase" e você vai achar tópicos cheios de correções: trocar getUser() por getSession(), voltar a versão da biblioteca, colocar uma espera, reiniciar o projeto. Elas supõem que algo quebrou entre o seu app e o Supabase. Nenhuma requisição foi feita. O erro nasce dentro do seu app, escrito pela biblioteca do Supabase, antes que qualquer coisa saia do navegador.

O que significa "Auth session missing!" no Supabase?

Perguntaram ao supabase-js quem estava logado; ele procurou uma sessão, não achou nenhuma e respondeu sozinho.

Uma sessão é o que o Supabase Auth entrega a uma pessoa quando ela faz login: um par de tokens que dizem quem ela é, guardados pelo navegador para que o próximo carregamento de página ainda a reconheça. O supabase-js é a biblioteca com que o seu app conversa com o Supabase, e toda chamada que precisa de uma pessoa começa tirando essa sessão do lugar.

A mensagem está escrita no próprio supabase-js, no arquivo errors.ts da biblioteca de autenticação: um AuthSessionMissingError com o texto Auth session missing! e o status 400. Esse 400 é um rótulo que a biblioteca dá a si mesma. Nenhum servidor respondeu com ele, e os logs do Supabase não vão mostrar nada, porque o getUser() procura uma sessão antes de fazer qualquer outra coisa. Um comentário no código dele diz isso: devolve um erro se não houver nem token de acesso nem um cabeçalho Authorization próprio.

Pense numa recepção que sabe dizer qual visitante é quem. Cada pessoa que faz login recebe um crachá na porta, e o supabase-js guarda esse crachá no bolso do navegador. Quando o seu código pergunta "quem é este?", o supabase-js olha primeiro no bolso. Se há um crachá, ele leva até a recepção e volta com um nome. Se o bolso está vazio, ele nem vai até a recepção. Volta na hora e diz que não há crachá.

Com uma sessão, o supabase-js pergunta ao Supabase Auth. Sem ela, ele para dentro do seu app, e o erro vem de lá.

Ele é devolvido, não lançado

O getUser() devolve o erro ao lado de um usuário vazio, e o seu código decide o que acontece depois.

A chamada é assim, e sem sessão user fica null e error traz o AuthSessionMissingError:

const { data: { user }, error } = await supabase.auth.getUser()

Nada é lançado, então um try … catch em volta da chamada não pega nada. É daí que vem o estrago visível, numa de duas formas:

  • O código mostra qualquer error que recebe. Um visitante que ainda não fez login vê "Auth session missing!" num aviso ou numa caixa vermelha, numa página que ele podia ver.
  • O código usa o usuário sem conferir. Ele lê user.id e a página quebra com Cannot read properties of null, o que parece um bug completamente diferente.

Os dois são o mesmo engano: tratar "ninguém está logado" como uma falha. Confira o usuário primeiro, e dê a um visitante sem login a tela feita para ele:

const { data: { user } } = await supabase.auth.getUser()
if (!user) {
  // ninguém está logado: mostrar a tela de login ou a página pública
  return
}

Se você quiser diferenciar isso de uma falha de verdade, a biblioteca dá nome ao caso: error.name é 'AuthSessionMissingError'.

Onde a sessão se perde

Em três lugares, e só o primeiro é inofensivo.

1. Ninguém fez login neste navegador. O seu app chama getUser() numa página que qualquer pessoa pode abrir, talvez para decidir se mostra "Entrar" ou "Minha conta". Para um visitante, o bolso vazio é a resposta verdadeira. É aqui que a maioria encontra a mensagem, e a correção de código acima é tudo de que ela precisa.

2. Uma página de redefinição de senha ou de link mágico. Definir uma senha nova chama updateUser({ password }), e o guia de senhas do Supabase é claro quanto à ordem: é depois de ter uma sessão que se coleta a senha nova e se chama updateUser. Nessa página, a sessão deve vir do link do e-mail, que faz o login da pessoa ao abrir. Quando isso não acontece, o bolso está vazio e o updateUser() responde "Auth session missing!". Os motivos de sempre:

  • O link já tinha sido usado, ou tinha expirado, antes de ser aberto. Por que o Supabase diz que o link do e-mail é inválido ou expirou passa por cada um.
  • Ele foi aberto num navegador ou app diferente do que o pediu. Quando um app usa o login PKCE do Supabase, parte da troca fica no navegador que a começou, e segundo a documentação do Supabase a troca do código precisa ser iniciada no mesmo navegador e dispositivo em que o fluxo começou.
  • Ele mandou a pessoa para outro lugar, como localhost, e a sessão chegou num endereço que não é o seu app. Por que o Supabase redireciona para localhost cobre esse caso.

3. O seu servidor ou uma Edge Function nunca recebeu a sessão. O crachá está no navegador do seu visitante. Código que roda em outro lugar, uma Edge Function do Supabase ou a metade servidor de um app Next.js feito com v0 ou Cursor, cria o próprio cliente Supabase, e um cliente novo começa com o bolso vazio. Enquanto a sessão do visitante não for passada para ele, o getUser() ali não encontra nada e responde "Auth session missing!" sem perguntar ao Supabase.

Numa Edge Function a sessão viaja no cabeçalho Authorization da requisição, e o supabase-js só a usa se você a entregar ao criar o cliente. Num app Next.js ela viaja em cookies, que a ferramenta de servidor do Supabase só lê se você passar os da requisição. Se uma Edge Function devolve essa falha como erro, o seu app mostra "Edge Function returned a non-2xx status code", com esta mensagem esperando no log da função.

A sessão é do navegador. Uma função ou um servidor só a tem se a requisição a levar e o código a passar para o seu cliente.

Como saber qual é o seu caso

Veja onde a mensagem aparece, e depois o que o navegador realmente enviou.

No console do navegador, numa página que qualquer pessoa pode abrir. Caso 1. Abra as ferramentas de desenvolvedor do navegador (F12), escolha Application, depois Local Storage e o endereço do seu app. O supabase-js guarda a sessão sob uma chave formada por sb-, o identificador do seu projeto e -auth-token. Se não houver uma entrada assim, este navegador não tem sessão, e a mensagem está dizendo a verdade. Um app Next.js que usa as ferramentas de servidor do Supabase guarda a sessão em cookies, então procure o mesmo nome em Cookies.

Numa página de redefinição ou de link mágico. Caso 2. Veja o endereço em que o link abriu. Se não for o seu app, ou se for um link que você ou um scanner de e-mail já tinha aberto, a sessão nunca teve chance de chegar.

No log de uma Edge Function, ou do seu servidor. Caso 3. Abra a aba Network, repita a ação e clique na requisição para a sua função. Se o cabeçalho Authorization dela tem uma sequência longa que começa com eyJ, o navegador enviou o token de uma pessoa, e a sua função não o passou para o cliente dela.

O que você não vai achar em nenhum caminho é uma requisição para /auth/v1/user que voltou com esta mensagem. Se vir uma que falhou, é outro erro. Uma resposta dizendo invalid claim: missing sub claim quer dizer que a chave do seu projeto foi enviada onde deveria ir uma pessoa, e aquele artigo é o gêmeo deste.

A correção para cada caso

Dê a cada um a sessão que falta, ou uma resposta que não precise dela.

Caso 1, o visitante sem login: confira user antes de usá-lo, como no código acima, e mostre a tela feita para quem não fez login.

Caso 2, a página de redefinição: espere o link fazer o login da pessoa antes de mostrar o formulário. O Supabase emite um evento PASSWORD_RECOVERY quando alguém chega por um link de recuperação, e a referência dele mostra a página esperando por ele:

supabase.auth.onAuthStateChange((event) => {
  if (event === 'PASSWORD_RECOVERY') {
    // o link fez o login da pessoa: agora mostrar o formulário de senha nova
  }
})

Se o evento nunca vier, o link não funcionou. Diga isso e ofereça mandar um novo, em vez de mostrar um formulário que com certeza vai falhar.

Caso 3, a função ou o servidor: passe a sessão do visitante para o cliente que você cria. Numa Edge Function, crie-o com o cabeçalho que chegou:

const supabase = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_ANON_KEY')!,
  { global: { headers: { Authorization: req.headers.get('Authorization')! } } },
)
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
  return new Response(JSON.stringify({ error: 'Sign in first' }), {
    status: 401,
    headers: { ...corsHeaders, 'Content-Type': 'application/json' },
  })
}

O wrapper mais novo do Supabase, withSupabase({ auth: 'user' }), faz o mesmo antes do seu handler rodar, e o artigo sobre Edge Functions mostra a configuração. Mantenha o 401 para quem chama sem sessão, para que uma requisição de uma página sem login receba uma recusa clara. Com o cabeçalho repassado, o getUser() ali pode devolver invalid claim: missing sub claim para quem chama sem login: o mesmo visitante visto do outro lado, e o 401 cobre os dois.

Num app Next.js, crie o cliente de servidor com a ferramenta @supabase/ssr do Supabase e os cookies da requisição, como mostra a configuração de servidor dela, e chame getUser() nesse cliente.

Duas correções que pioram tudo

As duas são sugeridas para esta mensagem, e cada uma para o erro tirando a verificação que está por trás dele.

Trocar getUser() por getSession(). No navegador, isso não muda nada que importe: o armazenamento continua vazio, então você não recebe nem sessão nem usuário, só que sem a mensagem. Num servidor é perigoso. O guia de servidor do Supabase diz para nunca confiar em supabase.auth.getSession() em código de servidor, porque ele lê a sessão do cookie sem revalidar. Um cookie é algo que o visitante manda para você, e qualquer pessoa pode mandar o cookie que quiser. getUser() é o método que o supabase-js documenta como fazendo uma requisição ao servidor do Supabase Auth, de modo que o resultado é autêntico. Mantenha-o em todo lugar onde a resposta decide o que alguém pode fazer.

Colocar a chave secreta no cliente para a chamada parar de falhar. Um cliente criado com a chave service_role ou sb_secret_ não precisa de sessão para ler e escrever, porque ignora por completo as regras do seu banco. A função volta a funcionar, para qualquer um que a chame, logado ou não. Se um builder faz essa mudança em código que vai para o navegador, a chave vai junto para cada visitante. Se isso já aconteceu, troque a chave primeiro.

Dá para conferir isso de fora. O nosso escaneamento grátis lê o JavaScript do seu app publicado como um estranho leria e nomeia cada chave que encontra, inclusive se é uma das que devem estar ali. Leva uns 20 segundos e não pede conta: escaneie o seu app.

Manter a verificação depois da próxima mudança

A verificação de quem está logado é a parte de uma página que mais facilmente some quando você pede ao seu builder para fazer um erro sumir, e as duas correções rápidas aí de cima a tiram.

O Reeve Monitor varre o seu app publicado de novo a cada hora:

  • as nove verificações a cada hora, em até três apps
  • um e-mail no dia em que uma nova varredura avaliar o seu app pior que a anterior; uma chave secreta no seu JavaScript limita sozinha a nota a D
  • se o app está no ar, a cada 60 segundos
  • um relatório mensal do que ele viu

Um servidor que acredita em qualquer cookie, ou uma função que roda com a chave secreta, deixa um estranho mudar as linhas dos seus usuários. O Reeve Care guarda uma cópia do seu banco Supabase de antes disso acontecer.

  • uma cópia criptografada conforme uma agenda, guardada no armazenamento do próprio Reeve na UE, fora da sua conta Supabase
  • cada cópia verificada antes de contar, contando as linhas de cada tabela
  • uma restauração em um clique que salva o que está lá agora antes de substituir
  • também os seus arquivos enviados, assim que você conectar uma credencial de Storage
  • tudo o que o Monitor faz

O que fazer agora

O que fazer

  • Leia "Auth session missing!" como "ninguém está logado aqui". A requisição nunca saiu do seu app.
  • Confira se user é null antes de usá-lo, e dê a um visitante sem login a tela feita para ele em vez do erro.
  • Numa página de redefinição, espere o evento PASSWORD_RECOVERY antes de mostrar o formulário, e ofereça um link novo se ele nunca chegar.
  • Numa Edge Function ou num servidor, passe o cabeçalho Authorization ou os cookies do visitante para o cliente que você cria, e responda 401 se ainda não houver usuário.
  • Mantenha getUser() no servidor. Lá, getSession() acredita em qualquer cookie que receber.

Se as suas sessões existem e depois somem, e pessoas que estavam logadas de repente estão fora, é outro problema com causas próprias, e por que os usuários do Supabase são deslogados cobre isso. O checklist de segurança de 10 minutos cobre o que mais um app recém-lançado costuma deixar aberto.

Perguntas frequentes

O que significa "Auth session missing!" no Supabase?

Significa que o supabase-js, a biblioteca do Supabase dentro do seu app, foi consultado sobre o usuário logado e não tinha nenhuma sessão com que perguntar. Uma sessão é o que o Supabase Auth entrega a uma pessoa quando ela faz login, e a biblioteca a guarda no navegador. Se não há nenhuma, ela nem contata o Supabase. Ela devolve AuthSessionMissingError com o status 400 e um usuário null.

Por que vejo "Auth session missing!" quando ninguém está logado?

Porque essa é a resposta para um visitante que não fez login. O seu app chamou getUser() numa página que qualquer pessoa pode abrir, e o supabase-js informou que ninguém estava logado. Nada está quebrado. O seu código só precisa ler o usuário como null e mostrar a tela de login, em vez de mostrar o erro.

Por que a minha página de redefinição de senha diz "Auth session missing!"?

Trocar uma senha com updateUser() precisa de uma sessão, e nessa página a sessão deve vir do link do e-mail. Se o link já tinha sido usado ou tinha expirado, foi aberto num navegador diferente do que o pediu, ou mandou a pessoa para outro endereço, nenhuma sessão chegou e o updateUser() não tem ninguém para alterar. Espere o evento PASSWORD_RECOVERY antes de mostrar o formulário, e ofereça um link novo se ele nunca vier.

É a mesma coisa que "invalid claim: missing sub claim"?

Não. "Auth session missing!" é decidido dentro do seu app, antes que qualquer requisição saia dele. "invalid claim: missing sub claim" volta do Supabase Auth, que recebeu uma requisição e encontrou a chave do seu projeto onde deveria estar o token de uma pessoa. O primeiro quer dizer que não havia nada para enviar. O segundo, que foi enviada a coisa errada.

Devo usar getSession() em vez de getUser()?

Não para fazer esse erro sumir. No navegador, getSession() encontra o mesmo armazenamento vazio e não devolve nenhuma sessão, então nada muda além de a mensagem parar. Num servidor é pior: o supabase-js documenta que getSession() lê a sessão direto do armazenamento, por exemplo dos cookies da requisição, que podem não ser autênticos, e recomenda getUser() no lugar porque ele confere com o Supabase Auth.

Escrito por

Vlad Tkachenko

Fundador da Reeve

Passo meus dias olhando apps criados com Lovable, Bolt, v0, Cursor e Replit, e a curta lista de erros que aparecem neles sem parar.

Mais sobre o autor

Leia depois

Todos os artigos

Faça backup do seu banco Supabase antes da próxima mudança

O Reeve guarda uma cópia criptografada em horários programados e a restaura para você se algo quebrar.

Ver backups do Supabase

Verificação externa automatizada, não uma auditoria completa. A ausência de achados não é garantia de segurança.