JWT do jeito certo: erros comuns de implementação e como evitar

1. Fundamentos do JWT: o que todo desenvolvedor precisa saber

JSON Web Token (JWT) é um padrão aberto (RFC 7519) que define uma forma compacta e autossuficiente de transmitir informações entre partes como um objeto JSON. Um token JWT é composto por três partes separadas por pontos:

  • Header: contém o tipo do token e o algoritmo de assinatura
  • Payload: contém as claims (declarações) como sub, exp, iss, aud
  • Signature: garante que o token não foi alterado

Exemplo de estrutura:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

O fluxo típico de autenticação envolve: emissão do token pelo servidor após login, transporte via cabeçalho HTTP Authorization: Bearer <token>, e verificação em cada requisição protegida.

É crucial entender a diferença entre JWT, JWS e JWE:
- JWT: o conceito geral
- JWS (JSON Web Signature): JWT assinado, mas não criptografado — o mais comum
- JWE (JSON Web Encryption): JWT criptografado, garantindo confidencialidade

Use JWS para transmitir informações públicas não sensíveis; use JWE quando precisar proteger dados confidenciais no payload.

2. Erro fatal: armazenar segredos e chaves no código-fonte

Um dos erros mais comuns e perigosos é hardcodar segredos de assinatura diretamente no código. Isso expõe a aplicação a ataques graves se o repositório vazar.

Exemplo do que NÃO fazer:

const jwt = require('jsonwebtoken');
const SECRET = 'minha-senha-super-secreta-123'; // NUNCA FAÇA ISSO

Boa prática:

const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET; // Use variáveis de ambiente

Para ambientes mais robustos, utilize gerenciadores de secrets como HashiCorp Vault ou AWS Secrets Manager. Além disso, implemente rotação periódica de chaves.

Quanto ao algoritmo de assinatura:
- HS256 (HMAC com SHA-256): usa uma chave secreta simétrica — mais simples, mas exige que o segredo nunca seja exposto
- RS256 (RSA com SHA-256): usa par de chaves pública/privada — recomendado para sistemas distribuídos, pois a chave pública pode ser compartilhada com segurança

3. Validação de token: o que NÃO pode faltar

Validar um token vai muito além de decodificar o payload. É obrigatório verificar:

  1. Assinatura: garante que o token não foi forjado
  2. exp (expiration): impede uso de tokens vencidos
  3. iss (issuer): confirma que o token veio da fonte esperada
  4. aud (audience): crucial para evitar que tokens de um serviço sejam usados em outro

Exemplo de validação correta:

jwt.verify(token, SECRET, {
  algorithms: ['RS256'],
  issuer: 'https://auth.meusistema.com',
  audience: 'https://api.meusistema.com'
}, (err, decoded) => {
  if (err) return res.status(401).json({ error: 'Token inválido' });
  // continua...
});

Ataques comuns a evitar:
- None algorithm attack: o invasor altera o header para "alg":"none" e remove a assinatura. A biblioteca deve rejeitar tokens sem assinatura.
- Algorithm confusion: se o servidor espera RS256 mas o token usa HS256, o invasor pode usar a chave pública (que é conhecida) para assinar com HS256. Sempre restrinja os algoritmos aceitos.

Implemente também uma lista de revogação (blacklist) para tokens comprometidos, armazenando o jti (JWT ID) ou o hash do token em um cache Redis com TTL igual ao tempo restante do token.

4. Armazenamento inseguro do token no cliente

O local onde o token é armazenado no cliente determina a superfície de ataque:

Armazenamento Risco XSS Risco CSRF Recomendado?
LocalStorage Alto (qualquer script acessa) Baixo Não
sessionStorage Alto Baixo Não
Cookie HttpOnly Baixo (inacessível via JS) Médio Sim, com cuidados

Padrão recomendado:

Set-Cookie: access_token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/api

Para proteção contra CSRF, utilize tokens CSRF ou implemente o padrão SameSite=Strict.

