refactor: enhance JWT authentication and CORS configuration

Updated JWT authentication to use configuration values instead of hardcoded secrets, improving security and flexibility. Enhanced CORS policy to conditionally allow origins based on configuration settings, with logging for permissive defaults. Updated README to reflect project renaming and clarify service context.
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-14 19:48:25 +03:00
parent 2fe394d732
commit 7025f4d075
74 changed files with 8494 additions and 19 deletions
@@ -0,0 +1,93 @@
# Flow F5 — JWT bearer validation
> Cross-cutting flow that runs on every `[Authorize]` request. Local validation only — this service never calls back to the issuing `admin` service.
## Description
ASP.NET Core's `JwtBearerHandler` validates incoming `Authorization: Bearer <jwt>` headers against the shared HMAC secret (`JWT_SECRET`). On success, the request continues to the controller with a `ClaimsPrincipal` attached. On signature / lifetime failure → `401`. On valid token but missing `"FL"` permission claim → `403`. The `iss` / `aud` claims are intentionally NOT validated today (CMMC L2 finding tracked at suite level under AZ-487 / AZ-494 — see `05_identity` § Implementation Details).
## Preconditions
- `JWT_SECRET` is resolved at startup (env or hardcoded dev fallback per `architecture.md` ADR-005).
- `AddJwtAuth(jwtSecret)` was called during `Program.cs` startup (F6).
## Sequence Diagram
```mermaid
sequenceDiagram
autonumber
participant Client as UI / Operator API client
participant Pipeline as ASP.NET Pipeline
participant Handler as JwtBearerHandler
participant Policy as Auth policy "FL"
participant Ctrl as Feature Controller
participant Errs as 06_http_conventions
Client->>Pipeline: HTTP request + Authorization: Bearer <jwt>
Pipeline->>Errs: enter ErrorHandlingMiddleware
Errs->>Handler: hand off (anonymous endpoints skip this)
Handler->>Handler: parse token; verify HMAC-SHA256 signature using SymmetricSecurityKey(UTF-8(JWT_SECRET))
alt Signature invalid OR token expired (ClockSkew = 1 minute)
Handler-->>Client: 401 Unauthorized
else Valid token
Handler->>Handler: build ClaimsPrincipal; skip iss/aud validation
Handler->>Policy: evaluate policy "FL" (requires permissions claim == "FL")
alt Claim missing or != "FL"
Policy-->>Client: 403 Forbidden
else permissions=FL
Policy-->>Ctrl: forward to controller action
Ctrl-->>Client: business response
end
end
```
## Flowchart
```mermaid
flowchart TD
Start([Incoming request]) --> AnonEP{Endpoint requires auth?}
AnonEP -->|no| Forward([Forward to controller])
AnonEP -->|yes| Header{Authorization: Bearer present?}
Header -->|no| Unauth1([401 Unauthorized])
Header -->|yes| Sig{HMAC-SHA256 signature valid?}
Sig -->|no| Unauth2([401 Unauthorized])
Sig -->|yes| Life{Lifetime valid? ClockSkew=1min}
Life -->|no| Unauth3([401 Unauthorized — expired])
Life -->|yes| BuildPrincipal[Build ClaimsPrincipal — skip iss/aud]
BuildPrincipal --> Policy{permissions claim == FL?}
Policy -->|no| Forbid([403 Forbidden])
Policy -->|yes| Forward
```
## Data Flow
| Step | From | To | Data | Format |
|------|------|----|------|--------|
| 1 | Client | Pipeline | `Authorization: Bearer <jwt>` header | HTTP header |
| 2 | `JwtBearerHandler` | (in-process) | parsed JWT (header, payload, signature) | JSON Web Token |
| 3 | `JwtBearerHandler` | (in-process) | `ClaimsPrincipal` | .NET principal object |
| 4 | Authorization policy evaluator | Controller | "policy satisfied" / `403` | flag |
| 5 | `JwtBearerHandler` | Client (only on failure) | `401` / `403` | HTTP status (no body) |
## Error Scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| Missing `Authorization` header on `[Authorize]` route | `JwtBearerHandler` | Header absent | `401`. Client must obtain a token from `admin` |
| Malformed JWT | `JwtBearerHandler` | Token parse failure | `401` |
| Signature mismatch (wrong / rotated `JWT_SECRET`) | `JwtBearerHandler` | HMAC verify fails | `401`. Suite-wide secret rotation is coordinated re-deploy of every backend that shares the secret + UI re-login |
| Expired token | `JwtBearerHandler` | `ValidateLifetime = true` (`ClockSkew = 1 min`) | `401`. Tighter than .NET's 5-min default — caller may experience earlier expiration than expected |
| `permissions` claim missing or wrong value | Policy `"FL"` evaluator | claim lookup | `403` |
| Token signed with the well-known dev fallback secret | (silent acceptance) | None | **Security risk in production**. ADR-005 carry-forward; suite-tracked under CMMC L2 row 3 |
| Token from a third-party that knows `JWT_SECRET` | (silent acceptance) | None | **Trust model is shared-secret intra-suite**. Any third-party with the secret can mint accepted tokens. Out of this Epic's scope; suite-wide concern |
## Performance Expectations
| Metric | Target | Notes |
|--------|--------|-------|
| Validation latency | sub-millisecond typical | Pure HMAC + claim lookup; no I/O, no network call |
| Throughput | bounded by request throughput | No back-pressure; no token cache (no DB / network round-trip to cache) |
## Notes on `iss` / `aud` validation (suite-tracked)
`ValidateIssuer = false`, `ValidateAudience = false` — consistent with the shared-secret intra-suite model. The CMMC L2 scorecard (`../../../suite/_docs/05_security/cmmc_l2_scorecard.md` row 3) flags this as a finding. The remediation will copy the `satellite-provider` pattern across `annotations` and `missions` (suite work, AZ-487 / AZ-494). It is **NOT** in this Epic's scope and will not change as part of the rename refactor.