mirror of
https://github.com/azaion/missions.git
synced 2026-06-21 09:51:07 +00:00
chore: update configuration and Docker setup for JWT and test results
ci/woodpecker/push/build-arm Pipeline was successful
ci/woodpecker/push/build-arm Pipeline was successful
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.
This commit is contained in:
@@ -2,96 +2,133 @@
|
||||
|
||||
**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.
|
||||
**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`
|
||||
**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.
|
||||
**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.
|
||||
**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**: None internally.
|
||||
**Upstream dependencies**: `Infrastructure/ConfigurationResolver.cs` (shared with `07_host`) for fail-fast value resolution.
|
||||
|
||||
**Downstream consumers**: `07_host` (calls `AddJwtAuth(jwtSecret)` once); `01_vehicle_catalog`, `02_mission_planning` (controllers carry `[Authorize(Policy = "FL")]`).
|
||||
**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, string jwtSecret);
|
||||
public static IServiceCollection AddJwtAuth(this IServiceCollection services, IConfiguration configuration);
|
||||
```
|
||||
|
||||
Side effects: registers `JwtBearerDefaults.AuthenticationScheme` and one named authorization policy in DI:
|
||||
`AddJwtAuth` reads three required values via `ConfigurationResolver.ResolveRequiredOrThrow`:
|
||||
|
||||
| Policy | Requirement |
|
||||
|--------|-------------|
|
||||
| `"FL"` | JWT contains a `permissions` claim with value `"FL"` |
|
||||
| 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`) |
|
||||
|
||||
## 3. Suite-wide JWT pattern
|
||||
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.
|
||||
|
||||
This is the canonical "every backend service" identity model in the Azaion suite. Per `../../../suite/_docs/00_top_level_architecture.md` and `../../../suite/_docs/10_auth.md`:
|
||||
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 │
|
||||
│ │ ◄────────────── │ mints HS256 JWT │
|
||||
│ │ Bearer JWT │ (claim: permissions)│
|
||||
└──────────┬──────────┘ └──────────────────────┘
|
||||
│ Bearer JWT (the SAME token reused for every service)
|
||||
│
|
||||
├──────────────────► annotations (.NET, edge) -- ANN claim
|
||||
├──────────────────► missions (.NET, edge) -- FL claim ◄── this service
|
||||
├──────────────────► satellite-provider (.NET, remote) -- ADM claim
|
||||
└──────────────────► (any future .NET service)
|
||||
│ │ ◄────────────── │ 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
|
||||
```
|
||||
|
||||
Every service (admin, annotations, missions, satellite-provider, ...) shares one HMAC secret (`JWT_SECRET`) and validates tokens locally with no network round-trip. The user logs in once at the UI; the resulting bearer token is reusable across every service. **This service neither issues tokens nor talks to the central user DB** -- it only validates.
|
||||
`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 here require `FL`.
|
||||
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.
|
||||
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.
|
||||
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
|
||||
|
||||
**Algorithm**: HMAC-SHA256 signature validation via `SymmetricSecurityKey(UTF-8(jwtSecret))`. Matches the suite-wide shared-secret model.
|
||||
**Mechanism**: ECDSA-SHA256 signature validation against public keys retrieved from `admin`'s JWKS endpoint. The keys are wrapped in a `ConfigurationManager<JsonWebKeySet>` configured with:
|
||||
|
||||
**Token validation flags**:
|
||||
- `ValidateIssuerSigningKey = true`
|
||||
- `ValidateLifetime = true` (with `ClockSkew = 1 minute` -- tighter than .NET's 5-minute default)
|
||||
- `ValidateIssuer = false`, `ValidateAudience = false` -- `iss` / `aud` NOT enforced (consistent with shared-secret intra-suite model). Per the CMMC L2 scorecard (`../../../suite/_docs/05_security/cmmc_l2_scorecard.md` row 3), this is a known finding tracked at the suite level under AZ-487/AZ-494; the remediation will copy the `satellite-provider` pattern across `annotations` and `missions`.
|
||||
- `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.Tokens` | (transitive) | `SymmetricSecurityKey`, `TokenValidationParameters` |
|
||||
| `Microsoft.IdentityModel.Protocols` | (transitive) | `ConfigurationManager<T>`, `HttpDocumentRetriever`, `IConfigurationRetriever<T>`, `IDocumentRetriever` |
|
||||
| `Microsoft.IdentityModel.Tokens` | (transitive) | `JsonWebKeySet`, `TokenValidationParameters`, `SecurityAlgorithms` |
|
||||
|
||||
## 7. Extensions and Helpers
|
||||
|
||||
None.
|
||||
- `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. **Shared-secret trust model** -- any service that knows `JWT_SECRET` can mint tokens this API will accept. Not safe for multi-tenant or third-party token issuance. Consistent with the rest of the suite; tightening this is suite-wide work, not a per-service decision.
|
||||
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. **Hardcoded fallback secret** in `Program.cs` (`"development-secret-key-min-32-chars!!"`) is dev-only. Production deployments MUST set `JWT_SECRET`.
|
||||
5. **`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`.
|
||||
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**: nothing.
|
||||
**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`.
|
||||
|
||||
@@ -99,4 +136,4 @@ None.
|
||||
|
||||
## 10. Logging Strategy
|
||||
|
||||
ASP.NET Core's JwtBearer handler logs token validation outcomes at default levels (Information / Debug). Not customized.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user