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.
Atributos de cookie que importam
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
Securesão descartados em HTTP simples, o que afeta o desenvolvimento local. - SameSite — controla o envio entre sites (cross-site).
Strictnunca envia o cookie cross-site, o que é o mais seguro, mas quebra links externos que, de outra forma, manteriam você logado.Laxo envia em navegações de nível superior, como ao clicar em um link, e é o padrão correto para a maioria dos apps.Noneo envia para todos os lugares e requerSecure. - Path — limita o cookie a um prefixo de URL.
Path=/é o comum. Limitar ao/appreduz 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-Ageexplícito é mais claro. - Prefixo
__Host-— nomear um cookie como__Host-sidforçaSecure,Path=/e a ausência deDomain, 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
}
CSRF: o custo da autenticação via cookie
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=Laxe umMax-Ageexplí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=NonesemSecure, 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-Agemas 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.