# Module: Azaion.AdminApi.Program ## Purpose Application entry point: configures DI, middleware, authentication, authorization, CORS, Swagger, logging, and defines all HTTP endpoints using ASP.NET Core Minimal API. ## Public Interface (HTTP Endpoints) > **Cycle 1 (2026-05-13) note** — endpoint surface changed by AZ-513 (detection-class CRUD), AZ-196 (device auto-registration), AZ-197 (hardware-binding removal). AZ-183 (OTA update check + publish) was reverted later the same day after the security audit (finding F-1). > > **Cycle 2 (2026-05-14) note A** — three resource endpoints removed as obsolete: `POST /resources/get/{dataFolder?}`, `GET /resources/get-installer`, `GET /resources/get-installer/stage`. The encrypted-download support stack went with them. ADR-003 in `architecture.md` was retired. > > **Cycle 2 (2026-05-14) note B (auth modernization)** — eight endpoints added or replaced as part of Epic AZ-529 (Auth Modernization) + AZ-530 (CMMC Hardening). The `/login` shape is now dual-token (access + refresh) when MFA is off, or `MfaRequiredResponse` when MFA is enabled. CORS dropped the cleartext origin (AZ-538). HSTS + HTTPS redirection are wired in non-Development environments. Per-IP sliding-window rate limit added to `/login` (and `/login/mfa`). Public-key JWKS feed live at `/.well-known/jwks.json` (AZ-532). | Method | Path | Auth | Summary | Cycle origin | |--------|------|------|---------|--------------| | GET | `/health/live` | Anonymous | Liveness check (`Cache-Control: no-store`); excluded from Swagger | AZ-510 | | GET | `/health/ready` | Anonymous | Readiness check — pings both DB connections with a 2-s timeout; 503 with reason on failure | AZ-510 | | POST | `/login` | Anonymous + per-IP rate limit | Validates credentials. Returns `LoginResponse` (access + refresh) when MFA is off, `MfaRequiredResponse` when MFA is enabled. | AZ-531 / AZ-534 / AZ-537 | | POST | `/login/mfa` | Anonymous + per-IP rate limit | Second-factor verification (TOTP or recovery code). Returns `LoginResponse`. | AZ-534 | | POST | `/token/refresh` | Anonymous (token in body) | Rotates a refresh token; returns a fresh `LoginResponse`. Reuse-detection kills the family. | AZ-531 | | POST | `/logout` | Authenticated | Revokes the caller's current session (idempotent — returns `{ alreadyRevoked }`). | AZ-535 | | POST | `/logout/all` | Authenticated | Revokes every active session for the caller's user (returns `{ revoked: N }`). | AZ-535 | | POST | `/sessions/{sid:guid}/revoke` | ApiAdmin | Admin revoke-by-session-id. | AZ-535 | | GET | `/sessions/revoked` | revocationReader (Service or ApiAdmin) | Verifier-poll snapshot of revoked-but-not-yet-expired sessions. `Cache-Control: no-cache`; `since` clamped to `now - 12h`. | AZ-535 | | POST | `/sessions/mission` | Authenticated | Mints a long-lived no-refresh mission token bound to one aircraft. AZ-533 AC-6 step-up MFA gate is a TODO comment until org-wide MFA adoption. | AZ-533 | | POST | `/users/me/mfa/enroll` | Authenticated | Returns TOTP secret + otpauth URL + QR PNG + 10 recovery codes (ONCE). | AZ-534 | | POST | `/users/me/mfa/confirm` | Authenticated | Validates one TOTP code and flips `mfa_enabled=true`. | AZ-534 | | POST | `/users/me/mfa/disable` | Authenticated | Removes MFA (requires password + valid code). | AZ-534 | | GET | `/.well-known/jwks.json` | Anonymous (excluded from Swagger) | Public JWKS feed for verifiers; `Cache-Control: public, max-age=3600`. | AZ-532 | | POST | `/users` | ApiAdmin | Creates a new user. | — | | POST | `/devices` | ApiAdmin | Creates a CompanionPC device user (auto serial / email / 32-hex password). | AZ-196 | | GET | `/users/current` | Any authenticated | Returns current user from JWT claims. | — | | GET | `/users` | ApiAdmin | Lists users with optional email/role filters. | — | | PUT | `/users/queue-offsets/set` | Any authenticated | Updates user's queue offsets. | — | | PUT | `/users/{email}/set-role/{role}` | ApiAdmin | Changes a user's role. | — | | PUT | `/users/{email}/enable` | ApiAdmin | Enables a user account. | — | | PUT | `/users/{email}/disable` | ApiAdmin | Disables a user account. | — | | DELETE | `/users/{email}` | ApiAdmin | Removes a user. | — | | POST | `/resources/{dataFolder?}` | Any authenticated | Uploads a resource file. | — | | GET | `/resources/list/{dataFolder?}` | Any authenticated | Lists files in a resource folder. | — | | POST | `/resources/clear/{dataFolder?}` | ApiAdmin | Clears a resource folder. | — | | POST | `/classes` | ApiAdmin | Creates a detection class. | AZ-513 | | PATCH | `/classes/{id:int}` | ApiAdmin | Updates a detection class (partial-merge). | AZ-513 | | DELETE | `/classes/{id:int}` | ApiAdmin | Deletes a detection class. | AZ-513 | ### Removed endpoints The following endpoints have been removed and now return `404`: | Method | Path | Removed in | Reason | |--------|------|------------|--------| | PUT | `/users/hardware/set` | cycle 1 (AZ-197) | hardware-binding feature deleted | | POST | `/resources/check` | cycle 1 (AZ-197) | hardware-binding side-effect probe | | POST | `/get-update` | post-cycle-1 (AZ-183 reverted) | security audit F-1 | | POST | `/resources/publish` | post-cycle-1 (AZ-183 reverted) | OTA flow obsolete | | POST | `/resources/get/{dataFolder?}` | cycle 2 | obsolete; ADR-003 retired | | GET | `/resources/get-installer` | cycle 2 | installer-shipping era over | | GET | `/resources/get-installer/stage` | cycle 2 | same as above | ## Internal Logic ### DI Registration - `IJwtSigningKeyProvider` → `JwtSigningKeyProvider` (Singleton; eagerly built before `app.Build()` so `JwtBearer` and DI share one instance) — **AZ-532** - `IUserService` → `UserService` (Scoped) - `IAuthService` → `AuthService` (Scoped) - `IRefreshTokenService` → `RefreshTokenService` (Scoped) — **AZ-531** - `ISessionService` → `SessionService` (Scoped) — **AZ-535** - `IMissionTokenService` → `MissionTokenService` (Scoped) — **AZ-533** - `IMfaService` → `MfaService` (Scoped) — **AZ-534** - `IResourcesService` → `ResourcesService` (Scoped) - `IDetectionClassService` → `DetectionClassService` (Scoped) - `IAuditLog` → `AuditLog` (Scoped) — **AZ-537 / AZ-534** - `IDbFactory` → `DbFactory` (Singleton) - `ICache` → `MemoryCache` (Scoped) - `LazyCache` via `AddLazyCache()` - ASP.NET Core `DataProtection` — `SetApplicationName("Azaion.AdminApi")`; if `DataProtection:KeysFolder` is set, persisted to filesystem (production requirement for MFA-secret durability) — **AZ-534** - FluentValidation validators auto-discovered from `RegisterUserValidator` assembly - `BusinessExceptionHandler` registered as exception handler ### Middleware Pipeline 1. Swagger (Development only) 2. **HSTS + HTTPS redirection (non-Development only)** — AZ-538 3. CORS (`AdminCorsPolicy`) 4. Authentication (JWT Bearer with `ValidAlgorithms = [ES256]` and an `IssuerSigningKeyResolver` that picks by `kid` from `IJwtSigningKeyProvider.All`) 5. Authorization 6. **Rate limiter (`UseRateLimiter`)** — AZ-537 7. URL rewrite: root `/` → `/swagger` 8. Exception handler ### Authorization Policies - `apiAdminPolicy`: requires `RoleEnum.ApiAdmin` role - `revocationReaderPolicy`: requires `RoleEnum.Service` OR `RoleEnum.ApiAdmin` (gates `/sessions/revoked`) — **AZ-535** ### Rate Limit Policies - `LoginPerIpPolicy = "login-per-ip"` — sliding-window limiter keyed on `RemoteIpAddress`. Configured from `AuthConfig.RateLimit.PerIpPermitLimit` / `PerIpWindowSeconds`. On rejection, sets `Retry-After` from the `RetryAfter` lease metadata. Applied to `/login` and `/login/mfa`. ### Configuration Sections - `JwtConfig` — JWT signing/validation (Issuer, Audience, KeysFolder, ActiveKid, AccessTokenLifetimeMinutes) - `SessionConfig` — refresh-token sliding/absolute window (RefreshSlidingHours, RefreshAbsoluteHours) — **AZ-531** - `AuthConfig` — rate-limit and lockout knobs — **AZ-537** - `ConnectionStrings` — DB connections - `ResourcesConfig` — file storage path ### Kestrel - Max request body size: 200 MB ### Logging - Serilog: console + rolling file (`logs/log.txt`) ### CORS - Allowed origin: `https://admin.azaion.com` (the cleartext `http://` origin was dropped by AZ-538) - All methods and headers allowed; credentials allowed ### Helpers Local static helpers used by logout / mission endpoints: - `ParseSidClaim(ClaimsPrincipal)` — extracts the `sid` claim; throws `InvalidRefreshToken` (401) if missing/malformed. - `ParseUserIdClaim(ClaimsPrincipal)` — extracts `NameIdentifier`; same error semantics. - `IssueDualTokens(...)` — shared by `/login` and `/login/mfa`; calls `IRefreshTokenService.IssueForNewLogin`, `IAuthService.CreateToken`, plus `ISessionService.RevokeMissionsForAircraft` when the caller is `RoleEnum.CompanionPC` (AZ-533 AC-4 reconnect trigger). ## Dependencies All services, configs, entities, and request types from `Azaion.Common` and `Azaion.Services`. New dependencies wired in cycle 2: `Microsoft.AspNetCore.RateLimiting`, `Microsoft.AspNetCore.DataProtection`. ## Consumers None — application entry point. ## Data Models None defined here. ## Configuration Reads `JwtConfig`, `SessionConfig`, `AuthConfig`, `ConnectionStrings`, `ResourcesConfig` from `IConfiguration`. Optional `DataProtection:KeysFolder` for MFA-secret durability. ## External Integrations - PostgreSQL (via DI-registered `DbFactory`) - Local filesystem (via `ResourcesService` and `JwtSigningKeyProvider` for PEM keys) ## Security - JWT Bearer with full validation: `ValidateIssuer`, `ValidateAudience`, `ValidateLifetime`, `ValidateIssuerSigningKey`, `ValidAlgorithms = [ES256]` (AZ-532 AC-5). - Issuer signing keys resolved per-`kid` via `IJwtSigningKeyProvider`; supports rotation overlap. - Public JWKS endpoint exposes only public components (`x`/`y` for EC); `Cache-Control: public, max-age=3600`. - Per-IP sliding-window rate limit on `/login` and `/login/mfa` (AZ-537). - HSTS (1 year, includeSubDomains, preload) + HTTPS redirect in non-Development envs (AZ-538). - CORS restricted to HTTPS origin only (AZ-538). - DataProtection key folder must be a persistent volume in Production so encrypted MFA secrets survive restarts (AZ-534 known operational requirement; **carry-forward F3** asks for a startup warning when running in Production with the folder unset). - Role-based authorization for admin endpoints; new `Service` role gates the verifier-poll feed. ## Tests None directly; tested through `e2e/Azaion.E2E/Tests/` (Login, RefreshToken, RateLimitLockout, Logout, Jwks, MissionToken, MfaEnrollment, MfaLogin, PasswordHashing).