Segurança

JWT

Tudo sobre JSON Web Tokens: estrutura, claims, algoritmos, geração, validação, refresh, segurança e boas práticas

O que é JWT?

JWT (JSON Web Token) é um padrão aberto (RFC 7519) para transmitir informações entre partes de forma compacta e segura como um objeto JSON. As informações são verificáveis e confiáveis porque são assinadas digitalmente (JWS) ou encriptadas (JWE).

Na maioria dos casos, “JWT” na prática significa JWS — o token é assinado, não encriptado. O payload é visível para quem tiver o token (é apenas base64url), mas a assinatura garante que não foi adulterado.


Estrutura

Um JWT é composto por 3 partes separadas por ponto (.):

HEADER.PAYLOAD.SIGNATURE

Exemplo real:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Codificado em base64url do JSON:

{
  "alg": "HS256",   // algoritmo de assinatura
  "typ": "JWT"      // tipo do token (sempre JWT)
}

Outros campos possíveis no header:

  • kid — Key ID: identifica qual chave usar para verificar (útil com múltiplas chaves / rotação)
  • jku — JWK Set URL: URL com as chaves públicas (atenção: vulnerabilidade se não validado)
  • x5u, x5c — certificados X.509

Payload

Codificado em base64url do JSON contendo os claims (afirmações sobre o usuário/entidade):

{
  "sub": "user-uuid-123",
  "name": "Rafael Marques",
  "email": "rafael@example.com",
  "roles": ["admin", "user"],
  "iat": 1700000000,
  "exp": 1700003600
}

Atenção: O payload é apenas encodado, não encriptado. Qualquer um com o token consegue ler o conteúdo. Nunca inclua senhas, cartões de crédito ou dados sensíveis.

Signature

HMACSHA256(
  base64url(header) + "." + base64url(payload),
  secret
)

Para RS256 (assimétrico):

RSA-SHA256(
  base64url(header) + "." + base64url(payload),
  privateKey
)

Claims Padrão (RFC 7519)

