# JWT Key Management

Allegro signs member session tokens with per-tenant RSA-2048 key pairs using RS256. This page covers the key lifecycle and how to manage keys in production.

## Key Lifecycle[​](#key-lifecycle "Direct link to Key Lifecycle")

```text
active → retiring → expired

```

| Status     | Description                                                                                                                                                             |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `active`   | The current signing key. All new tokens are signed with this key. Appears in JWKS.                                                                                      |
| `retiring` | A previously active key after rotation. Still appears in JWKS until `retires_at`. Allegro accepts tokens signed by this key indefinitely (until the token's own `exp`). |
| `expired`  | No longer appears in JWKS. Allegro still accepts tokens signed by this key until the token's own `exp` (1-year TTL).                                                    |

Retiring keys stay in JWKS for 1 year (matching the JWT TTL) so that external consumers like AWS API Gateway can always verify any valid token.

## Commands[​](#commands "Direct link to Commands")

### Generate Initial Keys[​](#generate-initial-keys "Direct link to Generate Initial Keys")

Run once per tenant after provisioning (automatically called by `tenant:create`):

```bash
php artisan jwt:keys:generate --tenant=*

```

Idempotent — skips tenants that already have an active key. Omit `--tenant=*` to run for the current tenant context.

### Rotate Keys[​](#rotate-keys "Direct link to Rotate Keys")

Generates a new active key and moves the current active key to `retiring`:

```bash
php artisan jwt:keys:rotate --tenant=*

```

All new tokens are signed with the new key immediately. Old tokens remain verifiable via JWKS for 1 year.

### Prune Expired Keys[​](#prune-expired-keys "Direct link to Prune Expired Keys")

Marks retiring keys as `expired` once `retires_at` has passed. Runs daily via the scheduler — you should not need to run this manually:

```bash
php artisan jwt:keys:prune --tenant=*

```

Expired keys are **not deleted** — Allegro continues to accept tokens signed by them until the token's own `exp`. Only JWKS visibility is removed.

## Scheduler[​](#scheduler "Direct link to Scheduler")

`jwt:keys:prune` is registered in the Laravel scheduler and runs daily automatically. No additional configuration is required.

## Emergency Key Revocation[​](#emergency-key-revocation "Direct link to Emergency Key Revocation")

There is no automated revocation. If a private key is compromised:

1. Run `php artisan jwt:keys:rotate --tenant=<id>` to immediately stop signing new tokens with the compromised key. Existing tokens signed by the compromised key remain verifiable via JWKS until you complete step 3.
2. The compromised key enters `retiring` status and continues to appear in JWKS for up to 1 year (its `retires_at`). External consumers will still accept tokens signed by it during this window.
3. If immediate revocation is needed, manually update the compromised key's `status` to `expired` in the tenant database. This removes it from JWKS immediately. However, external consumers (e.g., AWS API Gateway) cache JWKS responses — typically for up to 1 hour — so tokens signed by the compromised key may continue to be accepted until those caches expire. Clear JWKS caches on your external consumers where possible.
