# Flights — parallel H1/H2/H3 change spec Drop-in equivalent of the H1/H2/H3 fixes landed in `annotations/` this cycle. The workspace boundary rule (`.cursor/rules/workspace-boundary.mdc`) prevents this agent from editing the `flights/` repo directly; this document is the contract the flights workspace should implement on its own branch. Source of truth for the new files / patterns is the annotations workspace: - `annotations/src/Auth/JwtExtensions.cs` — JWKS verifier wiring. - `annotations/src/Infrastructure/ConfigurationResolver.cs` — fail-fast env-var helper. - `annotations/src/Infrastructure/CorsConfigurationValidator.cs` — CORS allow-list guard. - `annotations/src/Program.cs` — composition root. The flights changes are byte-equivalent except for the items called out under "Differences" below. ## H1 — JWKS verifier (replace HS256 shared secret) In `flights/Auth/JwtExtensions.cs`: - Replace `AddJwtAuth(string jwtSecret)` with `AddJwtAuth(IConfiguration configuration)`. - Resolve `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL` via the new `ConfigurationResolver.ResolveRequiredOrThrow` helper (no fallbacks). - Build a `ConfigurationManager` over a minimal `IConfigurationRetriever` (admin only exposes JWKS, not the full OIDC discovery doc — copy the `JwksRetriever` private class verbatim from annotations). - `TokenValidationParameters` must be: - `ValidateIssuer = true`, `ValidIssuer = issuer` - `ValidateAudience = true`, `ValidAudience = audience` - `ValidateLifetime = true`, `ValidateIssuerSigningKey = true` - `ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256]` (pinned) - `RequireSignedTokens = true`, `RequireExpirationTime = true` - `ClockSkew = TimeSpan.FromSeconds(30)` - `IssuerSigningKeyResolver` returns `jwks.GetSigningKeys()` filtered by `kid`. - Keep the existing authorization policies in place (`FL`, `GPS`). ## H2 — fail-fast env vars (drop insecure defaults) In `flights/Program.cs`: - Delete `?? "Host=localhost;Database=azaion;Username=postgres;Password=changeme"` for `DATABASE_URL` and resolve it through `ConfigurationResolver.ResolveRequiredOrThrow`. - Delete `?? "development-secret-key-min-32-chars!!"` for `JWT_SECRET` and remove the variable entirely (`AddJwtAuth` now takes `IConfiguration`). ## H3 — config-driven CORS allow-list In `flights/Program.cs`: - Read `CorsConfig:AllowedOrigins` (string array) and `CorsConfig:AllowAnyOrigin` (bool). - Call `CorsConfigurationValidator.EnsureSafeForEnvironment(...)` before `AddCors`. In `Production` with empty origins and `AllowAnyOrigin=false`, throw. - Build the default policy with `WithOrigins(allowedOrigins)` (locked) or `AllowAnyOrigin()` (permissive opt-in) per `ShouldUsePermissivePolicy`. - After `builder.Build()`, log a warning when running with the permissive default in a non-Production environment (`ShouldWarnAboutPermissiveDefault`). Copy `CorsConfigurationValidator.cs` verbatim, only changing the namespace to `Azaion.Flights.Infrastructure`. ## Side-effect: local token minting If flights has its own `Services/TokenService.cs` or `Controllers/AuthController.cs` that mints tokens with HS256 (matching the pattern annotations had before this cycle), it MUST be removed; otherwise the new validator (`ValidAlgorithms` pinned to `EcdsaSha256`) will reject the locally-minted tokens at the next `[Authorize]` hop. Admin is the sole token issuer for the suite after this change. If flights had no local token minting before, this section does not apply. ## Differences from annotations - Authorization policies in `JwtExtensions`: keep flights' existing `FL` and `GPS` policies; do NOT add annotations' `ANN`/`DATASET`/`ADM` policies. - Namespace prefix: `Azaion.Flights` instead of `Azaion.Annotations`. ## `.env.example` (new file) Mirror annotations' template; required keys: ``` DATABASE_URL= JWT_ISSUER=AzaionApi JWT_AUDIENCE=Annotators/OrangePi/Admins JWT_JWKS_URL=https://admin.azaion.com/.well-known/jwks.json # CorsConfig__AllowedOrigins__0=https://... # CorsConfig__AllowAnyOrigin=false ``` Confirm the `Issuer` / `Audience` values against the production admin deployment before merging. ## Docs to update in `flights/_docs/` - `02_document/modules/auth-identity.md` (or equivalent) — verifier-only role, remove any HS256 references, document the JWKS resolver wiring. - `02_document/deployment/environment_strategy.md` (or equivalent) — required-vs-optional env table; remove `JWT_SECRET`, add the three new JWT vars and the CORS config keys. - `02_document/architecture.md` (or equivalent) — retire any ADRs that pinned HS256 / wide-open CORS. ## Verification before merge 1. `dotnet build` succeeds. 2. Manually unset `JWT_ISSUER` (or `JWT_AUDIENCE`, `JWT_JWKS_URL`, `DATABASE_URL`) and confirm startup throws `InvalidOperationException` with a helpful message naming the env var. 3. With `ASPNETCORE_ENVIRONMENT=Production` and no `CorsConfig:AllowedOrigins`, confirm startup throws. 4. With a valid admin-issued ES256 token, confirm `[Authorize]` endpoints return 200. 5. With a token forged using `alg=HS256` and admin's public key as the HMAC secret, confirm the endpoint returns 401 (alg-confusion attack rejected).