Key rotation
The signing key lives entirely in environment variables — no key files on disk, no database:
| Variable | Role |
|---|---|
OIDC_PRIVATE_KEY |
Signs tokens |
OIDC_PUBLIC_KEY |
Published in JWKS |
OIDC_PREVIOUS_PUBLIC_KEY |
The last rotated-out public key, kept in JWKS during the overlap |
Rotating
Section titled “Rotating”Generate a keypair with:
php artisan oidc:rotate-keys- Writes
OIDC_PRIVATE_KEY,OIDC_PUBLIC_KEY, andOIDC_PREVIOUS_PUBLIC_KEYinto your.env(as quoted,\n-escaped single-line values), rolling the current public key intoOIDC_PREVIOUS_PUBLIC_KEYso tokens signed before the rotation keep validating. - Prompts for confirmation first; pass
--forceto skip it. - Pass
--printto print the three variables to stdout instead of writing.env— use this when your keys come from a secrets manager rather than a file.--printnever touches.env. - Restart the app (and queue workers) afterwards so the new keys load.
For a first-time setup (no existing key), the command simply writes a fresh
OIDC_PRIVATE_KEY/OIDC_PUBLIC_KEY and omits OIDC_PREVIOUS_PUBLIC_KEY.
flowchart LR
A["php artisan oidc:rotate-keys"] --> B["New keypair signs all new tokens<br/>OIDC_PRIVATE_KEY / OIDC_PUBLIC_KEY"]
A --> C["Previous public key stays in JWKS<br/>OIDC_PREVIOUS_PUBLIC_KEY"]
B --> D{"Every token signed by<br/>the old key expired?"}
C --> D
D -- yes --> E["Remove OIDC_PREVIOUS_PUBLIC_KEY<br/>and redeploy"]
The overlap window
Section titled “The overlap window”OIDC_PREVIOUS_PUBLIC_KEY flows into config('oidc.additional_public_keys'), which the JWKS
endpoint serves alongside the active key (deduplicated by kid). During the overlap, tokens
signed by either the current or the previous key verify.
Once every token signed by the previous key has expired (i.e. past your access-/id-token TTL),
remove OIDC_PREVIOUS_PUBLIC_KEY and redeploy. The old private key is already gone after
rotation, so it can never sign new tokens — leaving the old public key in JWKS a little too long is
harmless, not a security hole.