Files
missions/_docs/02_document/components/05_identity/description.md
T
Oleksandr Bezdieniezhnykh 2840ccb9b6
ci/woodpecker/push/build-arm Pipeline was successful
refactor: rename project from Flights to Missions and update related components
This commit transitions the project from Azaion.Flights to Azaion.Missions, updating namespaces, DTOs, services, and database entities accordingly. The Docker configuration and entry points have been modified to reflect the new project structure. Additionally, the README and documentation have been updated to clarify the ongoing renaming process and its implications. All references to flights have been replaced with missions, ensuring consistency across the codebase.
2026-05-15 04:35:49 +03:00

11 KiB

05 — Identity & Authorization

Spec source: ../../../suite/_docs/10_auth.md (suite-wide JWT model), ../../../suite/_docs/00_roles_permissions.md (the FL permission code).

Implementation status: implemented. Single policy FL is declared and consumed by every controller in the post-rename target scope.

Note

: B7 has landed (2026-05-15). The "GPS" policy and the GPS-Denied entities (Orthophoto, GpsCorrection) have been removed from this service. Only "FL" remains.

Files: Auth/JwtExtensions.cs, Infrastructure/ConfigurationResolver.cs (consumed for fail-fast value resolution)

1. High-Level Overview

Purpose: Validate JWT bearer tokens issued by the remote admin service and expose the named authorization policy (FL) used by controllers in the feature components. This service does not issue tokens — it consumes them.

Architectural pattern: ASP.NET Core extension method (AddJwtAuth) configuring IServiceCollection at DI time. JWT signature validation is asymmetric (ECDSA-SHA256) against public keys retrieved from admin's JWKS endpoint and cached locally; admin is not contacted on the request path after the first JWKS fetch.

Upstream dependencies: Infrastructure/ConfigurationResolver.cs (shared with 07_host) for fail-fast value resolution.

Downstream consumers: 07_host (calls AddJwtAuth(builder.Configuration) once); 01_vehicle_catalog, 02_mission_planning (controllers carry [Authorize(Policy = "FL")]).

2. Internal Interface

public static IServiceCollection AddJwtAuth(this IServiceCollection services, IConfiguration configuration);

AddJwtAuth reads three required values via ConfigurationResolver.ResolveRequiredOrThrow:

Env var Config key Purpose
JWT_ISSUER Jwt:Issuer Expected iss claim value
JWT_AUDIENCE Jwt:Audience Expected aud claim value
JWT_JWKS_URL Jwt:JwksUrl HTTPS URL of admin's JWKS document (e.g. https://admin.azaion/.well-known/jwks.json)

Each value is resolved env-var-first, then config-key, then throws InvalidOperationException at startup. There is no dev fallback. The legacy JWT_SECRET env var is no longer consulted.

Side effects: registers JwtBearerDefaults.AuthenticationScheme and one named authorization policy in DI:

Policy Requirement Notes
"FL" JWT contains a permissions claim with value "FL" Only policy declared by this service. The legacy "GPS" policy was removed when B7 landed (2026-05-15).

3. JWT model (this service) vs. suite-wide pattern

This service's implementation is described in code below. The suite-wide pattern lives in ../../../suite/_docs/00_top_level_architecture.md and ../../../suite/_docs/10_auth.md — those documents currently describe the legacy HS256 / shared-secret model and have not yet been updated to reflect the ECDSA-on-JWKS evolution captured here. The drift between this service and the suite docs is flagged in _docs/02_document/05_drift_findings_2026-05-14.md and will be picked up at the suite level on the next suite /autodev invocation. The remaining .NET consumers (annotations, satellite-provider) may or may not have made the same transition; their docs are the source of truth for their own implementation.

What is verified against Auth/JwtExtensions.cs today:

┌─────────────────────┐                 ┌──────────────────────┐
│ Operator UI         │  POST /login    │ admin (.NET, remote) │
│ (React, edge)       │ ──────────────► │ central user DB      │
│                     │ ◄────────────── │ ECDSA-signs JWT,     │
│                     │   Bearer JWT    │ exposes JWKS         │
└──────────┬──────────┘                 └──────┬───────────────┘
           │ Bearer JWT                        │
           │                                   │ /.well-known/jwks.json
           │                                   │ (HTTPS, fetched once at startup,
           │                                   │  cached by ConfigurationManager,
           │                                   │  refreshed on default schedule)
           └────────────► missions ◄───────────┘
                          (this service)
                          validates: ECDSA-SHA256 signature,
                                     iss = JWT_ISSUER,
                                     aud = JWT_AUDIENCE,
                                     exp (with 30s clock skew),
                                     alg pinned to EcdsaSha256

admin holds the private ECDSA key and signs tokens. This service fetches the public JWKS document from admin once at startup (on the first protected request after process start) and caches it. Request-path validation is purely cryptographic against the cached keys; admin is not contacted per request. The user logs in once at the UI; the resulting bearer token is reusable across every backend service for its lifetime.

The permissions claim drives per-service [Authorize(Policy = "...")] checks. The role → permission matrix lives in ../../../suite/_docs/00_roles_permissions.md. All routes in 01_vehicle_catalog and 02_mission_planning require FL.

4. External API