ClaimNome CompletoTipoDescrição
issIssuerStringQuem emitiu o token (ex: https://auth.example.com)
subSubjectStringAssunto do token — geralmente o ID único do usuário
audAudienceString/[]Para quem o token é destinado (ex: api.example.com)
expExpiration TimeNumericDateData/hora de expiração (Unix timestamp)
nbfNot BeforeNumericDateToken não válido antes desta data
iatIssued AtNumericDateData/hora em que foi emitido
jtiJWT IDStringIdentificador único do token (para evitar replay attacks)
{
  "iss": "https://auth.meusite.com",
  "sub": "usr_01HXYZ123456",
  "aud": ["https://api.meusite.com", "https://app.meusite.com"],
  "exp": 1700003600,
  "nbf": 1700000000,
  "iat": 1700000000,
  "jti": "tok_01HXYZ789ABC"
}

Validações obrigatórias ao verificar um JWT:

  1. Verificar a assinatura
  2. exp — não pode estar expirado
  3. iss — deve corresponder ao emissor esperado
  4. aud — deve incluir o identificador da sua aplicação
  5. nbf — data atual deve ser após nbf

Claims Customizadas

Além das claims padrão, você pode adicionar qualquer dado relevante ao seu domínio. Boas práticas:

  • Use namespace para evitar colisões: https://meusite.com/roles em vez de apenas roles
  • Mantenha o payload pequeno — ele viaja em todo request
  • Prefira IDs a dados completos (busque detalhes no banco quando necessário)
{
  "sub": "usr_01HXYZ123456",
  "iat": 1700000000,
  "exp": 1700003600,
  "https://meusite.com/roles": ["admin", "editor"],
  "https://meusite.com/tenant": "org_ABC123",
  "https://meusite.com/plan": "pro",
  "https://meusite.com/email_verified": true
}

Algoritmos de Assinatura

HS256 / HS384 / HS512 — HMAC com SHA-2

Tipo: Simétrico (mesma chave para assinar e verificar)

Quando usar:

  • Sistemas onde o emissor e o verificador são o mesmo serviço (monolito, microsserviços internos confiáveis)
  • Simplicidade é importante
  • Não há necessidade de compartilhar a chave com terceiros

Riscos:

  • Se a chave vazar, qualquer um pode criar tokens válidos
  • Não adequado para cenários multi-tenant onde terceiros precisam verificar tokens
# Python — PyJWT com HS256
import jwt

secret = "super-secret-key-min-256-bits-for-hs256"
payload = {"sub": "123", "exp": 1700003600}

token = jwt.encode(payload, secret, algorithm="HS256")
decoded = jwt.decode(token, secret, algorithms=["HS256"])

RS256 / RS384 / RS512 — RSA com SHA-2

Tipo: Assimétrico (chave privada assina, chave pública verifica)

Quando usar:

  • Auth servers (Keycloak, Auth0) que emitem tokens consumidos por múltiplas APIs
  • Precisa publicar a chave pública para verificação (JWKS endpoint)
  • Separação entre serviço que emite e serviço que consome

Vantagens:

  • APIs de terceiros podem verificar sem ter a chave privada
  • Rotação de chaves via kid no header
# Gerar par de chaves RSA 2048 bits
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem

ES256 / ES384 / ES512 — ECDSA com SHA-2

Tipo: Assimétrico (curvas elípticas)

Quando usar:

  • Mesmos casos do RS256, mas com tokens menores e assinaturas mais rápidas
  • Ambientes com restrição de tamanho (mobile, IoT)
  • Curva P-256 (ES256) é amplamente suportada

Comparação de tamanho de assinatura:

  • HS256: 32 bytes
  • RS256: 256 bytes (chave 2048 bits)
  • ES256: 64 bytes
# Gerar par de chaves ECDSA P-256
openssl ecparam -name prime256v1 -genkey -noout -out ec_private.pem
openssl ec -in ec_private.pem -pubout -out ec_public.pem

EdDSA (Ed25519)

Algoritmo moderno baseado em Curve25519. Mais rápido que ECDSA, assinaturas determinísticas (sem risco de nonce fraco). Suporte ainda crescendo nas bibliotecas.


Fluxo de Autenticação Completo

1. Login → Gerar Token

Cliente                     Auth Server
  |                              |
  |-- POST /auth/login -------->|
  |   { email, senha }          |
  |                              |-- valida credenciais
  |                              |-- gera access_token (exp: 15min)
  |                              |-- gera refresh_token (exp: 7 dias)
  |<-- 200 OK -----------------|
  |   { access_token,           |
  |     refresh_token,          |
  |     expires_in: 900 }       |

2. Usar Token nas Requisições

Cliente                       API Resource Server
  |                                    |
  |-- GET /api/profile --------------->|
  |   Authorization: Bearer <token>    |
  |                                    |-- extrai JWT do header
  |                                    |-- verifica assinatura
  |                                    |-- verifica exp, iss, aud
  |                                    |-- extrai claims (sub, roles)
  |<-- 200 OK + { dados } ------------|

3. Refresh do Token

Cliente                     Auth Server
  |                              |
  |-- POST /auth/refresh ------->|
  |   { refresh_token }         |
  |                              |-- valida refresh_token
  |                              |-- verifica se não foi revogado
  |                              |-- gera novo access_token
  |                              |-- (opcional) rotaciona refresh_token
  |<-- 200 OK -----------------|
  |   { access_token,           |
  |     refresh_token (novo) }  |

Geração em Java (JJWT)

Dependência no build.gradle:

implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'

Gerando com HS256

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Map;

public class JwtService {

    // Chave deve ter no mínimo 256 bits para HS256
    private final SecretKey secretKey;

    public JwtService(String base64EncodedSecret) {
        // Decodifica a chave de uma string base64
        byte[] keyBytes = java.util.Base64.getDecoder().decode(base64EncodedSecret);
        this.secretKey = Keys.hmacShaKeyFor(keyBytes);
    }

    public String generateAccessToken(String userId, String email, List<String> roles) {
        Instant now = Instant.now();
        Instant expiration = now.plusSeconds(900); // 15 minutos

        return Jwts.builder()
            // Claims padrão
            .issuer("https://auth.meusite.com")
            .subject(userId)
            .audience().add("https://api.meusite.com").and()
            .issuedAt(Date.from(now))
            .notBefore(Date.from(now))
            .expiration(Date.from(expiration))
            .id(java.util.UUID.randomUUID().toString()) // jti
            // Claims customizadas
            .claim("email", email)
            .claim("roles", roles)
            .claim("type", "access")
            // Assina
            .signWith(secretKey)
            .compact();
    }

    public String generateRefreshToken(String userId) {
        Instant now = Instant.now();
        Instant expiration = now.plusSeconds(604800); // 7 dias

        return Jwts.builder()
            .issuer("https://auth.meusite.com")
            .subject(userId)
            .issuedAt(Date.from(now))
            .expiration(Date.from(expiration))
            .id(java.util.UUID.randomUUID().toString())
            .claim("type", "refresh")
            .signWith(secretKey)
            .compact();
    }
}

Validando com JJWT

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;

public class JwtValidator {

    private final SecretKey secretKey;

    public JwtValidator(SecretKey secretKey) {
        this.secretKey = secretKey;
    }

    public Claims validateAndExtract(String token) {
        try {
            Jws<Claims> jws = Jwts.parser()
                .verifyWith(secretKey)
                .requireIssuer("https://auth.meusite.com")
                .requireAudience("https://api.meusite.com")
                .build()
                .parseSignedClaims(token);

            Claims claims = jws.getPayload();

            // Verificação extra do tipo de token
            String type = claims.get("type", String.class);
            if (!"access".equals(type)) {
                throw new IllegalArgumentException("Token não é do tipo access");
            }

            return claims;

        } catch (io.jsonwebtoken.ExpiredJwtException e) {
            throw new RuntimeException("Token expirado", e);
        } catch (io.jsonwebtoken.security.SignatureException e) {
            throw new RuntimeException("Assinatura inválida", e);
        } catch (JwtException e) {
            throw new RuntimeException("Token inválido: " + e.getMessage(), e);
        }
    }
}

Com RS256 em Java

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;

public class RsaJwtService {

    private final PrivateKey privateKey;
    private final PublicKey publicKey;

    // Em produção: carregue as chaves de um KeyStore ou vault
    public RsaJwtService() throws Exception {
        KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
        gen.initialize(2048);
        KeyPair pair = gen.generateKeyPair();
        this.privateKey = pair.getPrivate();
        this.publicKey = pair.getPublic();
    }

    public String generateToken(String userId) {
        return Jwts.builder()
            .subject(userId)
            .issuedAt(new java.util.Date())
            .expiration(new java.util.Date(System.currentTimeMillis() + 900_000))
            .signWith(privateKey)  // JJWT detecta RS256 automaticamente
            .compact();
    }

    public Claims validate(String token) {
        return Jwts.parser()
            .verifyWith(publicKey)
            .build()
            .parseSignedClaims(token)
            .getPayload();
    }
}

Geração em Python (PyJWT)

pip install PyJWT cryptography

HS256

import jwt
from datetime import datetime, timedelta, timezone
import uuid

SECRET_KEY = "seu-secret-com-no-minimo-256-bits-aqui"
ALGORITHM = "HS256"

def generate_access_token(user_id: str, email: str, roles: list[str]) -> str:
    now = datetime.now(timezone.utc)

    payload = {
        # Claims padrão
        "iss": "https://auth.meusite.com",
        "sub": user_id,
        "aud": ["https://api.meusite.com"],
        "iat": now,
        "exp": now + timedelta(minutes=15),
        "nbf": now,
        "jti": str(uuid.uuid4()),
        # Claims customizadas
        "email": email,
        "roles": roles,
        "type": "access",
    }

    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def generate_refresh_token(user_id: str) -> str:
    now = datetime.now(timezone.utc)

    payload = {
        "iss": "https://auth.meusite.com",
        "sub": user_id,
        "iat": now,
        "exp": now + timedelta(days=7),
        "jti": str(uuid.uuid4()),
        "type": "refresh",
    }

    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def validate_token(token: str) -> dict:
    try:
        payload = jwt.decode(
            token,
            SECRET_KEY,
            algorithms=[ALGORITHM],
            # Valida claims automaticamente
            options={
                "verify_exp": True,
                "verify_iss": True,
                "verify_aud": True,
            },
            issuer="https://auth.meusite.com",
            audience="https://api.meusite.com",
        )

        if payload.get("type") != "access":
            raise ValueError("Token não é do tipo access")

        return payload

    except jwt.ExpiredSignatureError:
        raise ValueError("Token expirado")
    except jwt.InvalidSignatureError:
        raise ValueError("Assinatura inválida")
    except jwt.DecodeError as e:
        raise ValueError(f"Token malformado: {e}")
    except jwt.InvalidTokenError as e:
        raise ValueError(f"Token inválido: {e}")

RS256 em Python

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend

# Carregar chaves de arquivos (em produção: use vault/HSM)
def load_private_key(path: str):
    with open(path, "rb") as f:
        return serialization.load_pem_private_key(
            f.read(), password=None, backend=default_backend()
        )

def load_public_key(path: str):
    with open(path, "rb") as f:
        return serialization.load_pem_public_key(f.read(), backend=default_backend())

private_key = load_private_key("private.pem")
public_key = load_public_key("public.pem")

# Gerar token com RS256
token = jwt.encode(
    {"sub": "123", "exp": datetime.now(timezone.utc) + timedelta(minutes=15)},
    private_key,
    algorithm="RS256"
)

# Verificar token
decoded = jwt.decode(token, public_key, algorithms=["RS256"])

Geração em Node.js (jsonwebtoken)

npm install jsonwebtoken
npm install --save-dev @types/jsonwebtoken  # TypeScript

JavaScript/TypeScript

import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';

const SECRET = process.env.JWT_SECRET!;
const ISSUER = 'https://auth.meusite.com';
const AUDIENCE = 'https://api.meusite.com';

interface TokenPayload {
  userId: string;
  email: string;
  roles: string[];
}

// Gerar access token
export function generateAccessToken(data: TokenPayload): string {
  return jwt.sign(
    {
      // Claims customizadas (sub vai nas options)
      email: data.email,
      roles: data.roles,
      type: 'access',
    },
    SECRET,
    {
      algorithm: 'HS256',
      subject: data.userId,
      issuer: ISSUER,
      audience: AUDIENCE,
      expiresIn: '15m',
      jwtid: uuidv4(),
      notBefore: 0,
    }
  );
}

// Gerar refresh token
export function generateRefreshToken(userId: string): string {
  return jwt.sign(
    { type: 'refresh' },
    SECRET,
    {
      algorithm: 'HS256',
      subject: userId,
      issuer: ISSUER,
      expiresIn: '7d',
      jwtid: uuidv4(),
    }
  );
}

// Validar e decodificar token
export function verifyToken(token: string): jwt.JwtPayload {
  try {
    const decoded = jwt.verify(token, SECRET, {
      algorithms: ['HS256'],
      issuer: ISSUER,
      audience: AUDIENCE,
      complete: false,
    }) as jwt.JwtPayload;

    if (decoded.type !== 'access') {
      throw new Error('Token não é do tipo access');
    }

    return decoded;

  } catch (err) {
    if (err instanceof jwt.TokenExpiredError) {
      throw new Error('Token expirado');
    }
    if (err instanceof jwt.JsonWebTokenError) {
      throw new Error(`Token inválido: ${err.message}`);
    }
    throw err;
  }
}

// Decodificar sem verificar (apenas para debug/logging — NUNCA use para auth!)
export function decodeWithoutVerify(token: string) {
  return jwt.decode(token, { complete: true });
}

Middleware Express

import { Request, Response, NextFunction } from 'express';

export function authMiddleware(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Token ausente' });
  }

  const token = authHeader.substring(7); // Remove "Bearer "

  try {
    const payload = verifyToken(token);
    req.user = {
      id: payload.sub!,
      email: payload.email,
      roles: payload.roles,
    };
    next();
  } catch (err) {
    return res.status(401).json({ error: (err as Error).message });
  }
}

