Privacidade Por Equipe DevBras

Capa do artigo: "Sair de todos os aparelhos" que funciona: revogue a sessão no servidor

"Sair de todos os aparelhos" que funciona: revogue a sessão no servidor

Monte uma tabela de sessões que permite encerrar um aparelho, todos os outros ou tudo ao trocar a senha, e entenda por que token sem lista não revoga.

O notebook ficou no banco do táxi. A dona da conta pega o celular, entra no sistema e clica em "Sair". O celular sai. O notebook, aberto no colo de outra pessoa, continua logado, porque o botão só apagou o cookie do celular. A resposta curta: sessão que se pode revogar mora no servidor, numa tabela ou num store que você controla, e o botão "encerrar outras sessões" apaga ou invalida as linhas dos outros aparelhos. O próximo pedido que o notebook fizer encontra a sessão morta e cai para o login.

Isso também precisa acontecer sozinho quando a pessoa troca a senha ou quando o suporte suspeita de invasão. Se o seu sistema não tem uma lista de sessões ativas por usuário, nenhum desses três cenários tem solução hoje.

Por que apagar o cookie não desloga ninguém além de você

O cookie de sessão é uma cópia do identificador guardada em cada navegador. O logout comum faz duas coisas: manda o navegador apagar o cookie e, nos sistemas bem feitos, apaga a sessão correspondente no servidor. Mas apaga só aquela. Cada aparelho que entrou recebeu um identificador próprio, e os outros continuam válidos.

Com token autocontido, como um JWT de acesso conferido só pela assinatura, a situação é pior. O servidor não guarda nada; ele aceita qualquer token com assinatura válida até a data de expiração. Não existe linha para apagar. Para revogar, você precisa de algum estado no servidor: uma lista de tokens revogados, um contador de versão por usuário ou tokens de vida curta com um refresh token guardado no banco. O post JWT que expira trata dessa troca; aqui o foco é a sessão clássica com cookie.

A tabela de sessões do exemplo

O sistema de exemplo é uma área de cliente em Node.js com MySQL. Cada login cria uma linha. O cookie carrega um valor aleatório; o banco guarda só o hash desse valor, para que um vazamento da tabela não entregue sessões prontas para uso.

CREATE TABLE sessoes (
  id            BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  usuario_id    BIGINT UNSIGNED NOT NULL,
  token_hash    CHAR(64) NOT NULL UNIQUE,
  criada_em     DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  vista_em      DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  expira_em     DATETIME NOT NULL,
  revogada_em   DATETIME NULL,
  aparelho      VARCHAR(120) NOT NULL,
  INDEX (usuario_id)
);

A coluna aparelho guarda um rótulo legível, como "Chrome no Windows", derivado do User-Agent no momento do login. É ele que aparece na tela de sessões ativas. Mostrar um IP cru assusta mais do que ajuda; uma cidade aproximada e a data do último acesso costumam bastar.

Como implementar a revogação passo a passo

  1. No login, crie a linha e o cookie. Gere 32 bytes aleatórios, envie no cookie com HttpOnly, Secure e SameSite, e grave o SHA-256 na tabela. Detalhes das flags em cookie de sessão com HttpOnly, Secure e SameSite.
  2. Em todo pedido autenticado, consulte a tabela. A sessão vale se existe, não foi revogada e não expirou. Atualize vista_em com moderação, por exemplo uma vez a cada poucos minutos, para não escrever no banco a cada clique.
  3. "Sair" revoga a linha atual e apaga o cookie.
  4. "Encerrar outras sessões" revoga todas as linhas do usuário menos a atual.
  5. Troca de senha revoga todas as outras sessões na mesma transação em que grava o novo hash.
  6. Uma tarefa periódica apaga linhas expiradas ou revogadas há algum tempo, para a tabela não crescer sem fim.

O código em Express

const crypto = require('crypto');
const pool = require('./db'); // mysql2/promise

const COOKIE = '__Host-sessao';
const hash = (valor) => crypto.createHash('sha256').update(valor).digest('hex');

async function criarSessao(res, usuarioId, aparelho) {
  const token = crypto.randomBytes(32).toString('base64url');
  await pool.execute(
    'INSERT INTO sessoes (usuario_id, token_hash, expira_em, aparelho) VALUES (?, ?, NOW() + INTERVAL 30 DAY, ?)',
    [usuarioId, hash(token), aparelho]
  );
  res.cookie(COOKIE, token, {
    httpOnly: true, secure: true, sameSite: 'lax', path: '/', maxAge: 30 * 24 * 3600 * 1000
  });
}

