Skip to content

Signing keys

Every tenant gets an RSA keypair when it is created. The private half is encrypted at rest; the public half is published as a JWK through that tenant’s JWKS endpoint and no other.

GET https://jons.auth.example/.well-known/jwks.json
{"keys":[{"kty":"RSA","use":"sig","alg":"RS256","kid":"e9914114-…","n":"…","e":"AQAB"}]}

Application scopes fail by omission — someone forgets one and rows leak. Row-level security fails silently if the connecting role happens to be a superuser. Both are real defences and both fail quietly.

A per-tenant signing key cannot fail quietly. Present one tenant with another’s token and the rejection is not a policy decision at all:

{"error":"invalid_token",
"error_description":"Could not find public key for kid e9914114-08c3-438b-a4f4-d71b70489bde"}

There is no code path where the wrong tenant considers the token and decides against it. The key that signed it was never published where they could look, so the token is unreadable rather than unauthorized.

SigningKey.rotate! mints a replacement, activates it immediately, and marks the outgoing key retired in the future rather than now:

SigningKey.rotate!(tenant: tenant)

The outgoing key keeps being published for an overlap window (24 hours by default), so tokens signed just before the rotation still verify while nothing new is signed with it. SigningKey.published is the JWKS scope and includes both; SigningKey.active is what signing uses and includes only the new one.