# Security Tests — Auth & Claims **Task**: AZ-581_test_security_auth_claims **Name**: Security tests — auth & claims (NFT-SEC-01..06 + 04b) **Description**: Implement xUnit blackbox tests for the 7 JWT authn/authz scenarios — missing/invalid header, invalid signature (single-byte flip + foreign-keypair), expired-outside-skew vs inside-30s-skew, wrong `iss`, wrong `aud`, missing `permissions`, wrong/multi-value `permissions` claim (contains-match accepts `["FL","ADMIN"]`). **Complexity**: 5 points **Dependencies**: AZ-576_test_infrastructure **Component**: Blackbox Tests **Tracker**: AZ-581 **Epic**: AZ-575 ## Problem JWT validation is the only thing standing between the open `e2e-net` and the protected `/vehicles` + `/missions` + `/missions/{id}/waypoints` surface. Six failure modes (no header / bad signature / expired / wrong iss / wrong aud / wrong perm) MUST all produce `401` or `403` deterministically — any drift means an attacker who learns the JWKS public bytes could shape a token that bypasses one rule and rides through. The drift re-verification of 2026-05-14 split AC-5.3 into two checks (`iss` AND `aud`) and tightened the clock skew from .NET's 5-min default to 30s; this task pins both. NFT-SEC-06 specifically asserts the `RequireClaim("permissions","FL")` is contains-match — a multi-permission token `["FL","ADMIN"]` must be accepted, while `"fl"` / `"FLight"` / `"ADMIN"` alone must be rejected. ## Outcome - All seven NFT-SEC-01..06 + 04b scenarios run and pass against the dockerised `missions` service. - Each test produces a CSV row with `Category=Sec`, `Traces=AC-5.x` or `AC-9.x`, `Result=pass`. - NFT-SEC-02 covers BOTH the single-byte-flip case AND the foreign-keypair case (token signed by a separate ECDSA keypair never published in the JWKS). - NFT-SEC-03 verifies the 30s skew BOTH ways — `exp_offset_seconds=-60` rejected, `exp_offset_seconds=-15` accepted. - NFT-SEC-06 verifies multi-permission token acceptance — `permissions: ["FL","ADMIN"]` → `200`. - NFT-SEC-01 asserts no DB side-effect on the `POST /vehicles` 401 path (side-channel count unchanged). ## Scope ### Included - NFT-SEC-01 Missing `Authorization` header on `/vehicles` GET/POST, `/missions` GET, `/missions/{any}/waypoints` GET — all `401`, no DB row written on the POST. - NFT-SEC-02 Invalid signature — single-byte-flipped signature segment AND foreign-keypair tokens. - NFT-SEC-03 Expired token — `exp_offset_seconds=-60` → `401`; `exp_offset_seconds=-15` → `200` (inside 30s skew). - NFT-SEC-04 Wrong `iss` — `POST /sign { "iss": "https://attacker.example.com" }` → `401`; default `iss` → `200`. - NFT-SEC-04b Wrong `aud` — `POST /sign { "aud": "wrong-audience" }` → `401`. - NFT-SEC-05 Missing `permissions` claim — `403`. - NFT-SEC-06 Wrong `permissions` value AND multi-permission acceptance — `"fl"`, `"FLight"`, `"ADMIN"` → `403`; `["FL","ADMIN"]` → `200`. ### Excluded - NFT-SEC-07 health-exempt-from-auth lives in Task 15. - NFT-SEC-08 stacktrace-not-leaked overlaps with FT-N-08 in Task 13 (and lives in Task 15 for the security-shaped variant). - NFT-SEC-09 SQL injection guard lives in Task 15. - NFT-SEC-10 alg-pin lives in Task 15. - NFT-SEC-11 unknown-kid rotation lag lives in Task 15. - NFT-SEC-12 missing-env startup throw lives in Task 15. - NFT-SEC-13 CORS Production-gate lives in Task 15. ## Acceptance Criteria **AC-1: NFT-SEC-01 missing header rejects every protected endpoint with 401, no side-effect** Given the running test stack When the consumer issues `GET /vehicles`, `GET /missions`, `GET /missions/{any}/waypoints`, and `POST /vehicles` with a valid body — all without an `Authorization` header Then each response is `401`, AND side-channel `SELECT COUNT(*) FROM vehicles` before and after the `POST` are equal **AC-2: NFT-SEC-02 invalid signature rejects two attack shapes** Given a valid signed token `T_good` from `jwks-mock POST /sign` When the consumer flips a single byte in `T_good`'s signature segment producing `T_bad`, and separately mints `T_foreign` signed by an ECDSA keypair never published in the JWKS Then `GET /vehicles` with `T_bad` returns `401` AND `GET /vehicles` with `T_foreign` returns `401` **AC-3: NFT-SEC-03 30s clock skew is enforced on both sides** Given the mock with default issuer/audience When the consumer mints two tokens via `POST /sign { exp_offset_seconds: -60 }` and `POST /sign { exp_offset_seconds: -15 }` Then `GET /vehicles` with the −60s token returns `401` AND `GET /vehicles` with the −15s token returns `200` **AC-4: NFT-SEC-04 wrong `iss` rejected, matching `iss` accepted** When the consumer mints a token via `POST /sign { iss: "https://attacker.example.com" }` and another via `POST /sign {}` (default iss) Then `GET /vehicles` with the attacker-iss token returns `401` AND with the default-iss token returns `200` **AC-5: NFT-SEC-04b wrong `aud` rejected** When the consumer mints a token via `POST /sign { aud: "wrong-audience" }` Then `GET /vehicles` returns `401` **AC-6: NFT-SEC-05 missing `permissions` claim rejected with 403** When the consumer mints a token with no `permissions` claim (mock body `{ permissions: "" }` or `{ permissions: null }` per the mock's contract) Then `GET /vehicles` returns `403` (NOT 401 — signature is valid) **AC-7: NFT-SEC-06 contains-match policy on `permissions`** When the consumer mints tokens with `permissions` values `"ADMIN"`, `"fl"` (lowercase), `"FLight"`, AND `["FL","ADMIN"]` (multi-value array) Then `GET /vehicles` returns `403` for the first three AND `200` for the multi-value `["FL","ADMIN"]` array (contains-match accepts `"FL"` among the values) ## Non-Functional Requirements **Performance** - NFT-SEC-01..06: ≤ 5s each. The Authorization-header failure paths are cheap (no DB round-trip on the 401/403 short-circuit). **Reliability** - NFT-SEC-02 requires an out-of-band ECDSA-keypair helper that lives inside the test project, NOT in `jwks-mock` (the mock must never publish a public key it does not control). The helper generates a P-256 keypair at test-start and signs a token directly using `System.Security.Cryptography.ECDsa` — the public key is never registered with `missions`. ## Blackbox Tests | AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References | |--------|------------------------|-------------|-------------------|----------------| | AC-1 | running stack | 4 endpoints w/o Authorization | all 401; POST no DB write | AC-5.4 | | AC-2 | `T_good` from mock + foreign keypair | flipped signature; foreign-keypair token | both 401 | AC-5.5 | | AC-3 | mock with default iss/aud | exp_offset −60s vs −15s | 401 / 200 | AC-5.2, AC-5.6 | | AC-4 | mock | iss=attacker vs default | 401 / 200 | AC-5.3, AC-5.11 | | AC-5 | mock | aud=wrong | 401 | AC-5.3, AC-5.12 | | AC-6 | mock | permissions missing | 403 | AC-5.8, AC-9.1 | | AC-7 | mock | permissions=ADMIN/fl/FLight/["FL","ADMIN"] | 403/403/403/200 | AC-9.1, AC-9.2 | ## Constraints - HTTP only against `http://missions:8080`. Tokens minted via `https://jwks-mock:8443/sign` with parameterised overrides. - NFT-SEC-02 foreign-keypair: a test-only helper inside `Azaion.Missions.E2E.Tests` MAY use `System.Security.Cryptography.ECDsa` directly for the attack-token construction; this is the ONLY in-test signing path allowed — every other test must use the mock. - NFT-SEC-06 multi-permission token requires the mock's `POST /sign` body to accept `permissions` as either a string OR a JSON array; the test-infrastructure ticket (AZ-576) covers this in the mock's contract. - AAA pattern with `// Arrange` / `// Act` / `// Assert` per test. ## Risks & Mitigation **Risk 1: NFT-SEC-03 flaky due to wall-clock variability** - *Risk*: A −15s offset could fail if Docker time skew between the mock and `missions` is large. - *Mitigation*: Both containers run on the same host clock (no `--init` time isolation); test asserts only at offsets well clear of the 30s boundary (−60s and −15s — 30s and 15s away from the boundary respectively). **Risk 2: NFT-SEC-06 multi-permission shape varies between systems** - *Risk*: If the spec for `permissions` claim later changes from "contains-match string" to "exact-array-membership", the multi-value assertion breaks. - *Mitigation*: Test traces explicitly to AC-9.2 and references `Auth/JwtExtensions.cs` policy registration; any change there must update this test in the same commit. **Risk 3: Foreign-keypair token validation might pass if the SUT silently trusts any well-formed ECDSA token** - *Risk*: A regression that disables `IssuerSigningKeyResolver` would let the foreign-keypair token through. - *Mitigation*: Mitigated by the structure of AC-2 — both bad-signature shapes (flipped byte AND foreign keypair) must return 401. ## System Under Test Boundary - Tests drive the product through the public HTTP surface (`http://missions:8080/{vehicles,missions}*`) and acquire signed tokens via `https://jwks-mock:8443/sign` (with the test-only foreign-keypair helper for NFT-SEC-02). Expected outputs are the documented HTTP status codes from `_docs/00_problem/input_data/expected_results/results_report.md` AC-5 rows and AC-9 rows. - Stubs are allowed ONLY for: the external `admin` JWT issuer (`jwks-mock` container). - Stubs, fakes, deterministic fallbacks, monkeypatches, or direct imports are NOT allowed for any internal product module — including `JwtExtensions`, `Program.cs` (auth pipeline registration), the `[Authorize(Policy = "FL")]` filter, or `ErrorHandlingMiddleware`. If any of these is not implemented, the test MUST fail/block as missing product implementation — it must not pass by replacing the module with a test stub.