[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
+3 -2
View File
@@ -53,7 +53,7 @@ Cycle 1's A01 / A07 verdicts were `N/A (with caveat)` because the service shippe
| A04 | Cryptographic Failures | N/A | **PASS** | HS256 token validation uses `Microsoft.IdentityModel`'s `SymmetricSecurityKey` with `RequireSignedTokens = true` and `RequireExpirationTime = true`. The `alg=none` bypass is blocked by `RequireSignedTokens`; algorithm-confusion is bounded because only one signing key is registered. Secret length ≥ 32 bytes enforced at startup. |
| A05 | Injection | PASS | **PASS** | No new SQL / shell / template surfaces. The new JSON parse (`PermissionsAuthorizationHandler`) runs on signature-validated token bytes — see F-UAV-2 disposition. |
| A06 | Insecure Design | FAIL (S3, S4, I3) | **FAIL** (+ F-AUTH-3, F-UAV-3) | Rate limiting still absent (now also a 401-flood vector). UAV reject reasons disclose gate structure — accepted UX trade-off, flagged for operator awareness. |
| A07 | Identification & Authentication Failures | N/A (with caveat) | **PASS_WITH_WARNINGS** (+ F-AUTH-2) | HS256 with secret ≥ 32 bytes; lifetime + signature validation; ClockSkew = 30 s. **Warning**: `ValidateIssuer = false`, `ValidateAudience = false` per the suite contract — any service that holds `JWT_SECRET` can mint tokens accepted here. Track until admin team defines `iss`/`aud`. No token revocation list — leaked tokens stay valid until `exp`. |
| A07 | Identification & Authentication Failures | N/A (with caveat) | **PASS_WITH_WARNINGS** **PASS_WITH_WARNINGS** (cycle 3: F-AUTH-2 resolved by AZ-494) | HS256 with secret ≥ 32 bytes; lifetime + signature validation; ClockSkew = 30 s. **Cycle 3 (AZ-494)**: `ValidateIssuer` / `ValidateAudience` now `true`; values sourced from `JWT_ISSUER` / `JWT_AUDIENCE` env vars with fail-fast contract. Production iss/aud values are admin-team-confirmed at deploy time. Remaining warning: no token revocation list — leaked tokens stay valid until `exp`. |
| A08 | Software or Data Integrity Failures | PASS | **PASS** | AZ-488 file-first-then-row write order documented; same migration / CI discipline as cycle 1. |
| A09 | Security Logging Failures | PASS_WITH_WARNINGS (I4) | **PASS_WITH_WARNINGS** (unchanged) | No new logging changes; 401 responses are not currently aggregated for alerting (out of scope for internal service). |
| A10 | Mishandling of Exceptional Conditions | PASS | **PASS** | UAV decode failures wrapped in scoped `try/catch` for `UnknownImageFormatException` / `InvalidImageContentException` — produce structured `INVALID_FORMAT` rejects, no stack-trace leak. SEC-11 test verifies reject details have no path / exception-type leakage.
@@ -68,5 +68,6 @@ Cycle 1's A01 / A07 verdicts were `N/A (with caveat)` because the service shippe
| A03 | F-DEPS-UAV — ImageSharp decode exposure widened | Medium | Phase 1 |
| A06 | F-AUTH-3 — rate-limit gap now also covers 401 floods | Low (recurrence of I3) | Phase 2 |
| A06 | F-UAV-3 — reject reasons disclose gate structure | Informational (accepted) | Phase 2 |
| A07 | F-AUTH-2 — `iss`/`aud` not validated; no revocation list | Medium | Phase 2 |
| A07 | F-AUTH-2 — `iss`/`aud` not validated | Medium → **Resolved cycle 3 (AZ-494)** | n/a |
| A07 | No token revocation list (residual after AZ-494) | Low | Phase 2 — out of scope until requirement emerges |
| (claim handler) | F-UAV-2 — `JsonDocument.Parse` on token claim values | Low | Phase 2 |