None directly. Auth contract is observable only via 401 Unauthorized / 403 Forbidden on protected routes, plus the HTTPS JWKS fetch to admin at startup (out-of-band).

5. Data Access Patterns

None against the local PostgreSQL. One outbound HTTPS GET to the configured JWT_JWKS_URL at process start, cached by ConfigurationManager<JsonWebKeySet> and refreshed on its default schedule (matches admin's Cache-Control: public, max-age=3600 on the JWKS endpoint).

6. Implementation Details

Mechanism: ECDSA-SHA256 signature validation against public keys retrieved from admin's JWKS endpoint. The keys are wrapped in a ConfigurationManager<JsonWebKeySet> configured with:

  • jwksUrl — resolved at startup from JWT_JWKS_URL / Jwt:JwksUrl (fail-fast if missing).
  • A custom JwksRetriever : IConfigurationRetriever<JsonWebKeySet> (private nested class in JwtExtensions.cs) that wraps an IDocumentRetriever and parses the response as a JsonWebKeySet. The stock OpenIdConnectConfigurationRetriever targets the full OIDC discovery document, which admin does not publish — only the JWKS endpoint is exposed — so the minimal retriever is used.
  • HttpDocumentRetriever { RequireHttps = true } — non-HTTPS JWKS URLs are rejected at configuration time.

Token validation parameters (TokenValidationParameters):

Parameter Value
ValidateIssuer true
ValidIssuer <resolved JWT_ISSUER>
ValidateAudience true
ValidAudience <resolved JWT_AUDIENCE>
ValidateLifetime true
ValidateIssuerSigningKey true
ValidAlgorithms [SecurityAlgorithms.EcdsaSha256]
RequireSignedTokens true
RequireExpirationTime true
ClockSkew TimeSpan.FromSeconds(30)
IssuerSigningKeyResolver Delegate that fetches the cached JsonWebKeySet and returns the matching kid's keys (or all keys if kid is empty)

Key Dependencies:

Library Version Purpose
Microsoft.AspNetCore.Authentication.JwtBearer 10.0.5 JWT bearer middleware + handler
Microsoft.IdentityModel.Protocols (transitive) ConfigurationManager<T>, HttpDocumentRetriever, IConfigurationRetriever<T>, IDocumentRetriever
Microsoft.IdentityModel.Tokens (transitive) JsonWebKeySet, TokenValidationParameters, SecurityAlgorithms

7. Extensions and Helpers

  • JwksRetriever — private nested class in JwtExtensions.cs. Minimal IConfigurationRetriever<JsonWebKeySet> implementation; ~5 lines. Exists because Microsoft does not ship a JWKS-only retriever.

8. Caveats & Edge Cases

  1. admin reachability at startup — the first protected request blocks on the JWKS fetch. If admin is unreachable when that fetch happens, the request fails with a 500 (the IssuerSigningKeyResolver delegate throws while resolving signing keys). On the local LAN this is single-digit ms typical. Once cached, subsequent requests do not call admin.
  2. No claim type for "user id" is consumed — only the permissions claim is checked. Services don't know who is calling them; per-user audit trails / business rules cannot be enforced at the service layer today. When a future feature needs an "applied by" attribution this gap will need to close.
  3. No offline-grace-window logic in this service../../../suite/_docs/10_auth.md describes an offline JWT cache; that lives in the UI / admin consumption pattern, not here.
  4. Fail-fast on missing configuration: JWT_ISSUER, JWT_AUDIENCE, JWT_JWKS_URL are all required at startup. A production deploy without any of them throws InvalidOperationException from ConfigurationResolver.ResolveRequiredOrThrow before the host is built. There is no hardcoded fallback (ADR-005's "dev-fallback secret" branch is obsolete for JWT).
  5. JWKS rotation does NOT require a coordinated redeploy — when admin rotates keys, the next refresh tick on every consumer's ConfigurationManager picks up the new public key. Old tokens signed by the previous key remain valid until expiry as long as the old kid is still published. This is the major operational improvement over the legacy HS256 shared-secret model.
  6. Algorithm pin (ValidAlgorithms = [EcdsaSha256]) prevents the classic "HS256 confusion" attack — without the pin, an attacker who learned the JWKS public key could forge alg: HS256 tokens using the public key as the HMAC secret. The pin forces ECDSA regardless of the token header's alg claim.
  7. FL permission code carries the legacy "Flight" name even after the service rename to missions. The plan documents this explicitly: changing the permission code is a fleet-wide auth change (would break every issued token until new ones are minted) and is NOT in this Epic's scope. Tracked as a TODO in ../../../suite/_docs/00_roles_permissions.md.

9. Dependency Graph

Must be implemented after: Infrastructure/ConfigurationResolver.cs (the fail-fast resolver — shared with 07_host).

Can be implemented in parallel with: 04_persistence, 06_http_conventions.

Blocks: 07_host, 01_vehicle_catalog, 02_mission_planning.

10. Logging Strategy

ASP.NET Core's JwtBearer handler logs token validation outcomes at default levels (Information / Debug). Not customized. The custom JwksRetriever does not emit logs of its own; the ConfigurationManager<JsonWebKeySet> may log refresh failures at Warning per its built-in instrumentation.