kid (key ID) map: you add a new key, promote it to active, and remove the old key only after every token signed under it has naturally expired.
Single-secret mode vs kid mode
The package supports two keyring shapes, and you choose between them based on whether you need graceful rotation. Single-secret mode uses onesecret value. Every token is signed and verified with that secret, and no kid header is embedded in the token. This is the right choice for development, low-churn internal APIs, and any context where you can afford to invalidate all outstanding tokens during a rotation (for example, by restarting the service with a new secret).
Kid mode uses a keys map of kid → secret pairs plus an active_kid pointer. New tokens are signed with the active key and carry its kid in the JWT header. The verifier accepts any kid present in the map, so old tokens remain valid while you drain traffic to the new key.
Use kid mode whenever you need to rotate secrets without forcing every active session to re-authenticate.
Configuring kid rotation
Replace the singlesecret field in your authentication.php config (or in the per-guard jwt block inside config/auth.php) with a keys map and an active_kid pointer:
active_kid is absent from the keys map, or if any secret in the map is empty, the guard throws InvalidJwtConfigurationException at resolution time rather than silently accepting tokens.
How it works
When you configure kid mode, the package’sJwtKeyring operates as follows:
- Issuing: every new token is signed with the key identified by
active_kidand carries that kid in the JWTkidheader field. - Verifying: the verifier reads the
kidheader from the incoming token, looks it up in thekeysmap, and verifies the signature using that key. Any kid present in the map is accepted — the verifier is not limited to the active kid. - Fail-closed: a token whose
kidis not in the map is rejected immediately; the guard never falls back to a different key or to an empty-secret verification.
Rotation procedure
Generate the new secret
Create a strong random secret for the new key and add it to your secrets manager or Set the new environment variable:
.env file:Add the new kid to the keys map
Add the new kid entry to
authentication.jwt.keys (or the per-guard jwt.keys block) while keeping all existing entries in place. Deploy this change before promoting the new kid — both keys must be in the map and the service must be running before any token is signed with the new one:Promote the new kid to active
Update
active_kid (and its environment variable) to point at the new kid. From this moment, all newly issued tokens are signed with 2026-05. Tokens signed under 2026-04 and 2026-03 continue to verify normally because both kids remain in the map:Wait for old tokens to expire
Do not remove old kids from the map until every token signed under them has expired. Check your
access_ttl_minutes and refresh_ttl_minutes settings — the safe wait period is the longer of the two. If you issue long-lived refresh tokens, wait for those to expire as well.Per-guard key isolation
Each JWT guard can carry its own independentjwt sub-block in config/auth.php, including its own keys map and active_kid. This lets you run fully separate signing-key lifecycles for different trust boundaries:
aud claim check rejects it before the key lookup even runs. Per-guard isolation means you can rotate the staff signing key without touching customer tokens, and vice versa.