> Markdown version of https://authpi.com/docs/reference/jwks-key-rotation/ — fetch the complete AuthPI docs index at https://authpi.com/llms.txt to discover all available pages.

# 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.

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:

```bash
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:

```json
{
  "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](/docs/reference/token-claims).
- **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

| Property | Value |
|----------|-------|
| Rotation schedule | Monthly — last day of each month, 01:00 UTC |
| New keys per rotation | One per algorithm (ES256, EdDSA, RS256) |
| Old keys retained until | At least 45 days after creation |
| Effective publication lifetime | ~2 months (dropped at the first rotation after day 45) |
| Signing key | The newest key for the token's algorithm |
| Longest-lived token | Refresh tokens, 7 days |
| Maximum configurable token lifetime | 21 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`:

```ts
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](/docs/guides/validate-tokens) 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

- [Validate tokens in your API](/docs/guides/validate-tokens) — complete verifier setups for Workers, Node, and Python
- [Token claims reference](/docs/reference/token-claims) — what's inside the tokens you're verifying
- [OIDC reference](/docs/reference/oidc) — the discovery document and standard endpoints