Pular para o conteúdo

Noções de segurança

"Invalid login credentials" no Supabase: as três causas

O Supabase diz "Invalid login credentials" para senha errada, e-mail desconhecido e conta Google. Como saber qual é, e por que é vago.

Vlad Tkachenko12 min de leitura
Um teclado ao lado de uma porta com sua única luz acesa em vermelho e, na frente dele, um crachá para o qual o teclado não tem leitor.

Em resumo

  • O Supabase responde "Invalid login credentials" a três pessoas diferentes: quem digitou a senha errada, quem tem um e-mail sem conta e quem se cadastrou com o Google e nunca teve senha.
  • Ele faz isso de propósito. Um formulário de login que dissesse qual dos três casos foi contaria a qualquer pessoa com uma lista de e-mails quais pertencem aos seus usuários.
  • Você mesmo pode distinguir os três com uma consulta em auth.users. Sua página de login deve manter a mensagem vaga e oferecer todas as formas pelas quais a conta pode ter sido criada.

Alguém escreve dizendo que não consegue entrar no seu app e tem certeza de que a senha está certa. Você testa o endereço da pessoa e recebe a mesma linha vermelha embaixo do formulário:

Invalid login credentials

O Supabase responde "Invalid login credentials" para uma senha errada. E responde com exatamente as mesmas palavras para outras duas pessoas: quem tem um e-mail sem conta no seu app e quem se cadastrou com o Google e nunca teve senha. Seu app feito no Lovable, Bolt, v0, Cursor, Claude Code ou Replit está funcionando do jeito que foi construído. Ele dá uma resposta só para três situações diferentes.

Esta é a parte que a correção óbvia erra: a mensagem vaga é proposital, e deixá-la mais útil abre um buraco. O que ajuda é descobrir qual dos três casos você tem na frente, algo que o seu próprio painel consegue dizer, enquanto o formulário continua dizendo tão pouco quanto agora.

O que significa "Invalid login credentials" no Supabase?

Significa que o Supabase Auth, a parte do Supabase que faz o login das pessoas, recusou um e-mail e uma senha e guarda o motivo para si.

A mensagem está escrita no próprio código do servidor de Auth. Na função que trata um login com senha, o mesmo erro é devolvido em três pontos: quando nenhum usuário tem aquele e-mail, quando o usuário não tem senha salva e quando a senha não confere. Em todos eles a resposta é um HTTP 400 com o código invalid_credentials, que a referência de códigos de erro do Supabase descreve como "Login credentials or grant type not recognized."

No seu app ele chega como um AuthApiError vindo de signInWithPassword, com error.code igual a invalid_credentials. A mesma referência pede para identificar os erros sempre por error.code e error.name, e isso importa mais adiante, quando você decidir o que o seu formulário diz. Se os seus usuários estavam logados e depois foram jogados para fora, é outro erro, com uma página só dele.

Pense no seu formulário de login como o teclado ao lado da porta de um prédio. Alguém digita um código e uma luz responde. A verde abre a porta. A vermelha acende para um código errado, para um código que nunca foi dado a ninguém e para um morador que sempre entrou pela porta lateral com um crachá e nunca teve código.

As três pessoas por trás da mensagem

Cada uma precisa de uma correção diferente, então vale saber qual você tem na frente.

A senha errada. A conta existe, tem senha, e o que foi digitado não confere. Isso inclui uma senha trocada em outro aparelho e esquecida depois, e um gerenciador de senhas que preenche uma antiga.

O endereço sem conta. Ninguém no seu projeto se cadastrou com aquele e-mail. Pode ser um segundo endereço (o do trabalho, quando a pessoa se cadastrou com o pessoal) ou um erro de digitação no cadastro, que agora está salvo como o e-mail dela.

A conta sem senha. A pessoa se cadastrou com Google, GitHub ou outro botão de login, e o Supabase nunca salvou uma senha para ela. Quando ela volta e digita uma, não há nada com que comparar. O servidor de Auth confere se existe uma senha salva antes de olhar a digitada, e uma conta sem senha recebe o mesmo 400.

