12k
All articles

Como adicionar o Login com GitHub

Adicione o login com GitHub usando Node e Express. Implemente OAuth com state e PKCE, troque códigos com segurança, obtenha o e-mail verificado e atualize tokens.

OpenReplay Team
OpenReplay Team
Como adicionar o Login com GitHub

Para adicionar o Login com GitHub, você redireciona o usuário para https://github.com/login/oauth/authorize, recebe um code temporário na sua URL de callback e troca esse código por um access token fazendo um POST para https://github.com/login/oauth/access_token a partir do seu servidor.

Você está com as configurações do OAuth app abertas em uma aba e o seu route handler em outra, e a maioria dos tutoriais entrega todo o trabalho para um SDK. Este guia constrói o fluxo no servidor em Node usando fetch e Express, sem nenhuma biblioteca de OAuth. Para entender o modelo por trás, consulte All about OAuth 2.0. O guia aborda state e PKCE, o problema do e-mail nulo, scopes e as mudanças em redirect URIs e na renovação de tokens que o GitHub lançou em 14 de agosto de 2026.

Principais conclusões

  • O authorization code do GitHub expira 10 minutos após ser emitido, e a troca do código precisa ser executada no seu servidor, porque exige o client secret.
  • Gere o state no servidor, armazene-o em um cookie httpOnly e interrompa o callback se o valor retornado for diferente. Isso bloqueia o login CSRF.
  • GET https://api.github.com/user retorna email: null quando o usuário não tem um e-mail público; por isso, solicite user:email e use a entrada primária e verificada de GET /user/emails.
  • Desde a atualização do GitHub de 14 de agosto de 2026, um OAuth app pode registrar até 10 redirect URIs, e novos apps emitem, por padrão, access tokens de 8 horas com refresh tokens de 6 meses.

Registre o OAuth app no GitHub

Crie um OAuth app em Settings → Developer settings → OAuth Apps, depois copie o client ID e gere um client secret. O guia de registro do GitHub lista todos os campos.

Desde o lançamento de 14 de agosto de 2026, um único OAuth app pode ter até 10 redirect URIs, que você adiciona com o botão Add callback URL. Assim, é possível cobrir produção, deploys de preview e desenvolvimento local com um único app. Para o trabalho local, registre http://127.0.0.1:3000/auth/github/callback. localhost funciona, mas as orientações do GitHub sobre loopback ressaltam que a RFC do OAuth recomenda o literal 127.0.0.1.

A correspondência por wildcard agora é uma configuração de cada redirect URI. Se o seu app tinha apenas uma URL de callback antes de 3 de agosto de 2026, o GitHub ativou a correspondência por wildcard para ela, a fim de manter o comportamento de redirecionamento anterior. Desative-a, a menos que você controle todos os subdomínios e caminhos abaixo dessa URL.

Redirecione para o endpoint de autorização do GitHub

O botão de login aponta para uma rota no servidor. Essa rota gera o state e um verifier PKCE, armazena ambos em cookies de curta duração e redireciona para GET https://github.com/login/oauth/authorize.

import crypto from 'node:crypto';
import express from 'express';
import cookieParser from 'cookie-parser';

const app = express();
app.use(cookieParser());

const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_REDIRECT_URI } = process.env;
// GITHUB_CLIENT_SECRET is read only here, on the server. Never ship it to the browser.
const isProd = process.env.NODE_ENV === 'production';

const tempCookie = {
  httpOnly: true,
  secure: isProd,
  sameSite: 'lax',
  maxAge: 10 * 60 * 1000, // matches the 10-minute code lifetime
  path: '/auth/github',
};

app.get('/auth/github', (req, res) => {
  const state = crypto.randomBytes(32).toString('base64url');
  const verifier = crypto.randomBytes(32).toString('base64url'); // 43 chars
  const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');

  res.cookie('gh_state', state, tempCookie);
  res.cookie('gh_verifier', verifier, tempCookie);

  const params = new URLSearchParams({
    client_id: GITHUB_CLIENT_ID,
    redirect_uri: GITHUB_REDIRECT_URI, // one value per environment
    scope: 'read:user user:email',
    state,
    code_challenge: challenge,
    code_challenge_method: 'S256',
  });
  res.redirect(`https://github.com/login/oauth/authorize?${params}`);
});

