OAuth2 com GitHub e Google em Node.js
Login social com Google e GitHub elimina o cadastro com senha para o usuário — e transfere a responsabilidade de autenticação segura para provedores com bilhões de usuários. Implementar OAuth2 do zero parece complexo, mas o fluxo Authorization Code com PKCE cabe em poucas funções.
Fluxo OAuth2: Authorization Code com PKCE
Entender o fluxo antes de implementar:
# Fluxo OAuth2 Authorization Code com PKCE (Proof Key for Code Exchange):
# 1. Frontend gera code_verifier (aleatório) e code_challenge (SHA256 do verifier)
# 2. Frontend redireciona o usuário para o provedor OAuth com code_challenge
# → https://github.com/login/oauth/authorize?
# client_id=SEU_CLIENT_ID
# &redirect_uri=https://api.seudominio.com.br/auth/github/callback
# &scope=user:email
# &state=CSRF_TOKEN_ALEATORIO
# &code_challenge=BASE64URL(SHA256(code_verifier))
# &code_challenge_method=S256
# 3. Usuário faz login no GitHub e autoriza
# 4. GitHub redireciona para redirect_uri com ?code=AUTHORIZATION_CODE&state=...
# 5. Backend verifica o state (anti-CSRF)
# 6. Backend troca code + code_verifier por access_token:
# POST https://github.com/login/oauth/access_token
# { client_id, client_secret, code, code_verifier, redirect_uri }
# 7. Backend usa access_token para buscar perfil do usuário
# 8. Backend cria/atualiza usuário no banco e gera JWT próprio para a sessão
# Por que PKCE?
# Protege contra roubo do authorization code em trânsito
# Obrigatório para SPAs (sem client_secret seguro no browser)OAuth2 com GitHub: implementação manual
Implementar o fluxo OAuth2 sem bibliotecas externas:
// routes/auth.ts — OAuth2 com GitHub
// Passo 1: Iniciar o fluxo (frontend chama esta rota):
app.get('/api/auth/github', (req, res) => {
const state = crypto.randomUUID()
// Armazenar state na sessão ou cookie para verificação no callback:
res.cookie('oauth_state', state, { httpOnly: true, maxAge: 600_000 })
const params = new URLSearchParams({
client_id: process.env.GITHUB_CLIENT_ID!,
redirect_uri: process.env.GITHUB_REDIRECT_URI!,
scope: 'user:email',
state,
})
res.redirect(`https://github.com/login/oauth/authorize?${params}`)
})
// Passo 2: Callback do GitHub:
app.get('/api/auth/github/callback', async (req, res) => {
const { code, state } = req.query as { code: string; state: string }
const storedState = req.cookies.oauth_state
// Verificar CSRF (state deve bater):
if (!state || state !== storedState) {
return res.status(400).json({ error: 'State inválido (possível CSRF)' })
}
res.clearCookie('oauth_state')
// Trocar code por access_token do GitHub:
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
client_id: process.env.GITHUB_CLIENT_ID,
client_secret: process.env.GITHUB_CLIENT_SECRET,
code,
redirect_uri: process.env.GITHUB_REDIRECT_URI,
}),
})
const { access_token } = await tokenRes.json() as { access_token: string }
// Buscar perfil do usuário:
const profileRes = await fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${access_token}`, 'User-Agent': 'minha-api' },
})
const profile = await profileRes.json() as { id: number; login: string; email: string; name: string }
// Buscar email (pode ser privado):
const emailsRes = await fetch('https://api.github.com/user/emails', {
headers: { Authorization: `Bearer ${access_token}`, 'User-Agent': 'minha-api' },
})
const emails = await emailsRes.json() as Array<{ email: string; primary: boolean; verified: boolean }>
const primaryEmail = emails.find(e => e.primary && e.verified)?.email ?? profile.email
// Upsert do usuário no banco:
const usuario = await db.query(
`INSERT INTO usuarios (github_id, email, nome, avatar_url)
VALUES ($1, $2, $3, $4)
ON CONFLICT (github_id) DO UPDATE SET email = EXCLUDED.email, nome = EXCLUDED.nome
RETURNING *`,
[profile.id, primaryEmail, profile.name, `https://github.com/${profile.login}.png`]
).then(r => r.rows[0])
// Gerar JWT próprio e redirecionar para o frontend:
const accessToken = await gerarAccessToken({ userId: usuario.id, email: usuario.email, role: usuario.role })
res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${accessToken}`)
})OAuth2 com Google
Login com Google usando a API de identidade do Google:
// Google usa OpenID Connect (extensão do OAuth2) — expõe um id_token JWT
app.get('/api/auth/google', (req, res) => {
const state = crypto.randomUUID()
res.cookie('oauth_state', state, { httpOnly: true, maxAge: 600_000 })
const params = new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID!,
redirect_uri: process.env.GOOGLE_REDIRECT_URI!,
response_type: 'code',
scope: 'openid email profile',
state,
access_type: 'offline', // para receber refresh_token do Google
prompt: 'consent',
})
res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`)
})
app.get('/api/auth/google/callback', async (req, res) => {
const { code, state } = req.query as { code: string; state: string }
if (state !== req.cookies.oauth_state) return res.status(400).send('State inválido')
res.clearCookie('oauth_state')
// Trocar code por tokens:
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
client_id: process.env.GOOGLE_CLIENT_ID!,
client_secret: process.env.GOOGLE_CLIENT_SECRET!,
redirect_uri: process.env.GOOGLE_REDIRECT_URI!,
grant_type: 'authorization_code',
}),
})
const { access_token, id_token } = await tokenRes.json()
// Verificar o id_token JWT do Google (contém o perfil):
// Opção 1: usar google-auth-library
// Opção 2: buscar perfil via API:
const profileRes = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
headers: { Authorization: `Bearer ${access_token}` },
})
const profile = await profileRes.json() as {
sub: string; email: string; name: string; picture: string; email_verified: boolean
}
if (!profile.email_verified) return res.status(400).json({ error: 'Email não verificado' })
const usuario = await upsertUsuario({
googleId: profile.sub,
email: profile.email,
nome: profile.name,
avatarUrl: profile.picture,
})
const accessToken = await gerarAccessToken({ userId: usuario.id, email: usuario.email, role: usuario.role })
res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${accessToken}`)
})Configurar as credenciais OAuth2
Criar apps no GitHub e Google para obter client_id e client_secret:
# GitHub — criar OAuth App:
# 1. github.com/settings/developers → OAuth Apps → New OAuth App
# 2. Application name: Minha API
# 3. Homepage URL: https://seudominio.com.br
# 4. Authorization callback URL: https://api.seudominio.com.br/api/auth/github/callback
# 5. Copiar: Client ID e Client Secret (gerar com "Generate a new client secret")
# Google — criar credenciais OAuth2:
# 1. console.cloud.google.com → Criar projeto
# 2. APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client IDs
# 3. Application type: Web application
# 4. Authorized redirect URIs: https://api.seudominio.com.br/api/auth/google/callback
# 5. Copiar: Client ID e Client Secret
# 6. APIs & Services → OAuth consent screen → configurar domínio e email de suporte
# .env de produção:
GITHUB_CLIENT_ID=Ov23liXXXXXXXXXXXXXX
GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
GITHUB_REDIRECT_URI=https://api.seudominio.com.br/api/auth/github/callback
GOOGLE_CLIENT_ID=123456789-xxxxxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxxx
GOOGLE_REDIRECT_URI=https://api.seudominio.com.br/api/auth/google/callback
FRONTEND_URL=https://seudominio.com.brPassport.js: simplificar múltiplos providers
Usar Passport.js para gerenciar vários provedores OAuth2:
// npm install passport passport-github2 passport-google-oauth20
import passport from 'passport'
import { Strategy as GithubStrategy } from 'passport-github2'
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
passport.use(new GithubStrategy({
clientID: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
callbackURL: process.env.GITHUB_REDIRECT_URI!,
scope: ['user:email'],
}, async (accessToken, refreshToken, profile, done) => {
const email = profile.emails?.[0]?.value
const usuario = await upsertUsuario({
githubId: profile.id,
email: email ?? '',
nome: profile.displayName,
})
done(null, usuario)
}))
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: process.env.GOOGLE_REDIRECT_URI!,
}, async (accessToken, refreshToken, profile, done) => {
const email = profile.emails?.[0]?.value
const usuario = await upsertUsuario({
googleId: profile.id,
email: email ?? '',
nome: profile.displayName,
})
done(null, usuario)
}))
app.use(passport.initialize())
// Rotas:
app.get('/api/auth/github', passport.authenticate('github'))
app.get('/api/auth/github/callback',
passport.authenticate('github', { session: false }),
async (req, res) => {
const usuario = req.user as Usuario
const token = await gerarAccessToken({ userId: usuario.id, email: usuario.email, role: usuario.role })
res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${token}`)
}
)$ runstack deploy --plan starter
Não quer configurar manualmente?
Não quer configurar manualmente? Implante o VPS para APIs em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.
Perguntas frequentes
Conteúdos relacionados