Quem se cadastrou com um magic link, o link de login enviado por e-mail, é uma variação do primeiro caso. O código de magic link do Supabase cria a conta com uma senha aleatória que nunca é mostrada a ninguém, então qualquer senha que a pessoa digite está errada.

Uma senha errada, um endereço desconhecido e uma conta sem senha chegam à mesma verificação e recebem a mesma resposta.

Como descubro qual dos três foi?

Pergunte ao seu próprio banco de dados. Cada conta do seu projeto é uma linha numa tabela chamada auth.users, e essa linha diz como a pessoa faz login.

O guia de usuários do Supabase descreve o campo que importa: segundo o Supabase, app_metadata.providers é a lista de provedores com os quais o usuário pode fazer login. Um usuário pode ter várias identidades, então a mesma pessoa pode ter senha e Google ao mesmo tempo. Se existe uma senha salva está na coluna que o próprio servidor confere.

Abra o SQL Editor no painel do Supabase e rode isto com o endereço que a pessoa passou:

select email,
       raw_app_meta_data->'providers' as signs_in_with,
       coalesce(encrypted_password, '') <> '' as has_password,
       email_confirmed_at
from auth.users
where lower(email) = lower('person@example.com');

A consulta lê uma linha e nunca mostra uma senha, só se existe uma.

O que voltaO que significa
Nenhuma linhaNenhuma conta tem esse endereço neste projeto. Pergunte que outro endereço a pessoa pode ter usado
has_password é false, e signs_in_with lista google, github ou algo parecidoA pessoa se cadastrou com esse botão e nunca teve senha
has_password é trueA senha digitada está errada, ou a pessoa se cadastrou com um magic link e nunca soube a dela
email_confirmed_at está vazioO e-mail ainda não foi confirmado, o que vira outra mensagem quando a senha está certa

Se você prefere clicar a digitar, Authentication → Users no painel mostra as mesmas contas.

A conta que nunca teve senha

Mande a pessoa de volta ao botão que ela usou, ou deixe que ela crie uma senha pelo seu link de redefinição.

No teclado, esse é o morador que sempre usou a porta lateral. Não há nenhum código cadastrado para ele, então todo código que ele tenta é recusado.

O primeiro conserto fica na sua página de login. Se o cadastro oferece "Continuar com o Google", a página de login precisa do mesmo botão, para que quem se cadastrou com ele o veja quando voltar.

Se a pessoa também quiser uma senha, o seu link "Esqueceu a senha?" já resolve. O código de redefinição de senha do Supabase envia o e-mail de recuperação para qualquer conta que encontrar, sem conferir como ela foi criada. Um usuário do Google que segue o link e define uma senha nova consegue entrar dos dois jeitos depois. Isso depende de o seu app ter a página que define a senha nova, a que chama supabase.auth.updateUser({ password }). Se o link cai numa página que nunca pede senha, a pessoa entra uma vez e continua sem senha.

"Email not confirmed" quer dizer que a senha estava certa

É outra mensagem, com outro código, e só chega a quem já passou pela verificação da senha.

Na mesma função do servidor de Auth, a verificação do e-mail confirmado vem depois da verificação da senha. Então Email not confirmed, código email_not_confirmed, só volta para quem digitou um e-mail existente e a senha correta dele. A referência de erros do Supabase descreve isso como "Signing in is not allowed for this user as the email address is not confirmed." No teclado é outra luz: o código estava certo, e a porta continua fechada até a papelada da mudança terminar.

Você vê essa mensagem quando Confirm email está ativado no provedor Email do seu projeto. Segundo o guia de configuração do Supabase, os usuários precisam então confirmar o endereço de e-mail antes de fazer login pela primeira vez. A correção é o e-mail em que a pessoa ainda não clicou: diga para ela conferir a caixa de entrada e o spam, e ofereça reenviar com supabase.auth.resend({ type: 'signup', email }). Se esses e-mails não chegam de jeito nenhum, o limite de envio de e-mails é a primeira coisa a descartar. Se eles chegam e o link responde "Email link is invalid or has expired", provavelmente alguma coisa o abriu antes da pessoa.