Os parâmetros de autorização do GitHub aceitam um único método PKCE, S256, e rejeitam plain. O challenge precisa ser um valor SHA-256 de 43 caracteres, que é exatamente o que você obtém ao codificar em base64url um digest SHA-256. Se seus usuários costumam ter mais de uma conta no GitHub, adicione prompt=select_account para que o GitHub sempre exiba o seletor de contas.

Valide o state para impedir login CSRF

O parâmetro state é um valor aleatório que ninguém consegue adivinhar. Seu servidor o cria e o armazena na sessão ou em um cookie httpOnly. Quando o GitHub redirecionar de volta, compare-o com o state recebido no callback. Se os dois forem diferentes, interrompa o login e não troque o código.

Sem essa verificação, seu app fica vulnerável a login CSRF, um ataque que autentica a vítima na conta do atacante:

  1. O atacante inicia o seu fluxo de login com GitHub usando a própria conta do GitHub.
  2. Ele aprova o acesso, mas para antes de o callback ser executado, guardando o seu code válido.
  3. Ele faz o navegador da vítima carregar https://yourapp.com/auth/github/callback?code=ATTACKER_CODE, por exemplo, por meio de uma imagem oculta ou de um link com envio automático.
  4. Seu servidor troca o código e autentica a vítima como o atacante. Pior: se a vítima já estava logada, ele vincula a identidade do GitHub do atacante à conta da vítima.
  5. Tudo o que a vítima salvar a partir daí vai parar em uma conta controlada pelo atacante. Uma identidade vinculada também permite que o atacante entre como a vítima depois.

Com o state, o passo 4 falha. O navegador da vítima ou não tem o cookie gh_state ou tem um que não corresponde ao valor do atacante, então o servidor rejeita a requisição. O PKCE acrescenta um segundo vínculo: o código só funciona junto com o code_verifier armazenado no navegador que iniciou o fluxo. A RFC 6749 define o state para essa finalidade, e o OAuth 2.0 Security Best Current Practice recomenda PKCE para todos os clients. As orientações sobre state e PKCE em Securing OAuth best practices se aprofundam no assunto.

function statesMatch(received, stored) {
  if (typeof received !== 'string' || typeof stored !== 'string') return false;
  const a = Buffer.from(received);
  const b = Buffer.from(stored);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Os atributos do cookie também são importantes. HttpOnly mantém o valor fora do alcance de scripts, e Secure o restringe a HTTPS. SameSite=Lax permite que o cookie sobreviva ao redirecionamento top-level do GitHub de volta para você. Um único cookie gh_state significa que uma segunda aba de login sobrescreve a primeira; por isso, exiba uma mensagem clara para tentar novamente em vez de um erro em branco.

Trate o callback e troque o código

O callback verifica o state, limpa os cookies temporários e faz um POST do código para https://github.com/login/oauth/access_token com Accept: application/json. Sem esse header, o endpoint de token do GitHub responde com um corpo form-encoded. O código expira 10 minutos após ser emitido, então troque-o imediatamente.

app.get('/auth/github/callback', async (req, res) => {
  const { code, state } = req.query;
  const { gh_state: savedState, gh_verifier: verifier } = req.cookies;

  res.clearCookie('gh_state', { path: '/auth/github' });
  res.clearCookie('gh_verifier', { path: '/auth/github' });

  if (typeof code !== 'string' || !verifier || !statesMatch(state, savedState)) {
    return res.status(400).send('Sign-in failed. Please try again.');
  }

  const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      client_id: GITHUB_CLIENT_ID,
      client_secret: GITHUB_CLIENT_SECRET,
      code,
      redirect_uri: GITHUB_REDIRECT_URI, // must match the authorize request
      code_verifier: verifier,
    }),
  });
  const token = await tokenRes.json();
  if (token.error || !token.access_token) {
    return res.status(400).send('Sign-in failed. Please try again.');
  }

  const profile = await getGitHubProfile(token.access_token);
  const user = await upsertUserByGitHubId(profile); // key on profile.id
  await saveGitHubTokens(user.id, token);           // server-side only
  await createSession(res, user.id);                // your own session cookie
  res.redirect('/');
});

