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.
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.csalso 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 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.