# JWT validation baseline (HS256, JWT_SECRET, all endpoints)

**Task**: AZ-487_jwt_validation_baseline
**Name**: JWT validation baseline
**Description**: Add HS256 JWT validation to satellite-provider so every existing endpoint requires a valid token issued by the centralized Admin API; align with the suite-wide auth contract documented in `suite/_docs/10_auth.md`.
**Complexity**: 2 points
**Dependencies**: None (consumes the suite-level JWT contract `suite/_docs/10_auth.md`; no in-repo task dependency)
**Component**: WebApi (`SatelliteProvider.Api`)
**Tracker**: AZ-487
**Epic**: none (cross-cutting hardening; AZ-488 hard-depends on this)

## Problem

Satellite-provider currently has zero authentication — every endpoint is open. The suite-level auth design (`suite/_docs/10_auth.md`) requires every .NET API instance in the suite to validate JWTs locally using a shared HMAC key supplied via the `JWT_SECRET` env var. The Security Audit (Step 14, cycle 1, `_docs/05_security/owasp_review.md`) flagged the absence of authentication as the largest hardening gap blocking public-network exposure. The next planned PBI (AZ-488 — UAV upload endpoint) cannot ship without the auth baseline because UAV uploads must carry an authenticated user identity.

## Outcome

- Every existing HTTP endpoint (except possibly Swagger UI in non-Production) returns `401 Unauthorized` when the request has no `Authorization: Bearer …` header, an expired token, or a token whose HMAC signature does not verify against the `JWT_SECRET` env var.
- Authenticated requests reach the existing handlers unchanged — no behavioral change for valid-token callers.
- A request's user identity is exposed to handlers via the standard `HttpContext.User` claims principal (`sub`, `email`, `role`, `permissions[]` per the suite contract).
- Swagger UI accepts a Bearer token via the standard "Authorize" button so manual testing keeps working.
- The `JWT_SECRET` env var is documented in `appsettings.Development.json` (with a clearly-fake dev value), `docker-compose.yml`, and (eventually) the deployment scripts.
- Existing 213 unit + 5 smoke tests continue to pass after test setup is updated to attach a valid dev token.

## Scope

### Included

- New ServiceCollection extension `SatelliteProvider.Api.Authentication.AuthenticationServiceCollectionExtensions.AddSatelliteJwt(this IServiceCollection, IConfiguration)`:
  - Adds `Microsoft.AspNetCore.Authentication.JwtBearer` (use the same minor version as the existing `Microsoft.AspNetCore.OpenApi` package — currently 8.0.x; if a security upgrade is happening alongside, pin both consistently).
  - Configures `AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => …)` with `TokenValidationParameters`:
    - `ValidateIssuerSigningKey = true`, `IssuerSigningKey = SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWT_SECRET))`
    - `ValidateLifetime = true`, `ClockSkew = TimeSpan.FromSeconds(30)` (match suite default; document the choice in code)
    - `ValidateIssuer = false`, `ValidateAudience = false` (per suite doc — issuer/audience semantics are not specified yet)
    - `RequireSignedTokens = true`, `RequireExpirationTime = true`
  - Throws on startup if `JWT_SECRET` is missing or shorter than 32 bytes (HMAC-SHA256 minimum-secure key size).
- New ServiceCollection extension call wired into `Program.cs`: `builder.Services.AddSatelliteJwt(builder.Configuration);` followed by `builder.Services.AddAuthorization();` and the matching `app.UseAuthentication(); app.UseAuthorization();` middleware ordering (after `UseCors`, before endpoint routing).
- `.RequireAuthorization()` applied to every existing `MapGet`/`MapPost` in `Program.cs`:
  - `GET /tiles/{z}/{x}/{y}` (`ServeTile`)
  - `GET /api/satellite/tiles/latlon` (`GetTileByLatLon`)
  - `GET /api/satellite/tiles/mgrs` (`GetSatelliteTilesByMgrs`)
  - `POST /api/satellite/upload` (`UploadImage` — currently 501 stub)
  - `POST /api/satellite/request` (`RequestRegion`)
  - `GET /api/satellite/region/{id}` (`GetRegionStatus`)
  - `POST /api/satellite/route` (`CreateRoute`)
  - `GET /api/satellite/route/{id}` (`GetRoute`)
