# 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.