mirror of
https://github.com/azaion/missions.git
synced 2026-06-22 11:41:06 +00:00
[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>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user