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.SIGNATUREExemplo real:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cHeader
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)
| Claim | Nome Completo | Tipo | Descrição |
|---|---|---|---|
iss | Issuer | String | Quem emitiu o token (ex: https://auth.example.com) |
sub | Subject | String | Assunto do token — geralmente o ID único do usuário |
aud | Audience | String/[] | Para quem o token é destinado (ex: api.example.com) |
exp | Expiration Time | NumericDate | Data/hora de expiração (Unix timestamp) |
nbf | Not Before | NumericDate | Token não válido antes desta data |
iat | Issued At | NumericDate | Data/hora em que foi emitido |
jti | JWT ID | String | Identificador ú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:
- Verificar a assinatura
exp— não pode estar expiradoiss— deve corresponder ao emissor esperadoaud— deve incluir o identificador da sua aplicaçãonbf— data atual deve ser apósnbf
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/rolesem vez de apenasroles - 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
kidno header
# Gerar par de chaves RSA 2048 bits
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pemES256 / 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.pemEdDSA (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 cryptographyHS256
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 # TypeScriptJavaScript/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 loginImplementaçã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
| Aspecto | JWT (Stateless) | Sessions (Stateful) |
|---|---|---|
| Escalabilidade | Excelente — sem estado no server | Precisa de session store compartilhado (Redis) |
| Revogação imediata | Difícil — requer blacklist | Trivial — delete a sessão |
| Tamanho do payload | Cresce com claims (viaja em todo req) | Apenas session ID no cookie |
| Microsserviços | Ideal — qualquer serviço valida | Todos precisam acessar o session store |
| Logout em todos dispositivos | Requer blacklist ou versionamento | Delete todas as sessões |
| Segurança padrão | Dados visíveis no payload | Dados ficam no server |
| Latência | Zero — validação local | Round-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, sessionStoragehttpOnly Cookie — RECOMENDADO
// 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 PyJWT2. 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ção3. 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: a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a14. 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):
- https://jwt.io — decoder visual com validação
- https://token.dev — alternativa ao jwt.io
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,nbfem 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
jtiem access tokens de alto valor (permite blacklist) - Implementar blacklist no Redis para revogação rápida
- Nunca logar o token completo (pode logar
jtipara 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