[AZ-494] Enable JWT iss/aud validation with fail-fast startup

Option B per user decision: production ships with empty Jwt.Issuer /
Jwt.Audience in appsettings.json so the API process refuses to start
unless JWT_ISSUER + JWT_AUDIENCE env vars are supplied. Development
ships with grep-friendly DEV-ONLY- placeholders so local + docker
flows keep working unchanged.

AuthenticationServiceCollectionExtensions flips ValidateIssuer +
ValidateAudience to true and wires ValidIssuer / ValidAudience via a
new ResolveRequiredOrThrow helper that all three required values
(secret, iss, aud) now share. JwtTokenFactory.Create + CreateExpired
gain optional iss / aud parameters (default null) so existing call
sites compile unchanged. JwtTestHelpers adds MintAuthenticated /
MintExpired wrappers that resolve iss + aud from env, plus
ResolveIssuerOrThrow / ResolveAudienceOrThrow. PerfBootstrap.MintToken
+ Program.cs JWT bootstrap migrated to the new surface so the perf
harness and the integration runner both validate against the same
contract.

Adds 4 fail-fast unit tests (missing/empty issuer + audience), 2
negative integration scenarios (WrongIssuer_Returns401,
WrongAudience_Returns401), and re-tags every existing integration
mint site via MintAuthenticated.

Compose, .env.example, run-tests.sh, run-performance-tests.sh all
load + export JWT_ISSUER + JWT_AUDIENCE alongside JWT_SECRET.

Resolves F-AUTH-2 (security_report.md + owasp_review.md). AC-7
(cross-repo suite/_docs/10_auth.md write) deferred — outside this
workspace; tracked in deploy_cycle2.md R3 follow-up.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-12 02:28:48 +03:00
parent 080441db5d
commit f979e18811
27 changed files with 543 additions and 57 deletions
@@ -1,140 +0,0 @@
# JWT iss/aud validation
**Task**: AZ-494_jwt_iss_aud_validation
**Name**: JWT iss/aud validation
**Description**: Flip `ValidateIssuer` and `ValidateAudience` from `false` to `true` in `AddSatelliteJwt`, configure the expected `iss` / `aud` values via configuration, and update Bearer-token consumers and tests to mint tokens with matching claims. Closes Medium-severity finding F-AUTH-2 from the cycle-2 security audit.
**Complexity**: 2 points
**Dependencies**: AZ-487 (extends `AddSatelliteJwt` configuration); external dependency: admin team must confirm the expected `iss` / `aud` values before implementation can begin.
**Component**: WebApi (`SatelliteProvider.Api/Authentication`) + Test infrastructure
**Tracker**: AZ-494
**Epic**: none (cycle-3 security hardening)
## Problem
The cycle-2 security audit (`_docs/05_security/security_report.md`, finding F-AUTH-2) noted that `AddSatelliteJwt` currently sets `ValidateIssuer = false` and `ValidateAudience = false` (`SatelliteProvider.Api/Authentication/AuthenticationServiceCollectionExtensions.cs`). The cycle-2 deploy report R3 carries the same item as an open follow-up.
This was intentional during AZ-487 — the suite-level auth contract (`suite/_docs/10_auth.md`) does not currently mandate specific `iss`/`aud` values, and the admin team had not yet decided what those values should be. The mitigation in cycle 2 was: signature + lifetime validation alone are sufficient to keep this finding at Medium, not High. But the door is left open for token reuse across other satellite-suite services that share the same `JWT_SECRET` — a service-specific `aud` claim is the proper remediation.
OWASP A07 (`_docs/05_security/owasp_review.md`) lists iss/aud validation as part of the JWT hardening backlog; this PBI closes the remaining gap.
**Prerequisite: cross-team input required.** Before implementation can begin, the admin team must confirm:
1. The exact `iss` value the admin API stamps into tokens (e.g., `https://admin.azaion.example/`).
2. The `aud` value satellite-provider should require (e.g., `satellite-provider` or a URI).
If the admin team has not provided these values when the PBI is picked up, the implementer STOPS and surfaces the blocker to the user. No fallback / placeholder value should be hardcoded.
## Outcome
- `AddSatelliteJwt` accepts `ValidIssuer` and `ValidAudience` from configuration (env var `JWT_ISSUER` / `JWT_AUDIENCE` preferred; `Jwt:Issuer` / `Jwt:Audience` config keys as fallback).
- `TokenValidationParameters` sets `ValidateIssuer = true`, `ValidateAudience = true`, `ValidIssuer = <configured>`, `ValidAudience = <configured>`.
- Startup fails fast if `JWT_ISSUER` or `JWT_AUDIENCE` is unset — same contract as `JWT_SECRET` (cycle-2 AC-5 of AZ-487).
- Test fixtures (consolidated JWT factory, or per-project helpers if `01_consolidate_jwt_test_helpers` hasn't shipped yet) mint tokens with matching `iss` / `aud` claims so existing tests continue to pass.
- `appsettings.Development.json` includes clearly-tagged dev values for `Jwt:Issuer` and `Jwt:Audience`.
- `.env.example` documents the two new env vars.
- `docker-compose.yml` + `docker-compose.tests.yml` forward `JWT_ISSUER` / `JWT_AUDIENCE` to both the API and the test runner.
- `_docs/05_security/security_report.md` and `owasp_review.md` are updated: F-AUTH-2 status moves from `Open` to `Resolved` with a reference to this PBI.
## Scope
### Included
- Extend `AuthenticationServiceCollectionExtensions.AddSatelliteJwt` to read `JWT_ISSUER` / `JWT_AUDIENCE` from env / config and assign them to `TokenValidationParameters`.
- Add fail-fast validation: throw `InvalidOperationException` at startup if either value is missing or whitespace-only.
- Update test fixtures to mint tokens with the configured `iss` / `aud`:
- `JwtTokenFactory.Create` (or whatever shape exists post-`01_consolidate_jwt_test_helpers`) gains optional `issuer` / `audience` parameters with sensible test defaults.
- `SatelliteProvider.IntegrationTests/Program.cs` reads `JWT_ISSUER` / `JWT_AUDIENCE` from env and passes them to the default token.
- Update `appsettings.json` (empty placeholders), `appsettings.Development.json` (DEV-ONLY values).
- Update `.env.example` and `scripts/run-tests.sh` to load the two new env vars from `.env`.
- Update `_docs/02_document/modules/api_program.md` § Security and `architecture.md` § Security Architecture to document the iss/aud validation.
- Update `_docs/05_security/security_report.md` (mark F-AUTH-2 Resolved) and `owasp_review.md` § A07 (update the iss/aud bullet).
- Coordinate write-back: append a one-line "iss/aud validation enabled" entry to `suite/_docs/10_auth.md` if (and only if) the suite-level contract needs an update — open question for the admin team.
### Excluded
- Any change to the signing algorithm (still HS256).
- Any change to the `JWT_SECRET` validation, clock skew, or `RequireSignedTokens` semantics.
- Adding `nbf` (not-before) validation — already implicitly handled by `JwtSecurityToken`.
- Token expiry duration changes.
- Multi-tenant token validation (multiple valid issuers / audiences) — out of scope unless the admin team's confirmed values require it.
## Acceptance Criteria
**AC-1: Issuer validation enforced**
Given the API is running with `JWT_ISSUER=https://admin.example/`
When a request arrives with a Bearer token whose `iss` claim is `https://other.example/`
Then the request returns `401 Unauthorized` with the `WWW-Authenticate` header indicating issuer mismatch.
**AC-2: Audience validation enforced**
Given the API is running with `JWT_AUDIENCE=satellite-provider`
When a request arrives with a Bearer token whose `aud` claim is `mission-planner`
Then the request returns `401 Unauthorized` with the `WWW-Authenticate` header indicating audience mismatch.
**AC-3: Matching iss + aud accepted**
Given the API is running with configured `JWT_ISSUER` and `JWT_AUDIENCE`
When a request arrives with a Bearer token whose `iss` and `aud` claims match exactly
Then the request is authenticated and the existing endpoint handler is reached.
**AC-4: Missing config fails fast**
Given the API starts without `JWT_ISSUER` (or with empty value)
When the application boots
Then it throws `InvalidOperationException` with a clear message naming the missing env var.
**AC-5: Existing tests pass with matched fixtures**
Given the full integration suite is updated to mint tokens with the configured `iss`/`aud`
When the suite runs
Then all scenarios pass.
**AC-6: Security artifacts updated**
Given the post-PBI repo
When `_docs/05_security/security_report.md` is read
Then F-AUTH-2 is marked Resolved with a reference to this PBI's tracker ID.
**AC-7: Suite contract reflects reality**
Given the admin team's confirmed values
When implementation lands
Then `suite/_docs/10_auth.md` either documents the values or explicitly notes that satellite-provider validates them locally without dictating suite-wide values.
## Non-Functional Requirements
**Security**
- The configured `JWT_ISSUER` / `JWT_AUDIENCE` are NOT secrets — they can be public. But they MUST be operator-configurable so a production rotation (e.g., admin API URL change) does not require a code change.
**Compatibility**
- Backwards-compatible with existing token consumers ONLY IF those consumers update their token-issuance to include the new claims. This is a coordinated rollout — see Risk 1.
## Unit Tests
| AC Ref | What to Test | Required Outcome |
|--------|-------------|-----------------|
| AC-1 | `TokenValidationParameters.ValidIssuer` matches configured value | Returns the configured value |
| AC-2 | `TokenValidationParameters.ValidAudience` matches configured value | Returns the configured value |
| AC-4 | `AddSatelliteJwt` with `JWT_ISSUER` unset | Throws `InvalidOperationException` with `JWT_ISSUER` in message |
| AC-4 | `AddSatelliteJwt` with `JWT_AUDIENCE` unset | Throws `InvalidOperationException` with `JWT_AUDIENCE` in message |
## Blackbox Tests
| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|--------|------------------------|-------------|-------------------|----------------|
| AC-1 | API running with `JWT_ISSUER` set; token minted with wrong `iss` | `GET /api/satellite/tiles/latlon` with the wrong-iss token | 401 Unauthorized | Security |
| AC-2 | API running with `JWT_AUDIENCE` set; token minted with wrong `aud` | Same as AC-1 with wrong `aud` | 401 Unauthorized | Security |
| AC-3 | API running with matching values; token minted with matching `iss` + `aud` | Same probe | 200 OK | — |
## Constraints
- The configured `JWT_ISSUER` and `JWT_AUDIENCE` must come from environment / configuration — never hardcoded in source.
- Test environment uses dev-only values clearly tagged (e.g., `DEV-ONLY-iss-…`).
- Coordinated rollout: admin team confirms iss / aud values BEFORE implementation begins.
## Risks & Mitigation
**Risk 1: Cross-team coordination delay**
- *Risk*: Admin team has not yet decided on `iss` / `aud` values. Implementer is blocked.
- *Mitigation*: This PBI is explicitly gated on cross-team input. The blocker is recorded as a hard prerequisite in the task header. If the admin team is unresponsive after a reasonable window, this PBI is parked, not force-completed with placeholder values.
**Risk 2: Coordinated breakage of existing token consumers**
- *Risk*: `gps-denied-onboard` and the mission-planner UI currently mint tokens whose `iss` / `aud` may not match what we configure. Flipping the validation flag breaks them.
- *Mitigation*: Coordinate the rollout with consumer teams BEFORE this PBI ships to production. Stage in `dev` first; verify consumer-side tokens still authenticate. The deploy report (analogous to cycle-2 R1) will flag this risk.
**Risk 3: Future multi-tenant support**
- *Risk*: At some point we may need to accept tokens from multiple issuers (e.g., legacy + new admin API). Hardcoding a single `ValidIssuer` complicates that.
- *Mitigation*: The configuration shape allows array values (`TokenValidationParameters.ValidIssuers`) if/when needed. This PBI implements the single-value case; the upgrade path is straightforward.