- `Program.cs` Swagger configuration extended with a `SecurityDefinition("Bearer", ...)` and a global `SecurityRequirement` so the "Authorize" button appears in Swagger UI.
- `appsettings.json` and `appsettings.Development.json`: add `Jwt:Secret` configuration key reading `JWT_SECRET` via the standard ASP.NET Core env-var binding. The dev file ships a 32+ byte placeholder secret clearly marked `DEV-ONLY-DO-NOT-USE-IN-PROD`.
- `docker-compose.yml`: add `JWT_SECRET` to the api service's `environment` block, sourcing from the host env (or a `.env` entry the operator supplies).
- `.env.example` (create if missing): include `JWT_SECRET=` line with comment.
- Test infrastructure update:
  - Add a small test helper `SatelliteProvider.Tests.TestUtilities.JwtTokenFactory` (or reuse if a similar one exists) that signs a valid dev token using the same secret used by the integration test environment.
  - Update `scripts/run-tests.sh` (or the relevant test setup) to ensure the integration test container starts with `JWT_SECRET` set and the test runner attaches a Bearer token to every request.
  - Update existing smoke tests' HTTP request setup to attach the test token by default.
- New unit tests:
  - `AuthenticationServiceCollectionExtensionsTests`: `AddSatelliteJwt_RegistersJwtBearerScheme`, `AddSatelliteJwt_ThrowsOnMissingSecret`, `AddSatelliteJwt_ThrowsOnShortSecret`.
  - Token factory: `JwtTokenFactory_ProducesTokenAcceptedByValidationParameters`.
- New integration tests:
  - `JwtIntegrationTests.AnonymousRequest_To_AnyEndpoint_Returns401`
  - `JwtIntegrationTests.ExpiredToken_Returns401`
  - `JwtIntegrationTests.InvalidSignature_Returns401`
  - `JwtIntegrationTests.ValidToken_Returns200_OnHealthyEndpoint`
- Documentation:
  - Update `_docs/02_document/architecture.md` § Architecture Vision to add an "Authentication & authorization" sub-section noting that satellite-provider validates JWTs issued by the centralized Admin API per the suite-level auth contract.
  - Update `_docs/02_document/components/01_web_api/description.md` to describe the JWT middleware ordering and where `.RequireAuthorization()` is applied.
  - Update `_docs/02_document/modules/api_program.md` to reflect the new middleware chain.
  - Reference (do not duplicate) `suite/_docs/10_auth.md` from this task spec and from the WebApi component description.

### Excluded

- `GET /users/me` and `PUT /users/me` endpoints — explicitly not shipped (decision recorded in task discussion: nothing depends on them, the suite-level "hosted on every .NET API instance" policy is over-specified for satellite-provider).
- Permission-claim enforcement on the existing endpoints — they only require *any* valid token in this PBI. Per-endpoint permission checks land later (AZ-488 enforces `permissions` claim contains `GPS` for the upload endpoint).
- Refresh/logout endpoints — admin API only, per the suite contract.
- Issuer/audience claim validation — see Constraints below.
- Asymmetric (RS256 / ES256) signature support — out of scope; the suite contract specifies HMAC.
- JWKS endpoint fetching — not needed for HMAC.
- Rate limiting on `401` responses — separate hardening item from `_docs/05_security/security_report.md`.

## Acceptance Criteria

**AC-1: Anonymous request returns 401**
Given the API is running with `JWT_SECRET` configured
When any request to any endpoint (except Swagger metadata) is sent without an `Authorization` header
Then the response is HTTP 401 Unauthorized.

**AC-2: Expired token returns 401**
Given a JWT signed with the configured secret but with `exp` in the past
When the API receives a request carrying that token
Then the response is HTTP 401 Unauthorized and the body identifies the failure reason at the `WWW-Authenticate` header level (no leakage of internal details in the response body).

**AC-3: Invalid signature returns 401**
Given a JWT whose payload has been tampered with so the HMAC signature no longer verifies
When the API receives a request carrying that token
Then the response is HTTP 401 Unauthorized.

