[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
@@ -0,0 +1,89 @@
# Batch Report — Batch 05 cycle 3
**Batch**: 05 (cycle 3)
**Tasks**: AZ-494 (JWT iss/aud validation — enable + configure)
**Date**: 2026-05-12
## Task Results
| Task | Status | Files Modified | Tests | AC Coverage | Issues |
|------|--------|---------------|-------|-------------|--------|
| AZ-494_jwt_iss_aud_validation | Done (Option B) | 1 prod source + 1 TestSupport + 4 IntegrationTests + 1 unit test + 4 config + 4 scripts/compose + 6 docs | 4 new unit tests + 2 new integration scenarios; existing 13 unit + 8 integration cases re-tagged to mint via env iss/aud | 6/7 ACs addressed; AC-7 deferred (cross-repo `suite/_docs/10_auth.md` write) | 0 blockers; 1 Low (cross-repo doc), 1 acknowledged operational gate (admin team must supply real prod iss/aud — fail-fast at deploy enforces this). |
## AC Test Coverage: 6 of 7 addressed (AC-7 deferred — cross-repo)
## Code Review Verdict: pending (this batch report precedes per-batch review)
## Auto-Fix Attempts: 0
## Stuck Agents: None
## What was implemented
The task spec offered three options for handling the blocker (admin team has not yet confirmed production `iss` / `aud` values). The user selected **Option B**: implement the full validation plumbing now with **clearly-tagged DEV-only values** in `appsettings.Development.json` so local tests work, but leave `appsettings.json` empty so production deploys without explicit `JWT_ISSUER` / `JWT_AUDIENCE` environment variables fail at startup, not at runtime.
The implementation therefore:
1. Enables `ValidateIssuer = true` and `ValidateAudience = true` in the production token-validation pipeline.
2. Sources both values from `JWT_ISSUER` / `JWT_AUDIENCE` env vars with `Jwt:Issuer` / `Jwt:Audience` config keys as fallback (same resolution pattern as `JWT_SECRET` from AZ-487).
3. Throws `InvalidOperationException` at startup if either value is missing or whitespace — the message names the env var, the config key, and the AZ-494 task spec.
4. Threads `iss` / `aud` through the canonical `SatelliteProvider.TestSupport.JwtTokenFactory.Create` surface (the post-AZ-491 single source of truth) so every existing test path continues to mint matching tokens.
5. Adds a thin convenience layer in `SatelliteProvider.IntegrationTests/JwtTestHelpers` (`MintAuthenticated`, `MintExpired`, `ResolveIssuerOrThrow`, `ResolveAudienceOrThrow`) so integration test call sites stay terse and centrally fail-fast on missing env vars.
6. Adds two new negative integration tests (`WrongIssuer_Returns401`, `WrongAudience_Returns401`) and four new unit fail-fast tests (`AddSatelliteJwt_ThrowsOnMissingIssuer` / `_ThrowsOnEmptyIssuer` / `_ThrowsOnMissingAudience` / `_ThrowsOnEmptyAudience`).
7. Updates security artefacts (`security_report.md` flips F-AUTH-2 to **RESOLVED**, `owasp_review.md` A07 reflects same), the architecture + module docs (`architecture.md`, `modules/api_program.md`, `modules/tests_integration.md`, `modules/tests_unit.md`), the cycle-2 deploy report (R3 follow-up note), and the traceability matrix (5 new rows for AZ-494 AC-1..AC-7).
### Added
- `SatelliteProvider.IntegrationTests/JwtTestHelpers.cs` — three new public helpers:
- `ResolveIssuerOrThrow()` / `ResolveAudienceOrThrow()` — mirror the existing `ResolveSecretOrThrow` pattern (read env, throw `InvalidOperationException` with a humanised message if missing).
- `MintAuthenticated(...)` — convenience wrapper: defaults issuer + audience to the env-resolved values, accepts explicit overrides for negative test cases.
- `MintExpired(...)` — convenience wrapper for the existing `JwtTokenFactory.CreateExpired` overload, same env-resolution behaviour.
- 4 unit tests in `SatelliteProvider.Tests/Authentication/AuthenticationServiceCollectionExtensionsTests.cs`: `AddSatelliteJwt_ThrowsOnMissingIssuer`, `AddSatelliteJwt_ThrowsOnEmptyIssuer`, `AddSatelliteJwt_ThrowsOnMissingAudience`, `AddSatelliteJwt_ThrowsOnEmptyAudience`.
- 2 integration tests in `SatelliteProvider.IntegrationTests/JwtIntegrationTests.cs`: `WrongIssuer_Returns401`, `WrongAudience_Returns401`.
### Modified
- `SatelliteProvider.Api/Authentication/AuthenticationServiceCollectionExtensions.cs` — added `JwtIssuerEnvVar` / `JwtIssuerConfigKey` / `JwtAudienceEnvVar` / `JwtAudienceConfigKey` constants; flipped `ValidateIssuer` / `ValidateAudience` to `true` and wired `ValidIssuer` / `ValidAudience`; extracted a single `ResolveRequiredOrThrow` helper that all three required values now flow through.
- `SatelliteProvider.TestSupport/JwtTokenFactory.cs``Create(...)` and `CreateExpired(...)` gained optional `issuer` / `audience` parameters defaulted to `null` (old call sites still produce identical tokens; new call sites pass real values).
- `SatelliteProvider.IntegrationTests/PerfBootstrap.cs``MintToken()` now also resolves iss + aud and passes them through to `JwtTokenFactory.Create`. Without this the perf harness's bearer token would fail validation against the AZ-494-hardened API.
- `SatelliteProvider.IntegrationTests/Program.cs` — JWT bootstrap now resolves all three required values (secret + iss + aud) inside a single `try/catch` and prints them at startup. The `MintAuthenticated` helper replaces the inline `JwtTokenFactory.Create` call that used to live in `Main`.
- `SatelliteProvider.IntegrationTests/JwtIntegrationTests.cs``AnonymousRequest_*`, `ExpiredToken_Returns401`, `InvalidSignature_Returns401`, `ValidToken_Returns200_OnHealthyEndpoint` all migrated to `JwtTestHelpers.MintAuthenticated` / `JwtTestHelpers.MintExpired`. Adds the two new scenarios to `RunAll`.
- `SatelliteProvider.IntegrationTests/UavUploadTests.cs` — every `JwtTokenFactory.Create(...)` call replaced with `JwtTestHelpers.MintAuthenticated(...)`. Direct `using SatelliteProvider.TestSupport;` dropped (no longer needed at this seam).
- `SatelliteProvider.Tests/Authentication/AuthenticationServiceCollectionExtensionsTests.cs` — existing `_ConfiguresTokenValidationParameters_AsPerContract` and `_PrefersEnvironmentVariableOverConfiguration` cases updated to assert the AZ-494 contract; `BuildValidConfiguration()` now seeds iss + aud; static `[Fact]` setup/teardown saves and restores both env vars.
- `SatelliteProvider.Api/appsettings.json` — added empty `Jwt.Issuer` and `Jwt.Audience` keys (the fail-fast contract requires env-var or non-empty config; empty here forces ops to supply env vars in prod).
- `SatelliteProvider.Api/appsettings.Development.json` — placeholder dev values prefixed `DEV-ONLY-` so a grep for that prefix surfaces every "remember to replace" site.
- `.env.example` — documents `JWT_ISSUER` and `JWT_AUDIENCE` with the fail-fast contract and a one-line example value pair (same `DEV-ONLY-` prefix).
- `docker-compose.yml` / `docker-compose.tests.yml``JWT_ISSUER` and `JWT_AUDIENCE` now passed through to both the `api` and `integration-tests` services.
- `scripts/run-tests.sh` / `scripts/run-performance-tests.sh``.env` load + fail-fast checks for both new vars mirror the existing `JWT_SECRET` flow; both are exported so Docker Compose and the perf bootstrap see them.
### Documentation
- `_docs/02_document/architecture.md` — token contract bullet + Security Architecture authentication paragraph updated.
- `_docs/02_document/modules/api_program.md` — JWT authentication section + Configuration section.
- `_docs/02_document/modules/tests_integration.md` — env-var prerequisites updated; `JwtIntegrationTests` and `JwtTestHelpers` entries describe the new AZ-494 surface.
- `_docs/02_document/modules/tests_unit.md``AuthenticationServiceCollectionExtensionsTests` entry now lists the four AZ-494 fail-fast cases plus the updated config-precedence and contract assertions.
- `_docs/02_document/tests/traceability-matrix.md` — 5 new rows for AC-1..AC-7 (AC-7 marked deferred); the AZ-487 NFR rows updated to acknowledge the AZ-494 extension.
- `_docs/03_implementation/deploy_cycle2.md` — R3 follow-up note marked **RESOLVED in cycle 3 (AZ-494)** with the residual operational gate spelled out.
- `_docs/05_security/security_report.md` — F-AUTH-2 flipped to **RESOLVED cycle 3 (AZ-494)**; verdict reconciliation + recommendations updated.
- `_docs/05_security/owasp_review.md` — A07 row updated; new Low finding for residual "no token revocation list" gap noted as a separate follow-up.
## AC verification
| AC | Description | Verification |
|---|---|---|
| AC-1 | Wrong `iss` token returns 401 | `JwtIntegrationTests.WrongIssuer_Returns401` (integration; runtime gate at Step 16) |
| AC-2 | Wrong `aud` token returns 401 | `JwtIntegrationTests.WrongAudience_Returns401` (integration; runtime gate at Step 16) |
| AC-3 | Matching iss + aud accepted | `JwtIntegrationTests.ValidToken_Returns200_OnHealthyEndpoint` retains its assertion; tokens now minted via env-resolved iss/aud through `MintAuthenticated` |
| AC-4 | Missing config fails fast | 4 new unit tests in `AuthenticationServiceCollectionExtensionsTests`; manual `docker compose up` without env vars throws `InvalidOperationException` per the contract |
| AC-5 | Existing tests pass with matched fixtures | All `JwtTokenFactory.Create` direct call sites in the integration project removed in favour of `MintAuthenticated` (verified via `Grep`); unit suite still mints via the factory with explicit iss/aud |
| AC-6 | Security artefacts updated | `security_report.md` + `owasp_review.md` updated this batch |
| AC-7 | Suite-level contract reflects validation | **Deferred**`suite/_docs/10_auth.md` lives in the parent monorepo, outside this workspace. Cross-repo write is out of scope for satellite-provider's autodev. `deploy_cycle2.md` notes the cross-repo obligation. |
## Static / process checks
- `dotnet format whitespace --verify-no-changes` will run as part of `scripts/run-tests.sh` at Step 16.
- `ReadLints` on every modified C# file returned 0 warnings.
- Repo-wide grep for `JwtTokenFactory.Create` confirms only `SatelliteProvider.Tests` (unit, which intentionally exercises the factory directly with explicit iss/aud) + `PerfBootstrap.MintToken` + `JwtTestHelpers.MintAuthenticated` / `MintExpired` call it now — the integration suite never bypasses the env-resolution wrapper.
- `.env.example` keeps the `DEV-ONLY-` prefix grep-friendly so a future ops review can surface every placeholder site at once.
## Risks & follow-ups
- **Operational gate** (intentional, by Option B) — production deploy WITHOUT `JWT_ISSUER` + `JWT_AUDIENCE` env vars will fail at process start with the `InvalidOperationException` message documented above. This is the controlled deploy-time forcing function for admin-team confirmation.
- **Cross-repo doc** (AC-7) — `suite/_docs/10_auth.md` write deferred. Will surface as a `_docs/_process_leftovers/` entry if the suite repo still needs the update after this autodev finishes.