Decisão de arquitetura — Sessão no servidor ou JWT: qual modelo de autenticação para qual aplicação

Escrito por

em

A decisão aparece cedo em todo projeto novo e quase nunca é revisitada: o servidor guarda o estado da sessão e devolve um identificador opaco, ou emite um token assinado que carrega as claims e não precisa de consulta posterior. As duas abordagens funcionam. O que muda é o custo de revogar acesso, o comportamento sob múltiplos serviços e a superfície de ataque quando o token vaza.

O erro comum não é escolher errado — é escolher JWT por reflexo, tratar como stateless de verdade e descobrir na primeira demissão de funcionário que não existe botão para invalidar o token dele.

O que cada modelo realmente faz

Na sessão em servidor, o backend cria um registro (em Redis, Postgres ou memória), gera um identificador aleatório sem significado e devolve num cookie. Toda requisição autenticada faz uma leitura desse registro. O estado da verdade fica no servidor; o cliente carrega só a chave.

No modelo JWT, o servidor assina um payload com sub, exp, papéis e o que mais for necessário. A validação é uma verificação de assinatura local — nenhum acesso a banco. O estado da verdade fica no token, congelado no instante da emissão.

Essa última frase é a origem de praticamente todos os problemas de JWT em produção. Um token com validade de 24 horas afirma, por 24 horas, algo que era verdade quando foi emitido. Se o usuário perdeu o papel de admin há dez minutos, o token não sabe.

// sessão: uma leitura por requisição, verdade sempre atual
const session = await store.get(cookie.sid);
if (!session) return unauthorized();

// JWT: verificação local, verdade congelada na emissão
const claims = jwt.verify(bearer, publicKey);
// claims.role pode estar desatualizado desde o login

Revogação é o critério que separa os dois

Com sessão em servidor, revogar é apagar uma linha. Encerrar todas as sessões de um usuário é apagar as linhas com aquele user_id. Deslogar de todos os dispositivos, forçar re-login após troca de senha, cortar acesso de alguém desligado da empresa — tudo é a mesma operação trivial, com efeito imediato.

Com JWT, revogar exige uma denylist consultada a cada requisição. E uma denylist consultada a cada requisição é uma sessão em servidor com passos extras: voltou o acesso ao banco, voltou o ponto único de estado, e ainda sobrou a complexidade de gerenciar chaves de assinatura e rotação.

A saída praticada é o par access token curto mais refresh token. O access vive de 5 a 15 minutos e é validado localmente; o refresh vive dias, fica no servidor e é revogável. Funciona — mas note o que aconteceu: voltou o estado no servidor, só que agora com dois tipos de credencial, duas validades e um fluxo de renovação para depurar. O ganho é que a janela de exposição de um token comprometido cai para minutos.

Se o refresh token está guardado em banco e é consultado a cada renovação, o sistema é stateful. Chamar a arquitetura de stateless nesse ponto só atrapalha quem for debugar depois.

Onde o token fica é mais importante que o formato

Boa parte das discussões entre sessão e JWT confunde formato da credencial com meio de transporte. São eixos independentes: dá para colocar JWT em cookie e dá para mandar identificador de sessão em header.

O que decide a superfície de ataque é o armazenamento no cliente. Token em localStorage é legível por qualquer script que rode na página — uma dependência npm comprometida basta para exfiltrar. Cookie com HttpOnly, Secure e SameSite=Lax não é acessível por JavaScript, ao custo de exigir proteção contra CSRF nas requisições que alteram estado.

Set-Cookie: sid=<opaco>; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1209600

Para aplicação web servida no mesmo domínio da API, cookie HttpOnly é a opção mais segura e a mais simples de acertar. Para app mobile nativo, cookies não ajudam: o token vai no Keychain ou no Keystore e viaja em header Authorization. Esse é um caso em que JWT resolve melhor, não por ser stateless, mas porque o cliente não tem gerenciamento de cookie nem origem para amarrar.

Quando o JWT compensa de verdade

O argumento sólido a favor do token assinado não é performance — é atravessar fronteira de confiança sem chamada de volta.

Em arquitetura com vários serviços, um serviço que recebe uma requisição precisa saber quem é o chamador. Com sessão centralizada, cada serviço consulta o serviço de identidade, ou consulta o Redis compartilhado. Com token assinado, cada serviço valida a assinatura com a chave pública e segue. Some a isso um gateway que já validou na borda e propaga as claims internamente, e o desenho fica limpo.

A economia de latência isolada raramente justifica a escolha: uma leitura em Redis na mesma rede custa algo na casa de um milissegundo, enquanto verificar assinatura RS256 custa dezenas de microssegundos. A diferença só aparece em volume alto ou quando a alternativa é uma chamada HTTP entre serviços, não uma leitura em cache.

O outro caso legítimo é federação: OIDC, login social, SSO corporativo. Aí o formato do token não é escolha do projeto — é o que o provedor emite.

Critério Sessão no servidor JWT
Revogação imediata Nativa, apagar registro Exige denylist ou access curto
Mudança de papel reflete Na requisição seguinte Só na renovação
Custo por requisição Leitura em cache (~1 ms) Verificação local (~dezenas de µs)
Múltiplos serviços Estado compartilhado Validação independente
Ponto único de falha Store de sessão Chave e rotação
Complexidade operacional Baixa Média a alta
Cliente mobile nativo Desconfortável Natural

Os erros que aparecem em revisão de código

Alguns são recorrentes o bastante para virar item de checklist.

Aceitar o algoritmo declarado no header do token. Bibliotecas antigas honravam alg vindo do próprio JWT, o que permite trocar por none ou rebaixar RS256 para HS256 usando a chave pública como segredo HMAC. A validação deve fixar o algoritmo esperado, nunca lê-lo do token.

Guardar dado mutável nas claims. Plano contratado, limite de crédito e permissões finas envelhecem dentro do token. Vale carregar apenas identificador e papéis grosseiros, buscando o resto no serviço dono do dado.

Validade longa por conveniência de UX. Token de 30 dias para não incomodar o usuário transforma qualquer vazamento em acesso de 30 dias. O caminho é access curto com renovação silenciosa, não expiração distante.

Payload inchado. Um JWT com lista completa de permissões vira um header de vários kilobytes repetido em toda requisição, inclusive nas de asset.

Não planejar rotação de chave. Sem kid no header e sem endpoint JWKS, trocar a chave assinadora significa derrubar todo mundo. Isso precisa existir antes do incidente que obriga a rotação.

A recomendação

Para aplicação web monolítica ou com poucos serviços, servida no mesmo domínio da API: sessão no servidor, identificador opaco em cookie HttpOnly, store em Redis com fallback no banco. É menos código, revogação é grátis, e a superfície de ataque é menor. A escala em que isso vira gargalo está muito acima do ponto em que a maioria dos sistemas chega.

Para app mobile, API pública com clientes de terceiros, ou malha de serviços que precisam validar identidade sem consultar um ponto central: JWT com access token de 10 a 15 minutos, refresh token opaco e revogável guardado no servidor, rotação de refresh a cada uso e detecção de reuso para invalidar a família inteira de tokens.

Modelo híbrido — sessão para o painel web, JWT para o app e para integrações — é decisão defensável e comum, desde que a revogação seja centralizada num único lugar. Duas fontes de verdade sobre quem está logado é o começo do próximo incidente.

Se o sistema já está em produção com JWT de validade longa e sem revogação, o próximo passo é encurtar o access token e introduzir o refresh revogável antes de qualquer refatoração maior. É a mudança que reduz mais risco por linha de código alterada.

Comentários

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *