Assinatura, chaves e validação de JWT no back-end

Capítulo 6

Tempo estimado de leitura: 10 minutos

+ Exercício

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árioRecomendaçãoMotivo
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 tokensRS256 ou ES256Consumidores não precisam de chave privada; menor risco de forja
Tokens validados por terceirosRS256/ES256Distribui-se chave pública; não se compartilha segredo
Alta performance e chaves assimétricasES256Assinaturas 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 como alg=none e 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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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 kid estiver ausente, você pode: (a) rejeitar, ou (b) tentar uma chave padrão (menos recomendado em ambientes com rotação).
  • Se kid for 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 kid nã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=none e tokens sem assinatura.
  • [ ] Seleciona chave por kid (com cache/atualização segura) e rejeita kid desconhecido.
  • [ ] Verifica assinatura com biblioteca confiável.
  • [ ] Valida exp e nbf com 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.

Agora responda o exercício sobre o conteúdo:

Em uma arquitetura com vários serviços internos que precisam validar JWTs, qual abordagem de assinatura tende a reduzir o risco de um serviço consumidor conseguir forjar tokens?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Em ambientes com múltiplos validadores, algoritmos assimétricos (RS256/ES256) permitem distribuir apenas a chave pública para validação. Assim, serviços consumidores não conseguem assinar tokens, reduzindo o risco de forja em caso de vazamento.

Próximo capitúlo

Refresh tokens e expiração segura em autenticação por tokens

Arrow Right Icon
Capa do Ebook gratuito Autenticação e Autorização no Back-end: Sessões, JWT e Boas Práticas
33%

Autenticação e Autorização no Back-end: Sessões, JWT e Boas Práticas

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.