Authentication

Autenticação por Sessão

A autenticação por sessão mantém a verdade no servidor. Um ID de sessão aleatório viaja em um cookie HttpOnly, enquanto a identidade e as permissões do usuário residem em um store que você controla e pode revogar a qualquer momento.

beginner14 min readUpdated 16 de set. de 2026
server.ts
ts
// server.ts
import express from "express";
import session from "express-session";
import { RedisStore } from "connect-redis";

const app = express();

app.use(
  session({
    store: new RedisStore({ client: redis }),
    secret: process.env.SESSION_SECRET!,
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      secure: true,
      sameSite: "lax",
      maxAge: 1000 * 60 * 60 * 24, // 1 day
    },
  })
);

app.post("/login", async (req, res) => {
  const user = await verifyCredentials(req.body);
  if (!user) return res.status(401).json({ error: "invalid_credentials" });

  // Rotate the session id so a fixated cookie cannot be reused.
  req.session.regenerate((err) => {
    if (err) return res.status(500).json({ error: "session_error" });
    req.session.userId = user.id;
    res.json({ ok: true });
  });
});
Transporte
ID de sessão opaco em um cookie
Estado do servidor
Obrigatório (um session store)
Flags de cookie
HttpOnly, Secure, SameSite
Store típico
Redis ou um banco de dados
Tempo de vida comum
30 minutos a 2 semanas
Revogação
Excluir o registro no servidor
Risco de CSRF
Sim, cookies são enviados automaticamente
Especificação
RFC 6265 (cookies)

Por que importa

O que as sessões server-side oferecem

O estado vive no servidor

O navegador detém apenas um identificador opaco. Identidade, roles e permissões permanecem em um store que você controla totalmente, portanto, nada sensível é exposto ao cliente.

Revogue em um instante

Excluir um registro de sessão desconecta o usuário imediatamente em todos os dispositivos que a compartilham, sem precisar esperar que um token expire.

Cookies fazem o transporte

O navegador anexa o cookie automaticamente, e o HttpOnly o mantém fora do alcance do JavaScript, eliminando toda uma classe de roubo de tokens.

O panorama completo

Três partes móveis

Um registro que você possui, um cookie que carrega apenas um ID opaco e um store compartilhado que permite que qualquer servidor encontre o registro.

Registro de sessão

Armazenar

Uma linha ou chave que mapeia um ID aleatório a um usuário, uma expiração e quaisquer dados server-side, como um carrinho ou uma mensagem flash.

Cookie

Transportar

Um cabeçalho Set-Cookie entrega o ID ao navegador, que o retorna em cada requisição correspondente até que ele expire ou seja limpo.

Store compartilhado

Escalar

Mover as sessões da memória do processo para Redis ou um banco de dados permite que cada instância leia a mesma sessão.

Fluxo

Como um login se torna uma sessão

Cada requisição após o login repete a mesma busca, e o logout é simplesmente uma exclusão.

  1. 1

    Verificar credenciais

    O POST /login verifica o e-mail e a senha enviados contra o hash da senha armazenada.

  2. 2

    Criar um registro de sessão

    O servidor gera um ID aleatório e armazena uma sessão contendo o ID do usuário e a expiração em seu session store.

  3. 3

    Enviar Set-Cookie

    A resposta carrega o ID opaco em um cookie HttpOnly, Secure e SameSite.

  4. 4

    Retornar o cookie

    O navegador anexa o cookie automaticamente a cada requisição posterior para o mesmo site.

  5. 5

    Carregar a sessão

    O middleware lê o ID, busca-o no store e anexa o usuário autenticado à requisição.

  6. 6

    Destruir no logout

    O logout exclui o registro do store e limpa o cookie na resposta.

Uma breve historia

Três décadas de cookies

  1. 1994

    Netscape lança cookies

    Um pequeno token persistente permite que um servidor HTTP stateless lembre quem está perguntando.

    94
  2. 1997

    Cookies ganham uma especificação

    A RFC 2109 formaliza o Set-Cookie e os atributos que ainda definem o comportamento hoje.

    97
  3. 2011

    RFC 6265 consolida as regras

    A especificação de cookies é reescrita com base no que os navegadores realmente fazem, e não na proposta original.

    11
  4. 2016

    SameSite chega

    Um novo atributo permite que os servidores excluam cookies de requisições cross-site e da superfície de CSRF que elas criam.

    16
  5. 2020

    Lax torna-se o padrão

    O Chrome trata cookies sem SameSite como Lax, alterando silenciosamente o comportamento de muitos sites.

    20
  6. 2023

    Cookies particionados

    O CHIPS vincula um cookie a um site de nível superior, combatendo o rastreamento de terceiros incorporado.

    23

