Pular para o conteúdo

Noções de segurança

Supabase "Invalid Refresh Token: Refresh Token Not Found": 3 causas

Supabase "Invalid Refresh Token: Refresh Token Not Found" desconecta seus usuários. Três causas, e como o log do Auth separa cada uma.

Vlad Tkachenko13 min de leitura
Um bilhete rasgado em dois, a metade esquerda acesa e a direita apagada e caindo, diante de uma fileira de bilhetes inteiros.

Em resumo

  • Supabase "Invalid Refresh Token: Refresh Token Not Found" significa que o Supabase Auth não tem registro do refresh token que seu app enviou, então a sessão não pode ser renovada e o usuário é desconectado.
  • Três coisas apagam esse registro: um logout (que no JavaScript encerra todos os dispositivos por padrão), um token já usado que chega uma segunda vez e um limite de sessão nas suas configurações do Auth.
  • Parte disso é uma linha inofensiva de uma aba antiga. Outra parte desconecta todos os seus usuários todo dia. O log do Auth diz qual é, e cada causa tem sua própria correção.

Seus usuários escrevem para dizer que foram desconectados. Um estava no meio de um formulário, e outra tem certeza de que não apertou nada. Quando você testa o app, ele funciona. Aí você abre seus logs e encontra a mesma linha, de novo e de novo:

AuthApiError: Invalid Refresh Token: Refresh Token Not Found

Supabase "Invalid Refresh Token: Refresh Token Not Found" é o Supabase Auth, a parte do Supabase que faz as pessoas entrarem, dizendo que não tem registro do token que seu app enviou para manter uma sessão aberta. Sem nada para renovar, seu app desconecta o usuário.

Esta é a parte que é fácil entender ao contrário: esse erro é o último passo, e a causa veio antes. Quando o Supabase não encontra mais o token, a sessão a que ele pertencia já terminou. Quem a encerrou foi um logout, um token enviado duas vezes ou um limite que alguém definiu. Fazer o access token durar mais, ou desligar a verificação que encerra uma sessão, deixa a linha mais rara nos seus logs e deixa a causa exatamente onde estava.

O que significa "Invalid Refresh Token: Refresh Token Not Found"?

Significa que o Supabase Auth recebeu um refresh token do qual não tem registro, e por isso se recusou a renovar a sessão.

Todo usuário conectado tem dois tokens. O access token funciona como uma pulseira: prova quem ele é por um tempo, uma hora se você não mudou a configuração, e seu app a mostra em cada requisição. O refresh token é um bilhete. Antes que a pulseira expire, seu app leva o bilhete ao balcão, que é o Supabase Auth, e o troca por uma pulseira nova e um bilhete novo. O guia de sessões do Supabase diz que refresh tokens "never expire but can only be used once".

Refresh Token Not Found é o balcão procurando um bilhete e não encontrando nada. O servidor responde com HTTP 400 e o código refresh_token_not_found, que a lista de códigos de erro do Auth do Supabase descreve como "Session containing the refresh token not found." Se a pulseira também expirou, o cliente JavaScript do Supabase remove a sessão do navegador e avisa seu app de que o usuário saiu. É nesse momento que seu usuário dá de cara com a página de login.

Nenhum dos dois tokens é uma das chaves de API do seu projeto, que são outra questão: quais delas pertencem ao seu app. Se a sua mensagem diz Invalid JWT, foi a pulseira que foi recusada, e não o bilhete, e esse erro tem uma página própria. Se depois a pessoa não consegue entrar de novo e o formulário de login diz Invalid login credentials, essa mensagem cobre três pessoas diferentes.

Por que meus usuários do Supabase são desconectados?

Porque a sessão por trás do bilhete deles terminou, e a pulseira que eles ainda usam os manteve conectados até expirar. Esse atraso, de até uma hora com a configuração padrão, é o que faz as desconexões parecerem aleatórias: o usuário é jogado para fora um tempo depois de a sessão terminar.

O Supabase Auth tem quatro jeitos de dizer que um bilhete não vale, e cada um aponta para uma causa diferente. Estas são as mensagens exatas do código do próprio servidor do Supabase Auth:

A mensagem no seu logO códigoO que aconteceu
Invalid Refresh Token: Refresh Token Not Foundrefresh_token_not_foundO balcão não tem registro deste bilhete. Um logout encerrou a sessão dele, ou ele foi trocado há tanto tempo que o registro sumiu.
Invalid Refresh Token: Already Usedrefresh_token_already_usedO bilhete foi trocado há mais de dez segundos e voltou. O Supabase encerra a sessão inteira quando isso acontece.
Invalid Refresh Token: Session Expired, às vezes com (Inactivity) ou (Revoked by Newer Login) depoissession_expiredUm limite de sessão nas suas configurações do Auth a encerrou.
Invalid Refresh Token: No Valid Session Foundsession_not_foundA sessão a que o bilhete pertencia não existe mais.

