# OAuth JWT Key Rotation Playbook (Dual-Key) ## Purpose Ensure marketing/tenant OAuth tokens remain valid during RSA key rotations by keeping the previous signing key available until all legacy tokens expire. ## Prerequisites - Environment variable `OAUTH_KEY_STORE` points to a shared filesystem (default `storage/app/oauth-keys`). - `OAUTH_JWT_KID` set to the current signing key id. - Application deploy tooling able to propagate `.env` changes promptly. - Operations access to run artisan commands in the target environment. ## Rotation Workflow 1. **Review existing keys** ```bash php artisan oauth:list-keys ``` Confirm the `current` entry matches `OAUTH_JWT_KID`, note any legacy KIDs that should remain trusted until rotation completes. 2. **Generate new key pair** ```bash php artisan oauth:rotate-keys --kid=fotospiel-jwt-$(date +%Y%m%d%H%M) ``` - The command now *copies* the existing key into the `/archive` folder but leaves it in-place for token verification. - After the command, run `php artisan oauth:list-keys` again to verify both the old and new KIDs exist. 3. **Update environment configuration** - Set `OAUTH_JWT_KID` to the newly generated value. - Deploy the updated config (restart queue workers/web instances if they cache config). 4. **Smoke test issuance** - Request a fresh OAuth token (PKCE flow) and inspect the JWT header — `kid` must match the new value. - Use an existing token issued **before** the rotation to hit a tenant API route; it should continue to verify because the old key remains present. 5. **Monitor** - Watch application logs for `Invalid token` / `JWT public key not found` errors over the next 24h. - Investigate any anomalies before pruning. ## Pruning Legacy Keys After the longest access-token + refresh-token lifetime (default: 30 days for refresh), prune the legacy signing directory. ```bash php artisan oauth:prune-keys --days=45 --force ``` - Use `--dry-run` first to see which directories would be removed. - The prune command never deletes the `current` KID. - Archived copies remain under `storage/app/oauth-keys/archive/...` for forensics. ## Runbook Summary | Step | Command | Outcome | | --- | --- | --- | | Inspect | `php artisan oauth:list-keys` | Inventory current + legacy keys | | Rotate | `php artisan oauth:rotate-keys --kid=...` | Creates new key while keeping legacy key active | | Verify | Issue new token + test old token | Ensures dual-key window works | | Prune | `php artisan oauth:prune-keys --days=45` | Removes legacy key once safe | Document completion of `SEC-IO-01` in `docs/todo/security-hardening-epic.md` when the rotation runbook has been rehearsed in staging.