Refresh Tokens — Estratégia e Rotação

Por que usar Refresh Tokens?

  • Access tokens têm vida curta (5-15 min) para limitar dano em caso de vazamento
  • Refresh tokens têm vida longa (dias/semanas) e são usados apenas para obter novos access tokens
  • Refresh tokens ficam armazenados com mais segurança (httpOnly cookie), access tokens podem ficar em memória

Rotação de Refresh Tokens (Refresh Token Rotation)

1. Cliente usa refresh_token_v1 para obter novo access token
2. Auth Server:
   a. Valida refresh_token_v1
   b. Invalida refresh_token_v1 no banco (marca como usado)
   c. Emite access_token_v2 (novo)
   d. Emite refresh_token_v2 (novo)
3. Se alguém tentar reusar refresh_token_v1:
   → Server detecta token já usado
   → REVOGA TODA A FAMÍLIA de tokens (indício de roubo)
   → Força novo login

Implementação em Java (Spring Boot)

@Service
@Transactional
public class RefreshTokenService {

    private final RefreshTokenRepository tokenRepo;
    private final JwtService jwtService;

    // Cria e persiste um refresh token
    public RefreshToken createRefreshToken(String userId) {
        RefreshToken token = new RefreshToken();
        token.setUserId(userId);
        token.setToken(UUID.randomUUID().toString());
        token.setExpiryDate(Instant.now().plusSeconds(604800)); // 7 dias
        token.setUsed(false);
        token.setFamily(UUID.randomUUID().toString()); // para rastrear a família

        return tokenRepo.save(token);
    }