Como essa mensagem só aparece depois de uma senha correta, pedir à pessoa que confira a caixa de entrada não dá a um estranho nada que ele já não tivesse.

Por que a mensagem continua vaga

Porque a versão útil contaria a estranhos quem são os seus usuários.

Imagine o teclado respondendo "ninguém mora no 4B". Qualquer pessoa passando poderia percorrer a lista de apartamentos e saber quais estão ocupados.

O seu formulário funciona do mesmo jeito. Se ele dissesse "Nenhuma conta com esse e-mail", qualquer pessoa com uma lista de endereços poderia digitar um por um e saber quais pertencem aos seus usuários. Um vazamento de algum outro site deixa uma lista assim para trás, às vezes com as senhas que as pessoas usavam lá. Quem sabe quais endereços são seus usuários sabe de quem vale a pena testar senhas antigas no seu app, e quem provavelmente vai abrir um e-mail que parece vir de você.

O Supabase aplica a mesma regra em todo lugar onde se digita um endereço. O código de cadastro dele, quando recebe o pedido de criar uma conta para um e-mail já cadastrado e confirmado, pode devolver um usuário inventado com um ID aleatório, por meio de uma função cujo comentário diz que ela existe para impedir que vaze se um usuário está cadastrado ou não. A redefinição de senha responde a um endereço desconhecido com o mesmo sucesso vazio que dá a um conhecido.

O lado do cadastro tem uma chave que você controla. Esse usuário inventado é o que volta quando a confirmação é exigida tanto para cadastros por e-mail quanto por telefone. Se qualquer uma das duas estiver desligada (a do telefone conta mesmo que o seu app nunca use login por telefone), um endereço cadastrado recebe User already registered, código user_already_exists, e o seu formulário de cadastro vira, sozinho, um verificador de listas.

A mensagem útil também precisa vir de algum lugar. O Supabase não conta ao seu app se um e-mail tem conta, então, para mostrar "Nenhuma conta com esse e-mail", o seu app tem que descobrir sozinho. Isso significa uma consulta que funcione para um visitante sem login: uma leitura da sua tabela profiles que só dá certo se qualquer pessoa puder ler os e-mails dela, ou uma função do banco de dados que procura em auth.users para quem perguntar. De um jeito ou de outro, a lista dos seus usuários fica legível de fora.

A primeira delas dá para ver de fora, e é assim que a nossa verificação gratuita procura por ela. Ela pergunta a cada tabela que o seu app cita quantas linhas um visitante sem login conseguiria obter, lê só a contagem e leva cerca de 20 segundos, sem conta: verifique seu app.

Um formulário que diz quando a conta não existe separa qualquer lista de endereços entre os seus usuários e todo o resto. Uma resposta só para todos deixa a lista como estava.

O que colocar na sua página de login

Mantenha a mensagem vaga e coloque todas as formas de voltar a entrar na mesma página.

  • Uma mensagem só para os três casos, em palavras simples, com os outros caminhos ao lado: "O e-mail e a senha não conferem. Se você se cadastrou com o Google, use o botão abaixo." Ela ajuda o usuário do Google e não conta a um estranho nada que a sua página de cadastro já não diga.
  • Todos os métodos da sua página de cadastro. O botão do Google, o magic link e o campo de senha, todos também na página de login.
  • Um link "Esqueceu a senha?" para uma página que defina uma. O Supabase responde do mesmo jeito a qualquer endereço ali, então é seguro mostrar, e é assim que uma pessoa sem senha consegue uma.
  • Decisões pelo error.code. Mostre a sua mensagem vaga para invalid_credentials, e "confira sua caixa de entrada" com um botão de reenvio para email_not_confirmed. A referência de erros do Supabase pede para identificar os erros pelo código em vez do texto da mensagem, e avisa que os códigos de status HTTP podem mudar sem aviso.

