Reference

JWKS & Key Rotation

How AuthPI publishes and rotates JWT signing keys: the JWKS endpoint, monthly rotation cadence, the 45-day overlap guarantee, the enforced 21-day token-lifetime cap, caching headers, and unknown-kid handling.

Last updated 2026-06-13

AuthPI signs every token — access, ID, and refresh — with asymmetric keys whose public halves are published as a JSON Web Key Set (RFC 7517). Your services verify tokens against this set and never need a shared secret. This page is the operational contract: where the keys live, when they rotate, how long old keys remain valid, and what your verifier must do about it.

The JWKS endpoint

Always take the URL from the discovery document rather than constructing it:

curl -s https://idp.authpi.com/{issuer_id}/.well-known/openid-configuration | jq -r .jwks_uri
# https://idp.authpi.com/{issuer_id}/jwks.json

The response is a standard key set:

{
  "keys": [
    {
      "kty": "EC",
      "crv": "P-256",
      "alg": "ES256",
      "use": "sig",
      "kid": "7d3bde53-3945-49e4-8594-42e44d9f538e",
      "x": "...", "y": "...",
      "ext": true
    },
    { "kty": "OKP", "crv": "Ed25519", "alg": "EdDSA", "use": "sig", "kid": "...", "x": "...", "ext": true },
    { "kty": "RSA", "alg": "RS256", "use": "sig", "kid": "...", "n": "...", "e": "...", "ext": true }
  ]
}

What to rely on:

  • Three algorithms are always published: ES256 (EC P-256), EdDSA (Ed25519), and RS256 (RSA-2048). Which one signs a given client’s tokens follows the client’s response_signature_alg setting — see the token claims reference.
  • Key IDs are opaque UUIDs. Match the token header’s kid against the set exactly; never parse meaning out of a kid or assume ordering.
  • Expect multiple keys per algorithm. Outside the first weeks of a new issuer the set normally holds two generations of each algorithm (six keys). Never filter to “the” key for an algorithm — select by kid.
  • Only public material is served. Keys carry use: "sig" and the public parameters; private parameters and internal metadata are stripped.

Rotation cadence and the overlap guarantee

PropertyValue
Rotation scheduleMonthly — last day of each month, 01:00 UTC
New keys per rotationOne per algorithm (ES256, EdDSA, RS256)
Old keys retained untilAt least 45 days after creation
Effective publication lifetime~2 months (dropped at the first rotation after day 45)
Signing keyThe newest key for the token’s algorithm
Longest-lived tokenRefresh tokens, 7 days
Maximum configurable token lifetime21 days (enforced — API rejects higher)

These numbers compose into the guarantee your verifier actually cares about: every token remains verifiable against the published JWKS for its entire lifetime. A signing key is superseded at the next rotation (at most ~31 days after its creation), but it isn’t removed until the first rotation after it turns 45 days old — roughly two months after creation. The gap between the last token a key signs and the key leaving the set is therefore always at least one full rotation period (≥ 28 days). With access tokens living minutes and refresh tokens 7 days by default, there is no moment where a valid token’s kid is missing from the JWKS.

AuthPI enforces this margin for you rather than leaving it to configuration. The three client token-age settings — default_access_token_age, default_id_token_age, and default_refresh_token_age — are capped at 21 days; the API rejects any higher value at create and update. On top of that, every token’s exp is clamped to at most 21 days from issuance at minting time, so a token can never outlive its signing key’s publication even if a longer lifetime slipped through. For long sessions, use refresh-token rotation rather than long-lived refresh tokens.

The flip side: a key you fetched and cached can gain siblings at any rotation, and keys older than 45 days disappear. Your verifier must treat the set as a cache to refresh, not a fixture to pin.

Caching the JWKS

The endpoint sends:

Cache-Control: public, max-age=3600, s-maxage=3600, stale-if-error=120
ETag: "<hash of the key set>"
  • Cache for an hour (max-age=3600). The set changes at most monthly, so an hour of staleness is comfortably inside the 45-day overlap. Shared caches (CDNs, corporate proxies) may cache it too (public, s-maxage).
  • Revalidate with If-None-Match. The ETag changes only when the key set does; a 304 Not Modified costs no body transfer. Send the ETag from your cached copy when refreshing.
  • stale-if-error=120 lets caches serve a recently-expired copy for two minutes if a refresh fails — your verifier keeps working through a transient blip.

Most JWT libraries handle all of this for you. jose’s createRemoteJWKSet caches the set, honors cache headers, and re-fetches on an unknown kid:

import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(new URL('https://idp.authpi.com/{issuer_id}/jwks.json'));

const { payload } = await jwtVerify(token, JWKS, {
  issuer: 'https://idp.authpi.com/{issuer_id}',
  audience: 'your-api',
});

Handling an unknown kid

When a token’s kid is not in your cached set, the correct interpretation depends on freshness:

  1. Your cache may be stale — a rotation happened since you fetched. Re-fetch the JWKS once and retry the lookup. This is the normal path in the hours after a monthly rotation.
  2. The token may be garbage. If the kid is still absent after a fresh fetch, reject the token. Do not retry further.

Rate-limit the re-fetch (for example, at most once per minute per process): attackers can send tokens with random kid values, and an unthrottled verifier turns each one into an upstream request. createRemoteJWKSet implements exactly this cooldown; if you hand-roll verification, copy the behavior. The validate tokens guide has complete verifier examples.

What not to do

  • Don’t hardcode or vendor keys. They rotate monthly; a pinned key breaks within two rotations. Pin the endpoint, not the keys.
  • Don’t assume one key per algorithm. Two generations coexist by design. Select by kid.
  • Don’t skip kid matching and “try every key”. It works until it masks a key-confusion bug. The header names the key; use it.
  • Don’t fetch the JWKS per request. At one fetch per verification you add a network hop to every API call and turn the IdP into a single point of latency. Cache for the advertised hour.
  • Don’t accept algorithms you don’t expect. Configure your verifier with the explicit allowlist (ES256, EdDSA, RS256 — or just the one your client uses) so an attacker can’t downgrade verification.

Next steps