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