Refreshes _docs/02_document/ to reflect the cycle-2 auth-modernization
+ CMMC hardening landings (AZ-531..AZ-538). Authoritative source for
the ripple set is ripple_log_cycle2.md.
Covered:
- architecture.md (section 1 rewritten, ADRs 6-9 added)
- data_model.md (sessions, audit_events, user columns, migrations)
- system-flows.md (F1 rewritten; F11-F17 added; F2/F7/F9 minor)
- module-layout.md (cycle-2 sub-component table)
- diagrams/flows/flow_login.md (dual-token + MFA)
- components/{01_data_layer,03_auth_and_security,05_admin_api}
- modules/ (12 new, 8 modified — full Argon2id/ES256/MFA/refresh
/mission/session/audit/jwks rollup)
- tests/{blackbox,security,traceability-matrix}
Step 13 (Update Docs) output for cycle 2.
Co-authored-by: Cursor <cursoragent@cursor.com>
10 KiB
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 inarchitecture.mdwas 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
/loginshape is now dual-token (access + refresh) when MFA is off, orMfaRequiredResponsewhen 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 beforeapp.Build()soJwtBearerand DI share one instance) — AZ-532IUserService→UserService(Scoped)IAuthService→AuthService(Scoped)IRefreshTokenService→RefreshTokenService(Scoped) — AZ-531ISessionService→SessionService(Scoped) — AZ-535IMissionTokenService→MissionTokenService(Scoped) — AZ-533IMfaService→MfaService(Scoped) — AZ-534IResourcesService→ResourcesService(Scoped)IDetectionClassService→DetectionClassService(Scoped)IAuditLog→AuditLog(Scoped) — AZ-537 / AZ-534IDbFactory→DbFactory(Singleton)ICache→MemoryCache(Scoped)LazyCacheviaAddLazyCache()- ASP.NET Core
DataProtection—SetApplicationName("Azaion.AdminApi"); ifDataProtection:KeysFolderis set, persisted to filesystem (production requirement for MFA-secret durability) — AZ-534 - FluentValidation validators auto-discovered from
RegisterUserValidatorassembly BusinessExceptionHandlerregistered as exception handler
Middleware Pipeline
- Swagger (Development only)
- HSTS + HTTPS redirection (non-Development only) — AZ-538
- CORS (
AdminCorsPolicy) - Authentication (JWT Bearer with
ValidAlgorithms = [ES256]and anIssuerSigningKeyResolverthat picks bykidfromIJwtSigningKeyProvider.All) - Authorization
- Rate limiter (
UseRateLimiter) — AZ-537 - URL rewrite: root
/→/swagger - Exception handler
Authorization Policies
apiAdminPolicy: requiresRoleEnum.ApiAdminrolerevocationReaderPolicy: requiresRoleEnum.ServiceORRoleEnum.ApiAdmin(gates/sessions/revoked) — AZ-535
Rate Limit Policies
LoginPerIpPolicy = "login-per-ip"— sliding-window limiter keyed onRemoteIpAddress. Configured fromAuthConfig.RateLimit.PerIpPermitLimit/PerIpWindowSeconds. On rejection, setsRetry-Afterfrom theRetryAfterlease metadata. Applied to/loginand/login/mfa.
Configuration Sections
JwtConfig— JWT signing/validation (Issuer, Audience, KeysFolder, ActiveKid, AccessTokenLifetimeMinutes)SessionConfig— refresh-token sliding/absolute window (RefreshSlidingHours, RefreshAbsoluteHours) — AZ-531AuthConfig— rate-limit and lockout knobs — AZ-537ConnectionStrings— DB connectionsResourcesConfig— 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 cleartexthttp://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 thesidclaim; throwsInvalidRefreshToken(401) if missing/malformed.ParseUserIdClaim(ClaimsPrincipal)— extractsNameIdentifier; same error semantics.IssueDualTokens(...)— shared by/loginand/login/mfa; callsIRefreshTokenService.IssueForNewLogin,IAuthService.CreateToken, plusISessionService.RevokeMissionsForAircraftwhen the caller isRoleEnum.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
ResourcesServiceandJwtSigningKeyProviderfor PEM keys)
Security
- JWT Bearer with full validation:
ValidateIssuer,ValidateAudience,ValidateLifetime,ValidateIssuerSigningKey,ValidAlgorithms = [ES256](AZ-532 AC-5). - Issuer signing keys resolved per-
kidviaIJwtSigningKeyProvider; supports rotation overlap. - Public JWKS endpoint exposes only public components (
x/yfor EC);Cache-Control: public, max-age=3600. - Per-IP sliding-window rate limit on
/loginand/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
Servicerole gates the verifier-poll feed.
Tests
None directly; tested through e2e/Azaion.E2E/Tests/ (Login, RefreshToken, RateLimitLockout, Logout, Jwks, MissionToken, MfaEnrollment, MfaLogin, PasswordHashing).