Files
tech-log-backend/docs/superpowers/specs/2026-08-02-web-security-boundary-design.md
T

73 lines
3.8 KiB
Markdown

# Web Security Boundary Design
**Date:** 2026-08-02
**Status:** approved by the user's instruction to apply the reviewed P1/P2 work sequentially
**Scope:** JWT/OIDC/JWKS and CORS behavior at the `adapter:inbound:web` Spring Security filter boundary
## Context
The module has unit contracts for JWT validators, exception classification, envelope writers, and
CORS settings. It does not yet prove that a real bearer request crosses issuer discovery, JWKS
retrieval, signature/claim validation, principal conversion, `SecurityFilterChain`, and the public
error envelope. CORS configuration is likewise untested at the filter boundary, where preflight
ordering relative to authentication is the important behavior.
These are release-boundary checks and must not silently skip because an external IdP, environment
variable, or optional flag is absent.
## Decision
Add a dedicated `webSecurityBoundaryTest` task that reuses the ordinary test output/classpath and
runs only JUnit tests tagged `security-boundary`. Ordinary `test` excludes that tag so each contract
runs once. The dedicated task:
- fails when no tests are discovered;
- disables up-to-date reuse;
- fails the root suite when any test reports `SKIPPED`;
- is required by the inbound-web `check` task;
- uses UTC and no environment-dependent conditions or assumptions.
JWT tests use a JDK loopback `HttpServer` bound to `127.0.0.1` on an ephemeral port. It serves the
minimum OIDC discovery document and JWKS response. Tests generate ephemeral RSA keys and compact
RS256 JWTs with the already-resolved Nimbus dependency; no new library or external network is
allowed. Each failure case uses a fresh server and Spring context to prevent decoder/JWK cache
cross-contamination.
CORS tests build the production `SecurityConfig` and real `springSecurityFilterChain` with direct
configuration properties. They issue real preflight and actual-origin MockMvc requests. A test JWT
decoder bean is allowed here because CORS ordering—not token decoding—is the owned boundary.
## JWT/JWKS Contract
- application context startup performs zero discovery/JWKS calls (lazy decoder);
- a correctly signed token reaches a protected controller and exposes the expected
`AuthenticatedPrincipal` subject/roles;
- expiry beyond the configured 60-second skew, issuer mismatch, audience mismatch, wrong
signature, and unknown `kid` produce their exact stable 401 error codes and bounded
`WWW-Authenticate`/`Retry-After` headers;
- deterministic JWKS 503 produces `AUTH_JWKS_UNAVAILABLE`, HTTP 503, and `Retry-After: 30`;
- after that first-request 503, the same lazy decoder/context retries initialization and succeeds
once the JWKS endpoint recovers;
- discovery metadata that is fetched successfully but is internally inconsistent produces the
fixed 500 `INTERNAL_AUTH_MISCONFIGURATION` envelope rather than a raw initialization exception;
- responses never contain the bearer token, issuer URL, `kid`, JWK material, or internal decoder
diagnostics.
## CORS Contract
- an approved credentialed preflight to an authenticated endpoint succeeds before bearer
authentication and emits exact origin/credentials/method/header/max-age policy;
- an unapproved origin receives 403 without allow-origin or allow-credentials reflection;
- disabled CORS emits no CORS response headers;
- wildcard origin without credentials returns `*` and no credentials header;
- an approved actual-origin request receives matching CORS and bounded `Vary` headers;
- wildcard plus credentials remains a settings startup failure (already covered by settings tests).
## Non-Goals
- external IdP/TLS/rotation rehearsal;
- browser-engine SameSite behavior;
- Redis-backed session continuity (the next P1 batch);
- new test libraries, Docker, or changes to production dependency direction;
- staging, commit, amend, or push by an agent.