Programação Por Equipe DevBras

Capa do artigo: Entrar com Google: como ligar o login à conta certa sem tomar a conta de ninguém

Entrar com Google: como ligar o login à conta certa sem tomar a conta de ninguém

Use o sub do Google como chave da identidade, decida o que fazer quando o e-mail já existe, peça só os escopos de login e faça um logout que encerra a sua sessão de verdade.

A Ana tinha conta com senha no sistema de agendamento desde 2022. Hoje alguém clicou em "Entrar com Google" com um endereço igual ao dela, e o sistema, achando o e-mail, entrou direto na conta antiga, com histórico e cartão salvo. A resposta curta: o identificador da pessoa no Google é o campo sub, não o e-mail; e juntar uma conta Google a uma conta existente exige confirmação de quem já é dono dela. O botão não entrega a senha do Gmail para você e não precisa entregar mais do que nome, e-mail e esse identificador.

Este guia trata do que acontece depois que o Google responde: como modelar a conta, como decidir entre criar, entrar ou vincular, quais escopos pedir e como sair. A validação do token em si está em conferir o token do Google no servidor; aqui partimos de um token já conferido.

O que o Google realmente manda

No login com Google Identity Services, o navegador recebe um ID token, um JWT assinado pelo Google. Depois que o seu servidor confere a assinatura, o emissor e a audiência (o seu client ID), os campos que importam são:

  • sub: identificador estável e único da conta Google. Não muda quando a pessoa troca de e-mail.
  • email e email_verified: o endereço e se o Google confirmou que a pessoa controla esse endereço.
  • name e picture: conforto para a tela, se o escopo profile foi pedido.

Nenhuma senha passa pelo seu servidor. Se um código pede a senha do Gmail num formulário seu, aquilo não é login do Google, é phishing com o seu logotipo.

Por que o e-mail não serve de chave

O e-mail muda: a pessoa sai da empresa e o endereço corporativo vai para outro funcionário, ou o domínio expira e outra pessoa registra. Se a conta é encontrada pelo e-mail, quem ganha o endereço ganha a conta. O sub não tem esse problema, porque pertence à conta Google, não à caixa de correio.

O segundo problema é a fusão silenciosa, a cena da Ana. Provar que controla um e-mail hoje não prova que foi você quem criou a conta com senha há três anos. A decisão de vincular precisa de uma prova feita dentro da conta existente.

Como modelar as contas

Separe a pessoa (usuário) das formas de entrar (identidades). Uma pessoa pode ter senha, Google e, amanhã, outro provedor.