**AC-4: Valid token reaches the handler unchanged**
Given a JWT signed with the configured secret with `exp` in the future
When the API receives a `GET /api/satellite/tiles/latlon` request carrying that token with valid query parameters
Then the response is identical (status, headers other than `Authorization`-related, body) to the pre-AZ-487 behavior for the same request.

**AC-5: Startup fails on missing or short secret**
Given `JWT_SECRET` is unset, empty, or shorter than 32 bytes
When the API starts
Then startup fails with a clear error message identifying the missing/invalid secret; the API does not bind to its port.

**AC-6: HttpContext.User exposes claims**
Given a JWT containing claims `sub`, `email`, `role`, `permissions`
When a handler reads `HttpContext.User`
Then `HttpContext.User.Identity.IsAuthenticated` is `true`, the `sub` claim is present, and the `permissions` claim values are accessible via the standard claims API.

**AC-7: Swagger UI works with the Authorize button**
Given the API is running in Development
When the operator opens Swagger UI, clicks "Authorize", pastes a valid token, and triggers any endpoint
Then the request is sent with the Bearer token and the response is whatever the handler would return for an authenticated request.

**AC-8: All existing tests pass with the test token attached**
Given the test infrastructure attaches a valid dev-token to every test HTTP request
When `scripts/run-tests.sh --full` runs
Then all 213 unit tests + the existing 5 smoke scenarios + the new JWT integration tests pass.

## Non-Functional Requirements

**Performance**
- JWT validation per request must add < 1 ms overhead (HMAC-SHA256 + claims parsing); no cryptographic operations beyond what `JwtBearer` does by default. No caching required.

**Compatibility**
- The HTTP contract change (every endpoint can now return 401) is documented in the architecture doc. No request/response schema field changes for valid-token callers.
- Existing callers (`gps-denied-onboard`, mission planner UI) MUST coordinate to attach the Bearer token they already hold from the admin API. This is a coordination cost flagged as a deployment risk in Risks below.

**Reliability**
- Missing/invalid `JWT_SECRET` is fail-fast at startup, never at first request — operators learn about the misconfiguration immediately, not on first user traffic.

**Security**
- Secret length validation enforces ≥ 32 bytes — RFC 2104 §3 minimum for HMAC-SHA256.
- `RequireExpirationTime = true` rejects tokens that omit `exp` (defense against forged "never-expires" tokens).
- `ClockSkew = TimeSpan.FromSeconds(30)` is the maximum drift; do not relax further without explicit security review.

## Unit Tests

| AC Ref | What to Test | Required Outcome |
|--------|--------------|------------------|
| AC-5 | `AddSatelliteJwt` with missing `Jwt:Secret` configuration | Startup throws a clear `InvalidOperationException` (or equivalent) naming the missing key |
| AC-5 | `AddSatelliteJwt` with `Jwt:Secret` shorter than 32 bytes | Startup throws with a message identifying the minimum length requirement |
| AC-1, AC-3, AC-4 | `JwtTokenFactory` test helper round-trip via `TokenValidationParameters` | Tokens minted by the helper validate; tampered tokens fail validation |

## Blackbox Tests

| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|--------|--------------------------|--------------|-------------------|-----------------|
| AC-1 | API running with valid `JWT_SECRET` | Send `GET /api/satellite/tiles/latlon?...` with NO `Authorization` header | HTTP 401 | Compatibility |
| AC-2 | API running with valid `JWT_SECRET`; expired token minted | Send `GET /api/satellite/region/{id}` with the expired token | HTTP 401 | — |
| AC-3 | API running with valid `JWT_SECRET`; tampered token | Send any request with the tampered token | HTTP 401 | Security |
| AC-4 | API running with valid `JWT_SECRET`; valid token | Send `GET /api/satellite/tiles/latlon?Latitude=...&Longitude=...&ZoomLevel=18` | HTTP 200 with the tile metadata response (identical to pre-AZ-487 behavior) | Performance |
| AC-7 | API running in Development with Swagger UI accessible | Open Swagger, Authorize with a token, invoke an endpoint | Request includes `Authorization: Bearer <token>`, endpoint returns its normal response | — |
| AC-8 | Full test environment | Run `scripts/run-tests.sh --full` | All tests pass | — |