O guia completo

Autenticação por Sessão: Tudo que voce precisa saber

O que é autenticação por sessão

A autenticação por sessão é a forma mais antiga e ainda a mais comum de manter um usuário logado na web. A ideia é simples: o servidor lembra de você, e o navegador guarda um comprovante.

Quando você faz login, o servidor verifica suas credenciais e cria uma sessão — um registro que ele mesmo armazena. Esse registro pode conter seu user id, a hora de criação, quando expira e qualquer estado, como um carrinho de compras. O servidor então envia ao seu navegador um session id: uma string aleatória longa que identifica aquele registro específico. O navegador armazena o id em um cookie e o envia de volta em cada requisição. Um middleware no servidor lê o id, carrega o registro e agora sabe quem você é.

A propriedade definidora é que o navegador detém um identificador opaco, não a sua identidade. É como um ticket de guarda-volumes, não o casaco em si. Se o id vazar, um invasor pode se passar por você até que você ou o servidor invalidem o registro — mas o id por si só não revela nada, não pode ser decodificado e não pode ser editado para conceder permissões extras. Tudo o que é relevante reside por trás da consulta do servidor.

Isso é o oposto de um token autocontido, como um JWT, onde a credencial carrega claims assinadas e o servidor confia nelas sem precisar de uma ida ao banco de dados. A autenticação por sessão escolhe a consulta em troca de controle. Essa troca é o tema deste guia.

O ciclo de vida da sessão

Toda sessão segue o mesmo arco: criada no login, transportada por um cookie, carregada a cada requisição e destruída no logout.

Criação. Um POST /login bem-sucedido é o único lugar onde uma sessão deve nascer. Gere um ID criptograficamente aleatório — pelo menos 128 bits de um CSPRNG, nunca um contador ou um valor previsível — e armazene um registro indexado por ele. Defina uma expiração. Se o app suportar “lembrar-me”, escolha deliberadamente um tempo de vida maior, e não por acidente.

Transporte. Envie o ID com um header Set-Cookie. A flag HttpOnly mantém o ID longe do JavaScript, Secure restringe-o ao HTTPS e SameSite limita quando ele é anexado a requisições cross-site. Sem essas flags, o ID da sessão fica exposto a XSS e network sniffing.

Busca. Em cada requisição subsequente, o middleware lê o cookie, busca o ID no store e, ou anexa o usuário à requisição, ou a rejeita. Um ID ausente ou expirado significa uma requisição anônima, não um erro, permitindo que as páginas públicas continuem funcionando.

Destruição. O logout deleta o registro do store e limpa o cookie. Deletar o registro é o que realmente encerra a sessão; limpar o cookie é uma cortesia que impede o navegador de enviar um ID morto. Um job de expiração no lado do servidor também deve limpar sessões abandonadas para que o store não cresça indefinidamente.

O ciclo de vida é deliberadamente simples, e isso é uma vantagem. Existe um único lugar que cria sessões, um lugar que as carrega e um lugar que as destrói — fácil de auditar e fácil de testar.

Senhas são a primeira barreira

Uma sessão só pode ser tão confiável quanto o login que a criou, portanto, o armazenamento de senhas merece atenção antes mesmo de o cookie aparecer.

Nunca armazene uma senha. Armazene um hash lento e com salt, produzido por um algoritmo desenvolvido para esse fim, como Argon2id, bcrypt ou scrypt. Eles são deliberadamente custosos, o que transforma um vazamento de banco de dados — que seria um dump instantâneo de credenciais — em um trabalho de cracking longo e caro. Um hash de propósito geral, como o SHA-256, é a ferramenta errada: ele é rápido, que é exatamente o que um atacante deseja.

import argon2 from "argon2";

export async function hashPassword(password: string): Promise<string> {
  return argon2.hash(password, { type: argon2.argon2id });
}

export async function verifyPassword(
  password: string,
  hash: string
): Promise<boolean> {
  try {
    return await argon2.verify(hash, password);
  } catch {
    return false;
  }
}

A verificação deve ser em tempo constante ou, preferencialmente, delegada à própria comparação do algoritmo, para que o tempo de resposta não vaze informações. Mais importante ainda: retorne o mesmo erro para um e-mail desconhecido e para uma senha incorreta. Dizer “usuário não encontrado” entrega ao atacante um oráculo gratuito de enumeração de contas. Uma resposta genérica invalid_credentials, com uma quantidade comparável de processamento realizada em ambos os casos, não revela nada.