CREATE TABLE usuarios (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  nome VARCHAR(120) NOT NULL,
  email VARCHAR(255) NOT NULL UNIQUE,
  senha_hash VARCHAR(255) NULL,
  criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE identidades_externas (
  provedor VARCHAR(30) NOT NULL,
  sub VARCHAR(255) NOT NULL,
  usuario_id BIGINT NOT NULL,
  email_no_login VARCHAR(255) NOT NULL,
  vinculado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (provedor, sub),
  FOREIGN KEY (usuario_id) REFERENCES usuarios(id)
);

A chave primária (provedor, sub) garante que uma conta Google aponte para um único usuário. O senha_hash pode ser nulo, para quem só entra pelo Google; o post login com senha protegida cobre esse campo.

A decisão no servidor, passo a passo

  1. Confira o ID token com a biblioteca oficial, passando o seu client ID como audiência.
  2. Procure a identidade por provedor e sub. Achou: é login, abra a sessão do usuário vinculado.
  3. Não achou e email_verified é falso: recuse ou peça outro método. Não crie conta com e-mail não verificado.
  4. Não achou e o e-mail já existe em usuarios: não entre. Peça que a pessoa entre com a senha atual e, já logada, confirme "vincular minha conta Google".
  5. Não achou e o e-mail é novo: crie o usuário e a identidade na mesma transação.
const { OAuth2Client } = require('google-auth-library');

const CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const google = new OAuth2Client();

async function entrarComGoogle(idToken, db) {
  const ticket = await google.verifyIdToken({ idToken, audience: CLIENT_ID });
  const p = ticket.getPayload();

  const [vinculos] = await db.query(
    'SELECT usuario_id FROM identidades_externas WHERE provedor = ? AND sub = ?',
    ['google', p.sub]
  );
  if (vinculos.length) return { tipo: 'login', usuarioId: vinculos[0].usuario_id };

  if (!p.email_verified) return { tipo: 'recusado', motivo: 'email_nao_verificado' };

  const [existentes] = await db.query('SELECT id FROM usuarios WHERE email = ?', [p.email]);
  if (existentes.length) return { tipo: 'confirmar_vinculo', email: p.email };

  const conn = await db.getConnection();
  try {
    await conn.beginTransaction();
    const [novo] = await conn.query(
      'INSERT INTO usuarios (nome, email) VALUES (?, ?)',
      [p.name || p.email, p.email]
    );
    await conn.query(
      'INSERT INTO identidades_externas (provedor, sub, usuario_id, email_no_login) VALUES (?, ?, ?, ?)',
      ['google', p.sub, novo.insertId, p.email]
    );
    await conn.commit();
    return { tipo: 'cadastro', usuarioId: novo.insertId };
  } catch (erro) {
    await conn.rollback();
    throw erro;
  } finally {
    conn.release();
  }
}

O exemplo usa um pool do mysql2/promise. O resultado confirmar_vinculo leva a uma tela que explica: "Já existe uma conta com este e-mail. Entre com a sua senha para ligar o Google a ela." Só depois desse login a identidade é gravada, apontando para o usuário que acabou de provar a senha.

Escopos: peça só o que o login precisa

Para entrar, bastam openid, email e profile. Agenda, Drive ou contatos são outra conversa: cada escopo a mais aparece na tela de consentimento, assusta e aumenta o que você precisa proteger. Se um recurso futuro precisar da agenda, peça a permissão no momento em que a pessoa usar esse recurso. O post agenda do Google no produto mostra esse caso.

Para login, você não precisa guardar o ID token nem pedir refresh token. Confira o token, crie a sua sessão e descarte o resto. A sessão vive num cookie seu, com HttpOnly e Secure, como em cookie HttpOnly.

Sair de verdade

O logout do seu sistema apaga a sua sessão no servidor e o cookie. Ele não desloga a pessoa do Gmail, e não deve. Mas, se o botão do Google estiver com seleção automática de conta, a pessoa clica em "Sair" e entra de novo sozinha na próxima página. Na biblioteca do Google Identity Services, chame google.accounts.id.disableAutoSelect() no logout para evitar isso.

Ofereça também desvincular o Google, desde que a conta tenha outro jeito de entrar. Se a única forma é o Google, desvincular deixa a conta inacessível.

Erros comuns

  • Usar o e-mail como chave da conta Google. Quem herda o endereço herda a conta.
  • Vincular automaticamente ao e-mail existente. Entrega a conta antiga para quem provou só o e-mail de hoje.
  • Conferir o token apenas no navegador. Qualquer um forja a resposta e entra como quiser.
  • Ignorar email_verified. Abre espaço para cadastrar contas com e-mails alheios.
  • Pedir escopos de agenda e Drive para um simples login. A taxa de recusa sobe e você passa a guardar acesso que não usa.
  • Logout que só limpa a tela. A sessão continua válida no servidor e o próximo usuário do computador entra na conta.

Checklist

  • O ID token é conferido no servidor, com o seu client ID como audiência.
  • A identidade é buscada por provedor e sub.
  • Contas não são criadas com email_verified falso.
  • E-mail já existente exige login com o método atual antes do vínculo.
  • Os escopos pedidos são só openid, email e profile.
  • Nenhum token do Google fica guardado depois do login.
  • O logout apaga a sessão no servidor e desativa a seleção automática.

Entre com uma conta Google, saia e entre com outra conta Google no mesmo navegador. O sistema mostrou a segunda pessoa, ou a primeira sessão continuou viva porque o seu logout não apagou nada no servidor?

Avalie este artigo

Referência: https://mariovalney.com/como-colocar-o-login-do-google-no-meu-site/