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.
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 fromJWT_JWKS_URL/Jwt:JwksUrl(fail-fast if missing).- A custom
JwksRetriever : IConfigurationRetriever<JsonWebKeySet>(private nested class inJwtExtensions.cs) that wraps anIDocumentRetrieverand parses the response as aJsonWebKeySet. The stockOpenIdConnectConfigurationRetrievertargets the full OIDC discovery document, whichadmindoes 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 inJwtExtensions.cs. MinimalIConfigurationRetriever<JsonWebKeySet>implementation; ~5 lines. Exists because Microsoft does not ship a JWKS-only retriever.
8. Caveats & Edge Cases
adminreachability at startup — the first protected request blocks on the JWKS fetch. Ifadminis unreachable when that fetch happens, the request fails with a 500 (theIssuerSigningKeyResolverdelegate throws while resolving signing keys). On the local LAN this is single-digit ms typical. Once cached, subsequent requests do not calladmin.- No claim type for "user id" is consumed — only the
permissionsclaim 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. - No offline-grace-window logic in this service —
../../../suite/_docs/10_auth.mddescribes an offline JWT cache; that lives in the UI /adminconsumption pattern, not here. - Fail-fast on missing configuration:
JWT_ISSUER,JWT_AUDIENCE,JWT_JWKS_URLare all required at startup. A production deploy without any of them throwsInvalidOperationExceptionfromConfigurationResolver.ResolveRequiredOrThrowbefore the host is built. There is no hardcoded fallback (ADR-005's "dev-fallback secret" branch is obsolete for JWT). - JWKS rotation does NOT require a coordinated redeploy — when
adminrotates keys, the next refresh tick on every consumer'sConfigurationManagerpicks up the new public key. Old tokens signed by the previous key remain valid until expiry as long as the oldkidis still published. This is the major operational improvement over the legacy HS256 shared-secret model. - Algorithm pin (
ValidAlgorithms = [EcdsaSha256]) prevents the classic "HS256 confusion" attack — without the pin, an attacker who learned the JWKS public key could forgealg: HS256tokens using the public key as the HMAC secret. The pin forces ECDSA regardless of the token header'salgclaim. FLpermission code carries the legacy "Flight" name even after the service rename tomissions. 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.