Implementação completa de OAuth Google com popup, tratamento de mensagens cross-origin e integração segura com cookies no Restaurantix.
O OAuth Google permite que usuários façam login no Restaurantix usando suas contas do Google, sem nunca compartilhar senhas com nossa aplicação. Vamos implementar o fluxo completo usando popup windows para uma experiência fluída.
Frontend abre popup direcionando para endpoint OAuth do backend
Usuário faz login diretamente no Google (sem compartilhar senha)
Backend troca authorization code por access token com Google
Backend busca dados do usuário na API do Google
Backend cria conta/login e define cookie seguro com JWT
Backend sinaliza sucesso via postMessage para o frontend
Aplicação nunca vê ou armazena senhas dos usuários
Protocolo padrão da indústria, amplamente testado
Só solicitamos dados essenciais (email, nome, foto)
Usuário pode revogar acesso direto no Google
Popup isolado evita vazamento de dados sensíveis
OAuth 2.0:
Protocolo de autorização que permite aplicações acessarem recursos de usuários sem suas credenciais.
Authorization Code:
Código temporário que Google envia após usuário autorizar, trocado por access token.
PostMessage API:
Permite comunicação segura entre popup e janela principal de diferentes origens.
Scopes OAuth:
Permissões específicas que aplicação solicita (email, profile, etc).
O Google precisa saber que nossa aplicação é legítima antes de permitir OAuth. Configuramos Client ID e Client Secret que funcionam como "credenciais" da nossa aplicação no ecossistema Google.
console.cloud.google.com
Estas URLs devem estar EXATAMENTE configuradas no Google Console, ou o OAuth falhará com erro de redirect_uri_mismatch:
// 🌍 Desenvolvimento Local
http://localhost:3000/auth/google/callback
// 🚀 Produção (substitua pelo seu domínio)
https://restaurantix.fly.dev/auth/google/callback
// 💡 Dica: URL deve terminar com /callback
// ❌ Não funciona: /auth/google
// ✅ Funciona: /auth/google/callback
Após criar o OAuth Client, você receberá Client ID eClient Secret. Configure no backend:
// 📁 Backend .env
GOOGLE_CLIENT_ID=sua_client_id_aqui.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=seu_client_secret_aqui
GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback
// 🔍 Exemplo de Client ID real:
// 123456789-abc123def456.apps.googleusercontent.com
// ⚠️ NUNCA commite o Client Secret!
// Use .env.local ou .env (já no .gitignore)
// 📁 sign-in-form.tsx (baseado no arquivo fornecido)
export function SignInForm() {
const router = useRouter();
const getProfile = useAuthStore((state) => state.getProfile);
const [oauthWindow, setOauthWindow] = useState<Window | null>(null);
// 🎯 Handler para abrir popup OAuth
const handleGoogleSignIn = () => {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
// 📐 Dimensões e posicionamento do popup
const width = 500;
const height = 600;
const left = window.screenX + (window.outerWidth - width) / 2;
const top = window.screenY + (window.outerHeight - height) / 2;
// 🪟 Abre popup direcionando para endpoint OAuth do backend
const popup = window.open(
`${apiUrl}/auth/google`, // 🎯 Endpoint que inicia OAuth
'google-oauth', // 🏷️ Nome da janela
`width=${width},height=${height},left=${left},top=${top},popup=yes`
);
if (popup) {
setOauthWindow(popup);
} else {
toast.error('Erro ao abrir janela de autenticação');
}
};
return (
// ... resto do formulário
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleGoogleSignIn}
disabled={isLoading}
>
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
{/* 🎨 Ícone oficial do Google */}
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
</svg>
Entrar com Google
</Button>
);
}
width/height:
Dimensões otimizadas para tela de login do Google
left/top:
Centraliza popup na tela atual do usuário
popup=yes:
Remove barras de navegação/endereço
Nome 'google-oauth':
Identifica janela para comunicação posterior
// 🎨 Melhorias adicionais para experiência do usuário
const handleGoogleSignIn = () => {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
// 🎯 Validação se popup blocker está ativo
const popup = window.open(
`${apiUrl}/auth/google`,
'google-oauth',
'width=500,height=600,left=300,top=200,popup=yes,scrollbars=yes'
);
if (!popup) {
// 🚨 Popup foi bloqueado
toast.error('Popup bloqueado! Por favor, permita popups para este site.');
return;
}
// 💫 Foco no popup para melhor UX
popup.focus();
// 🔄 Monitoring: detecta se usuário fechou popup manualmente
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
// 🛑 Usuário cancelou OAuth
console.log('OAuth cancelado pelo usuário');
setOauthWindow(null);
}
}, 1000);
setOauthWindow(popup);
};
Popup Blockers:
Navegadores bloqueiam popups que não são resultado direto de clique do usuário. Sempre chame window.open()
dentro do handler do botão.
CORS Issues:
Popup carrega conteúdo do backend, que já tem CORS configurado. Problema comum é URL incorreta para o backend.
Mobile Safari:
Alguns navegadores mobile tratam popups diferente. Considere fallback para redirect completo em mobile.
/auth/google
do seu backend. O próximo passo é tratar a comunicação entre popup e janela principal.// 📁 sign-in-form.tsx (baseado no arquivo fornecido)
export function SignInForm() {
const router = useRouter();
const getProfile = useAuthStore((state) => state.getProfile);
const [oauthWindow, setOauthWindow] = useState<Window | null>(null);
useEffect(() => {
// 🎧 Listener para receber mensagens do popup OAuth
const handleMessage = async (event: MessageEvent) => {
// 🛡️ CRÍTICO: Verifica a origem da mensagem
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
if (event.origin !== apiUrl) {
console.warn('Mensagem de origem não confiável:', event.origin);
return;
}
// 🔍 Verifica se é uma mensagem de autenticação
if (event.data?.type === 'auth-complete') {
const { success, error } = event.data.data;
if (success) {
// ✅ OAuth completado com sucesso
try {
// 👤 Os cookies já foram definidos pelo backend com httpOnly
// Apenas fazemos a requisição para buscar o perfil atualizado
await getProfile();
toast.success('Login realizado com sucesso!');
router.push('/dashboard');
} catch {
toast.error('Erro ao completar login');
}
} else {
// ❌ Erro no OAuth
toast.error(error || 'Erro ao fazer login com Google');
}
// 🚪 Fecha o popup se ainda estiver aberto
if (oauthWindow && !oauthWindow.closed) {
oauthWindow.close();
}
setOauthWindow(null);
}
};
// 📡 Registra o listener
window.addEventListener('message', handleMessage);
// 🧹 Cleanup: remove listener quando componente desmonta
return () => {
window.removeEventListener('message', handleMessage);
};
}, [oauthWindow, router, getProfile]); // 🔄 Deps necessárias
// ... resto do componente
}
A verificação event.origin !== apiUrl
é CRÍTICA para segurança. Sem ela, qualquer site malicioso poderia enviar mensagens falsas:
// ❌ NUNCA faça isso - INSEGURO:
const handleMessage = async (event: MessageEvent) => {
// Aceita mensagem de qualquer origem - VULNERÁVEL!
if (event.data?.type === 'auth-complete') {
// Atacante pode forjar esta mensagem
}
};
// ✅ SEMPRE verifique origem:
const handleMessage = async (event: MessageEvent) => {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
// 🛡️ Proteção essencial contra ataques
if (event.origin !== apiUrl) {
console.warn('Origem não confiável:', event.origin);
return; // Ignora mensagem
}
// Agora é seguro processar
if (event.data?.type === 'auth-complete') {
// Código de autenticação...
}
};
// 🏗️ Interface TypeScript para mensagens OAuth
interface OAuthMessage {
type: 'auth-complete';
data: {
success: boolean;
error?: string;
user?: {
id: string;
name: string;
email: string;
avatar?: string;
};
};
}
// 🎯 Handler tipado
const handleMessage = async (event: MessageEvent<OAuthMessage>) => {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
if (event.origin !== apiUrl) return;
if (event.data?.type === 'auth-complete') {
const { success, error, user } = event.data.data;
if (success) {
console.log('✅ Login bem-sucedido:', user?.name);
await getProfile(); // Atualiza store
router.push('/dashboard');
} else {
console.error('❌ Erro OAuth:', error);
toast.error(error || 'Falha na autenticação');
}
// Limpa popup
if (oauthWindow && !oauthWindow.closed) {
oauthWindow.close();
}
setOauthWindow(null);
}
};
window.open() abre popup direcionando para /auth/google
Backend redireciona popup para tela de login do Google
Após login, Google redireciona para /auth/google/callback
Backend processa OAuth e envia HTML com postMessage
PostMessage notifica janela principal sobre resultado
Fecha popup, atualiza estado e redireciona usuário
Popup Fechado Manualmente:
Use setInterval para detectar popup.closed
e limpar estado.
Timeout:
Defina timeout de 5 minutos para auto-fechar popup em caso de problema.
Múltiplos Popups:
Disable botão OAuth enquanto popup está aberto para evitar confusão.
// 📁 src/http/routes/auth-google.ts
import { Elysia } from 'elysia'
import { env } from '../../env'
export const authGoogle = new Elysia()
.get('/auth/google', async ({ set }) => {
// 🔧 Configuração dos parâmetros OAuth
const googleAuthUrl = 'https://accounts.google.com/o/oauth2/v2/auth'
// 🎯 Parâmetros necessários para OAuth Google
const params = new URLSearchParams({
client_id: env.GOOGLE_CLIENT_ID,
redirect_uri: env.GOOGLE_CALLBACK_URL,
response_type: 'code',
scope: 'openid email profile', // 🔍 Dados que solicitamos
access_type: 'offline',
prompt: 'consent'
})
// 🌍 Redireciona para Google OAuth
set.redirect = `${googleAuthUrl}?${params.toString()}`
})
// 📁 src/http/routes/auth-google-callback.ts
import { Elysia } from 'elysia'
import { db } from '../../db'
import { users } from '../../db/schema'
import { eq } from 'drizzle-orm'
import { jwt } from '@elysiajs/jwt'
import { env } from '../../env'
export const authGoogleCallback = new Elysia()
.use(jwt({ name: 'jwt', secret: env.JWT_SECRET }))
.get('/auth/google/callback', async ({ query, set, jwt, setCookie }) => {
try {
const { code, error } = query
if (error || !code) {
return generateErrorResponse(error || 'Código não fornecido')
}
// 🔄 1. Trocar code por access token
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: env.GOOGLE_CLIENT_ID,
client_secret: env.GOOGLE_CLIENT_SECRET,
code: code as string,
grant_type: 'authorization_code',
redirect_uri: env.GOOGLE_CALLBACK_URL,
}),
})
const tokenData = await tokenResponse.json()
// 👤 2. Buscar dados do usuário
const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${tokenData.access_token}` },
})
const googleUser = await userResponse.json()
// 🔍 3. Buscar ou criar usuário
let user = await db.query.users.findFirst({
where: eq(users.email, googleUser.email),
})
if (!user) {
const [newUser] = await db.insert(users).values({
email: googleUser.email,
name: googleUser.name,
role: 'customer',
avatar: googleUser.picture,
googleId: googleUser.id,
}).returning()
user = newUser
}
// 🔐 4. Gerar JWT e definir cookie
const token = await jwt.sign({
sub: user.id,
email: user.email,
role: user.role,
})
setCookie('auth-token', token, {
httpOnly: true,
secure: env.NODE_ENV === 'production',
sameSite: env.NODE_ENV === 'production' ? 'none' : 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 dias
path: '/',
})
// ✅ 5. Retornar HTML com postMessage
return generateSuccessResponse(user)
} catch (error) {
console.error('Erro OAuth:', error)
return generateErrorResponse('Erro interno')
}
})
// 🎨 Função para resposta de sucesso
function generateSuccessResponse(user: any) {
return `
<!DOCTYPE html>
<html>
<head>
<title>Login Realizado</title>
<style>
body {
font-family: system-ui;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background: #f5f5f5;
}
.success { color: #22c55e; font-size: 24px; margin-bottom: 1rem; }
</style>
</head>
<body>
<div>
<div class="success">✅ Login realizado!</div>
<p>Redirecionando...</p>
</div>
<script>
if (window.opener) {
window.opener.postMessage({
type: 'auth-complete',
data: { success: true, user: ${JSON.stringify(user)} }
}, '${env.FRONTEND_URL}');
setTimeout(() => window.close(), 1000);
}
</script>
</body>
</html>
`
}
// 🚨 Função para resposta de erro
function generateErrorResponse(error: string) {
return `
<!DOCTYPE html>
<html>
<head><title>Erro</title></head>
<body>
<div>❌ Erro: ${error}</div>
<script>
if (window.opener) {
window.opener.postMessage({
type: 'auth-complete',
data: { success: false, error: '${error}' }
}, '${env.FRONTEND_URL}');
setTimeout(() => window.close(), 2000);
}
</script>
</body>
</html>
`
}
// 📁 src/http/server.ts
import { authGoogle } from './routes/auth-google'
import { authGoogleCallback } from './routes/auth-google-callback'
const app = new Elysia()
.use(cors())
.use(authGoogle)
.use(authGoogleCallback)
// ... outras rotas
.listen(3000)
// 📁 .env
GOOGLE_CLIENT_ID=sua_client_id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=seu_client_secret
GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback
FRONTEND_URL=http://localhost:3001
JWT_SECRET=seu_jwt_secret_super_seguro
Vamos acompanhar o que acontece desde o clique no botão até o usuário estar logado:
Usuário clica em "Entrar com Google" → handleGoogleSignIn()
executa
// window.open() abre popup
const popup = window.open(
'http://localhost:3000/auth/google',
'google-oauth',
'width=500,height=600,popup=yes'
)
Rota /auth/google
monta URL do Google com parâmetros OAuth
// Backend gera URL OAuth
const googleUrl = `https://accounts.google.com/o/oauth2/v2/auth?
client_id=${CLIENT_ID}&
redirect_uri=${CALLBACK_URL}&
response_type=code&
scope=openid email profile`
set.redirect = googleUrl
Popup mostra interface de login do Google. Usuário autoriza aplicação.
📱 "Restaurantix quer acessar sua conta Google"
📧 Email: user@gmail.com
🔐 Senha: ••••••••
✅ [Autorizar]
Google redireciona popup para nosso callback com código temporário
// Google redireciona para:
http://localhost:3000/auth/google/callback?code=4%2F1AX4XfWi...
// Code é temporário (10 minutos de validade)
// Usado para trocar por access token
Callback troca authorization code por access token via POST para Google
// POST para https://oauth2.googleapis.com/token
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: new URLSearchParams({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET, // 🔐 Só backend tem acesso
code: authorizationCode,
grant_type: 'authorization_code',
redirect_uri: CALLBACK_URL,
}),
})
// Retorna: { access_token, expires_in, token_type }
Com access token, busca dados do usuário na API do Google
// GET para API do Google
const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: {
Authorization: `Bearer ${access_token}`,
},
})
// Retorna dados do usuário:
{
"id": "1234567890",
"email": "user@gmail.com",
"name": "João Silva",
"picture": "https://lh3.googleusercontent.com/...",
"email_verified": true
}
Verifica se usuário existe no banco. Se não, cria novo. Gera JWT.
// Busca usuário existente
let user = await db.query.users.findFirst({
where: eq(users.email, googleUser.email),
})
if (!user) {
// Cria novo usuário
const [newUser] = await db.insert(users).values({
email: googleUser.email,
name: googleUser.name,
avatar: googleUser.picture,
googleId: googleUser.id,
role: 'customer',
}).returning()
user = newUser
}
// Gera JWT
const token = await jwt.sign({ sub: user.id, email: user.email })
Define cookie httpOnly e retorna HTML com script postMessage
// Define cookie seguro
setCookie('auth-token', jwt, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 dias
})
// Retorna HTML com postMessage
return `<script>
window.opener.postMessage({
type: 'auth-complete',
data: { success: true, user: {...} }
}, 'http://localhost:3001');
window.close();
</script>`
Event listener captura postMessage, fecha popup, atualiza estado
// Event listener ativo
const handleMessage = async (event) => {
if (event.origin !== 'http://localhost:3000') return
if (event.data?.type === 'auth-complete') {
const { success } = event.data.data
if (success) {
await getProfile() // Atualiza store
toast.success('Login realizado!')
router.push('/dashboard')
}
oauthWindow.close()
}
}
Usuário autenticado, popup fechado, redirecionado para dashboard
✅ Cookie httpOnly definido
✅ Store atualizado com dados do usuário
✅ Redirecionamento para /dashboard
✅ Toast de sucesso exibido
✅ Popup fechado automaticamente
Usuário Já Logado no Google:
~2-3 segundos (quase instantâneo)
Primeiro Login:
~10-15 segundos (inclui digitação)
Conexão Lenta:
~20-30 segundos (aguarda respostas)
Network Tab:
Acompanhe redirects e requisições para Google APIs
Console Logs:
Adicione logs em cada etapa do backend para debugar problemas
Application Tab:
Verifique se cookies estão sendo definidos corretamente
Você agora domina OAuth Google com arquitetura profissional. Seus usuários podem fazer login de forma segura e fluída, mantendo a mesma proteção por cookies da aula anterior.
💡 Dica Final: OAuth é uma das funcionalidades que mais impacta a conversão de usuários. Uma implementação fluída pode aumentar significativamente o número de cadastros na sua aplicação.
Esta implementação se integra perfeitamente com:
Aula 4 - Autenticação:
Mesma arquitetura de cookies, mesmos middlewares CSRF, mesmo store de autenticação.
Próximas Aulas:
Sistema de permissões, dashboard personalizado, e funcionalidades avançadas do Restaurantix.