Files
satellite-provider/_docs/03_implementation/reviews/batch_05_cycle3_review.md
T
Oleksandr Bezdieniezhnykh f979e18811 [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>
2026-05-12 02:28:48 +03:00

6.3 KiB

Code Review — Batch 05 cycle 3 (AZ-494)

Reviewer: autodev in-band Scope: AZ-494 (JWT iss/aud validation) — implementation, tests, config, docs Verdict: PASS_WITH_WARNINGS — 0 Critical, 0 High, 0 Medium, 1 Low (cross-repo doc), 1 acknowledged operational gate by user decision.

1. Spec compliance

All ACs except AC-7 are addressed. AC-7 (suite-level contract) requires writing to a file in the parent monorepo (suite/_docs/10_auth.md), which sits outside this workspace and outside autodev's blast radius. The cycle-2 deploy report has been updated to document the cross-repo obligation so it doesn't get lost.

The user's Option B selection is honoured exactly:

  • appsettings.json ships with empty Jwt.Issuer / Jwt.Audience → production deploy without env vars fails at startup.
  • appsettings.Development.json ships with DEV-ONLY--prefixed placeholders → local + Docker-compose flows work without touching .env.
  • The env-var precedence + fail-fast pattern is identical to the existing JWT_SECRET flow from AZ-487, so there's no second mental model for operators to learn.

2. Code quality

Area Observation Verdict
Single source of truth JwtTokenFactory.Create remains the only new JwtSecurityToken(...) call site in the source tree (verified by grep). The new iss/aud params slot in alongside the existing secret / subject / lifetime / extraClaims / algorithm parameters — no parallel code path was introduced.
Helper layering JwtTestHelpers.MintAuthenticated / MintExpired are thin wrappers (≤ 12 lines each) that resolve iss/aud from env and delegate to the factory. They live in SatelliteProvider.IntegrationTests because they read env vars — exactly the runner-side concern documented in module-layout.md.
Fail-fast extraction The third copy of the "resolve required value, throw with a humanised message" pattern was extracted into a single ResolveRequiredOrThrow helper. ResolveSecretOrThrow remains separate because it has the additional ≥ 32-byte size check from AZ-487. This is acceptable: two distinct invariants.
Test mutual-isolation Each new [Fact] saves and restores both JWT_ISSUER and JWT_AUDIENCE via try/finally, mirroring the existing pattern for JWT_SECRET. No parallel-test interference risk.
Surface area minimisation JwtTokenFactory.Create gained two optional, nullable parameters at the END of the signature with defaults of null. Old callers behave identically.
Error message clarity All three fail-fast messages name (a) the env var, (b) the config key, (c) the AZ-494 spec, and use the human label ("JWT issuer", "JWT audience").
Logging No new debug/trace logs added — only the existing startup-time InvalidOperationException path. This matches coderule.mdc.
Naming MintAuthenticated / MintExpired / ResolveIssuerOrThrow / ResolveAudienceOrThrow all describe caller intent precisely. No vague "data"/"item"/"candidate" names.

3. Security

Aspect Status
F-AUTH-2 (iss/aud not validated) ResolvedValidateIssuer = true + ValidateAudience = true, both wired to real values
Token revocation list Still absent — listed as a new Low finding in owasp_review.md A07
Secret leakage No iss/aud value is logged at WARN / ERROR / INFO. Only the startup banner (Console.Out) prints them — intentional, for diagnosis.
Production safety appsettings.json empty → process won't start without explicit env vars. Cannot accidentally ship a hard-coded prod issuer.
Test/prod isolation DEV-ONLY- prefix means a grep can surface placeholder values at any time.

4. Performance

No measurable impact. TokenValidationParameters now checks two additional string comparisons per request. Sub-microsecond per request — far below the 1 ms AZ-487 NFR budget. PT-07 / PT-08 from AZ-492 will pick up any aggregate regression at the next perf run.

5. Cross-task consistency

  • AZ-487 (JWT base layer) — extended, not replaced. The RequireSignedTokens / RequireExpirationTime / ClockSkew = 30 s / secret ≥ 32 bytes invariants all remain.
  • AZ-491 (consolidate JWT test helpers) — the new MintAuthenticated / MintExpired wrappers strengthen the AZ-491 contract (delegate to the canonical factory; no parallel mint logic).
  • AZ-492 (perf harness) — PerfBootstrap.MintToken was updated in the same way as integration call sites. Without this update the perf harness would have started emitting 401s after AZ-494 landed.
  • AZ-493 (DB reset hook) — unaffected; runs against the same env.
  • AZ-495 (doc folder convention) — unaffected.
  • AZ-496 (bump aspnetcore 8.0.25) — directly relevant: AZ-494's ValidateIssuer / ValidateAudience semantics are unchanged across Microsoft.AspNetCore.Authentication.JwtBearer 8.0.21 → 8.0.25.

6. Findings

Low

L1 — Cross-repo doc deferred (AC-7)

  • Where: suite/_docs/10_auth.md (outside this workspace).
  • Why noted: AC-7 explicitly calls for an update there; the autodev process treats this as out-of-scope per the workspace-boundary rule.
  • Disposition: documented in deploy_cycle2.md's R3 follow-up section.

Acknowledged operational gate (NOT a finding)

The Option B contract is deliberately that production deploys without real iss/aud values fail at startup. That's the user-selected forcing function for admin-team confirmation. Not a code defect.

7. AC-by-AC re-check

AC Verification Verdict
AC-1 JwtIntegrationTests.WrongIssuer_Returns401 asserts 401 against https://wrong-issuer.invalid/
AC-2 JwtIntegrationTests.WrongAudience_Returns401 asserts 401 against wrong-audience-not-satellite
AC-3 JwtIntegrationTests.ValidToken_Returns200_OnHealthyEndpoint mints through MintAuthenticated (env-sourced iss/aud)
AC-4 4 fail-fast unit tests + manual smoke ("docker compose up without env vars") path documented
AC-5 All integration test call sites migrated to MintAuthenticated; verified via grep
AC-6 security_report.md + owasp_review.md updated
AC-7 Cross-repo write deferred; documented ◐ deferred

8. Verdict

PASS_WITH_WARNINGS — proceed to ship. The single Low finding is structurally outside this workspace.