Um login bem-sucedido também é o momento de pensar em rate limiting, bloqueio após falhas repetidas e desafios de multi-fator. Tudo isso acontece antes de o registro da sessão ser criado.

O que manter em uma sessão

Um registro de sessão deve ser um ponteiro, não uma fotocópia. A tentação de colocar todo o objeto do usuário nele é forte e, geralmente, é um erro.

Armazene o user id e deixe que a requisição carregue o restante. Se você fizer cache do usuário por performance, defina um tempo de vida curto e invalide-o ao ocorrer mudanças, pois uma cópia obsoleta em uma sessão de longa duração gera bugs difíceis de reproduzir: um administrador que foi rebaixado mantém seu cargo antigo até fazer login novamente.

Coisas razoáveis para manter no lado do servidor incluem o user id, um role ou tenant id (quando for pequeno e mudar raramente), um CSRF token, uma flag de “lembrar-me” e estados curtos de UI, como uma flash message ou um destino de redirecionamento pós-login. Evite armazenar objetos grandes, secrets, tokens de outros serviços ou qualquer coisa que você não gostaria que aparecesse em um dump de debugging.

declare module "express-session" {
  interface SessionData {
    userId: string;
    tenantId: string;
    csrfToken: string;
    flash?: { type: "info" | "error"; message: string };
  }
}

Se o payload da sessão ultrapassar alguns kilobytes, é um sinal de que os dados pertencem ao banco de dados, indexados pelo usuário e carregados sob demanda. Sessões pequenas são mais rápidas de serializar, mais baratas de armazenar e mais fáceis de analisar.

Construindo a stack de middleware

O gerenciamento de sessões é melhor expressado como uma pequena cadeia ordenada: parse do cookie, carregamento da sessão, anexo do usuário e, por fim, a proteção de rotas restritas. Cada peça desempenha uma única função, o que torna todo o conjunto testável.

import cookieParser from "cookie-parser";
import session from "express-session";

app.use(cookieParser());
app.use(
  session({
    name: "__Host-sid",
    secret: process.env.SESSION_SECRET!,
    store,
    resave: false,          // do not rewrite unchanged sessions
    saveUninitialized: false, // do not create a session for anonymous visitors
    cookie: {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "lax",
      path: "/",
      maxAge: 1000 * 60 * 60 * 12,
    },
  })
);
app.use(loadUser());        // attach req.user when a session exists
app.use(csrf());            // issue and verify CSRF tokens
app.use("/api", routes);    // handlers can call requireAuth() themselves

Duas opções costumam causar a maior parte da confusão. resave: false impede que o middleware grave a sessão de volta no store em cada requisição, mesmo quando nada mudou, o que economiza um round trip. saveUninitialized: false impede a criação de uma sessão para cada visitante anônimo, evitando que o store seja preenchido com registros vazios e evitando a definição de um cookie antes que o usuário tenha realizado qualquer ação. Ambas são as configurações que você quase sempre desejará utilizar.

A ordem é fundamental. O cookie parser deve ser executado antes do middleware de sessão, o middleware de sessão antes de qualquer coisa que leia req.session, e o carregador de usuário antes de qualquer rota que verifique req.user. O middleware de guarda pode ser global ou anexado por rota; anexá-lo por rota mantém os endpoints públicos como públicos por padrão.

SameSite em profundidade

SameSite é o atributo que as pessoas mais erram, por isso vale a pena entender o que cada valor realmente permite.

Strict nunca envia o cookie em uma requisição cujo site seja diferente daquele que o definiu. Clicar em um link de um e-mail para o seu app fará com que a requisição chegue sem a sessão, então o usuário pode parecer deslogado nessa primeira navegação, e então logado após um refresh. É a configuração mais segura e ideal para ferramentas de administração de alto risco.

Lax envia o cookie em navegações de nível superior (top-level navigations) usando métodos seguros, mas não em POSTs, imagens ou iframes cross-site. Isso cobre o caso comum de seguir um link e permanecer logado, enquanto bloqueia o clássico post de formulário CSRF. É o padrão sensato para a maioria dos web apps e o padrão do navegador quando o atributo é omitido no Chrome moderno.

None envia o cookie em todas as requisições cross-site e requer Secure. É necessário apenas para fluxos cross-site genuínos, como um checkout incorporado ou um widget servido de uma origem diferente. Cada uso de None amplia sua superfície de CSRF, portanto, adicione tokens em nível de aplicação e confirme se a flag Secure está presente, caso contrário, o navegador rejeitará o cookie sumariamente.