Mantenha essa troca no servidor, porque ela exige o client secret, e esse secret nunca deve chegar ao navegador. Depois que a troca for bem-sucedida, emita o seu próprio cookie de sessão e mantenha o token do GitHub no servidor. Cookies vs localStorage for JWT auth aborda os trade-offs de armazenamento. O res.redirect('/') final remove code e state da barra de endereços, para que um clique em Voltar não reenvie um código já utilizado.

Session replays de logins com GitHub que falharam mostram a sequência completa: clique, GitHub, callback, página de erro. Essa sequência diferencia divergências de state de códigos expirados. Configure qualquer ferramenta de replay, analytics ou logging para mascarar os query parameters code e state.

Busque o usuário e um e-mail verificado

Chame GET https://api.github.com/user com o token para identificar o usuário. O endpoint do usuário autenticado retorna email: null quando o usuário não definiu um e-mail público. Tokens OAuth também precisam de read:user para obter a resposta privada do usuário.

const ghHeaders = (accessToken) => ({
  Authorization: `Bearer ${accessToken}`,
  Accept: 'application/vnd.github+json',
  'User-Agent': 'your-app-name',
});

async function getGitHubProfile(accessToken) {
  const userRes = await fetch('https://api.github.com/user', { headers: ghHeaders(accessToken) });
  if (!userRes.ok) throw new Error(`GET /user failed: ${userRes.status}`);
  const user = await userRes.json();

  let email = user.email;
  if (!email) {
    const emailsRes = await fetch('https://api.github.com/user/emails', { headers: ghHeaders(accessToken) });
    if (emailsRes.ok) {
      const emails = await emailsRes.json();
      email = emails.find((e) => e.primary && e.verified)?.email ?? null;
    }
  }
  return { id: user.id, login: user.login, name: user.name, email };
}

Com o scope user:email, GET /user/emails retorna todos os endereços junto com as flags primary e verified. Escolha a entrada em que ambas são verdadeiras e nunca vincule contas com base em um endereço não verificado. Identifique os usuários pelo id numérico, porque logins e e-mails podem mudar. A REST API do GitHub recusa qualquer requisição sem o header User-Agent, então defina um você mesmo. O GitHub também recomenda verificar quem é o usuário toda vez que você obtiver um novo token, já que o usuário pode ter trocado de conta na página de autorização.

Solicite o menor scope necessário

O Login com GitHub precisa de apenas dois scopes OAuth: read:user e user:email. A referência de scopes do GitHub define cada um deles, e os usuários veem todos os scopes que você solicita na tela de consentimento do GitHub.

Você precisa deScope
Perfil: id, login, nome, avatarread:user
Endereços de e-mailuser:email
Conteúdo de repositóriosrepo (acesso total, incluindo repositórios privados)

Solicitar repo para login significa pedir aos usuários que concedam acesso de leitura e escrita ao seu código privado, e muitos vão desistir nesse ponto. Se você precisar de acesso a repositórios para uma funcionalidade posterior, execute um fluxo separado nesse momento. O GitHub permite no máximo dez tokens para cada combinação de usuário, app e scope, e não mais do que dez novos tokens por hora. Se um loop de callback atingir esse limite por hora, o GitHub interrompe o fluxo e exibe ao usuário um prompt de reautorização.

Renove tokens que expiram

Para renovar um token do GitHub, faça um POST com grant_type=refresh_token para o endpoint de token. Usar um refresh token o invalida junto com o access token antigo, então salve o novo par antes de fazer outra chamada. Com tokens expiráveis ativados, access tokens expiram após oito horas, e um refresh token não utilizado expira após seis meses. Desde a mudança de 14 de agosto de 2026, novos apps têm esse comportamento por padrão. Apps existentes podem aderir para um único login adicionando o scope offline_access, que não adiciona um prompt de consentimento, ou podem ativá-lo nas configurações do app.

async function refreshGitHubToken(refreshToken) {
  // No refresh token means a non-expiring token: nothing to refresh.
  if (!refreshToken) return null;

  const res = await fetch('https://github.com/login/oauth/access_token', {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      client_id: GITHUB_CLIENT_ID,
      client_secret: GITHUB_CLIENT_SECRET,
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
    }),
  });
  const data = await res.json();
  if (data.error === 'bad_refresh_token') return { reauth: true };
  if (data.error) throw new Error(data.error);

  return {
    accessToken: data.access_token,
    refreshToken: data.refresh_token,
    expiresAt: Date.now() + data.expires_in * 1000,
  };
}