    // Troca refresh token por novo par de tokens
    public TokenPair rotateRefreshToken(String tokenValue) {
        RefreshToken stored = tokenRepo.findByToken(tokenValue)
            .orElseThrow(() -> new InvalidTokenException("Refresh token não encontrado"));

        // Detecta reutilização — possível roubo de token
        if (stored.isUsed()) {
            // Revoga toda a família de tokens
            tokenRepo.revokeFamily(stored.getFamily());
            throw new TokenReusedException("Refresh token já foi usado. Faça login novamente.");
        }

        // Verifica expiração
        if (stored.getExpiryDate().isBefore(Instant.now())) {
            throw new ExpiredTokenException("Refresh token expirado");
        }

        // Marca token atual como usado
        stored.setUsed(true);
        tokenRepo.save(stored);

        // Emite novo par
        String newAccessToken = jwtService.generateAccessToken(stored.getUserId());
        RefreshToken newRefreshToken = createRefreshToken(stored.getUserId());
        // Mantém a mesma família para detectar futuras reutilizações
        newRefreshToken.setFamily(stored.getFamily());
        tokenRepo.save(newRefreshToken);

        return new TokenPair(newAccessToken, newRefreshToken.getToken());
    }
}

Blacklisting e Revogação

Por natureza, JWTs são stateless — uma vez emitidos, são válidos até expirar. Para revogar antes do tempo:

