Files
missions/_docs/tasks/done/AZ-581_test_security_auth_claims.md
Oleksandr Bezdieniezhnykh 24c4561bef [AZ-581] [AZ-582] [AZ-583] [AZ-584] Sec+Res NFT tests
Batch 3 of test implementation cycle 1 (existing-code Step 6).

- AZ-581 AuthClaimsTests: NFT-SEC-01..06+04b (foreign-keypair, byte-flip,
  30s skew, iss/aud/perms, multi-value permissions array).
- AZ-582 CrossCutting/ErrorRedaction/JwksRotation/StartupConfig/CorsConfig:
  NFT-SEC-07..13 (alg pin, kid rotation grace window, env fail-fast, CORS
  Production gate).
- AZ-583 CascadeF3/CascadeF4/MigratorRestart: NFT-RES-01..04. CascadeF4
  pins current walk-order divergence with carry_forward AC-4.6.
- AZ-584 ConfigDbStartup/JwksRotationNoRestart/DefaultVehicleRace:
  NFT-RES-05..08. NFT-RES-08 pins current behaviour (unique-index closes
  the race) with carry_forward AC-1.4.

Mock contract: SignBody accepts permissions OR permissions_array (mutually
exclusive). TokenSigner validates kid_override against published keys so
NFT-SEC-11 can assert "mock refuses old kid post-grace".

Helpers added: ForeignKeypair (test-only ECDSA P-256),
MissionsContainerHelper (docker-run wrapper for startup-time scenarios),
DockerLogs.

7 of 22 new tests are Skippable, gated on COMPOSE_RESTART_ENABLED + docker
CLI in the e2e-consumer image (explicit skip reason; no silent pass).

Build green: test csproj + jwks-mock csproj.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 08:58:59 +03:00

9.5 KiB
Raw Permalink Blame History

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=-60401; exp_offset_seconds=-15200 (inside 30s skew).
  • NFT-SEC-04 Wrong issPOST /sign { "iss": "https://attacker.example.com" }401; default iss200.
  • NFT-SEC-04b Wrong audPOST /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.