# Module: Azaion.Common.Configs.JwtConfig + SessionConfig ## Purpose Configuration POCOs for JWT signing/validation and refresh-token TTLs. Bound from `appsettings.json` sections `JwtConfig` and `SessionConfig`. Both classes live in `Azaion.Common/Configs/JwtConfig.cs`. > **Cycle 2 (2026-05-14) note (AZ-531 / AZ-532)** — major reshape: > - HS256 shared-secret signing is gone. `Secret` is no longer read by any code path; the property is retained only as a temporary rollback escape hatch (AZ-532 spec). > - New: `KeysFolder` (PEM directory) and `ActiveKid` (currently-signing key id) for ES256. > - New: `AccessTokenLifetimeMinutes` (default 15) replaces the old `TokenLifetimeHours` (default 4) — short-lived access tokens are now paired with refresh-token rotation. > - New companion class `SessionConfig` carries refresh-token TTLs. ## Public Interface ### JwtConfig | Property | Type | Default | Description | |----------|------|---------|-------------| | `Issuer` | `string` | (required) | Token `iss` claim. Validated by JwtBearer middleware. | | `Audience` | `string` | (required) | Token `aud` claim for interactive sessions. (Mission tokens override to `satellite-provider`; MFA step-1 tokens override to `azaion-mfa-step2`.) | | `KeysFolder` | `string` | `secrets/jwt-keys` | Directory containing one ES256 PEM per key. The kid is the filename without `.pem`. | | `ActiveKid` | `string?` | `null` | Kid currently used to sign new tokens. If null, falls back to the first PEM by ordinal filename order with a startup log warning. | | `AccessTokenLifetimeMinutes` | `int` | 15 | Access-token TTL. | ### SessionConfig | Property | Type | Default | Description | |----------|------|---------|-------------| | `RefreshSlidingHours` | `int` | 8 | Each rotation extends `expires_at` by this many hours from `now`. | | `RefreshAbsoluteHours` | `int` | 12 | Family is rejected past this many hours since `family_started_at`, regardless of sliding rotations. | ## Internal Logic None — pure data classes. ## Dependencies None. ## Consumers - `Program.cs` - reads `JwtConfig` eagerly to fail-fast on missing Issuer/Audience and to construct the `JwtSigningKeyProvider` before `app.Build()` - registers `Configure` and `Configure` for downstream injection - `JwtSigningKeyProvider` — reads `KeysFolder`, `ActiveKid` - `AuthService.CreateToken` — reads `Issuer`, `Audience`, `AccessTokenLifetimeMinutes` - `RefreshTokenService` — reads `SessionConfig.RefreshSlidingHours`, `RefreshAbsoluteHours` - `MfaService.IssueMfaStepToken` / `ValidateMfaStepToken` — reads `Issuer` (audience is hard-coded to `azaion-mfa-step2`) - `MissionTokenService.MintToken` — reads `Issuer` (audience is hard-coded to `satellite-provider`) ## Data Models None. ## Configuration Bound via `builder.Configuration.GetSection(nameof(JwtConfig))` and `Configure`. Override via env vars: - `JwtConfig__Issuer=…`, `JwtConfig__Audience=…`, `JwtConfig__KeysFolder=/var/lib/azaion/jwt-keys`, `JwtConfig__ActiveKid=kid-2026-05-14` - `SessionConfig__RefreshSlidingHours=8`, `SessionConfig__RefreshAbsoluteHours=12` ## External Integrations Filesystem (read-only on `KeysFolder`). ## Security - Private signing keys live on disk only; the JWKS endpoint exports only public components. `chmod 600` is applied by `scripts/generate-jwt-key.sh`. - The legacy `Secret` field is retained but unused; remove on a follow-up cleanup ticket once the rollback window has closed. - `RefreshAbsoluteHours` is the hard cap on session lifetime — no rotation can extend past it. Bumping above 12 h needs a security review because it directly extends the leak-window of any one refresh token. ## Tests - `e2e/Azaion.E2E/Tests/JwksTests.cs` — exercises the rotation overlap (AC-3) by manipulating `KeysFolder` and `ActiveKid`. - `e2e/Azaion.E2E/Tests/RefreshTokenTests.cs` — exercises both the sliding and absolute caps (AC-4).