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:

    bash
    # 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:

    typescript
    // 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:

    typescript
    // 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:

    bash
    # 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.br

    Passport.js: simplificar múltiplos providers

    Usar Passport.js para gerenciar vários provedores OAuth2:

    typescript
    // 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