Um ponto sutil: SameSite compara sites, não origens, e a definição de um site é o domínio registrável. app.example.com e api.example.com são o mesmo site, portanto, um subdomínio comprometido não é protegido por SameSite de forma alguma. Essa é outra razão pela qual o prefixo de cookie __Host- e a higiene rigorosa de subdomínios são importantes.

Implementando do zero ou usando uma biblioteca

É tentador implementar sessões manualmente: definir um cookie, manter um Map, fazer a busca. Para fins de aprendizado, isso é excelente. Para produção, geralmente é um erro, pois os detalhes que realmente importam são exatamente aqueles que são fáceis de omitir.

Uma biblioteca madura, como express-session para Node, o framework de sessão do Django ou o session store do Rails, oferece IDs assinados, padrões de cookies seguros, stores plugáveis, regeneração de ID e gerenciamento de expiração. Você ainda deve entender cada um desses mecanismos — é para isso que este guia serve — mas você não deve ser a única pessoa a ter pensado neles.

Se você optar por construir a sua própria, o checklist mínimo é: um ID CSPRNG de pelo menos 128 bits, busca em tempo constante, cookies HttpOnly, Secure e SameSite, rotação de ID no login, expiração no lado do servidor e um fluxo de exclusão no logout. Se qualquer um desses itens estiver faltando, você tem uma vulnerabilidade, não um sistema de sessão.

Testando a autenticação de sessão

A autenticação é crítica para a segurança, por isso merece testes que validem os casos negativos, e não apenas o “caminho feliz” (happy path).

import request from "supertest";
import app from "../app.js";

test("protected route rejects anonymous requests", async () => {
  await request(app).get("/api/me").expect(401);
});

test("login sets an HttpOnly session cookie", async () => {
  const res = await request(app)
    .post("/login")
    .send({ email: "[email protected]", password: "correct-horse" })
    .expect(200);

  const cookie = res.headers["set-cookie"][0];
  expect(cookie).toContain("HttpOnly");
  expect(cookie).toContain("SameSite=Lax");
});

test("logout invalidates the session", async () => {
  const agent = request.agent(app);
  await agent.post("/login").send(credentials).expect(200);
  await agent.get("/api/me").expect(200);
  await agent.post("/logout").expect(204);
  await agent.get("/api/me").expect(401);
});

Usar um agent que preserve cookies permite que um único teste siga o comportamento de um navegador real. Valide que o session id muda após o login, que uma sessão expirada seja rejeitada e que um cookie adulterado seja ignorado. Esses testes capturam regressões que um teste manual passaria despercebido.

O ID da sessão é tão seguro quanto o cookie que o transporta. Estes atributos são a diferença entre um design sólido e um vulnerável.

  • HttpOnly — o cookie fica invisível para document.cookie. Esta é a flag mais importante de todas, pois neutraliza o roubo de sessão via XSS. Configure-a sempre.
  • Secure — o navegador só envia o cookie via HTTPS. Sem isso, um atacante na rede pode ler o ID. Configure-a sempre em produção e esteja ciente de que cookies Secure são descartados em HTTP simples, o que afeta o desenvolvimento local.
  • SameSite — controla o envio entre sites (cross-site). Strict nunca envia o cookie cross-site, o que é o mais seguro, mas quebra links externos que, de outra forma, manteriam você logado. Lax o envia em navegações de nível superior, como ao clicar em um link, e é o padrão correto para a maioria dos apps. None o envia para todos os lugares e requer Secure.
  • Path — limita o cookie a um prefixo de URL. Path=/ é o comum. Limitar ao /app reduz ligeiramente a exposição, mas raramente ajuda o suficiente para justificar comportamentos inesperados.
  • Domain — controla quais hosts recebem o cookie. Omiti-lo mantém o cookie no host exato, que é a escolha mais segura. Definir um domínio pai compartilha a sessão entre subdomínios, ampliando o raio de impacto de um subdomínio comprometido.
  • Max-Age e Expires — quando o cookie deve ser descartado. Um cookie de sessão sem nenhum dos dois dura até que o navegador seja fechado, o que geralmente não é o que os usuários esperam em dispositivos móveis. Um Max-Age explícito é mais claro.
  • Prefixo __Host- — nomear um cookie como __Host-sid força Secure, Path=/ e a ausência de Domain, o que impede que um subdomínio sobrescreva seu cookie de sessão.
HTTP/1.1 200 OK
Set-Cookie: __Host-sid=s%3A9f2c...; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=86400

Escolhendo um session store

O local onde as sessões ficam armazenadas é a decisão que mais afeta a escalabilidade do seu app.

