Segurança e identidade
Protegendo APIs REST com JWT: verificação, claims e revogação
Avançado9 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
Um JSON Web Token vale exatamente o que vale a verificação aplicada a ele, e as vulnerabilidades de JWT mais conhecidas são todas falhas dessa verificação, não do formato. Este guia aborda a estrutura JWS e JWE, a allow-list de algoritmos e a rotação de chaves com JWKS, os claims que precisam ser checados, o problema da revogação em designs stateless, onde o navegador deve guardar um token e os controles de que uma API bem protegida ainda precisa na borda.
O que você vai aprender
- Explicar a estrutura JWS e JWE e o que uma assinatura prova e o que não prova
- Verificar um token com um algoritmo em allow-list e uma chave escolhida no JWKS pelo kid
- Validar os claims iss, aud, exp, nbf e scope e rejeitar tudo o que estiver faltando
- Comparar tokens stateless com tokens opacos e projetar uma estratégia de revogação viável
- Decidir onde um cliente de navegador deve guardar um token e por quanto tempo ele deve valer
- Combinar rate limiting, validação de entrada e autorização no nível do recurso na borda da API
A segurança de APIs baseada em tokens tem um modo de falha confortável: tudo funciona. Um token malformado é rejeitado pelo parser, um válido deixa a requisição passar e a integração é dada como concluída. O que isso não põe à prova é justamente o caso para o qual o design existe: um atacante apresentando um token que ele mesmo forjou. Quase toda vulnerabilidade de JWT publicada é um atalho na verificação que só aparece nesse teste: um algoritmo não fixado, uma chave escolhida a partir do próprio header do token, uma audiência nunca comparada, uma expiração lida mas não aplicada.
Este guia trata de fazer direito o lado da verificação e das questões de design que decorrem da escolha de uma credencial stateless: como revogar, quanto tempo um token deve durar, onde o navegador deve guardá-lo e o que mais a borda da API ainda precisa garantir. Como os tokens são obtidos está em OAuth 2.0, e o ID token, específico de identidade, em OpenID Connect; aqui o token já chegou em um header Authorization e a pergunta é o que fazer com ele.
Do que um JWT é feito
Um JWT é um conjunto de claims em JSON, serializado de forma compacta. Na forma JWS, a mais comum, ele tem três segmentos base64url separados por pontos: header, payload e assinatura.
eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6InNpZy0yMDI2LTAzIn0
.eyJpc3MiOiJodHRwczovL3Nzby5leGFtcGxlLmNvbSIsInN1YiI6InUtODgyMzEiLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSJ9
.QVhNc2lnbmF0dXJlYnl0ZXNnb2hlcmU
{ "alg": "RS256", "typ": "at+jwt", "kid": "sig-2026-03" }
{
"iss": "https://sso.example.com",
"sub": "u-88231",
"aud": "https://api.example.com",
"exp": 1789012645,
"nbf": 1789012340,
"iat": 1789012345,
"jti": "3f7c1e02-77aa-4f9c-9d31-2b6c0a5e77b1",
"scope": "orders.read orders.write",
"client_id": "web-portal"
}
Base64url não é uma medida de segurança: um decodificador de JWT rodando no navegador, ou até um simples decodificador base64, lê o payload na hora. Parta do princípio de que o cliente, e qualquer pessoa que obtenha o token, consegue ler todos os claims, e nunca coloque nele identificadores internos, dados pessoais ou pistas de autorização que você não mostraria na tela. typ: at+jwt marca o token explicitamente como um access token OAuth, o que é uma defesa contra tokens destinados a um propósito serem aceitos para outro.
JWE é a alternativa criptografada: cinco segmentos, com o payload criptografado para a chave do destinatário. Isso exige gerenciamento de chaves no lado receptor e só compensa quando a confidencialidade dos claims realmente importa; um meio-termo comum é um access token opaco e pequeno na borda e um JWT assinado dentro da fronteira de confiança.
Verificando a assinatura
A assinatura prova que alguém com a chave de assinatura produziu exatamente essa sequência de bytes e que ela não foi alterada. Ela não prova nada sobre se o token era destinado a você, se ainda é válido ou se o usuário ainda existe. Acertar a etapa da assinatura exige três decisões específicas.
Coloque o algoritmo em allow-list. Decida no servidor quais algoritmos você aceita e rejeite todo o resto, sem pedir permissão ao header alg do token. Dois ataques clássicos dependem de confiar nesse campo: alg: none, em que uma biblioteca trata uma assinatura vazia como válida; e a confusão de algoritmo, em que um serviço configurado para RS256 recebe um token assinado com HS256 usando a chave pública RSA como segredo HMAC — afinal, a chave pública é pública. Os dois desaparecem quando o verificador é construído com um algoritmo fixo.
Selecione a chave pelo kid em um conjunto de chaves confiável. Nunca busque uma chave em uma URL informada no próprio header do token (jku) e nunca confie em uma chave embutida (jwk). Obtenha o JWKS do emissor a partir do documento de discovery, coloque-o em cache e atualize quando aparecer um kid desconhecido — com rate limit, para que um atacante não consiga forçar uma busca por requisição.
{
"keys": [
{ "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "sig-2026-03",
"n": "wV5f8Q9c2u...", "e": "AQAB" },
{ "kty": "EC", "use": "sig", "alg": "ES256", "kid": "sig-2026-09",
"crv": "P-256", "x": "f83OJ3D2xF...", "y": "x_FEzRu9m36..." }
]
}
Prefira assinatura assimétrica. Com RS256 ou ES256, o resource server tem apenas a chave pública, então consegue verificar, mas não emitir. HS256 compartilha um único segredo entre o emissor e todos os verificadores, o que significa que qualquer verificador pode forjar tokens para todo o ambiente.
@Bean
JwtDecoder jwtDecoder(OAuth2ResourceServerProperties props) {
NimbusJwtDecoder decoder = NimbusJwtDecoder
.withJwkSetUri(props.getJwt().getJwkSetUri())
.jwsAlgorithms(algs -> { algs.add(SignatureAlgorithm.RS256); algs.add(SignatureAlgorithm.ES256); })
.build();
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(Duration.ofSeconds(30)), // exp e nbf, clock skew limitado
new JwtIssuerValidator("https://sso.example.com"),
new JwtClaimValidator<List<String>>("aud",
aud -> aud != null && aud.contains("https://api.example.com")),
new JwtClaimValidator<String>("typ", "at+jwt"::equals)));
return decoder;
}
Como ligar esse decoder a uma filter chain é assunto de fundamentos do Spring Security; o ponto aqui é que a lista de algoritmos, o emissor, a audiência e o clock skew são todos explícitos, e não deixados no padrão.
Validando os claims
Uma assinatura verificada é o começo do checklist, não o fim.
| Claim | Regra |
|---|---|
iss | Correspondência exata com o emissor esperado; nada de comparar por prefixo ou sufixo |
aud | Deve conter o identificador desta API — é isso que barra um token emitido para outro serviço |
exp | Rejeite tokens expirados; permita no máximo 30 a 60 segundos de clock skew |
nbf | Rejeite tokens que ainda não são válidos, com a mesma tolerância |
iat | Útil para rejeitar tokens implausivelmente antigos e para políticas de recência |
sub | O principal; combine com iss antes de usá-lo como chave local |
scope | Aplique para o endpoint que está sendo chamado |
jti | Necessário se você implementar uso único ou uma deny list |
Rejeite claims ausentes em vez de assumir um valor padrão. Um validador que trata a ausência de exp como "sem expiração" transforma um único token vazado em uma credencial permanente. Faça todas as checagens antes que qualquer lógica de negócio toque a requisição e retorne 401 com um header WWW-Authenticate: Bearer error="invalid_token", como a RFC 6750 especifica, sendo vago sobre o motivo para quem chama e preciso nos seus logs.
Tokens stateless e o problema da revogação
A propriedade que torna atraentes os tokens autocontidos — qualquer serviço consegue validar um sem consultar o emissor — é exatamente a que torna a revogação difícil. Até expirar, um token válido continua válido, mesmo que a conta tenha sido desativada há um minuto. Há quatro respostas viáveis, geralmente combinadas:
- Tempos de vida curtos. De cinco a quinze minutos para um access token limitam a exposição e são a mitigação mais barata. Combine com a rotação de refresh tokens para manter a experiência do usuário fluida.
- Uma deny list. Guarde o
jtidos tokens revogados até oexpdeles e verifique a cada requisição. A lista é pequena e limitada, já que as entradas expiram sozinhas, mas reintroduz uma consulta compartilhada. - Um claim de versão do token. Mantenha um inteiro por usuário, inclua-o nos tokens emitidos e compare com um valor em cache. Incrementá-lo invalida, com uma única escrita, tudo o que foi emitido para aquele usuário — ideal para "sair de todos os dispositivos" e para trocas de senha.
- Introspecção. Consulte o authorization server a cada chamada, opcionalmente com cache de alguns segundos. É a opção mais precisa, ao custo de uma dependência no caminho crítico; um API gateway é um lugar sensato para fazer isso uma única vez para vários serviços.
Operações sensíveis merecem tratamento mais rigoroso de qualquer forma: reautenticar, checar auth_time ou exigir um token com prova de posse antes de uma transferência ou de uma alteração de permissões.
Bearer tokens, armazenamento e transporte
Um bearer token é um título ao portador: quem o tem pode usá-lo. Essa única frase dita a maior parte das regras operacionais. Envie-o apenas sobre TLS 1.3 (ou 1.2 com cifras modernas), apenas no header Authorization — nunca em uma query string, onde ele vai parar em logs de acesso, caches de proxy e headers Referer — e nunca o registre em log, nem mesmo truncado em um build de debug.
Para clientes de navegador, a questão do armazenamento não tem resposta gratuita:
| Local | Exposição | Observações |
|---|---|---|
localStorage | Qualquer script da página consegue ler | Uma falha de XSS equivale ao roubo completo do token; persiste entre abas e reinícios do navegador |
| Variável em memória | Scripts da página, enquanto ela estiver carregada | Perdido ao recarregar; melhor que persistir, mas ainda alcançável por XSS |
Cookie HttpOnly | Não pode ser lido por script | Enviado automaticamente, então defesas contra CSRF são obrigatórias |
| Backend-for-frontend | Fora do navegador | Os tokens ficam no servidor; o navegador guarda um cookie de sessão |
O padrão backend-for-frontend — um pequeno componente no servidor que faz a troca do código, guarda os tokens e faz proxy das chamadas à API — é a recomendação para novas aplicações de navegador. Quando o token precisa viver no navegador, mantenha-o em memória, com vida curta, e trate cross-site scripting como severidade crítica, porque, com um token na página, é isso que ele é.
Sender-constrained tokens eliminam de vez o problema do portador. No DPoP (RFC 9449), o cliente gera um par de chaves e envia um header de prova assinado junto com cada requisição; o token carrega uma impressão digital da chave pública, de modo que um token roubado sem a chave privada é inútil. O vínculo a um certificado mutual-TLS (RFC 8705) consegue o mesmo para clientes server-side. Os dois valem o esforço em APIs de alto valor.
GET /accounts/1042/transactions HTTP/1.1
Host: api.example.com
Authorization: DPoP eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2Iiwiandr...
O que o token não faz
A autenticação diz quem está chamando. Todo o resto continua sendo responsabilidade sua, e é aí que a maioria das brechas em APIs de fato acontece.
Scope não é autorização. Um token com orders.read diz que o cliente pode tentar ler pedidos. Se este sujeito pode ler o pedido 1042 é uma pergunta que só o seu domínio pode responder, e esquecer de fazê-la é broken object-level authorization — a falha grave mais comum em APIs. Nunca derive a propriedade de um identificador no path ou no corpo; derive de sub e verifique.
@GetMapping("/orders/{id}")
@PreAuthorize("hasAuthority('SCOPE_orders.read')")
public OrderView get(@PathVariable long id, @AuthenticationPrincipal Jwt jwt) {
Order order = orders.findById(id).orElseThrow(NotFoundException::new);
if (!order.customerId().equals(jwt.getSubject())) {
throw new NotFoundException(); // 404, não 403 — não confirme que o registro existe
}
return OrderView.of(order);
}
Valide toda entrada. Tipos, intervalos, tamanhos, enumerações e formatos, no servidor, rejeitando em vez de sanitizar sempre que possível. Limite o tamanho do corpo da requisição e o comprimento das coleções para que uma única chamada não consiga esgotar a memória, e imponha limites a qualquer parser que você expuser.
Aplique rate limiting por identidade, não só por endereço. Limites atrelados ao sujeito ou ao identificador do cliente sobrevivem a proxies compartilhados e redes móveis; limites por endpoint protegem operações caras; um teto global protege o serviço. Retorne 429 com Retry-After.
Não vaze informação pelos erros nem pelo formato das respostas. Um stack trace, uma mensagem diferente para "usuário não existe" e "senha incorreta" ou um 403 onde um 404 revelaria menos: tudo isso entrega informação a um atacante. As escolhas de status code que decorrem disso são discutidas junto com o design de APIs REST em geral.
Restrinja o CORS deliberadamente. Refletir o header Origin e permitir credenciais equivale a não ter política nenhuma; liste as origens que você realmente atende.
Registre as decisões, não os tokens. Grave sujeito, cliente, endpoint, resultado e identificador de correlação. É essa trilha que torna um incidente reconstituível, e ela não custa nada no momento da requisição.
Principais conclusões
- Um JWT assinado pode ser lido por qualquer pessoa que o tenha; a assinatura garante integridade e origem, não confidencialidade.
- Fixe no código os algoritmos aceitos e selecione as chaves pelo
kidem um JWKS em cache — nunca a partir dealg,jkuoujwkdo token. - Prefira assinaturas assimétricas para que um resource server consiga verificar sem poder emitir tokens.
- Valide
iss,aud,expenbfexplicitamente e rejeite claims ausentes em vez de assumir valores padrão. - Tokens stateless não podem ser tirados de circulação: use tempos de vida curtos mais uma deny list, um claim de versão do token ou introspecção quando a revogação imediata importar.
- Mantenha os tokens do navegador fora do armazenamento persistente — prefira um backend-for-frontend — e considere DPoP ou vínculo mTLS para APIs de alto valor.
- Scopes nunca substituem autorização no nível do recurso, validação de entrada, rate limiting e respostas de erro cuidadosas.
Fontes e leituras complementares
Perguntas frequentes
- Um JWT é criptografado?
- Um JWT assinado, não. O header e o payload são codificados em base64url, o que é uma codificação e não criptografia, e qualquer pessoa com o token consegue ler todos os claims. A assinatura garante integridade e origem, nada além disso. Se um claim precisa ficar confidencial para o cliente ou para intermediários, deixe-o fora do token ou use JWE, que criptografa o payload.
- Como revogar um JWT antes que ele expire?
- Não dá para revogar o token em si, porque a verificação é offline por design. As abordagens práticas são tempos de vida curtos para reduzir a janela, uma deny list de identificadores de token consultada a cada requisição, uma versão de token por usuário comparada com um claim ou introspecção junto ao emissor. A maioria dos sistemas combina access tokens curtos com a revogação do refresh token.
- O token deve ficar no localStorage ou em um cookie?
- Nenhuma das duas opções é correta incondicionalmente. O localStorage pode ser lido por qualquer script, então uma única falha de cross-site scripting basta para exfiltrar o token. Um cookie pode ser HttpOnly e invisível para scripts, mas é enviado automaticamente, o que traz de volta o cross-site request forgery, a menos que você configure SameSite e use um token anti-CSRF. A opção mais robusta é um backend-for-frontend que guarda os tokens no servidor e entrega ao navegador um cookie de sessão.
- Por que o algoritmo simétrico HS256 é arriscado para uma API pública?
- Ele usa um único segredo compartilhado tanto para assinar quanto para verificar, então todo serviço capaz de verificar um token também consegue emitir um. Em um sistema com vários resource servers, isso transforma qualquer serviço comprometido ou descuidado em uma fábrica de tokens. A assinatura assimétrica com RS256 ou ES256 dá aos verificadores apenas a chave pública, de modo que um resource server comprometido não consegue forjar tokens para os demais.