Em código, são dois caminhos:

const { error } = await supabase.auth.signInWithPassword({ email, password })
if (error?.code === 'email_not_confirmed') showCheckYourInbox(email)
else if (error) showSignInHelp()

Se um builder escreve a sua página de login, você pode colar esses quatro pontos como instrução, inclusive o que diz o que a página deve guardar para si.

Para a sua página de login continuar como você deixou

Duas coisas ainda podem dar errado depois de hoje: um prompt posterior reescreve a página para ficar "mais útil", e uma mudança em outro lugar perde as contas em que a página faz login.

Reeve Monitor continua verificando de fora:

  • as nove verificações a cada hora, em até três apps, incluindo quantas linhas um visitante sem login consegue contar nas tabelas que o seu app cita
  • um e-mail no dia em que uma nova varredura avaliar o seu app pior que a anterior
  • se o app está no ar, a cada 60 segundos
  • um relatório mensal, em linguagem simples, do que ele viu

Reeve Care guarda uma cópia do seu banco de dados Supabase, contas incluídas:

  • 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

  • Rode a consulta em auth.users com o endereço que a pessoa passou e veja qual dos três casos é.
  • Nenhuma linha: pergunte com que outro endereço ela pode ter se cadastrado.
  • Sem senha: indique o botão do Google ou do GitHub que ela usou, ou o seu link de redefinição.
  • Com senha e ainda recusada: mande o link de redefinição.
  • Mostre uma mensagem vaga para invalid_credentials, e "confira sua caixa de entrada" só para email_not_confirmed.
  • Nunca adicione uma verificação que diga a um visitante se um e-mail tem conta.

Se o problema começa um passo antes e as contas novas nem chegam a ser criadas, Database error saving new user cobre esse lado do formulário. O checklist de segurança de 10 minutos cobre o que costuma ficar aberto num app recém-lançado.

Perguntas frequentes

Por que o Supabase diz "Invalid login credentials" se a senha está certa?

Porque a senha pode estar certa para uma conta que não existe, ou a conta pode nem ter senha. Confira se o e-mail é o mesmo usado no cadastro, e se a pessoa se cadastrou com o Google ou outro botão de login, o que deixa a conta sem senha. Uma consulta em auth.users no SQL Editor mostra qual é o caso.

"Invalid login credentials" quer dizer que a conta não existe?

Às vezes. O Supabase devolve a mesma mensagem, com o mesmo código, invalid_credentials, quando nenhuma conta tem aquele e-mail, quando a conta não tem senha e quando a senha está errada. A mensagem sozinha não diz qual, e ela foi feita assim.

Por que quem se cadastrou com o Google não consegue entrar com senha?

Porque a conta foi criada sem senha. O Supabase confere se existe uma senha salva antes de comparar a que foi digitada, e uma conta sem senha recebe "Invalid login credentials". A pessoa pode continuar usando o botão do Google ou usar o link de redefinição de senha do seu app para criar uma: o Supabase envia esse e-mail para qualquer conta que encontrar, não importa como ela foi criada.

Meu formulário de login deveria dizer que o e-mail não está cadastrado?

Não. Essa mensagem conta a qualquer pessoa que digite um endereço se ele pertence a um dos seus usuários, e quem tem uma lista de endereços vazados pode conferir todos. Seu app também precisaria de uma consulta que respondesse essa pergunta para estranhos, e isso é um segundo buraco. Mantenha a mensagem vaga e mostre as outras formas de entrar.

Qual a diferença entre "Invalid login credentials" e "Email not confirmed"?

O Supabase só devolve "Email not confirmed" depois de conferir a senha e ver que ela está correta. Então a pessoa certa digitou a senha certa, e a conta está esperando que ela clique no link do e-mail de confirmação. É seguro dizer a ela que confira a caixa de entrada e oferecer o reenvio do e-mail.

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.