BlogJWKS in Production: Publish, Cache, and Rotate Public Keys Safely
·Updated August 26, 2026·12 min read·JWTSecrets Team

JWKS in Production: Publish, Cache, and Rotate Public Keys Safely

Production playbook for JSON Web Key Sets: public-only keys, JWKS URIs, caching, kid rotation, and how to inspect documents before go-live.

JWKS in Production: Publish, Cache, and Rotate Public Keys Safely

When your APIs verify RS256 or ES256 JWTs, every resource server needs the issuer’s public keys — without ever holding the private signing key. That distribution channel is a JWKS (JSON Web Key Set) document, usually served from a stable JWKS URI. Done well, JWKS unlocks microservice verification, partner integrations, and zero-downtime kid rotation. Done poorly, it causes intermittent invalid signature outages — or worse, forgery if you trust the wrong keys.

This guide is the production playbook: what belongs in JWKS, how to publish it, how to inspect documents before go-live, how verifiers should fetch and cache, and how to rotate without logging everyone out. Use the JWKS Viewer to inspect or build public-only documents in your browser, and the RSA Key Generator when you need fresh PEM material for RS256.

Why JWKS Exists

HMAC algorithms like HS256 use a shared secret. Every verifier must possess the same secret that signs tokens — which becomes painful (and risky) as soon as more than one service verifies.

Asymmetric algorithms flip the model:

  • The authorization server signs with a private key
  • Verifiers use the matching public key
  • Public keys can be published openly; private keys stay in a secrets manager or HSM

JWKS is the standard JSON envelope for those public keys. OIDC discovery documents expose jwks_uri so relying parties do not hardcode PEMs in every deploy. If you are still choosing between shared-secret and asymmetric signing, read HS256 vs RS256 and the RS256 algorithm page before locking architecture.

Public Keys Only — Non-Negotiable

A JWKS document must contain public JWKs. Fields like d, p, q, dp, dq, and qi are private key material. If they appear in a document you serve at /.well-known/jwks.json, treat it as a key compromise: rotate immediately, purge caches, and audit who could have fetched the endpoint.

Before you publish:

1. Paste the JSON into the JWKS Viewer

2. Confirm each key shows kty, kid, alg, and use as expected

3. If the viewer warns about private material, use Copy public-only and never deploy the private version

4. Prefer use: "sig" for signing keys and stable opaque kid values

HMAC secrets (kty: "oct") must never appear in public JWKS. Those belong in a secrets manager for HS256 issuers only.

Shape of a Production JWKS

A minimal RSA public JWKS looks like:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "2026-08-rsa-a",
      "use": "sig",
      "alg": "RS256",
      "n": "...",
      "e": "AQAB"
    }
  ]
}

During rotation you usually publish two keys with different kid values until outstanding access tokens expire. Verifiers select by the JWT header kid. Missing or colliding kids are a leading cause of “works in staging, fails in prod” signature errors.

Publish a Stable JWKS URI

Pick one HTTPS URL and treat it as authentication infrastructure:

  • Common path: https://auth.example.com/.well-known/jwks.json
  • Or the jwks_uri from your OIDC discovery document
  • TLS only — no plain HTTP
  • Cache-Control that allows short-to-medium caching and timely rotation (many teams use minutes to a few hours, then force refresh on unknown kid)

Configure that URI in each verifier’s environment (OIDC_JWKS_URI). Do not honor untrusted jku headers on inbound tokens for first-party APIs — that pattern lets attackers point you at malicious key sets. See the jku header glossary for the threat model.

Inspect Before You Ship

Operational habit that prevents outages:

1. Generate or export public keys (RSA PEM via the RSA Key Generator, or your HSM)

2. Build JWKS in the viewer’s Build mode (set kid + alg)

3. Sign a lab token with the private key

4. Confirm the token header kid matches a JWKS entry

5. Verify with your library against the remote or local JWKS

Language-specific verify snippets live under /guides/jwks-viewer/nodejs and siblings. Keep this post focused on operations; use those guides when you need stack-specific wiring.

Verify With Node.js (jose)

const { createRemoteJWKSet, jwtVerify } = require('jose');

const JWKS = createRemoteJWKSet(new URL(process.env.OIDC_JWKS_URI));

async function verifyAccessToken(token) {
  const { payload } = await jwtVerify(token, JWKS, {
    algorithms: ['RS256'],
    issuer: process.env.OIDC_ISSUER,
    audience: process.env.API_AUDIENCE,
  });
  return payload;
}

Pin algorithms. Never accept whatever alg the attacker puts in the header. After verification, still enforce iss, aud, and exp.

Verify With Python (PyJWT)

import os
import jwt
from jwt import PyJWKClient