Não é possível alterar scopes durante uma renovação. bad_refresh_token significa que o usuário precisa fazer login novamente. Não presuma que um refresh_token sempre será retornado: algumas versões do GitHub Enterprise Server ignoram offline_access e emitem um token que não expira.

Usar um OAuth app ou um GitHub App?

Apenas para o Login com GitHub, um OAuth app que solicita read:user user:email é suficiente, embora o GitHub, de modo geral, recomende GitHub Apps em vez de OAuth apps. A página de comparação do GitHub detalha todas as diferenças.

OAuth appGitHub App
Modelo de permissõesScopes amplosPermissões granulares
Seleção de repositóriosTodos os repositórios cobertos pelo scopeO usuário escolhe os repositórios
Tempo de vida do tokenExpira em 8 horas por padrão em novos apps; de longa duração em apps mais antigos, a menos que ativadoCurta duração
Recomendação do GitHubSuportadoPreferido

Escolha um GitHub App quando o seu produto atuar sobre repositórios. Para identidade, o fluxo de OAuth app descrito acima é suficiente.

Checklist de segurança

  • Envie state e PKCE S256 em todas as requisições e verifique ambos no callback.
  • Envie o mesmo redirect_uri na requisição de autorização e na requisição de token.
  • Desative a correspondência por wildcard de que você não precisa.
  • Mantenha o client secret e os tokens do GitHub no servidor. Entregue ao navegador apenas o seu próprio cookie de sessão.
  • Redirecione para fora do callback após a troca.
  • Trate uma resposta de token que não contenha refresh_token.

O Login com GitHub se resume a três requisições: autorizar, trocar o código e buscar o usuário. A segurança está nos detalhes em torno delas: um state mantido no servidor, um verifier PKCE, um secret que nunca sai do seu backend e um e-mail que você confirmou ser verificado. Como próximo passo, abra as configurações do seu OAuth app, adicione as URLs de callback de preview e de 127.0.0.1, desative a correspondência por wildcard e coloque em produção as três rotas acima.

Perguntas frequentes

Por que o endpoint de token do GitHub retorna bad_verification_code?

O GitHub retorna bad_verification_code quando o código enviado está incorreto, expirado ou não corresponde ao emitido na requisição de autorização. As causas mais comuns são um código com mais de 10 minutos ou um código que já foi trocado, por exemplo, após um clique em Voltar ou uma segunda requisição ao callback. Não é possível recuperar esse código. Reinicie o fluxo de login para obter um novo.

O que causa redirect_uri_mismatch no OAuth do GitHub?

O GitHub retorna redirect_uri_mismatch quando o redirect_uri enviado não corresponde a uma URL de callback registrada no OAuth app. Envie exatamente uma URL registrada ou omita o parâmetro, e o GitHub usará o padrão do app. Envie o mesmo valor na requisição de autorização e na requisição de token. O GitHub trata 127.0.0.1 e localhost como hosts diferentes, então registre aquele que o seu servidor local usa.

Sair do meu app revoga o token do GitHub do usuário?

Não. Encerrar a sua própria sessão mantém válidos o token do GitHub e a autorização. Para revogar um token, chame DELETE /applications/{client_id}/token. Para remover todos os tokens e a autorização do app para aquele usuário, chame DELETE /applications/{client_id}/grant. Ambos os endpoints usam Basic authentication com o client_id e o client_secret, além do access_token no corpo da requisição. Os usuários também podem revogar o acesso nas configurações da conta do GitHub.

Um token OAuth do GitHub que não expira ainda pode parar de funcionar?

Sim. O GitHub revoga automaticamente um token OAuth que fica sem uso por um ano. Um token também deixa de funcionar quando o usuário revoga a autorização do seu app ou quando o token é enviado para um repositório público ou gist. Se uma mesma combinação de usuário, app e scope acumular mais de dez tokens, o GitHub revoga um dos mais antigos. Quando um token armazenado parar de funcionar, conduza o usuário pelo login novamente.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.