# Admin API ## 1. High-Level Overview **Purpose**: HTTP API entry point — configures DI, middleware pipeline, authentication, authorization, CORS, HSTS, HTTPS redirection, rate limiting, Swagger, DataProtection, and defines all REST endpoints using ASP.NET Core Minimal API. **Architectural Pattern**: Composition root + Minimal API endpoints — top-level statements configure the application and map HTTP routes to service methods. A static `IssueDualTokens` helper centralises the access+refresh issuance pattern shared by `/login` (no MFA) and `/login/mfa` (with MFA), and a tiny `ParseSidClaim` / `ParseUserIdClaim` pair extracts session/user identity from the request principal. **Upstream dependencies**: Authentication & Security (AuthService, RefreshTokenService, SessionService, MissionTokenService, MfaService, JwtSigningKeyProvider, AuditLog, Security), User Management (IUserService), Resource Management (IResourcesService), Detection Classes (IDetectionClassService), Data Layer (IDbFactory, ICache, all configs). **Downstream consumers**: HTTP clients (admin web UI, verifier services, CompanionPC). ## 2. Internal Interfaces ### BusinessExceptionHandler | Method | Input | Output | Async | |--------|-------|--------|-------| | `TryHandleAsync` | `HttpContext`, `Exception`, `CancellationToken` | `bool` | Yes | Cycle 2 (AZ-537 / AZ-531 / AZ-533 / AZ-534 / AZ-535) — the handler now maps `BusinessException` → an exception-specific HTTP status code via a `MapStatusCode` switch, preserves the legacy `409 Conflict` default, and stamps a `Retry-After` response header when `RetryAfterSeconds` is set. It also handles `BadHttpRequestException` → `400 Bad Request` with `{ ErrorCode: 0, Message }` so malformed payloads have a consistent shape with business errors. | `ExceptionEnum` | HTTP status | |-----------------|-------------| | `AccountLocked` | `423 Locked` | | `LoginRateLimited` | `429 Too Many Requests` | | `InvalidRefreshToken` / `InvalidMfaCode` / `InvalidMfaToken` | `401 Unauthorized` | | `SessionNotFound` | `404 Not Found` | | `InvalidMissionRequest` / `AircraftNotFound` | `400 Bad Request` | | `MfaAlreadyEnabled` / `MfaNotEnrolling` / `MfaNotEnabled` | `409 Conflict` | | any other | `409 Conflict` (legacy default) | ### Static helpers in `Program.cs` - `IssueDualTokens(user, authService, refreshTokens, sessionService, amr, ct)` — issues a refresh token + an access token, also auto-revokes any open mission sessions if the just-authenticated user is a `CompanionPC` (AZ-533 AC-4). - `ParseSidClaim(ClaimsPrincipal)` / `ParseUserIdClaim(ClaimsPrincipal)` — read `sid` / `nameid` claims; throw `BusinessException(InvalidRefreshToken)` (→ 401) on missing/malformed. ## 3. External API Specification > **Cycle 2 (2026-05-14) — auth modernization**: `/login` is now multi-shape (MFA branch); `/login/mfa`, `/token/refresh`, `/logout`, `/logout/all`, `/sessions/*`, `/users/me/mfa/*`, `/.well-known/jwks.json` are all new. The legacy "single JWT" response is preserved as a `Token` getter on `LoginResponse` for compatibility with old clients (= same value as `AccessToken`). ### Authentication & Sessions | Endpoint | Method | Auth | Cycle | Description | |----------|--------|------|-------|-------------| | `/login` | POST | Anonymous | AZ-531/534/537 | Validates credentials. Returns `LoginResponse` (access + refresh + sid) OR `MfaRequiredResponse` (`mfa_required: true`, short-lived `mfa_token`). Per-IP rate limited. | | `/login/mfa` | POST | Anonymous | AZ-534 | Validates the step-1 `mfa_token` + the user's TOTP / recovery code. Returns `LoginResponse`. Per-IP rate limited. | | `/token/refresh` | POST | Anonymous | AZ-531 | Rotates a refresh token. Reuse of a rotated token revokes the entire session family. | | `/logout` | POST | Authenticated | AZ-535 | Revokes the caller's current `sid` (idempotent). | | `/logout/all` | POST | Authenticated | AZ-535 | Revokes every active session for the caller's user. | | `/sessions/{sid:guid}/revoke` | POST | ApiAdmin | AZ-535 | Admin-revoke by session id. | | `/sessions/revoked` | GET | revocationReader (Service or ApiAdmin) | AZ-535 | Verifier-poll snapshot of revoked sessions still within their TTL. `since` is clamped to a 12 h floor to prevent table scans. | | `/sessions/mission` | POST | Authenticated | AZ-533 | Pilot issues a long-lived no-refresh mission token bound to one aircraft + one mission. | | `/.well-known/jwks.json` | GET | Anonymous | AZ-532 | All loaded ES256 public keys (active + retiring). `Cache-Control: public, max-age=3600`. | ### MFA | Endpoint | Method | Auth | Description | |----------|--------|------|-------------| | `/users/me/mfa/enroll` | POST | Authenticated | Starts TOTP enrollment, returns secret + otpauth URL + PNG QR. | | `/users/me/mfa/confirm` | POST | Authenticated | Confirms with a TOTP code. Returns `{ mfaEnabled: true }`. | | `/users/me/mfa/disable` | POST | Authenticated | Requires password + TOTP. Returns `{ mfaEnabled: false }`. | ### User Management | Endpoint | Method | Auth | Description | |----------|--------|------|-------------| | `/users` | POST | ApiAdmin | Creates a new user (Argon2id-hashed password, AZ-536). | | `/devices` | POST | ApiAdmin | Provisions a CompanionPC device user (returns serial + email + plaintext password once). | | `/users/current` | GET | Authenticated | Returns current user. | | `/users` | GET | ApiAdmin | Lists users (optional email/role filters). | | `/users/queue-offsets/set` | PUT | Authenticated | Updates queue offsets. | | `/users/{email}/set-role/{role}` | PUT | ApiAdmin | Changes user role. | | `/users/{email}/enable` | PUT | ApiAdmin | Enables user. | | `/users/{email}/disable` | PUT | ApiAdmin | Disables user (revokes all active sessions for that user via `SessionService`). | | `/users/{email}` | DELETE | ApiAdmin | Removes user. | ### Resource Management | Endpoint | Method | Auth | Description | |----------|--------|------|-------------| | `/resources/{dataFolder?}` | POST | Authenticated | Uploads a file (up to 200 MB). | | `/resources/list/{dataFolder?}` | GET | Authenticated | Lists files. | | `/resources/clear/{dataFolder?}` | POST | ApiAdmin | Clears folder. | ### Detection Classes | Endpoint | Method | Auth | Description | |----------|--------|------|-------------| | `/classes` | POST | ApiAdmin | Creates a detection class. | | `/classes/{id:int}` | PATCH | ApiAdmin | Partial-merge update. | | `/classes/{id:int}` | DELETE | ApiAdmin | Deletes a detection class. | ### Health | Endpoint | Method | Auth | Description | |----------|--------|------|-------------| | `/health/live` | GET | Anonymous (excluded from Swagger) | Process liveness; never touches DB. | | `/health/ready` | GET | Anonymous (excluded from Swagger) | Pings both DB connections with a 2 s timeout; 503 on failure. | ### Authorization Policies | Policy | Roles | Notes | |--------|-------|-------| | `apiAdminPolicy` | `ApiAdmin` | The "admin endpoints" policy. | | `revocationReaderPolicy` | `Service`, `ApiAdmin` | AZ-535 — verifier services authenticate as `Service`-role identities and are the only callers (besides admin) allowed to read `/sessions/revoked`. | > The `apiUploaderPolicy` from AZ-183 was removed in the post-cycle-1 revert. `RoleEnum.ResourceUploader` remains as data only. ### CORS, HSTS, HTTPS (AZ-538) - **CORS** — single origin `https://admin.azaion.com`, `AllowAnyMethod` + `AllowAnyHeader` + `AllowCredentials`. The legacy `http://` origin combined with credentials would have permitted credentialed cleartext traffic; cycle 2 removed it. - **HSTS** — non-Development only: 1 y `MaxAge`, `IncludeSubDomains`, `Preload`. - **HTTPS redirection** — non-Development only. Development skips both so `dotnet watch` on plain HTTP keeps working. ### Rate limiting (AZ-537) - **Per-IP** — ASP.NET Core `RateLimiter` middleware with a `SlidingWindowRateLimiter`. Policy `login-per-ip` is attached to `/login` and `/login/mfa`. Permit limit + window seconds come from `AuthConfig.RateLimit`. Rejection sets `429` and stamps `Retry-After`. - **Per-account** — DB-backed sliding-window check in `UserService.ValidateUser` via `IAuditLog.CountRecentFailedLogins`. Survives process restarts. - **Per-account lockout** — `LockoutOptions` in `AuthConfig`. N consecutive failures → `LockoutUntil`; subsequent logins throw `AccountLocked` with `RetryAfterSeconds`. ## 4. Data Access Patterns No direct data access — delegates to service components. The composition root also fail-fast checks on missing connection strings (`AzaionDb`, `AzaionDbAdmin`) and missing `JwtConfig` (`Issuer` + `Audience` required). ## 5. Implementation Details **State Management**: Stateless — ASP.NET Core request pipeline. **DI registrations added in cycle 2**: - `IJwtSigningKeyProvider` (singleton, eager-built before DI so it's the same instance JwtBearer's `IssuerSigningKeyResolver` uses) - `IRefreshTokenService`, `ISessionService`, `IMissionTokenService`, `IMfaService` (scoped) - `IAuditLog` (scoped) - `IDataProtectionProvider` via `AddDataProtection().SetApplicationName("Azaion.AdminApi")` — production deployments MUST set `DataProtection:KeysFolder` to a persistent volume so encrypted MFA secrets survive restarts. **Middleware pipeline (cycle 2 order)**: 1. `UseSwagger`/`UseSwaggerUI` (Development only) 2. `UseHsts` + `UseHttpsRedirection` (non-Development only) 3. `UseCors("AdminCorsPolicy")` 4. `UseAuthentication` 5. `UseAuthorization` 6. `UseRateLimiter` 7. `UseRewriter` (root → `/swagger`) 8. Endpoint mappings 9. `UseExceptionHandler` (registered last so the `BusinessExceptionHandler` exception-handler component runs) **JWT Bearer config**: - `ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256]` — pinned to ES256 so a token forged with `alg=HS256` using the public key as the HMAC secret cannot pass validation (AZ-532 AC-5). - `IssuerSigningKeyResolver` consults the same `IJwtSigningKeyProvider` instance the rest of the app uses; if the token has a `kid` it's matched, otherwise all loaded keys are returned. - `ValidateIssuer`, `ValidateAudience`, `ValidateLifetime`, `ValidateIssuerSigningKey` all true. **Key Dependencies**: | Library | Purpose | |---------|---------| | Microsoft.AspNetCore.Authentication.JwtBearer | JWT bearer middleware | | Microsoft.AspNetCore.RateLimiting | Per-IP sliding window | | Microsoft.AspNetCore.DataProtection | Encrypt MFA secrets at rest | | Microsoft.AspNetCore.Rewrite | `/` → `/swagger` redirect | | Swashbuckle.AspNetCore | Swagger/OpenAPI | | FluentValidation.AspNetCore | Request validation pipeline | | Serilog | Structured logging (Console + rolling file) | **Error Handling Strategy**: - `BusinessException` → `BusinessExceptionHandler` → per-enum status code (see table above) + optional `Retry-After`. - `BadHttpRequestException` → `400 Bad Request` with `{ ErrorCode: 0, Message }`. - FluentValidation errors → 400 via `Results.ValidationProblem`. - Unhandled → default ASP.NET Core handling. ## 6. Extensions and Helpers - `IssueDualTokens` static helper (Program.cs) - `ParseSidClaim` / `ParseUserIdClaim` static helpers (Program.cs) ## 7. Caveats & Edge Cases - All endpoints are still defined in a single `Program.cs` file — cycle 2 added significantly more endpoints; consider splitting into endpoint groups in a future cycle. - Swagger UI only available in Development. - CORS origins are hardcoded — moving to config is a follow-up. - `BusinessExceptionHandler` lives under namespace `Azaion.Common` despite the file path `Azaion.AdminApi/`. Documented as historical accident; do not "fix" without coordinated rename. - Antiforgery disabled on resource upload. - Kestrel max request body 200 MB. - The eager `JwtSigningKeyProvider` construction means a missing or malformed PEM crashes the app at startup. This is intentional — it's safer than serving requests with no signing key. ## 8. Dependency Graph **Must be implemented after**: All other components (composition root). **Blocks**: Nothing. ## 9. Logging Strategy | Log Level | When | Notes | |-----------|------|-------| | `Warning` | Business exception caught by `BusinessExceptionHandler` | Includes the full exception | | `Warning` | `BadHttpRequestException` caught | | | `Information` | Default for everything else | Serilog minimum level | **Log format**: Serilog structured logging with context enrichment. **Log storage**: Console + rolling file (`logs/log.txt`, daily rotation). ## Modules Covered - `AdminApi/Program` - `AdminApi/BusinessExceptionHandler`