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)
59 lines
2.6 KiB
Markdown
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.
|