Login com Google no Astro + Supabase: guia completo (com as armadilhas que ninguém conta)
Como implementar entrar/cadastrar com um clique via Google OAuth num app Astro com Supabase Auth — incluindo o bug do e-mail apontando pra localhost, o pepino do redirect caindo no Site URL, SMTP customizado com Resend e vinculação de contas.
Cadastro com email e senha funciona, mas a fricção mata conversão. Quem chega no checkout precisa inventar uma senha, confirmar por email, voltar pro site… e uma fatia considerável desiste no meio do caminho. “Entrar com Google” num clique elimina tudo isso — e com o Supabase Auth integrar isso é mais simples do que parece.
O problema é que “mais simples do que parece” esconde um punhado de armadilhas que me custaram algumas horas na vida real. Neste artigo eu documento o caminho completo, do zero ao botão funcionando em produção, usando como exemplo a implementação deste próprio site (Astro + Supabase, deploy na Cloudflare). Incluindo os bugs que eu encarei e como os resolvi — porque é exatamente essa a parte que os tutoriais costumam pular.
O que você vai ter no final: um botão “Continuar com Google” que cadastra ou loga o usuário num clique, reutiliza o mesmo callback do fluxo de confirmação por email, vincula automaticamente contas duplicadas e ainda funciona no seu ambiente de dev.
Visão geral do fluxo
Antes de pôr a mão na massa, vale entender a coreografia completa. O OAuth não é uma chamada direta do seu site pro Google — o Supabase fica no meio como intermediário de confiança:
[ Usuário no /entrar ] --1--> [ Supabase /auth/v1/authorize ]
| |
| 2. redireciona
v v
[ Google — consentimento ] ----3----> [ Supabase /auth/v1/callback ]
|
4. troca o code
5. grava sessão (cookie PKCE)
|
v
[ Seu /entrar/callback?code=... ]
|
6. exchangeCodeForSession(code)
7. redireciona pra `next`
Os pontos que confundem:
- A URI de callback do Google aponta pro Supabase, não pro seu site. É
https://<seu-projeto>.supabase.co/auth/v1/callback. Todo mundo erra isso na primeira vez (eu errei). - O Supabase recebe o code do Google, ele mesmo troca por um token, cria a sessão, e só depois redireciona o navegador pro seu callback (
/entrar/callback) carregando um outro code (o do fluxo PKCE). - O seu callback roda no servidor (SSR), troca esse segundo code por sessão, grava o cookie e manda o usuário pra página final.
Entendeu por que existem dois codes e dois callbacks? Um par é Google↔Supabase, o outro é Supabase↔seu app. Não tente encurtar isso.
Pré-requisitos
- Um app Astro (ou qualquer framework SSR) com Supabase Auth já funcional por email/senha.
- Acesso ao Google Cloud Console e ao dashboard do Supabase.
- Um domínio próprio com DNS sob seu controle (aqui uso
valmor.net.brcomo exemplo — troque pelo seu). - O cliente Supabase no browser (
@supabase/ssr). Se você já tem login por senha funcionando, já tem isso.
Vou partir do princípio de que sua página de login é /entrar e seu callback é /entrar/callback — ajuste as rotas à sua realidade.
Passo 1 — Google Cloud: consent screen e OAuth Client
Comece pelo https://console.cloud.google.com. Crie (ou selecione) um projeto. Depois, no menu esquerdo, vá em Google Auth Platform (a interface nova; na clássica é APIs & Services → OAuth consent screen).
1.1 Tela de consentimento (Branding)
Preencha o que o usuário vai ver quando o Google pedir autorização:
| Campo | Valor |
|---|---|
| App name | o nome do seu site (ex.: valmor.net.br) |
| User support email | um email que você lê |
| Application home page | https://<seu-dominio> |
| Application privacy policy | https://<seu-dominio>/<sua-rota-de-privacidade> |
| Application terms of service | https://<seu-dominio>/<sua-rota-de-termos> |
| Authorized domains | <seu-dominio> (só o host, sem https://) |
Não suba logo agora. A própria tela avisa: fazer upload de logo dispara exigência de verificação pelo Google. Sem logo, e usando só scopes não-sensíveis (
openid,profile), você fica isento de verificação. O aviso vermelho “Your branding needs to be verified” é só sobre o logo — pode ignorar enquanto não subir um.
1.2 Crie o OAuth Client
Vá em Clients → Create client → Web application:
- Name: o que quiser (ex.:
webapp-supabase). É só pra você se localizar. - Authorized redirect URIs: cole exatamente
Substituahttps://<seu-projeto>.supabase.co/auth/v1/callback<seu-projeto>pelo ref do seu projeto Supabase (encontra em Project Settings → API → Project URL). É a URI do Supabase, não a do seu site. - Authorized JavaScript origins: pode deixar vazio. O fluxo via Supabase é redirect de página inteira, não XHR.
Clique em Create. Vão aparecer duas coisas que você copia uma única vez:
- Client ID — termina em
.apps.googleusercontent.com. - Client Secret — começa com
GOCSPX-.
⚠️ Segurança: o Google só mostra o Client Secret na criação. Se perder, é preciso gerar outro. Nunca commite isso no git. Eu colo direto no dashboard do Supabase, sem passar por arquivo nenhum.
1.3 Publique o app
Em Audience (ou Publishing status), certifique-se de que está “In production”. Se ficar em “Testing”, só 100 usuários de teste que você cadastrar manualmente conseguem autorizar — péssimo pra produção. Para scopes não-sensíveis, publicar é instantâneo e não exige revisão do Google.
Passo 2 — Supabase: habilitar o provider Google
No dashboard do Supabase, em Authentication → Sign In / Providers → Google:
- Ative o toggle.
- Cole o Client ID e o Client Secret do passo 1.2.
- Save.
Pronto, o Google já está conectado. Agora vem a parte que mais quebra gente: as URLs de redirecionamento.
2.1 Site URL e Redirect URLs (o coração do problema)
Vá em Authentication → URL Configuration. Há dois campos:
- Site URL — a URL fallback. O Supabase manda pra aqui quando um redirect pedido não bate com a allowlist. Não suporta wildcard.
- Redirect URLs — a allowlist de URLs permitidas. Suporta wildcard (
https://*.domain.com).
A regra de ouro do Supabase Auth:
Se a URL que o código pede pra redirecionar (
redirectTo) não casar com uma entrada da allowlist, o Supabase descarta tudo (incluindo query params customizados) e manda pro Site URL com?code=...limpo.
Isso é uma medida de segurança contra open-redirect, mas é a origem de ~80% dos bugs de “o login foi parar no lugar errado”.
No meu caso, a configuração final ficou:
Site URL:
https://valmor.net.br/entrar/callback
Redirect URLs:
https://valmor.net.br/entrar/callback
http://localhost:4321/entrar/callback
http://localhost:4322/entrar/callback
https://preview.<seu-projeto>.pages.dev/entrar/callback
https://valmor.net.br/entrar/redefinir-senha
Inclua produção, preview e dev local. Mais pra frente explico por que não dá pra incluir IPs privados tipo http://192.168.0.10:4321 aqui.
Passo 3 — O código
A boa notícia: se você já tem um callback de confirmação de email funcionando, não precisa criar outro. O OAuth do Supabase desemboca no mesmo callback PKCE.
3.1 O botão (em /entrar/index.astro)
No frontmatter da página, leio o destino (redirect) da query string, sanitizando pra evitar open-redirect:
---
function safeRedirect(value: string | null): string {
const fallback = "/conta/perfil";
if (!value) return fallback;
// Só caminhos relativos; bloqueia "//evil.com" e "https://..."
if (!value.startsWith("/") || value.startsWith("//")) return fallback;
return value;
}
const redirectTo = safeRedirect(Astro.url.searchParams.get("redirect"));
---
No template, o botão com o “G” multicolor do Google e um divisor “ou” acima dos formulários de email/senha:
<div class="auth-social">
<button type="button" id="google-btn" class="btn-google" data-redirect={redirectTo}>
<span class="btn-google__icon" aria-hidden="true">
<!-- o SVG do "G" multicolor do Google vai aqui -->
</span>
Continuar com Google
</button>
<div class="auth-divider" aria-hidden="true"><span>ou</span></div>
</div>
No <script>, o handler de clique. Repare em dois detalhes importantes: eu escondo o next num cookie (e não na query string do redirectTo — já já explico por quê), e o redirectTo vai sem nenhum ?param=:
const googleBtn = document.getElementById("google-btn") as HTMLButtonElement | null;
if (googleBtn) {
googleBtn.addEventListener("click", async () => {
if (!supabase) return;
const rawRedirect = googleBtn.dataset.redirect || "";
const next = rawRedirect.startsWith("/") && !rawRedirect.startsWith("//")
? rawRedirect
: "/conta/perfil";
// Stash do destino num cookie de 5 minutos. O callback vai ler depois.
const secure = window.location.protocol === "https:" ? "; Secure" : "";
document.cookie =
`auth-next=${encodeURIComponent(next)}; path=/; max-age=300; SameSite=Lax${secure}`;
await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${window.location.origin}/entrar/callback`,
queryParams: { prompt: "consent" },
},
});
});
}
O prompt: "consent" força o Google a mostrar a tela de consentimento sempre (em vez de reusar uma autorização anterior silenciosa). Útil pra garantir que o usuário passa pela tela do Google pelo menos uma vez.
3.2 O callback (em /entrar/callback.astro)
O callback roda no servidor. Ele troca o code PKCE por sessão, grava o cookie de auth, e redireciona pro destino final — que ele lê do nosso cookie auth-next (com fallback pra URL, que é o que o fluxo de confirmação por email ainda usa):
---
import { createSupabaseClient } from "../../lib/supabase";
function safeNext(value: string | null): string {
const fallback = "/conta/perfil";
if (!value) return fallback;
if (!value.startsWith("/") || value.startsWith("//")) return fallback;
return value;
}
const code = Astro.url.searchParams.get("code");
const errorMessage = Astro.url.searchParams.get("error_description");
// Preferencial: cookie stash do OAuth. Fallback: query string (email flow).
const cookieNext = safeNext(Astro.cookies.get("auth-next")?.value ?? null);
const urlNext = safeNext(Astro.url.searchParams.get("next"));
const next = Astro.cookies.has("auth-next") ? cookieNext : urlNext;
let error: string | null = errorMessage || null;
if (code && !error) {
const supabase = createSupabaseClient({
request: Astro.request,
cookies: Astro.cookies,
locals: Astro.locals as any,
});
if (!supabase) {
error = "Service unavailable";
} else {
const { error: exchangeError } =
await supabase.auth.exchangeCodeForSession(code);
if (exchangeError) {
error = exchangeError.message;
} else {
Astro.cookies.delete("auth-next", { path: "/" });
return Astro.redirect(next);
}
}
}
---
Pronto. O exchangeCodeForSession faz toda a mágica de PKCE — recebe o code, devolve a sessão, grava os cookies via @supabase/ssr. Em sucesso, redirecionamos pra next.
As armadilhas que eu encarei (e como resolver)
Aqui é onde o tutorial fica útil de verdade. Cada uma dessas me mordeu na prática.
Armadilha 1 — “O link do email foi parar em localhost:3000”
Sintoma: usuário se cadastra, recebe o email de confirmação, clica… e o link aponta pra http://localhost:3000, não pro seu domínio.
Causa: o Site URL no Supabase ainda era o default de projeto novo (http://localhost:3000). Quando o emailRedirectTo pedido pelo código não casa com a allowlist, o Supabase cai pro Site URL.
Solução: ir em Authentication → URL Configuration e configurar conforme a seção 2.1 acima. Esse único passo resolve ao mesmo tempo o bug do email, o OAuth e qualquer outro redirect.
Armadilha 2 — O remetente do email é “Supabase Auth”
Sintoma: usuário reclama que não associou o email de confirmação ao seu site, porque veio de um remetente genérico “Supabase Auth”.
Causa: o Supabase, no plano Free, envia emails pelo SMTP interno deles, com remetente fixo. O assunto você edita em Authentication → Emails → Templates. O remetente (From), não.
Solução: configurar SMTP customizado em Authentication → Emails → SMTP Settings. Usei o Resend (free tier: 3.000 emails/mês, 100/dia):
-
No Resend, adicione seu domínio e adicione os registros DNS (DKIM, SPF) que ele te der. Aguarde ficar Verified.
-
Crie uma API key (começa com
re_). -
No Supabase, preencha:
Campo Valor Sender email noreply@<seu-dominio>Sender name <seu-dominio>Host smtp.resend.comPort 465Username resendPassword sua API key re_... -
Em Emails → Templates, cole o HTML do seu template no campo Message body (Source) de cada fluxo (Confirm signup, Reset password, Change email) e troque o Subject pra algo como
Confirme seu email · <seu-dominio>.
Bônus: com DKIM assinando pelo seu domínio, deliverability melhora e os emails param menos no spam.
Armadilha 3 — 401: invalid_client na tela do Google
Sintoma: o usuário clica em “Continuar com Google”, o Google abre e mostra: “The OAuth client was not found. Erro 401: invalid_client”.
Causa: o Google não reconhece o Client ID que o Supabase enviou. No meu caso, o OAuth Client estava marcado com o aviso “This OAuth client will be deleted because it has not been used for over 6 months” — o Google limpou clientes ociosos, e o ID deixou de existir. Também pode ser simplesmente erro de digitação ao colar o ID no Supabase.
Solução: confira se o Client ID no Supabase bate exatamente com o do Google (incluindo o sufixo .apps.googleusercontent.com, sem espaços). Se o client foi deletado ou está marcado pra deleção, crie um OAuth Client novo no Google Cloud e reconfigure no Supabase.
Armadilha 4 — Depois do Google, o redirect cai no Site URL mesmo com tudo certo
Sintoma: login com Google funciona (o ?code= aparece), mas o navegador volta em https://<seu-dominio>/entrar/callback?code=... em vez de voltar pra http://localhost:4321/... onde você estava testando. E o ?next=... some.
Diagnóstico: repare que o next sumiu. Essa é a assinatura do fallback do Site URL — quando o Supabase aceita o redirect pedido, ele preserva seus query params e só acrescenta &code=. Quando ele rejeita, descarta tudo e manda pro Site URL com ?code= limpo.
Causa raiz: eu estava passando o destino dentro da própria URL de redirect:
// ❌ NÃO faça isso
redirectTo: `${origin}/entrar/callback?next=${encodeURIComponent(next)}`
O Supabase recusa casar um redirect com query string customizada contra a allowlist (que tem /entrar/callback puro). Resultado: fallback sempre.
Solução: tire o next da URL de redirect e transporte ele por fora — num cookie de vida curta, setado antes do signInWithOAuth e lido no callback:
// ✅ redirectTo limpo, next vai num cookie
document.cookie =
`auth-next=${encodeURIComponent(next)}; path=/; max-age=300; SameSite=Lax${secure}`;
await supabase.auth.signInWithOAuth({
provider: "google",
options: { redirectTo: `${origin}/entrar/callback` }, // sem query!
});
No callback, leia o cookie (com fallback pra query string, que o fluxo de email ainda usa). É exatamente o código da seção 3.2.
O cookie sobrevive porque é trafegado no request de volta (navegação top-level GET, permitida por SameSite=Lax), e não depende do Supabase preservar nada.
Armadilha 5 — “Criei conta com email/senha, agora não consigo entrar com o Google do mesmo email”
Sintoma: usuário cadastrou com [email protected] + senha. Depois tenta “Entrar com Google” com o mesmo email. Recebe erro de conta conflitante.
Causa: por padrão, o Supabase recusa OAuth quando o email já existe com outro provider (medida de segurança contra account takeover via email não-verificado).
Solução: se você confia que o email foi verificado (e no fluxo de signup por senha você exige confirmação por email, como deveria), ative a vinculação automática em Authentication → Sign In / Providers → Email → Enable automatic linking. Aí o Google vincula à conta existente e o usuário entra direto, sem duplicar contas.
Bônus: testando no dev por rede privada (ZeroTier, IP local)
Eu acesso meu dev de outros dispositivos pela rede do ZeroTier, num IP tipo http://172.29.73.113:4321. Tentei adicionar essa URL na allowlist do Supabase — ele aceita cadastrar, mas rejeita em runtime: só http://localhost (e 127.0.0.1) são isentos da exigência de HTTPS. Qualquer outro host, IP incluso, precisa ser https://.
Isso me deixou duas opções pra OAuth no dev fora da minha máquina:
- Testar OAuth só via
http://localhost:4321— funciona, é o suficiente pra maioria dos casos. - Cloudflare Tunnel (
cloudflared) — expõe olocalhost:4321num domínio público tipohttps://dev.<seu-dominio>com HTTPS do Cloudflare, sem abrir portas. Aí adicionahttps://dev.<seu-dominio>/entrar/callbackna allowlist e OAuth funciona de qualquer dispositivo da rede ZeroTier.
Pra debugar o resto do app (que não envolve redirect do Supabase), o IP do ZeroTier continua valendo normalmente — só o OAuth mesmo que exige HTTPS ou localhost.
Conclusão
O login com Google é uma daquelas features que parecem trivial até você implementar. A mecânica básica (um botão + signInWithOAuth + um callback PKCE) é pouco código — no meu caso, um arquivo modificado pra adicionar o botão, e o callback de email reutilizado sem mudança estrutural.
O tempo real vai nas configurações externas e nos bugs sutis:
- Site URL e allowlist configurados direito (resolve email, OAuth e redirect de uma vez).
- SMTP customizado pra o remetente não ser “Supabase Auth”.
- Client ID/Secret válidos e não prestes a serem deletados por inatividade.
- Sem query string no
redirectTo— transporte estado por cookie, não por URL. - Vinculação automática ligada se você quer fundir contas de email/senha com Google do mesmo endereço.
Feito isso, você entrega uma experiência de cadastro/login que reduz friction sem abrir mão de segurança. O usuário clica uma vez no Google, autoriza, e já está dentro — sem inventar senha, sem esperar email, sem saltar de aba.
Se você também usa ZeroTier pra acessar seu dev de outros dispositivos, vale o Cloudflare Tunnel só pro OAuth — pro resto, a rede privada segue perfeita.
Implementação de referência deste artigo é a mesma que roda neste site. Dúvidas? Me chame.