Opção 1: Blacklist com Redis (recomendado para access tokens)

@Service
public class TokenBlacklistService {

    private final StringRedisTemplate redis;

    // Adiciona token à blacklist até sua expiração natural
    public void revokeToken(String jti, Instant expiration) {
        Duration ttl = Duration.between(Instant.now(), expiration);
        if (ttl.isPositive()) {
            // Chave: "blacklist:{jti}", valor: "1"
            redis.opsForValue().set(
                "blacklist:" + jti,
                "1",
                ttl
            );
        }
    }

    public boolean isRevoked(String jti) {
        return Boolean.TRUE.equals(redis.hasKey("blacklist:" + jti));
    }
}

Adicionar verificação no middleware:

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    private final TokenBlacklistService blacklist;
    private final JwtValidator validator;

    @Override
    protected void doFilterInternal(HttpServletRequest request, ...) {
        String token = extractToken(request);
        if (token == null) { filterChain.doFilter(request, response); return; }

        Claims claims = validator.validateAndExtract(token);

        // Verifica blacklist pelo jti
        String jti = claims.getId();
        if (blacklist.isRevoked(jti)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        // Prossegue com autenticação
        SecurityContextHolder.getContext().setAuthentication(
            new JwtAuthToken(claims)
        );
        filterChain.doFilter(request, response);
    }
}