Por trás dessas mensagens estão três causas, segundo a própria documentação do Supabase e os relatos nos seus issue trackers, e cada uma tem sua correção. Se ninguém consegue entrar, ou o app inteiro está em branco, é outro problema, e o Supabase caiu, ou é o seu app? é por onde começar.

Um refresh token funciona uma vez

Toda vez que seu app renova uma sessão, ele entrega o bilhete e o balcão rasga o antigo. O Supabase chama isso de rotação de refresh tokens. O bilhete antigo continua funcionando por mais dez segundos, o que o guia de sessões chama de reuse interval, para que um servidor e um navegador renovando quase no mesmo momento não derrubem um ao outro. Um cliente que trocou o bilhete e perdeu a resposta numa conexão ruim também está coberto: ele recebe o bilhete atual de volta.

Fora desses dois casos, um bilhete antigo que volta conta como sinal de alerta, porque um token que aparece duas vezes é o que um token roubado parece. O guia diz que a sessão inteira então "is regarded as terminated and all refresh tokens belonging to it are marked as revoked". A resposta dessa vez é Already Used.

Mais um passo transforma Already Used em Not Found. O servidor do Supabase Auth apaga os registros dos bilhetes rasgados 24 horas depois de serem revogados. Então o bilhete vencido que hoje recebe Already Used amanhã recebe Refresh Token Not Found, porque o balcão não tem mais o registro para comparar.

Um bilhete em quatro momentos. Em até dez segundos ainda funciona. Depois é recusado como já usado, e quando o registro dele é apagado um dia depois, o balcão não o encontra mais.

Causa 1: alguém saiu, neste dispositivo ou em outro

Um logout apaga a sessão do lado do balcão, e cada bilhete dela vira um que o balcão não encontra. No cliente JavaScript do Supabase, signOut() sem nada dentro dos parênteses encerra todas as sessões daquele usuário, em todos os dispositivos. O guia de logout do Supabase diz isso com todas as letras: com esse padrão, as outras sessões falham com AuthApiError: Invalid Refresh Token: Refresh Token Not Found na próxima vez que tentam se renovar, "even though no one signed out on that device."

Então um usuário que sai no celular também é desconectado no notebook, até uma hora depois, quando a pulseira do notebook expira e o bilhete dele volta como desconhecido. Do ponto de vista dele, seu app o jogou para fora sem motivo.

Se cada dispositivo deve continuar conectado por conta própria, a chamada de logout precisa encerrar só a sessão de onde ela partiu:

await supabase.auth.signOut({ scope: 'local' })

Peça ao seu builder para usar essa forma em todo lugar onde seu app desconecta as pessoas. O outro jeito comum de encerrar uma sessão é uma troca de senha, que o guia de sessões do Supabase lista entre as coisas que encerram uma.

Sem scope, um único logout encerra a sessão em todos os dispositivos. Com scope local, encerra só a do dispositivo na sua mão.

Causa 2: o bilhete novo nunca chegou ao navegador

Se seu app gera as páginas num servidor, como faz um app Next.js, esse servidor também renova sessões. Ele lê o bilhete de um cookie, troca no balcão e precisa devolver o bilhete novo ao navegador num cabeçalho Set-Cookie da sua resposta. Quando esse cookie não chega, o navegador fica com o bilhete antigo, e a próxima requisição apresenta um bilhete que o servidor já trocou. Em até dez segundos, isso passa. Depois disso é Already Used e a sessão termina, e um dia depois o mesmo bilhete é Not Found.

O guia do Supabase para criar um cliente de servidor aponta o erro que causa isso num app Next.js. O código que renova a sessão, que o Next.js 16 chama de Proxy e as versões anteriores de middleware, precisa devolver a resposta em que os cookies novos foram escritos: "An earlier response doesn't carry the refreshed cookies, so the user is signed out on the next request." O mesmo guia avisa que no Next.js 15 e anteriores um arquivo chamado proxy.ts nunca é executado, "so sessions never refresh and users get signed out". Um assistente trabalhando com a documentação mais nova pode escrever exatamente esse arquivo num app mais antigo.