Memória in-process é o padrão em muitos frameworks e funciona bem para um app de instância única ou um protótipo. Os dados desaparecem ao reiniciar e não podem ser compartilhados, portanto, essa opção falha assim que você executa mais de um processo.

Redis é a escolha padrão para produção. É rápido, suporta a expiração de chaves nativamente e é compartilhado por todas as instâncias. Sessões são pequenos registros de chave-valor, que é exatamente o ponto forte do Redis. Um Redis gerenciado é barato e remove a carga operacional.

Um banco de dados relacional funciona bem quando você já utiliza PostgreSQL ou MySQL e deseja que as sessões sobrevivam a uma queda do Redis. Você ganha durabilidade e facilidade de inspeção com SQL, ao custo de uma busca mais lenta, a menos que a tabela seja pequena e indexada por id.

Uma tabela de sessão dedicada é comum em frameworks como Django e Rails. O store é uma tabela com uma chave de sessão, um payload serializado e uma coluna de expiração. Um índice na chave e um job de limpeza periódico são as únicas manutenções necessárias.

CREATE TABLE sessions (
  id         text PRIMARY KEY,
  user_id    bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  data       jsonb NOT NULL DEFAULT '{}'::jsonb,
  expires_at timestamptz NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX sessions_expires_idx ON sessions (expires_at);

A memória não escala

Vale a pena deixar claro por que sessões em memória são um bug em produção, pois a falha é intermitente e confusa.

Imagine duas instâncias da aplicação atrás de um load balancer sem um store compartilhado. Um usuário faz login e cai na instância A, que armazena a sessão em sua própria memória. A próxima requisição é roteada para a instância B, que não possui registro do id, então o usuário aparece como deslogado. Se ele fizer login novamente, poderá ficar alternando entre as duas. O usuário percebe logouts aleatórios; os logs mostram uma sessão que existe em um nó, mas não no outro.

Você pode tentar mascarar isso com sticky sessions, onde o load balancer fixa um cliente a uma única instância. Isso ajuda, mas é frágil: um deploy, um crash ou um evento de autoscaling derruba o nó e todas as sessões nele. Sticky sessions também atrapalham o escalonamento horizontal e tornam os canary releases mais difíceis.

A solução definitiva é um store compartilhado. Quando as sessões residem no Redis ou em um banco de dados, qualquer instância pode atender a qualquer requisição, os deploys tornam-se invisíveis para os usuários logados e você pode adicionar capacidade livremente. Use memória apenas para desenvolvimento local e torne o store configurável para que a produção nunca o utilize acidentalmente.

Fixação e rotação de sessão

Session fixation (fixação de sessão) é um ataque onde o invasor obtém um session id que a vítima irá utilizar, aguarda que a vítima se autentique e, então, assume o controle da sessão agora privilegiada. A forma clássica de entrega é através de um link manipulado, como ?sid=attacker-known-id, em um site que aceita um session id fornecido pelo cliente.

A defesa é simples e obrigatória: regenere o session id sempre que o nível de privilégio for alterado. Isso significa no login, após a redefinição de senha, ao acessar uma visão de administrador e após qualquer autenticação de etapa adicional (step-up authentication). O id de pré-login é descartado e a cópia do invasor torna-se inútil.

req.session.regenerate((err) => {
  if (err) return next(err);
  // A brand new id now identifies this session.
  req.session.userId = user.id;
  res.json({ ok: true });
});

A rotação possui um segundo benefício: ela evita a reutilização do session id ao longo do tempo. Um id que permaneceu válido por meses é um prêmio muito maior do que um que muda a cada login. Alguns frameworks também realizam a rotação com base em um timer, o que limita a janela de tempo em que um id vazado é útil.

Nunca aceite um session id vindo de uma query string, do corpo de uma requisição ou de um header customizado. A única fonte deve ser o cookie definido pelo próprio servidor, e somente após validar que o id existe no store.

Expiração e timeouts de inatividade

Sessões não devem durar para sempre. Dois cronômetros são importantes, e bons sistemas utilizam ambos.

Um idle timeout (timeout de inatividade) expira uma sessão após um período de inatividade, tipicamente de 15 a 60 minutos para aplicações sensíveis. Cada requisição que chega antes do timeout renova a expiração, portanto, um usuário ativo permanece logado, enquanto um laptop abandonado perde o acesso. Esta é a principal defesa contra um cookie roubado armazenado em um dispositivo.

Um absolute lifetime (tempo de vida absoluto) limita a idade total de uma sessão, independentemente da atividade, comumente de 8 a 24 horas, ou menos para apps de alto risco. Isso força a reautenticação periódica e limita por quanto tempo uma sessão comprometida pode ser abusada, mesmo que permaneça ativa.

Armazene a expiração junto com a sessão e a valide em cada busca, não apenas no cookie. O Max-Age de um cookie é apenas uma dica do lado do cliente que um atacante pode ignorar; a expiração no lado do servidor é a regra real. No logout, delete o registro em vez de apenas marcá-lo como expirado, para que o armazenamento não acumule entradas mortas.

const session = await store.get(id);
if (!session || session.expiresAt < Date.now()) {
  await store.delete(id);
  return next(); // anonymous
}

Como os navegadores anexam cookies automaticamente, uma página de outra origem pode fazer com que seu navegador envie uma requisição autenticada sem o seu conhecimento. Isso é o cross-site request forgery: um site malicioso envia um formulário para https://bank.example/transfer e seu cookie de sessão vai junto, fazendo com que a requisição pareça legítima.

O SameSite é a primeira linha de defesa. SameSite=Lax impede que os cookies sejam enviados em requisições POST cross-site, o que bloqueia o ataque clássico. Mas o SameSite sozinho não é uma estratégia completa: navegadores antigos, algumas configurações de subdomínio e fluxos cross-site legítimos ainda precisam de proteção no nível da aplicação.

Dois padrões cobrem o restante:

  • Synchronizer token. O servidor armazena um token aleatório na sessão e o renderiza em formulários ou o expõe em um header de resposta. Requisições inseguras devem repetir o token, e o servidor os compara em tempo constante. Como a página do atacante não consegue ler o token, ela não pode forjar uma requisição válida.
  • Double-submit cookie. O servidor define um valor aleatório em um cookie que não seja HttpOnly e o cliente envia esse mesmo valor em um header. O servidor verifica se os dois coincidem. É stateless e fácil de implementar, mas depende de o atacante não conseguir definir cookies para o seu domínio, portanto, combine-o com o prefixo __Host-.
const sent = req.get("x-csrf-token") ?? "";
const expected = req.session.csrfToken;
const ok =
  sent.length === expected.length &&
  crypto.timingSafeEqual(Buffer.from(sent), Buffer.from(expected));
if (!ok) return res.status(403).json({ error: "csrf_failed" });

Aplique a proteção CSRF a todo método que altere o estado — POST, PUT, PATCH, DELETE — e isente GET, HEAD e OPTIONS, que nunca devem mutar o estado. Verifique também o header Origin em requisições inseguras como um sinal adicional de baixo custo.

Cookies assinados versus criptografados

Um cookie pode carregar dados por si só, e não apenas um id, desde que você o proteja. Dois mecanismos são comuns, e eles resolvem problemas diferentes.

Um cookie assinado anexa um código de autenticação de mensagem com chave para que o servidor possa detectar adulterações. Um usuário não pode alterar role=member para role=admin porque a assinatura não coincidirá. No entanto, a assinatura não esconde nada: o payload está em base64 e pode ser lido por qualquer pessoa que tenha o cookie. A assinatura garante a integridade, não a confidencialidade.

Um cookie criptografado (muitas vezes chamado de sealed cookie) oculta e autentica o payload simultaneamente. O servidor pode armazenar uma pequena quantidade de dados de sessão no próprio cookie, eliminando a necessidade de busca no banco de dados, enquanto mantém as informações opacas para o cliente. Frameworks como cookie-session e os cookies criptografados do Rails adotam essa abordagem.

O trade-off é o mesmo dos JWTs: um cookie autocontido é mais difícil de revogar e aumenta o tamanho da requisição em cada chamada. Um meio-termo útil é o modelo híbrido: mantenha uma sessão no lado do servidor para identidade e permissões, e use um cookie assinado de curta duração para uma flag pequena e não sensível. Independentemente da sua escolha, nunca coloque segredos, roles que você não valide novamente ou objetos grandes em um cookie.

Escalando sessões entre servidores

Uma vez que o store é compartilhado, as preocupações restantes são performance e operações.

Mantenha as sessões pequenas. Uma sessão deve conter identificadores, não objetos completos. Armazenar uma cópia do documento do usuário significa que cada alteração de perfil deve atualizar todas as sessões, e dados obsoletos causam bugs confusos. Armazene userId e carregue o usuário, ou faça um cache breve.

Leia de forma eficiente. Um GET no Redis por requisição leva microssegundos, mas em alto volume ainda é um salto de rede. Muitas stacks fazem o cache da sessão durante o tempo de vida de uma única requisição para que múltiplos middlewares não precisem acessar o store individualmente.

Lide com a queda do store. Decida se uma interrupção no Redis deve deslogar todos os usuários ou falhar de forma fechada (fail closed) para rotas protegidas. Falhar de forma fechada é mais seguro: trate um store inacessível como “sem sessão” para endpoints autenticados e retorne um erro claro em vez de conceder acesso silenciosamente.

Faça a limpeza. Defina um TTL nas chaves do Redis e um DELETE FROM sessions WHERE expires_at < now() periódico para stores em banco de dados. Sem a limpeza, o store cresce indefinidamente e as buscas ficam mais lentas.

Evite sticky sessions quando você tiver um store compartilhado. Elas adicionam complexidade operacional sem trazer benefícios e tornam os deploys de zero-downtime mais difíceis. Se você realmente não puder compartilhar um store, sticky sessions são um paliativo, não um design.

Sessões versus JWTs

Essa comparação surge constantemente, e a resposta honesta é que ambos resolvem problemas semelhantes, mas com trade-offs diferentes.

Preocupação Sessão no servidor JWT
Onde o estado reside Armazenamento no servidor Dentro do token
Revogação Imediata Difícil até a expiração
Consulta por requisição Necessária Nenhuma
Dados sensíveis no cliente Nunca Evite; é legível
Escalabilidade Precisa de armazenamento compartilhado Stateless por design
Risco de CSRF no navegador Sim Apenas se baseado em cookie
Melhor uso Web apps first-party APIs, serviços, mobile

As sessões vencem no controle. Você pode revogar um único dispositivo, listar sessões ativas, forçar o logout em todos os lugares e alterar permissões que entram em vigor na próxima requisição. Os JWTs vencem na característica stateless: qualquer serviço pode verificar um token sem a necessidade de uma consulta compartilhada, o que é atraente para microsserviços e APIs onde o emissor e o verificador são sistemas diferentes.

Uma arquitetura comum e sensata utiliza ambos. O navegador recebe um cookie de sessão HttpOnly que faz referência a uma sessão no servidor. Essa sessão pode conter um JWT de curta duração usado para chamar serviços internos. A experiência do usuário é um login normal, e as chamadas internas permanecem stateless. Para analisar mais a fundo o outro lado, leia o guia de JWT.

Auditoria e observabilidade

Sessões são um limite de segurança, e limites devem ser observáveis. Registre os eventos que importam e nenhum dos segredos.

Registre logins bem-sucedidos e falhas com o id do usuário, uma origem genérica como o IP e o user agent, e um timestamp. Registre a criação, rotação e destruição de sessões. Nunca registre o session id em si, a senha ou o CSRF token; um session id em um arquivo de log é uma credencial em um arquivo de log. Se você precisar correlacionar, registre um hash curto do id em vez disso.

logger.info({
  event: "session.created",
  userId: user.id,
  ip: req.ip,
  userAgent: req.get("user-agent"),
});

Ofereça aos usuários uma maneira de visualizar e revogar suas sessões ativas. Uma página de “sessões” que lista os dispositivos com um botão de sign-out transforma uma suspeita de comprometimento em uma correção de um clique, e é um recurso que os usuários esperam cada vez mais. Para administradores, configure alertas para “viagens impossíveis”, picos de falhas de login ou muitas sessões criadas a partir de um único IP.

Melhores práticas

  • Gere IDs de sessão com um gerador aleatório criptograficamente seguro, nunca utilizando um contador ou um valor fornecido pelo usuário.
  • Configure HttpOnly, Secure, SameSite=Lax e um Max-Age explícito no cookie de sessão; considere o prefixo __Host-.
  • Regere o ID da sessão no login e após cada mudança de privilégio para evitar a fixação de sessão.
  • Use um store compartilhado, como Redis ou um banco de dados, em produção; nunca dependa da memória do processo entre instâncias.
  • Aplique tanto um timeout de inatividade quanto um tempo de vida absoluto no servidor, e não apenas no cookie.
  • Destrua o registro no logout e realize a limpeza de sessões expiradas periodicamente.
  • Adicione proteção CSRF a cada rota que altere o estado e verifique o header Origin.
  • Mantenha os payloads de sessão pequenos; armazene IDs e carregue o restante dos dados.
  • Implemente o “fail closed” (falha fechada) quando o session store estiver inacessível em rotas protegidas.
  • Solicite reautenticação para ações sensíveis, como alterar a senha ou adicionar um método de pagamento.

Erros comuns

  • Armazenar sessões na memória do processo e executar mais de uma instância.
  • Esquecer o HttpOnly, expondo o session id a XSS.
  • Deixar o SameSite=None sem Secure, fazendo com que os navegadores descartem o cookie silenciosamente.
  • Reutilizar o session id após o login em vez de rotacioná-lo.
  • Aceitar um session id via query parameter ou no corpo da requisição.
  • Colocar roles e permissões no cookie e confiar neles sem fazer uma nova verificação.
  • Definir um cookie Max-Age mas nunca expirar o registro no lado do servidor.
  • Retornar 200 com um corpo de erro para uma requisição não autenticada em vez de 401.
  • Ignorar a proteção CSRF porque “o SameSite resolve isso”.
  • Deixar o session store crescer indefinidamente sem um TTL ou um job de limpeza.

Próximos passos

A autenticação por sessão ensina os fundamentos nos quais qualquer outro esquema de autenticação se baseia: uma credencial, uma busca, uma expiração e uma forma de revogação. A comparação natural é com JWT, onde as claims viajam com o cliente e a revogação se torna a parte difícil. Se você precisa de login de terceiros ou acesso delegado, o OAuth 2.0 é o padrão. Para entender o cookie em si — Set-Cookie, SameSite e as regras de cabeçalho — leia o guia de HTTP. E para integrar o middleware em um servidor real, o guia de Node.js cobre o runtime que sustenta tudo isso.

Na pratica

Login, load, logout, defend

Quatro arquivos que juntos formam uma camada de sessão completa.

routes/login.ts
import { Router } from "express";
import { verifyPassword } from "../auth/passwords.js";

const router = Router();

router.post("/login", async (req, res) => {
  const { email, password } = req.body;
  const user = await db.user.findByEmail(email);
  if (!user || !(await verifyPassword(password, user.passwordHash))) {
    return res.status(401).json({ error: "invalid_credentials" });
  }

  // Regenerate to prevent session fixation.
  req.session.regenerate((err) => {
    if (err) return res.status(500).json({ error: "session_error" });
    req.session.userId = user.id;
    req.session.role = user.role;
    res.json({ id: user.id, email: user.email });
  });
});

export default router;

Onde a credencial reside

Um ID de sessão em um cookie HttpOnly não pode ser lido por JavaScript. Um JWT no localStorage é legível por qualquer script na página, então um único XSS torna-se um roubo total de conta.

Preferir
res.cookie("sid", sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: "lax",
  path: "/",
  maxAge: 86_400_000,
});
Evitar
const res = await fetch("/login", { method: "POST", body });
const { token } = await res.json();

