Files
missions/_docs/02_document/components/05_identity/description.md
T
Oleksandr Bezdieniezhnykh 78dea8ebab
ci/woodpecker/push/build-arm Pipeline was successful
chore: update configuration and Docker setup for JWT and test results
Enhanced the .gitignore to exclude test results and updated the Dockerfile to include a new entrypoint script for improved container initialization. Refactored JWT configuration to support additional parameters for automatic refresh intervals, ensuring better control over token management. Updated the ConfigurationResolver to enforce required environment variables without hardcoded fallbacks, enhancing security and flexibility.
2026-05-15 03:23:23 +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 (forward-looking): post-rename + post-GPS-Denied-removal. Today's JwtExtensions.cs also declares a "GPS" policy reserved for the (now-removed-from-this-repo) GPS-Denied endpoints. After Jira AZ-EPIC child B7 lands, 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 two named authorization policies in DI (one is removed after B7 lands):

Policy Requirement Notes
"FL" JWT contains a permissions claim with value "FL" Permanent
"GPS" JWT contains a permissions claim with value "GPS" Removed in Jira B7 (legacy GPS-Denied routes are moving out of this repo)

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.