O guia do Supabase também diz por que esse caso é difícil de achar: "it looks like users being signed out at random rather than an error in your code." Os relatos soam igual. Um desenvolvedor escreveu no issue tracker do @supabase/ssr "I'm inadvertently logged out of my web application every day", e o engenheiro do Supabase que respondeu apontou o jeito como o middleware repassava a resposta. O relato de um app de iOS descreve a desconexão chegando "2-3 days after logging in", e foi fechado depois que atualizações na biblioteca Swift do Supabase reduziram os relatos.

O servidor renovou a sessão, e o bilhete novo nunca voltou. O navegador ainda segura o que já foi trocado.

Para conferir, abra as ferramentas de desenvolvedor do navegador numa página que renovou a sessão e olhe a resposta na aba Network. Ela deve trazer um Set-Cookie com o token novo, e a requisição seguinte deve mandar esse token de volta.

Causa 3: um limite de sessão nas suas configurações do Auth

A partir do plano Pro, o Supabase pode encerrar sessões de propósito, pela página Sessions das configurações do Auth do seu projeto. São três limites, e o guia de sessões descreve cada um:

  • Sessões com tempo limitado terminam depois de um tempo fixo, por mais ativo que o usuário esteja.
  • Um tempo limite de inatividade encerra uma sessão que não é renovada há um tempo.
  • Uma sessão por usuário mantém só o login mais recente, então entrar pelo celular encerra a sessão no notebook.

Esses dizem isso na própria mensagem, Session Expired, com (Inactivity) ou (Revoked by Newer Login) depois, no segundo e no terceiro. O guia acrescenta que sessões encerradas são apagadas 24 horas depois, então um bilhete de uma delas que volta depois desse prazo recebe Not Found no lugar.

Se você configurou um desses, as desconexões são a configuração fazendo o seu trabalho. Se não era a intenção, ou se um template ou um assistente ligou isso por você, a página mostra qual limite está ativo.

O caso inofensivo

Um bilhete de uma sessão que já terminou, voltando de uma aba antiga ou de um navegador que ninguém abria havia semanas, também recebe Refresh Token Not Found. A pessoa por trás já tinha sido desconectada. O navegador dela perguntou uma vez, o Supabase disse não, e ela vê sua página de login, que é o que teria visto de qualquer jeito.

No log, aparece assim: uma linha para um usuário, muitas vezes logo depois de esse usuário sair em algum lugar, e depois nada mais dele. Linhas assim são o erro fazendo o seu trabalho. As que valem investigar são as mesmas pessoas de novo e de novo, ou um monte de pessoas diferentes no mesmo dia.

Como ler seus logs do Auth para isso

Abra Logs no painel do Supabase, escolha o tipo de log Auth e procure refresh token nas mensagens de evento. Selecione uma linha para ver os detalhes, incluindo o ID do usuário, e o filtro User então reduz a lista a essa pessoa. Até onde os logs voltam depende do seu plano, então leia enquanto as reclamações são recentes.

Depois leia o que se repete:

O que você vê no log do AuthPara onde aponta
Um Refresh Token Not Found para um usuário, e nada depoisUma aba antiga ou um navegador que voltou. Inofensivo.
Refresh Token Not Found para um usuário logo depois de ele sair em outro lugarCausa 1, um logout que encerrou todos os dispositivos
Already Used, e um dia depois Refresh Token Not Found, para os mesmos usuários, de novo e de novoCausa 2, bilhetes novos que não chegam ao navegador
Session Expired, com um motivo entre parêntesesCausa 3, um limite de sessão

Você também pode perguntar ao banco de dados com que frequência as pessoas entram. Cada login acrescenta uma linha em auth.sessions, e um logout a apaga. Cole isto no SQL Editor:

select user_id, count(*) as sessions, max(created_at) as latest
from auth.sessions
where created_at > now() - interval '14 days'
group by user_id
order by sessions desc
limit 20;

Um usuário com muito mais sessões do que dispositivos entrou de novo e de novo sem nunca sair, que é o que faz alguém que seu app joga para fora o tempo todo.

O que não mudar enquanto você corrige

Suas regras de Row Level Security, a detecção de reutilização e a duração do access token. As três parecem uma correção e deixam a causa onde está, e afrouxar a primeira pode abrir seus dados para estranhos.

Deixe suas regras de Row Level Security em paz. Row Level Security é o conjunto de regras que decide quais linhas cada visitante recebe. Enquanto os usuários estão sendo jogados para fora, toda tela cuja regra lê auth.uid() fica vazia para eles, porque segundo a documentação do Supabaseauth.uid() retorna null quando a sessão expirou. A correção tentadora é afrouxar essa regra até a tela encher de novo, e aí ela enche também para estranhos. Se alguém já fez isso, nossa verificação gratuita pergunta a cada tabela que seu app nomeia quantas linhas um visitante sem login receberia, lê o número sem baixar uma única linha e leva cerca de 20 segundos, sem conta: verifique seu app.

