O que significa “assinar” um JWT
Um JWT é composto por header.payload.signature. A assinatura é o mecanismo que permite ao back-end detectar adulteração do header e do payload. Em termos práticos: o servidor que valida o token recalcula a assinatura usando uma chave (segredo compartilhado ou chave pública/privada, dependendo do algoritmo) e compara com a assinatura recebida. Se não bater, o token é inválido.
Importante: a assinatura garante integridade e autenticidade do emissor (quem tinha a chave para assinar). Ela não garante confidencialidade; o payload continua legível (Base64URL) e não deve conter dados sensíveis.
Algoritmos de assinatura: HS256 vs RS256/ES256
HS256 (HMAC com SHA-256)
- Tipo: simétrico (um único segredo assina e valida).
- Prós: simples, rápido, fácil de operar em um único serviço.
- Contras: qualquer serviço que valide precisa conhecer o segredo; se um serviço vazar, um atacante pode assinar tokens válidos.
- Uso típico: monólitos ou arquiteturas onde um único back-end emite e valida, com segredo bem protegido.
RS256 (RSA) e ES256 (ECDSA)
- Tipo: assimétrico (chave privada assina; chave pública valida).
- Prós: serviços que apenas validam podem ter só a chave pública (não conseguem assinar); reduz o impacto de vazamento em serviços consumidores.
- Contras: maior complexidade operacional (distribuição/rotação de chaves públicas, JWKS), custo computacional maior que HMAC (especialmente RSA).
- Uso típico: múltiplos serviços (microserviços), gateways, integrações externas, cenários com separação clara entre emissor (IdP/Auth service) e consumidores.
Como escolher conforme arquitetura
| Cenário | Recomendação | Motivo |
|---|---|---|
| Monólito (um serviço emite e valida) | HS256 (ou RS/ES se já houver necessidade) | Menos complexidade; segredo fica em um único lugar |
| Vários serviços internos validando tokens | RS256 ou ES256 | Consumidores não precisam de chave privada; menor risco de forja |
| Tokens validados por terceiros | RS256/ES256 | Distribui-se chave pública; não se compartilha segredo |
| Alta performance e chaves assimétricas | ES256 | Assinaturas menores e, em geral, boa performance |
Regra prática: se mais de um componente precisa validar e você não quer que todos possam assinar, prefira RS256/ES256.
Validação correta de JWT no back-end (o que checar e por quê)
Validar JWT não é só “verificar assinatura”. Uma validação robusta inclui:
- Assinatura: garante que o token não foi alterado e foi assinado por quem tem a chave correta.
- Algoritmo permitido: aceite apenas algoritmos esperados (ex.: somente
RS256). Isso evita ataques comoalg=nonee confusão de algoritmo. exp(expiration time): rejeite tokens expirados.nbf(not before): rejeite tokens usados antes do horário permitido.iss(issuer): confirme que o emissor é o esperado (ex.: URL do seu serviço de autenticação).aud(audience): confirme que o token foi emitido para a sua API (ou para o seu gateway).clock skew(tolerância de relógio): permita pequena tolerância (ex.: 30–120s) para diferenças de horário entre servidores, sem abrir margem excessiva.
Erros comuns que viram vulnerabilidade
- Aceitar qualquer algoritmo do header: o atacante controla o header; o servidor deve impor o algoritmo.
- Permitir
alg=none: alguns validadores antigos aceitavam tokens sem assinatura. - Confusão HS/RS: em bibliotecas mal configuradas, um token pode ser validado como HMAC usando a chave pública RSA como “segredo”. Mitigação: fixar algoritmo e usar APIs que separam chaves por tipo.
- Não validar
iss/aud: permite que tokens válidos de outro contexto sejam aceitos indevidamente.
Passo a passo prático: pipeline de validação
1) Extrair o token com segurança
- Preferencialmente do header
Authorization: Bearer <token>. - Rejeite formatos inesperados (múltiplos tokens, espaços extras, token vazio).
2) Decodificar header/payload sem confiar neles
Você pode decodificar para ler kid, iss, aud, mas nada disso deve ser aceito sem validação criptográfica e checagens de claims.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
3) Impor algoritmo permitido
Configure explicitamente uma lista de algoritmos aceitos. Ex.: apenas RS256. Não derive algoritmo do token.
4) Selecionar a chave correta (com kid)
Quando há rotação de chaves, o header inclui kid (Key ID). O validador usa o kid para buscar a chave pública (RS/ES) ou o segredo (HS) correspondente.
- Se
kidestiver ausente, você pode: (a) rejeitar, ou (b) tentar uma chave padrão (menos recomendado em ambientes com rotação). - Se
kidfor desconhecido, rejeite e/ou tente atualizar o cache de chaves públicas (JWKS) e revalidar.
5) Verificar assinatura
Use biblioteca madura e APIs de verificação (não implemente criptografia “na mão”). A assinatura deve ser verificada antes de confiar no conteúdo do token.
6) Validar claims temporais e contexto
exp: agora <= exp (considerando clock skew).nbf: agora >= nbf (considerando clock skew).iss: igual ao emissor esperado.aud: contém o público esperado (string ou array).
7) Construir o “principal”/identidade para a requisição
Após validação, extraia apenas os campos necessários (ex.: sub, scope, roles) e anexe ao contexto da requisição. Evite repassar o token inteiro para camadas internas sem necessidade.
Rotação de chaves, kid e JWKS
Por que rotacionar
- Reduz impacto de vazamento de chave.
- Permite trocar chaves por política (ex.: a cada 30/60/90 dias).
- Facilita resposta a incidentes (revogar chave comprometida).
Estratégia prática de rotação (assimétrico)
- Manter duas chaves ativas por um período: uma “atual” para assinar novos tokens e uma “anterior” para validar tokens ainda válidos.
- Publicar chaves públicas via JWKS: endpoint com lista de chaves públicas e seus
kid. - Cache no consumidor: cache do JWKS com TTL curto (ex.: 5–15 min) e atualização sob demanda quando
kidnão for encontrado. - Desativar chave antiga: após expirar a janela máxima de validade dos tokens emitidos com ela.
Rotação em HS256
Em HMAC, rotação exige distribuir o novo segredo para todos os validadores. Também é comum manter “segredo atual” e “segredo anterior” para validação por um período. O risco operacional é maior em ambientes com muitos serviços.
Armazenamento seguro de segredos e chaves privadas
- Não versionar em repositório: nem em branches privados.
- Preferir um cofre de segredos: com controle de acesso, auditoria e rotação (ex.: HSM/KMS/Secret Manager).
- Princípio do menor privilégio: apenas o serviço emissor precisa da chave privada (RS/ES) ou do segredo (HS).
- Separar ambientes: chaves diferentes para dev/staging/prod.
- Proteção em memória e logs: nunca logar token bruto, segredo, chave privada; cuidado com dumps e traces.
- Backups e acesso: backups criptografados e acesso restrito; rotação deve considerar cópias antigas.
Checklist de validação de JWT (para colar no PR)
- [ ] Extrai token apenas de fonte esperada (ex.: Bearer) e valida formato.
- [ ] Impõe lista de algoritmos permitidos (não aceita o algoritmo do token como verdade).
- [ ] Rejeita explicitamente
alg=nonee tokens sem assinatura. - [ ] Seleciona chave por
kid(com cache/atualização segura) e rejeitakiddesconhecido. - [ ] Verifica assinatura com biblioteca confiável.
- [ ] Valida
expenbfcom clock skew definido. - [ ] Valida
iss(emissor esperado). - [ ] Valida
aud(público esperado). - [ ] Define comportamento para tokens sem claims obrigatórias (rejeitar).
- [ ] Não loga token bruto nem dados sensíveis do payload.
- [ ] Estratégia de rotação de chaves definida (inclui janela de convivência e desativação).
Exemplos de middleware/guard de verificação
Exemplo 1: Middleware em Node.js (Express) com RS256 e JWKS
// npm i express jose node-fetch (ou fetch nativo em Node 18+)
import express from 'express';
import { jwtVerify, createRemoteJWKSet } from 'jose';
const app = express();
const ISSUER = 'https://auth.sua-empresa.com';
const AUDIENCE = 'api://pedidos';
const JWKS_URL = new URL('https://auth.sua-empresa.com/.well-known/jwks.json');
// Busca e faz cache das chaves públicas automaticamente
const JWKS = createRemoteJWKSet(JWKS_URL);
function authGuard() {
return async (req, res, next) => {
try {
const auth = req.headers.authorization || '';
const [type, token] = auth.split(' ');
if (type !== 'Bearer' || !token) {
return res.status(401).json({ error: 'missing_token' });
}
// Impõe algoritmo e valida claims críticas
const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
issuer: ISSUER,
audience: AUDIENCE,
algorithms: ['RS256'],
clockTolerance: '60s' // clock skew
});
// Opcional: validar presença de claims essenciais
if (!payload.sub) {
return res.status(401).json({ error: 'invalid_token' });
}
// Anexa identidade ao request
req.user = {
sub: payload.sub,
scope: payload.scope,
roles: payload.roles,
kid: protectedHeader.kid
};
return next();
} catch (err) {
return res.status(401).json({ error: 'invalid_or_expired_token' });
}
};
}
app.get('/pedidos', authGuard(), (req, res) => {
res.json({ ok: true, user: req.user });
});
app.listen(3000);Exemplo 2: Middleware em Node.js com HS256 (monólito) e segredos rotacionados
// npm i express jose
import express from 'express';
import { jwtVerify } from 'jose';
const app = express();
const ISSUER = 'https://api.sua-empresa.com';
const AUDIENCE = 'api://monolito';
// Exemplo: segredo atual e anterior (rotação)
const secrets = [
new TextEncoder().encode(process.env.JWT_SECRET_CURRENT),
new TextEncoder().encode(process.env.JWT_SECRET_PREVIOUS)
].filter(Boolean);
async function verifyWithAnySecret(token) {
let lastErr;
for (const secret of secrets) {
try {
return await jwtVerify(token, secret, {
issuer: ISSUER,
audience: AUDIENCE,
algorithms: ['HS256'],
clockTolerance: '60s'
});
} catch (e) {
lastErr = e;
}
}
throw lastErr;
}
function authGuard() {
return async (req, res, next) => {
try {
const auth = req.headers.authorization || '';
const [type, token] = auth.split(' ');
if (type !== 'Bearer' || !token) {
return res.status(401).json({ error: 'missing_token' });
}
const { payload } = await verifyWithAnySecret(token);
if (!payload.sub) return res.status(401).json({ error: 'invalid_token' });
req.user = { sub: payload.sub, scope: payload.scope, roles: payload.roles };
next();
} catch {
res.status(401).json({ error: 'invalid_or_expired_token' });
}
};
}
app.get('/conta', authGuard(), (req, res) => res.json({ user: req.user }));
app.listen(3000);Exemplo 3: Guard em Java (Spring Security) com validação de issuer/audience
// build.gradle: implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
// application.yml (exemplo)
// spring.security:
// security:
// oauth2:
// resourceserver:
// jwt:
// issuer-uri: https://auth.sua-empresa.com
// Configuração para validar audience também
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.oauth2.jwt.*;
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.decoder(jwtDecoder())));
return http.build();
}
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder decoder = (NimbusJwtDecoder)
JwtDecoders.fromIssuerLocation("https://auth.sua-empresa.com");
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(
"https://auth.sua-empresa.com"
);
OAuth2TokenValidator<Jwt> withAudience = token -> {
return token.getAudience().contains("api://pedidos")
? OAuth2TokenValidatorResult.success()
: OAuth2TokenValidatorResult.failure(new OAuth2Error("invalid_token", "Invalid audience", null));
};
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(withIssuer, withAudience));
return decoder;
}
}Notas operacionais importantes
Clock skew: quanto usar
Use o mínimo necessário para evitar falsos negativos por diferença de relógio. Valores comuns: 30s a 120s. Se você precisar de mais do que isso, trate como problema de sincronização (NTP) e não como “configuração normal”.
Falhas e respostas
- 401 Unauthorized para token ausente/inválido/expirado.
- Evite mensagens detalhadas que ajudem um atacante (ex.: “kid não encontrado”); registre detalhes apenas em logs internos.