Files
admin/_docs/02_document/components/05_admin_api/description.md
T
Oleksandr Bezdieniezhnykh a77b3f8a59 [AZ-529] [AZ-530] Cycle-2 documentation refresh
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>
2026-05-14 09:22:53 +03:00

12 KiB

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 BadHttpRequestException400 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 lockoutLockoutOptions 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:

  • BusinessExceptionBusinessExceptionHandler → per-enum status code (see table above) + optional Retry-After.
  • BadHttpRequestException400 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