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.

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.
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 volta | O que significa |
|---|---|
| Nenhuma linha | Nenhuma 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 parecido | A pessoa se cadastrou com esse botão e nunca teve senha |
has_password é true | A senha digitada está errada, ou a pessoa se cadastrou com um magic link e nunca soube a dela |
email_confirmed_at está vazio | O 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.
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 parainvalid_credentials, e "confira sua caixa de entrada" com um botão de reenvio paraemail_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.userscom 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ó paraemail_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.