client = PyJWKClient(os.environ["OIDC_JWKS_URI"])

def verify_access_token(token: str) -> dict:
    key = client.get_signing_key_from_jwt(token)
    return jwt.decode(
        token,
        key.key,
        algorithms=["RS256"],
        audience=os.environ["API_AUDIENCE"],
        issuer=os.environ["OIDC_ISSUER"],
    )

On unknown-kid errors, refresh JWKS and compare the live document in the JWKS Viewer against the token header (decode-only with the JWT Decoder is fine for reading kid/alg — it does not prove authenticity).

Verify With Go (keyfunc + golang-jwt)

import (
    "os"

    "github.com/MicahParks/keyfunc/v2"
    "github.com/golang-jwt/jwt/v5"
)

func verifyAccessToken(accessToken string) (*jwt.Token, error) {
    jwks, err := keyfunc.Get(os.Getenv("OIDC_JWKS_URI"), keyfunc.Options{})
    if err != nil {
        return nil, err
    }
    return jwt.Parse(accessToken, jwks.Keyfunc, jwt.WithValidMethods([]string{"RS256"}))
}

Prefer stack-specific walkthroughs under /guides/jwks-viewer/go and /guides/jwks-viewer/python when integrating libraries end to end.

Caching Without Breaking Rotation

Good verifier behavior:

  • Cache JWKS responses according to HTTP cache headers
  • On signature failure or unknown kid, refetch once (with backoff) before failing the request
  • Do not refetch on every API call — that turns your IdP into a single point of overload
  • Monitor 401 spikes correlated with JWKS 5xx or stale kids

Issuers should publish the new public JWK before signing with the new private key, keep the old public JWK until the longest access-token TTL (+ skew buffer) passes, then remove the retired key. The same dual-window pattern applies to HMAC secrets with kid maps — see how to rotate JWT secrets.

Building JWKS From PEM

If your platform exports SPKI public PEM:

1. Open JWKS ViewerBuild

2. Paste -----BEGIN PUBLIC KEY----------END PUBLIC KEY-----

3. Set kid (random opaque ID is fine) and alg (RS256 or ES256)

4. Add the key, copy the JWKS JSON, serve it from your URI

The viewer corrects mismatched algorithms when the PEM is EC but RS256 was selected. Prefer explicit alg values that match how you sign.

Production Checklist

  • [ ] JWKS served only over HTTPS at a configured URI
  • [ ] Document contains public keys only (viewer private-field warning is clean)
  • [ ] Every signing key has a unique kid present on issued tokens
  • [ ] Verifiers pin algorithms and validate iss / aud / exp
  • [ ] Dual-key window rehearsed in staging with production-like TTLs
  • [ ] Alerts on JWKS fetch failures and unknown-kid rates
  • [ ] Private keys never in git, tickets, or JWKS
  • [ ] Runbooks link to the viewer for incident inspection

Common Pitfalls

Trusting jku from clients. Configure JWKS URI yourself.

Publishing private JWKs. Instant compromise — rotate and strip.

Removing the old kid too early. Causes mass invalid signatures until tokens expire.

Mixing hex/Base64 secrets with asymmetric JWKS. Different worlds — HS256 secrets are not JWKS entries.

Assuming decode equals verify. The decoder shows claims; only cryptographic verify with the right key proves integrity. Pair with how to validate a JWT and the JWT Validator for HS256 labs.

What to Read Next

Frequently Asked Questions

Should my JWKS endpoint be public?

Yes for the public keys. Anyone may download verification keys. Protect availability and integrity with HTTPS, monitoring, and change alerts — but do not put private keys or HMAC secrets in the document. If the JWKS Viewer flags private fields, treat that as an incident and rotate.

How often should verifiers refresh JWKS?

Follow Cache-Control from the response. Additionally refetch when you see an unknown kid or a sudden wave of signature failures that could indicate rotation. Avoid hammering the endpoint on every request; use backoff and shared caches in multi-instance fleets.

Can I use the same JWKS for RS256 and ES256?

You can publish multiple keys with different kty / alg values in one JWKS. Each token’s kid (and pinned algorithm allowlist) must still select a compatible key. Prefer clarity: do not leave ambiguous kids that could match the wrong algorithm family during mixed rollouts.

Does the JWKS Viewer fetch my production URL?

No. Paste JSON (or build from PEM) in the browser. That keeps network fetches under your control and matches a client-side trust model. Production apps should fetch the configured JWKS URI in your backend or API gateway — not via untrusted token headers like jku.

Written by

JWTSecrets Team

Editorial Team

The JWTSecrets editorial team writes practical guides on JWT authentication, cryptographic key management, and browser-based security tooling. Our content is reviewed against IETF RFCs and current library documentation.