Files
fotospiel-app/docs/deployment/oauth-key-rotation.md
Codex Agent 6290a3a448 Fix tenant event form package selector so it no longer renders empty-value options, handles loading/empty
states, and pulls data from the authenticated /api/v1/tenant/packages endpoint.
    (resources/js/admin/pages/EventFormPage.tsx, resources/js/admin/api.ts)
  - Harden tenant-admin auth flow: prevent PKCE state loss, scope out StrictMode double-processing, add SPA
    routes for /event-admin/login and /event-admin/logout, and tighten token/session clearing semantics (resources/js/admin/auth/{context,tokens}.tsx, resources/js/admin/pages/{AuthCallbackPage,LogoutPage}.tsx,
    resources/js/admin/router.tsx, routes/web.php)
2025-10-19 23:00:47 +02:00

59 lines
2.6 KiB
Markdown

# 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.