Opção 2: Versão de token no banco (para casos críticos)

-- Adicionar coluna token_version na tabela users
ALTER TABLE users ADD COLUMN token_version INTEGER DEFAULT 0;
// No JWT, inclua a versão atual do usuário
.claim("token_version", user.getTokenVersion())

// Na validação, compare com o banco
Integer tokenVersion = claims.get("token_version", Integer.class);
Integer dbVersion = userRepo.getTokenVersion(userId);
if (!tokenVersion.equals(dbVersion)) {
    throw new InvalidTokenException("Token revogado");
}

// Para revogar TODOS os tokens do usuário (ex: troca de senha):
userRepo.incrementTokenVersion(userId);

JWT vs Sessions — Trade-offs

AspectoJWT (Stateless)Sessions (Stateful)
EscalabilidadeExcelente — sem estado no serverPrecisa de session store compartilhado (Redis)
Revogação imediataDifícil — requer blacklistTrivial — delete a sessão
Tamanho do payloadCresce com claims (viaja em todo req)Apenas session ID no cookie
MicrosserviçosIdeal — qualquer serviço validaTodos precisam acessar o session store
Logout em todos dispositivosRequer blacklist ou versionamentoDelete todas as sessões
Segurança padrãoDados visíveis no payloadDados ficam no server
LatênciaZero — validação localRound-trip ao Redis

Recomendação:

  • Use sessions para aplicações tradicionais web com um único servidor/cluster
  • Use JWT para APIs consumidas por SPAs, mobile apps, ou múltiplos microsserviços
  • Para crítico em segurança (banking, saúde): combine JWT com blacklist ou use sessions

Armazenamento Seguro

localStorage — EVITE para tokens de autenticação

// ❌ RUIM — vulnerável a XSS
localStorage.setItem('access_token', token);

// Qualquer script na página pode ler:
// document.cookie, localStorage, sessionStorage
// Java Spring Boot — definindo cookie seguro
ResponseCookie cookie = ResponseCookie.from("access_token", token)
    .httpOnly(true)      // JavaScript não consegue ler
    .secure(true)        // Apenas HTTPS
    .sameSite("Lax")     // Proteção CSRF
    .path("/")
    .maxAge(Duration.ofMinutes(15))
    .build();

response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
// Node.js Express
res.cookie('access_token', token, {
  httpOnly: true,     // Não acessível via JS
  secure: true,       // Apenas HTTPS
  sameSite: 'lax',    // Proteção CSRF
  maxAge: 15 * 60 * 1000, // 15 minutos em ms
  path: '/',
});

Memória (para SPAs com refresh automático)

// Access token em memória (não persiste no reload, mas é mais seguro que localStorage)
let accessToken: string | null = null;

// Refresh token em httpOnly cookie (gerenciado pelo server)
export function setAccessToken(token: string) {
  accessToken = token;
}

export function getAccessToken(): string | null {
  return accessToken;
}

// Em axios interceptor: anexa automaticamente
axios.interceptors.request.use(config => {
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
});

// Em axios interceptor de resposta: refresh automático em 401
axios.interceptors.response.use(
  res => res,
  async err => {
    if (err.response?.status === 401 && !err.config._retry) {
      err.config._retry = true;
      // Tenta refresh (usa cookie httpOnly automaticamente)
      const { data } = await axios.post('/auth/refresh');
      setAccessToken(data.access_token);
      err.config.headers.Authorization = `Bearer ${data.access_token}`;
      return axios(err.config);
    }
    return Promise.reject(err);
  }
);

Vulnerabilidades Comuns

1. Algorithm Confusion (alg: none)

// ❌ NUNCA aceite o algoritmo do header do token sem validação
// Ataque: trocar "RS256" por "none" → sem assinatura necessária

// Versões antigas de bibliotecas aceitavam:
{ "alg": "none" }  // token sem assinatura é "válido"

