# 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 ```csharp 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` 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` configured with: - `jwksUrl` — resolved at startup from `JWT_JWKS_URL` / `Jwt:JwksUrl` (fail-fast if missing). - A custom `JwksRetriever : IConfigurationRetriever` (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` | `` | | `ValidateAudience` | `true` | | `ValidAudience` | `` | | `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`, `HttpDocumentRetriever`, `IConfigurationRetriever`, `IDocumentRetriever` | | `Microsoft.IdentityModel.Tokens` | (transitive) | `JsonWebKeySet`, `TokenValidationParameters`, `SecurityAlgorithms` | ## 7. Extensions and Helpers - `JwksRetriever` — private nested class in `JwtExtensions.cs`. Minimal `IConfigurationRetriever` 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` may log refresh failures at Warning per its built-in instrumentation.