Skip to main content
private_key_jwt is optional. Public clients use none with PKCE, and dynamically registered apps can use a client secret instead. Use a private key if your app runs on a server and can store and rotate keys.

How it works

Your app keeps a private key, and Whirl reads the matching public key from your JWKS. For each token or revocation request, your app signs a short-lived JWT. Whirl checks the signature and claims, and rejects any JWT ID it has seen before. A JWKS only contains public keys, so it isn’t a secret. PKCE still protects the code exchange. The signed JWT identifies your app, not the user.

Publishing your public keys

With a metadata document, add a jwks_uri. With Dynamic Client Registration, send either a jwks_uri or an inline jwks, not both. Give each key a unique kid. Whirl rejects private and symmetric keys. Supported algorithms are RS256, PS256 and ES256. RSA keys must be at least 2048 bits, and EC keys must use P-256. The authorization server’s discovery document lists the supported algorithms.

JWT claims

Whirl allows 30 seconds of clock skew. It only uses keys from your registration, never key URLs inside the JWT. Sign the JWT with a well-maintained JOSE library, with a new jti every time. Add these form fields to your authorization code, refresh or revocation request:
Don’t send a client secret as well. A client registered with only a key can’t fall back to a secret or to public authentication. Each jti can only be used once, across both token and revocation requests, so sign a new JWT whenever you retry.

Rotating keys

A hosted JWKS can be up to 64 KiB with 10 keys, and must load within five seconds, including DNS. Whirl caches it for up to an hour, or less if your cache headers say so. An unknown kid makes Whirl fetch it again, at most once a minute per URL.
  1. Publish the old and new public keys together, with different kid values.
  2. Wait long enough for Whirl’s cache to pick up the new key.
  3. Start signing with the new key.
  4. Remove the old public key. Whirl stops accepting it once its cache refreshes.
If a fetch fails, Whirl won’t fall back to expired cached keys. Temporary fetch failures return 503 temporarily_unavailable with Retry-After: 60. To change keys registered inline with Dynamic Client Registration, register a new client. Use a hosted JWKS if you want to rotate keys. Removing a key doesn’t revoke access tokens that have already been issued. Revoke the tokens, or ask the user to disconnect the app, to cut off access straight away.