## Constraints

- The suite contract `suite/_docs/10_auth.md` is authoritative for token shape and signing key distribution. This task implements the validator side; if the contract changes (e.g., admin migrates to RS256), this is a follow-up task.
- HMAC secret MUST come from the `JWT_SECRET` env var — never from a checked-in config file. The dev placeholder in `appsettings.Development.json` is acceptable because the file is committed and the placeholder is clearly fake; the `JWT_SECRET` env var (when set) overrides it.
- No new cross-component `ProjectReference` — JWT plumbing lives entirely in `SatelliteProvider.Api`.
- Issuer/audience claims are NOT validated in this PBI because the suite contract does not specify expected values. If the admin team confirms specific values later, add `ValidateIssuer = true` + `ValidIssuer = <value>` and same for audience as a small follow-up — not as part of this task.
- ClockSkew tightened from the JwtBearer default (5 minutes) to 30 seconds; document the reason in code (defense in depth — operators in mixed-clock environments may need to override, but the default should be tight).

## Risks & Mitigation

**Risk 1: Existing callers break the moment AZ-487 deploys**
- *Risk*: `gps-denied-onboard` and mission planner UI currently call satellite-provider with no `Authorization` header. The instant `.RequireAuthorization()` lands, every existing call returns 401 — production-incident-grade breaking change.
- *Mitigation*:
  - Coordinate the deploy with `gps-denied-onboard` and UI teams BEFORE merge: confirm both already hold a valid JWT from the admin API and can attach it to outbound calls.
  - Stage the deploy: ship to `dev` only first, validate that suite e2e tests pass, then promote to `stage`/`prod`.
  - Feature flag option (rejected during planning): a compile-time/config flag to bypass auth was considered. Decision: NO bypass flag — auth bypasses tend to become permanent. Coordinate the rollout instead.

**Risk 2: Test infrastructure leaks the dev secret**
- *Risk*: Hard-coding the test secret in test source could let a careless copy-paste land it in production config.
- *Mitigation*:
  - Test secret is ≥ 32 bytes BUT clearly tagged `TEST-ONLY-` prefix.
  - Test secret is read from an env var (`SATELLITE_TEST_JWT_SECRET`) in the test runner, not hard-coded — same separation pattern as production.
  - `.gitignore` already excludes `.env`; ensure no `.env.test` file is added without scrubbing.

**Risk 3: `JwtBearer` package version drift vs Microsoft.AspNetCore.OpenApi**
- *Risk*: Picking a major version that mismatches the existing ASP.NET Core 8 packages introduces transitive-dependency conflicts at build time. The Security Audit also flagged a recommended bump of `Microsoft.AspNetCore.OpenApi 8.0.21 → 8.0.25`.
- *Mitigation*:
  - Pin `Microsoft.AspNetCore.Authentication.JwtBearer` to the same minor version family as the rest of the ASP.NET Core 8 packages already in the project.
  - If the OpenApi bump from `_docs/05_security/security_report.md` happens in this PBI, align JwtBearer to the same version. Otherwise pin to the existing `8.0.21` series.

**Risk 4: HS256 secret rotation invalidates all live tokens**
- *Risk*: The HMAC pattern means rotating `JWT_SECRET` invalidates every issued token across every API instance simultaneously — there's no overlap window unless the validator accepts both old and new secrets briefly.
- *Mitigation*:
  - Out of scope for this PBI — operationally the suite's current rotation policy is "all tokens become invalid; all clients re-login". Document this in the WebApi component description so operators know.
  - If multi-secret support is needed later (admin team adds RS256 + JWKS), it's a follow-up task.

## Cross-component coordination

This PBI changes the HTTP contract observed by:

- `gps-denied-onboard` — must attach Bearer token to outbound calls; coordinate before merge.
- `mission planner UI` — same.
- Any other satellite-provider consumer not yet inventoried.

A smoke test in the suite-level e2e harness (`suite/e2e/`) MUST be updated alongside this PBI's deploy, OR temporarily skipped with an issue-tracked unskip task. Surface this to the suite repo owner during code review.
