JWT que expira: prazo curto, assinatura conferida e um jeito de revogar
Configure JWT com validade de minutos, assinatura e algoritmo conferidos no servidor e refresh token revogável, para que um token roubado não vire senha permanente.
Um token de acesso vazou num print de suporte. Você troca a senha da conta, desloga a pessoa no app e respira. Duas semanas depois, alguém ainda chama a API com aquele token e recebe 200. O token tinha validade de um ano e o servidor só conferia a assinatura. A resposta curta: JWT de acesso vive minutos, não meses; o servidor confere assinatura, algoritmo, emissor, audiência e prazo em toda chamada; e a renovação passa por um segundo token, guardado no servidor, que você consegue revogar.
O JWT é prático porque a API não precisa guardar sessão. Cada pedido traz o token, a API confere a assinatura e sabe quem é a pessoa. Essa mesma praticidade é o problema: enquanto o token não vence, quem tem o token é a pessoa. Ele é uma credencial, não um detalhe de cabeçalho.
Por que um JWT roubado continua valendo
Um JWT tem três partes separadas por ponto: cabeçalho, conteúdo (payload) e assinatura. As duas primeiras são só JSON codificado em base64url. Não estão criptografadas. Qualquer pessoa que tenha o token lê o que está dentro. A assinatura garante apenas que ninguém alterou o conteúdo sem a chave.
Quando a API recebe o token, ela recalcula a assinatura com a chave que só ela tem e compara. Se bate, e se o campo exp ainda está no futuro, o pedido passa. Repare no que não acontece: a API não consulta banco nenhum. É por isso que trocar a senha ou clicar em sair não mata o token. Nada no servidor lembra que aquele token existe.
Há três consequências diretas. Primeiro, o prazo é a sua principal defesa: um token de 15 minutos roubado dá 15 minutos de estrago, não um ano. Segundo, a conferência precisa ser completa, porque bibliotecas mal configuradas já aceitaram o algoritmo none ou trocaram um algoritmo assimétrico por um simétrico quando o próprio token pedia. Terceiro, se você quer revogar antes do prazo, precisa de algo com estado, e esse estado mora no refresh token.
Como emitir e conferir o JWT do jeito certo
- Gere uma chave forte e guarde fora do código. Para HS256, use pelo menos 32 bytes aleatórios, lidos de variável de ambiente ou cofre de segredos.
- Emita o token de acesso com validade curta, entre 5 e 15 minutos é um bom ponto de partida para app web.
- Inclua no token só o necessário: o identificador da pessoa (
sub), emissor (iss), audiência (aud) e prazo (exp). Nada de senha, CPF ou e-mail se não for indispensável. - Na conferência, fixe a lista de algoritmos aceitos. Não deixe a biblioteca escolher pelo cabeçalho do token.
- Confira emissor e audiência. Um token emitido para outro sistema da empresa não pode servir no seu.
- Para renovar, use um refresh token opaco, aleatório, guardado no banco como hash, com prazo próprio e que muda a cada uso.
- No logout e na troca de senha, revogue os refresh tokens da conta. O acesso atual morre sozinho em minutos.
Exemplo em Node.js com a biblioteca jsonwebtoken:
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const SEGREDO = process.env.JWT_SEGREDO;
const EMISSOR = 'https://api.exemplo.com.br';
const AUDIENCIA = 'app-web';
function emitirAcesso(usuarioId) {
return jwt.sign({ sub: String(usuarioId) }, SEGREDO, {
algorithm: 'HS256',
expiresIn: '15m',
issuer: EMISSOR,
audience: AUDIENCIA,
jwtid: crypto.randomUUID()
});
}
function conferirAcesso(token) {
return jwt.verify(token, SEGREDO, {
algorithms: ['HS256'],
issuer: EMISSOR,
audience: AUDIENCIA,
clockTolerance: 30
});
}
function exigirLogin(req, res, next) {
const [tipo, token] = (req.headers.authorization || '').split(' ');
if (tipo !== 'Bearer' || !token) return res.status(401).end();
try {
req.usuario = conferirAcesso(token);
return next();
} catch (erro) {
return res.status(401).end();
}
}
O clockTolerance: 30 aceita 30 segundos de diferença de relógio entre servidores. Segundos, não horas. Se você precisa de mais que isso, o problema é o NTP, não o token.
O refresh token que dá para revogar
O refresh token não precisa ser JWT. Um valor aleatório de 32 bytes resolve melhor, porque não carrega informação nenhuma e só vale se existir no banco. Guarde o hash SHA-256 dele, nunca o valor puro, junto com o id da pessoa, a data de expiração e um campo de revogado.
CREATE TABLE refresh_token (
hash CHAR(64) PRIMARY KEY,
usuario_id BIGINT NOT NULL,
expira_em TIMESTAMP NOT NULL,
revogado_em TIMESTAMP NULL
);
A cada renovação, o servidor confere se o hash existe, não está revogado e não venceu. Se estiver tudo certo, marca o antigo como revogado, cria um novo e devolve um novo acesso de 15 minutos. Se um refresh já revogado aparecer de novo, alguém copiou o token: revogue todos os refresh tokens daquela conta e peça login.
Onde guardar o token no navegador
Token em localStorage é lido por qualquer script da página. Se houver uma falha de injeção de script, o token vai embora junto. Um cookie com HttpOnly, Secure e SameSite tira o valor do alcance do JavaScript, o que reduz bastante essa superfície. O detalhe dessas flags está em cookie de sessão com HttpOnly. Escolha um modelo, documente e aplique em todo o projeto.
Nunca mande o token na query string. URL vai parar em log do servidor, do proxy, no histórico do navegador e no cabeçalho Referer de outros sites.
Erros comuns
- Token de acesso com validade de dias ou sem
exp. Um vazamento vira acesso permanente, e trocar a senha não resolve. - Conferir sem fixar o algoritmo. A biblioteca pode aceitar o que o token declara, inclusive combinações que permitem forjar a assinatura.
- Usar decode no lugar de verify. Decodificar só lê o conteúdo. Qualquer um monta um token com o
subque quiser. - Colocar dado sensível no payload. O conteúdo é legível por quem tem o token. CPF ou perfil de permissão interna ficam expostos.
- Refresh token sem registro no servidor. Sem lista, não há o que revogar no logout ou na troca de senha.
- Segredo curto ou commitado no repositório. Com a chave, qualquer pessoa emite tokens válidos para qualquer conta.
Checklist
- O token de acesso vence em minutos, e você confirmou isso decodificando um token real.
- A conferência fixa algoritmo, emissor e audiência.
- Nenhuma rota protegida usa decode sem verify.
- O payload não tem senha, documento nem dado que você não publicaria.
- Refresh tokens ficam no banco como hash, com prazo e campo de revogação.
- Logout e troca de senha revogam os refresh tokens da conta.
- A chave de assinatura vem de variável de ambiente ou cofre, não do código.
- Nenhum token aparece em query string ou em log.
Pegue um token emitido hoje pelo seu sistema e leia o campo exp com JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString()) no Node. Se a data está em anos, ou o campo nem existe, o prazo não é política. E a pergunta que importa: se esse token vazasse agora, você conseguiria impedir que ele funcionasse antes de vencer?
Referência: https://balta.io/artigos/aspnet-5-autenticacao-autorizacao-bearer-jwt