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), andRS256(RSA-2048). Which one signs a given client’s tokens follows the client’sresponse_signature_algsetting — see the token claims reference. - Key IDs are opaque UUIDs. Match the token header’s
kidagainst 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; a304 Not Modifiedcosts no body transfer. Send the ETag from your cached copy when refreshing. stale-if-error=120lets 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:
- 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.
- The token may be garbage. If the
kidis 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
kidmatching 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 — complete verifier setups for Workers, Node, and Python
- Token claims reference — what’s inside the tokens you’re verifying
- OIDC reference — the discovery document and standard endpoints