// Any injected script can read this.
localStorage.setItem("token", token);

Comportamento cross-site

SameSite=Lax envia o cookie em navegações de nível superior e requisições do mesmo site, o que é ideal para a maioria dos apps. None é necessário apenas quando um fluxo cross-site real exige, e então deve ser combinado com Secure e proteção CSRF.

Preferir
res.cookie("sid", id, {
  httpOnly: true,
  secure: true,
  sameSite: "lax",
});
Evitar
res.cookie("sid", id, {
  httpOnly: true,
  sameSite: "none",
  // missing Secure: the browser will drop it
});

Trade-offs

O estado de sessão server-side vale a pena?

Sessões trocam um pouco de infraestrutura por controle instantâneo. Essa costuma ser a escolha certa para web apps first-party.

Strengths

  • Revogação instantânea

    Excluir um registro de sessão encerra o acesso imediatamente. Não há janela onde uma credencial roubada continue funcionando até expirar.

  • Nada sensível no cliente

    O cookie contém um ID aleatório, não claims ou roles. Um usuário não pode adulterar suas próprias permissões porque o servidor detém a verdade.

  • Natural para navegadores

    Cookies, redirecionamentos e páginas renderizadas no servidor funcionam juntos sem a necessidade de um fluxo de refresh de token. Fazer login é apenas uma requisição.

Trade-offs

  • Você precisa de um store compartilhado

    Memória in-process funciona em um servidor e quebra no momento em que você roda dois. Redis ou um banco de dados é efetivamente obrigatório em produção.

  • CSRF torna-se seu problema

    Como o navegador envia o cookie automaticamente, você deve adicionar tokens CSRF e configurações de SameSite corretas. Autenticação por token em um header não tem esse problema.

  • Cada requisição toca o store

    Uma busca de sessão por requisição adiciona latência e carga. Caching ou cookies de sessão assinados de curta duração podem reduzir isso, ao custo da atualidade dos dados.

Perguntas frequentes

Perguntas frequentes

Keep learning

Related topics from the roadmap.

$ comecar a aprender

Pronto para aprender Session Auth?

Nosso tutorial interativo te guia por Session Auth passo a passo — com quizzes e codigo real que voce pode executar no navegador.