Alternativa moderna: BFF (Backend for Frontend). O frontend nunca recebe o JWT diretamente; o BFF gerencia tokens e mantém sessões HttpOnly, eliminando riscos de XSS no token.

5. Payload inchado e vazamento de dados sensíveis

Nunca coloque dados sensíveis no payload de um JWT comum (JWS). Lembre-se: JWT não é criptografado — qualquer um com o token pode ler o payload usando apenas base64.

Erro comum:

// Payload com dados sensíveis — NUNCA FAÇA
{
  "sub": "123",
  "name": "João",
  "cpf": "123.456.789-00",
  "password_hash": "$2b$10$..."
}

Certo:

{
  "sub": "123",
  "role": "admin",
  "iat": 1700000000,
  "exp": 1700086400
}

Se precisar transmitir dados sensíveis, use JWE (criptografia) ou armazene os dados em sessão no servidor e coloque apenas um identificador no JWT.

Além disso, evite payloads grandes. Tokens muito longos aumentam a latência de rede e podem exceder limites de cabeçalhos HTTP (8 KB em muitos servidores). Mantenha claims essenciais apenas.

6. Renovação e expiração mal planejadas

Tokens com expiração muito longa (dias ou meses) aumentam drasticamente a janela de ataque. Um token roubado com validade de 30 dias é um desastre de segurança.

Boa prática:
- Access token: 15-30 minutos
- Refresh token: 7-30 dias (com rotação)

Implementação de refresh tokens:

// Emissão
const accessToken = jwt.sign({ sub: userId }, ACCESS_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ sub: userId, jti: uuid() }, REFRESH_SECRET, { expiresIn: '7d' });

// Renovação
app.post('/refresh', (req, res) => {
  const { refreshToken } = req.body;
  jwt.verify(refreshToken, REFRESH_SECRET, (err, decoded) => {
    if (err) return res.status(401).json({ error: 'Refresh inválido' });
    // Verificar se o jti não está na blacklist
    const newAccess = jwt.sign({ sub: decoded.sub }, ACCESS_SECRET, { expiresIn: '15m' });
    const newRefresh = jwt.sign({ sub: decoded.sub, jti: uuid() }, REFRESH_SECRET, { expiresIn: '7d' });
    // Invalidar refresh antigo (rotação)
    blacklist.add(decoded.jti);
    res.json({ accessToken: newAccess, refreshToken: newRefresh });
  });
});

Para logout efetivo, invalide os refresh tokens no servidor (mantendo uma blacklist ou versão do token no banco).

7. Tratamento de erros e logging inadequados

Erros de validação de JWT podem revelar informações valiosas para atacantes.

Evite:

// NUNCA faça isso
res.status(401).json({ error: 'Token expirado em 2024-01-01 15:30:00 UTC' });

Faça:

res.status(401).json({ error: 'Token inválido ou expirado' });

No logging interno, registre apenas metadados não sensíveis:

logger.warn('Token inválido', { userId: decoded?.sub, ip: req.ip });

Nunca logue tokens completos — isso pode expor credenciais em sistemas de monitoramento.

8. Testes e segurança contínua

Implemente testes automatizados para cenários críticos:

// Exemplo de teste com Jest
describe('JWT Validation', () => {
  test('rejects token with invalid signature', () => {
    const fakeToken = jwt.sign({ sub: 1 }, 'wrong-secret');
    expect(validateToken(fakeToken)).toBe(false);
  });

  test('rejects expired token', () => {
    const expiredToken = jwt.sign({ sub: 1, exp: Math.floor(Date.now()/1000) - 3600 }, SECRET);
    expect(validateToken(expiredToken)).toBe(false);
  });

  test('rejects token with none algorithm', () => {
    const noneToken = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxIn0.';
    expect(validateToken(noneToken)).toBe(false);
  });
});

Utilize ferramentas de análise estática como GitLeaks ou TruffleHog para detectar segredos hard-coded antes do commit. Mantenha as bibliotecas JWT atualizadas e monitore CVEs nos canais oficiais (NVD, GitHub Security Advisories).

Referências