async function exigirSessao(req, res, next) {
  try {
    const token = req.cookies[COOKIE];
    if (!token) return res.status(401).json({ erro: 'Entre novamente.' });
    const [linhas] = await pool.execute(
      'SELECT id, usuario_id FROM sessoes WHERE token_hash = ? AND revogada_em IS NULL AND expira_em > NOW()',
      [hash(token)]
    );
    if (!linhas[0]) {
      res.clearCookie(COOKIE, { path: '/', secure: true });
      return res.status(401).json({ erro: 'Entre novamente.' });
    }
    req.sessaoId = linhas[0].id;
    req.usuarioId = linhas[0].usuario_id;
    return next();
  } catch (err) {
    return next(err);
  }
}

async function encerrarOutras(req, res, next) {
  try {
    const [r] = await pool.execute(
      'UPDATE sessoes SET revogada_em = NOW() WHERE usuario_id = ? AND id <> ? AND revogada_em IS NULL',
      [req.usuarioId, req.sessaoId]
    );
    res.json({ encerradas: r.affectedRows });
  } catch (err) {
    next(err);
  }
}

O req.cookies vem do middleware cookie-parser. O prefixo __Host- exige Secure, path igual a / e nenhum domain, o que impede um subdomínio de sobrescrever o cookie.

Troca de senha na mesma transação

async function trocarSenha(usuarioId, sessaoAtualId, novoHash) {
  const conn = await pool.getConnection();
  try {
    await conn.beginTransaction();
    await conn.execute('UPDATE usuarios SET senha_hash = ? WHERE id = ?', [novoHash, usuarioId]);
    await conn.execute(
      'UPDATE sessoes SET revogada_em = NOW() WHERE usuario_id = ? AND id <> ? AND revogada_em IS NULL',
      [usuarioId, sessaoAtualId]
    );
    await conn.commit();
  } catch (err) {
    await conn.rollback();
    throw err;
  } finally {
    conn.release();
  }
}

Se a troca vier de um link de redefinição, sem sessão atual, passe um id que não existe (por exemplo 0) e todas as sessões caem. Faz sentido: quem redefine a senha por e-mail pode estar fazendo isso justamente porque alguém entrou na conta.

A tela de sessões ativas

Liste as linhas não revogadas e não expiradas do usuário, com o rótulo do aparelho, a data de criação e o último acesso. Marque a sessão atual como "este aparelho". Ao lado de cada outra, um botão "encerrar" que revoga aquela linha específica, conferindo que ela pertence ao usuário logado. Sem essa conferência, qualquer pessoa autenticada poderia encerrar a sessão de outra trocando o id na requisição.

Erros comuns

  • Logout que só apaga o cookie. Quem copiou o valor antes continua entrando, porque o servidor nunca soube que a sessão acabou.
  • Guardar o token puro na tabela. Um dump do banco vira acesso imediato a todas as contas logadas.
  • JWT de longa duração sem lista de revogação. O botão "sair de todos" fica impossível de implementar honestamente.
  • Trocar a senha sem derrubar as sessões. O invasor que já estava dentro continua dentro, mesmo depois de a vítima "resolver".
  • Encerrar sessão pelo id sem conferir o dono. Um usuário derruba sessões de outros só trocando um número.
  • Atualizar vista_em a cada requisição. Cada clique vira uma escrita no banco; em pico, a tabela de sessões vira gargalo.

Checklist

  • Toda sessão tem uma linha no servidor com usuário, expiração e campo de revogação.
  • O banco guarda o hash do token, nunca o valor do cookie.
  • Todo pedido autenticado confere revogação e expiração no servidor.
  • "Encerrar outras sessões" revoga todas as linhas do usuário menos a atual.
  • Troca e redefinição de senha revogam as outras sessões na mesma transação.
  • A tela de sessões mostra aparelho e último acesso em linguagem humana.
  • Uma tarefa periódica limpa sessões expiradas e revogadas antigas.

Faça o teste: entre no seu sistema em dois navegadores, encerre as outras sessões em um deles e recarregue o segundo. Ele cai para o login no mesmo instante?

Avalie este artigo

Referência: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html