← All posts

Toda validação de token batia na nossa origem. Agora o JWKS mora na borda.

Authagonal·July 20, 2026
authoidcjwkscdncloudflarecachingkey-rotation

Para validar um dos nossos JWTs, um serviço busca dois documentos públicos no emissor: o documento de discovery em /.well-known/openid-configuration e o JWKS para o qual ele aponta — o conjunto de chaves públicas com que os tokens são assinados. Essas são as coisas mais requisitadas e menos secretas que servimos. São chaves públicas, que são públicas por definição, e metadados que mudam apenas quando fazemos rotação. E, até pouco tempo atrás, cada uma dessas buscas percorria todo o caminho de volta até a nossa origem. E, como somos multi-tenant, a situação era pior do que essa frase faz parecer.

A origem fazia um trabalho que não tinha razão para fazer

Cada tenant é o seu próprio emissor, com o seu próprio documento .well-known e o seu próprio JWKS. E cada relying party, cada resource server, cada SDK que valida um token puxa esses documentos — num cold start, quando o seu próprio cache curto expira, uma vez por instância de serviço. Multiplique os emissores por tenant pelos validadores por serviço pelos cache misses e você obtém uma mangueira de incêndio de requisições, tudo terminando na aplicação, tudo por documentos que são idênticos byte a byte para qualquer chamador e mudam apenas quando rotacionamos uma chave.

Esse é o perfil de manual de algo que deveria estar em cache: público, idêntico, que muda raramente. Em vez disso, a aplicação renderizava chaves públicas, dinamicamente, no caminho crítico da validação de token de todo mundo. As chaves não tinham nada que fazer sendo calculadas a cada requisição.

Colocando-os na borda

A mudança em si é pequena: definir cabeçalhos Cache-Control honestos nas respostas do discovery e do JWKS para que o Cloudflare as coloque em cache na borda. Agora um validador chega ao point of presence do Cloudflare mais próximo, e a origem serve cada documento mais ou menos uma vez por PoP por TTL, em vez de uma vez por validação. O discovery é trivialmente cacheável — quase nunca muda. O JWKS é aquele sobre o qual você precisa pensar com cuidado, porque o JWKS é o único documento cujo trabalho inteiro é mudar exatamente quando você rotaciona uma chave — e colocar em cache algo que precisa estar fresco no único momento em que ele muda é onde as pessoas se machucam.

Como um JWKS em cache morde você

Rotação de chave mais um conjunto de chaves em cache é uma armadilha, e ela falha na pior direção possível. Rotacione para uma nova chave de assinatura, comece a emitir tokens com ela, e um validador que buscou o JWKS antes de a nova chave aparecer está segurando uma cópia desatualizada. Chega um token carregando o id da nova chave; o validador o procura, não o encontra e rejeita o token. O token é perfeitamente válido. A assinatura é real. O usuário não fez nada de errado. O seu próprio cache de borda acabou de devolver um 401 a um login legítimo.

Repare em que direção isso falha. Uma chave privada desatualizada é um não-evento — você simplesmente ainda não começou a usar a nova. Um conjunto de chaves públicas desatualizado é uma queda autoinfligida, e você introduziu de propósito o cache que a causou, para economizar tráfego de origem. A segurança de todo o esquema vive ou morre na rotação.

O padrão seguro para rotação

A segurança está inteiramente na ordem, e a ordem é o oposto de uma troca atômica. Três regras:

  • Publique antes de assinar. A nova chave entra primeiro no JWKS, e você não assina um único token com ela até que esse JWKS — com o novo key id e tudo — esteja no ar na borda por pelo menos um TTL de cache completo. Quando qualquer token portando a nova chave finalmente puder alcançar um validador, o conjunto de chaves em cache do validador já conterá a chave.
  • Aposente após a expiração, não na rotação. A chave antiga permanece publicada no JWKS até que o último token que ela assinou tenha expirado. Antiga e nova coexistem por toda a sobreposição. Retire a chave antiga no instante em que você rotaciona e você deixa à deriva todos os tokens que ela assinou e que ainda são válidos.
  • TTL menor que a sobreposição. O max-age do cache de borda, mais uma margem para desvio de relógio, precisa ser menor que a janela de sobreposição das chaves. É permitido ao cache estar desatualizado; nunca lhe é permitido estar desatualizado por mais tempo que a janela em que ambas as chaves são válidas. É essa única desigualdade que torna seguro um JWKS agressivamente colocado em cache.

Juntando tudo, a rotação se torna um crossfade com uma ordem de execução fixa: adicione a nova chave, espere um TTL para que a borda e todos os validadores a enxerguem, comece a assinar com ela, espere os tokens antigos envelhecerem e saírem, e então descarte a chave antiga. Cada passo é seguro contra um cache que está um TTL atrás, porque o TTL foi escolhido para ser menor que qualquer janela em que um cache pudesse estar atrasado.

A lição, destilada

Cache costuma ser uma decisão de latência. Cache de chaves públicas é uma decisão de correção, porque aquilo que você está colocando em cache é justamente o que decide quais tokens são reais. Você pode perfeitamente colocá-lo na borda — e deveria, porque é público e é quente — mas só depois de a rotação ser expressa como uma sobreposição ordenada em vez de uma troca, e só depois de o TTL do cache ser comprovadamente menor que essa sobreposição. Acerte isso e os documentos mais movimentados e mais cacheáveis do seu sistema de autenticação deixam de tocar na sua origem, e ninguém jamais nota a diferença — que é exatamente o objetivo.