Deixe a detecção de reutilização ligada. O Supabase permite desligá-la, nas configurações avançadas da página Sessions, e chama isso de "generally not recommended". Com ela desligada, a causa 2 para de produzir Already Used, e o bilhete novo continua sem chegar ao navegador.

Mantenha o access token em uma hora. Uma pulseira mais longa adia a desconexão e deixa a causa onde está. O guia do Supabase diz que definir mais de uma hora é "generally discouraged for security reasons".

Mantendo os logins funcionando depois da correção

Duas coisas ainda podem dar errado quando as desconexões param: uma mudança posterior as traz de volta, e uma regra afrouxada no caminho deixa uma tabela aberta para estranhos.

Reeve Monitor continua verificando de fora:

  • se seu app está no ar, verificado a cada minuto, para que uma onda de mensagens de "fui desconectado" não seja confundida com uma queda
  • a verificação de segurança completa a cada hora, em até três apps
  • uma mensagem quando um resultado muda, para que uma regra afrouxada durante uma correção não espere você olhar
  • um relatório mensal, em linguagem simples, do que ele viu

Reeve Care guarda uma cópia do seu banco de dados Supabase:

  • uma cópia criptografada em uma programação, no armazenamento próprio da Reeve na UE, fora da sua conta do Supabase
  • o schema auth onde vivem as contas dos seus usuários, para que elas voltem com todo o resto
  • cada cópia verificada antes de contar, contando as linhas de cada tabela
  • seus arquivos enviados também, assim que você conectar uma credencial do Storage
  • uma restauração com um clique, com uma cópia de segurança do estado atual feita antes
  • tudo o que o Monitor faz

O que fazer agora

O que fazer

  • Procure refresh token nos seus logs do Auth e veja qual mensagem se repete, e com quem.
  • Se um logout em um dispositivo encerra os outros, troque para signOut({ scope: 'local' }).
  • Se seu app gera páginas num servidor, garanta que a resposta que renovou uma sessão é a que chega ao navegador, e procure o Set-Cookie dela na aba Network.
  • Veja na página Sessions das configurações do Auth se há um tempo limite, um limite de inatividade ou uma sessão por usuário que ninguém quis ligar.
  • Deixe Row Level Security, a detecção de reutilização e a duração do access token como estão enquanto corrige.
  • Trate um Not Found por usuário como ruído, e os mesmos usuários todo dia como um bug.

Comece pelo log do Auth, porque a mensagem que se repete diz qual das três causas você tem. Depois, quando as pessoas continuarem conectadas, verifique seu app para conferir que nada foi afrouxado no caminho.

Perguntas frequentes

Por que meus usuários do Supabase são desconectados do nada?

Porque a sessão por trás do refresh token deles terminou, e o access token que eles ainda têm os mantém conectados por até uma hora, então a desconexão chega depois da causa. Os motivos mais comuns são um logout em outro dispositivo, um token renovado que nunca chegou ao navegador e um limite de sessão nas suas configurações do Auth. O log do Auth mostra qual é.

O que é um refresh token?

É a metade duradoura de uma sessão do Supabase. O access token prova quem é o usuário por uma hora por padrão, e antes que ele expire seu app troca o refresh token por um par novo. Cada refresh token funciona uma vez, e o antigo para de funcionar dez segundos depois da troca.

"Refresh Token Not Found" é sinal de um ataque?

Sozinho, não. Significa que o Supabase Auth recusou um token do qual não tem registro, que é a resposta certa. A proteção contra um token roubado é a detecção de reutilização, que encerra uma sessão quando um token antigo volta, e o Supabase recomenda deixá-la ligada.

Por que isso só acontece em produção?

Porque a maioria das causas precisa de tempo ou de um segundo dispositivo. Um token vencido é recusado como já usado no primeiro dia e como não encontrado quando o registro dele é apagado um dia depois, e uma sessão que você abre enquanto constrói raramente dura tanto. Usuários de verdade também entram por vários dispositivos, e é aí que aparece um logout que encerra todas as sessões.

Quanto tempo dura uma sessão do Supabase?

Por padrão, até o usuário sair. O access token dura uma hora se você não mudar, e o refresh token nunca expira, mas só funciona uma vez. A partir do plano Pro, você pode limitar as sessões por tempo total, por inatividade ou a uma por usuário, na página Sessions das suas configurações do 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.