// ✅ SEMPRE especifique os algoritmos aceitos explicitamente
jwt.verify(token, key, { algorithms: ['HS256'] });  // Node.js
Jwts.parser().verifyWith(key).build();              // Java JJWT (correto)
jwt.decode(token, key, algorithms=["HS256"])        // Python PyJWT

2. Algorithm Confusion — RS256 para HS256

Ataque:
1. Servidor usa RS256 com chave pública/privada
2. Atacante obtém a chave pública (frequentemente pública mesmo)
3. Atacante assina um token com HS256 usando a chave pública como secret
4. Biblioteca vulnerável aceita porque a "assinatura é válida"

Defesa:
- Sempre fixe o algoritmo esperado na validação
- Não confie no campo "alg" do header para selecionar o método de verificação

3. Secret Fraco

# ❌ Secrets fracos podem ser quebrados por força bruta
secret = "secret"
secret = "password"
secret = "jwt_secret"

# Ferramenta de ataque: hashcat
# hashcat -a 0 -m 16500 token.txt wordlist.txt

# ✅ Secret forte: mínimo 256 bits de entropia
python3 -c "import secrets; print(secrets.token_hex(32))"
# output: a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1

4. Não Verificar Claims Essenciais

# ❌ ERRADO — decodifica sem verificar
payload = jwt.decode(token, options={"verify_signature": False})

# ❌ ERRADO — não verifica exp, iss, aud
payload = jwt.decode(token, secret, algorithms=["HS256"])

# ✅ CORRETO — verificação completa
payload = jwt.decode(
    token,
    secret,
    algorithms=["HS256"],
    options={"verify_exp": True},
    issuer="https://auth.meusite.com",
    audience="https://api.meusite.com",
)

5. JWT em URL / Query String

# ❌ Token visível em logs do servidor, browser history, referrer header
GET /api/data?token=eyJhbGc...

# ✅ Sempre use Authorization header
GET /api/data
Authorization: Bearer eyJhbGc...

Boas Práticas de Expiração

Access Token:
- APIs internas, microsserviços: 5-15 minutos
- APIs públicas: 1 hora (máximo)
- Tokens para scripts/automação: 24 horas com escopo limitado

Refresh Token:
- Sessão web normal: 7-30 dias (renova a cada uso)
- "Remember me": 90 dias
- Mobile apps: 6-12 meses (com rotação obrigatória)

Regras de ouro:
- Quanto mais sensível a operação, menor deve ser o exp do access token
- Refresh tokens NUNCA devem ser usados como access tokens
- Implemente rotação de refresh tokens para detectar roubos
- Refresh tokens de longa duração devem ter revogação explícita (logout)

Inspecionando JWTs

# Decodificar manualmente (base64url decode)
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
# {"alg":"HS256","typ":"JWT"}

# Ferramenta: jwt-cli
npm install -g jwt-cli
jwt decode eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# Python one-liner para debug
python3 -c "
import base64, json, sys
token = sys.argv[1]
parts = token.split('.')
for i, part in enumerate(parts[:2]):
    # Adiciona padding necessário para base64
    padded = part + '=' * (4 - len(part) % 4)
    print(f'Part {i}:', json.loads(base64.urlsafe_b64decode(padded)))
" "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.sig"

Ferramentas online (use apenas com tokens de teste):


Checklist de Segurança

  • Algoritmo fixo na validação (não confiar no header alg)
  • Secret com entropia mínima de 256 bits (HS256) ou par de chaves RSA/EC
  • Verificar exp, iss, aud, nbf em toda validação
  • Access tokens com vida curta (≤ 15 min para APIs sensíveis)
  • Refresh tokens com rotação ativada
  • Armazenar tokens em httpOnly cookie, não localStorage
  • HTTPS obrigatório — tokens em clear text são inúteis sem TLS
  • Incluir jti em access tokens de alto valor (permite blacklist)
  • Implementar blacklist no Redis para revogação rápida
  • Nunca logar o token completo (pode logar jti para rastreabilidade)
  • Nunca enviar token em URL ou query string
  • Não incluir dados sensíveis no payload (CPF, senha, cartão)
  • Testar com tokens expirados, tokens com assinatura inválida, tokens com alg:none