Skip to content

feat(security): Spring Security 6 parity — Tier 0 hardening + passwordless (26.6.29)#37

Merged
ancongui merged 12 commits into
mainfrom
feat/spring-security-tier0-hardening
Jun 18, 2026
Merged

feat(security): Spring Security 6 parity — Tier 0 hardening + passwordless (26.6.29)#37
ancongui merged 12 commits into
mainfrom
feat/spring-security-tier0-hardening

Conversation

@ancongui

Copy link
Copy Markdown
Contributor

Spring Security 6 parity — Tier 0 (hardening + passwordless)

An adversarially-verified audit of Firefly's security tier against Spring
Security 6 / Spring Boot 3
, then the Tier 0 increment that closes the silent
semantic divergences in shipping code and adds the two Spring 6.4 passwordless
mechanisms. Release 26.6.29.

See the new Spring Security Parity book appendix (EN + ES) for the full
coverage matrix, and CHANGELOG.md for the itemised entry + known limitations.

Hardening (H1–H14, all TDD'd red→green)

  • Method security works behind every auth mechanismSessionAuthenticationLayer
    now scopes the task-local context, so #[pre_authorize] / current_authentication()
    work for session- and OAuth2-login users (was bearer-only). (highest-severity bug)
  • hasRole('X') matches ROLE_X (Spring's prefix) on both the method-security
    guards and the URL FilterChain (cross-surface consistency).
  • Segment-aware path matcherpermit("/api") no longer leaks to /api-internal.
  • CSRF cookie Secure follows the request scheme; HSTS is secure-request-only;
    in-process TLS termination is recognised as secure.
  • CORS rejects wildcard-origin + credentials; glob/CORS gain fallible builders.
  • JWKS verifies EC (ES256/384) + EdDSA, validates nbf, and tolerates 60 s clock skew.
  • OIDC id_token is never trusted without validation; RFC 6750 WWW-Authenticate: Bearer
    challenge on rejection.
  • Postgres SessionRegistry rows expire (opt-in absolute TTL + pruning).
  • No user-enumeration timing oracle (unknown user runs comparable bcrypt work).
  • permit_all() admits anonymous (Spring permitAll()).

New (Spring Security 6.4)

  • One-time-token (magic-link) loginOneTimeTokenService + delivery handler +
    /ott/generate & /login/ott.
  • WebAuthn / passkeys — feature-gated webauthn module over webauthn-rs, with a
    pluggable credential repository and a real end-to-end ceremony test (software authenticator).
  • Configurable JWT clock-skew via SecurityProperties.

Verification

  • Per-fix TDD; full firefly-security (94 lib + webauthn-feature 96 + all integration
    suites), firefly-web, firefly-session-postgres (incl. real-Postgres pruning tests),
    firefly-idp-internal-db, and the reactive-banking sample e2e all green; clippy clean;
    default build (webauthn off) unchanged.
  • An adversarial multi-agent review of the whole diff surfaced 19 findings; the 5 substantive
    ones (incl. 2 HIGH regressions) are fixed, the rest documented as known limitations.

Notes

  • WebAuthn is opt-in (--features webauthn); the default build is untouched.
  • A few Spring-faithful defaults changed (HSTS gating, CSRF Secure, CORS *+creds,
    clock-skew); each has a config escape hatch and is flagged in the CHANGELOG.

🤖 Generated with Claude Code

Andrés Contreras Guillén and others added 12 commits June 18, 2026 21:22
H1: SessionAuthenticationLayer now scopes the task-local CURRENT_AUTH around
the downstream call (not just the request extension), so #[pre_authorize] /
check_access / current_authentication() work for session- and OAuth2-login-
authenticated callers — previously method security silently failed unless the
caller was behind BearerLayer.
H2: Authentication::has_role accepts Spring's ROLE_ prefix (hasRole('ADMIN')
matches authority ROLE_ADMIN) while keeping bare roles backward-compatible.
H14: guards::permit_all() admits anonymous/absent principals like Spring
permitAll().

Tests: +4 security unit tests (red→green); full firefly-security suite and the
reactive-banking e2e suite stay green.
H3: path-prefix rules are now path-segment aware (Spring AntPathRequestMatcher
semantics) — permit("/api") matches /api and /api/... but no longer leaks to
/api-internal or /apixyz, which a raw starts_with allowed.
H10: glob compilation no longer panics on an invalid pattern — FilterChain
gains try_layer() returning a recoverable SecurityError (layer() still panics
for ergonomic use). Builders defer validation to layer/try_layer.

Tests: +2 filter-chain unit tests (red→green); full suite + reactive-banking
e2e stay green.
H5: JwksVerifier now accepts EC (ES256/ES384) and OKP/EdDSA keys in addition
to RSA (RS*/PS*); a resource server fronting an EC/EdDSA-signing IdP can now
verify tokens. Default allowed-alg set is the asymmetric family (never HS*).
H6: OIDC id_token is never silently trusted — handle_callback rejects an
id_token it cannot validate (no JWKS) instead of falling through to userinfo;
exp/nbf now use a configurable clock-skew leeway (default 60s, Spring's
JwtTimestampValidator default) on both JwksVerifier and JwtService.
H7: nbf is now validated (future-dated tokens rejected).
H8: bearer rejections carry an RFC 6750 WWW-Authenticate: Bearer challenge
(bare when no token; error="invalid_token" when present-but-invalid).

Tests: +9 (jwks EC/EdDSA/nbf/leeway, jwt leeway/nbf, bearer challenge x2,
oauth2 id_token no-jwks); full suite + reactive-banking e2e green; clippy clean.
H4: CSRF cookie Secure attribute now follows the request scheme by default
(CookieSecure::Auto) instead of being unconditional, so the double-submit pair
works over plain-HTTP dev; Always/Never override. Applied to both the
firefly-web and firefly-security CSRF layers.
H9: HSTS is emitted only on secure requests by default (Spring's
HstsHeaderWriter); hsts_include_insecure forces it on.
H11: CorsLayer rejects the illegal wildcard-origin + credentials combination
(Spring's validateAllowCredentials) via try_new()->Result; new() panics.

BREAKING (Spring-faithful, flagged): default HSTS no longer sent over HTTP;
CSRF cookie no longer Secure over HTTP; wildcard-origin+credentials CORS config
now rejected. Three pyfly-parity tests updated to the new semantics; +5 tests.
Full security+web suites + reactive-banking e2e green; clippy clean.
H12: PostgresSessionRegistry gains an expires_at column (idempotent ALTER for
existing tables) and TTL-driven pruning, so an orphaned session row ages out
instead of inflating the per-principal concurrency count forever. Default TTL
30m; with_ttl() overrides (ZERO disables); prune_expired() exposed for a
scheduled sweep. Verified end-to-end against real Postgres.
H13: idp-internal-db login runs a comparable bcrypt op on the unknown-user path
so an unknown username can't be distinguished from a wrong password by latency
(user-enumeration oracle) — Spring's userNotFoundEncodedPassword guard.

Tests: +1 idp timing test (red→green), +H12 real-Postgres pruning test, +SQL
const guards; existing PG integration tests pin TTL off (synthetic timestamps).
clippy clean.
…eTokenLogin()

New ott module: OneTimeTokenService (generate/consume, single-use, expiry) with
an in-memory impl; OneTimeTokenGenerationSuccessHandler for out-of-band delivery
(default logs issuance only, never the token value); and ott_login_routes
exposing POST /ott/generate + GET /login/ott (magic link) that redeems a token,
rotates the session id (anti-fixation), and stores the SECURITY_CONTEXT for
SessionAuthenticationLayer to restore. Adds tracing dep.

Tests: +6 (generate/consume, single-use, unknown/expired rejection, generate
endpoint doesn't leak the token, login endpoint authenticates + sets session
context + rejects replay). Full security suite green; clippy clean.
A published reference companion to the design spec: the Spring Security 6
coverage matrix, the Spring-faithful behaviours of the Tier 0 hardening, the
passwordless (one-time-token + WebAuthn) story, and the tiered roadmap. Wired
into book.yaml + book-es.yaml (Appendix B) and SUMMARY.md; the designed book
builds (EN verified, ES wired symmetrically).
…ture-gated)

New, opt-in `webauthn` module (off by default; pulls in webauthn-rs 0.5.5):
- WebAuthnRelyingParty over webauthn_rs::Webauthn (start/finish passkey
  registration + authentication).
- Pluggable ports: PasskeyCredentialRepository, PublicKeyCredentialUserEntity-
  Repository, CeremonyStateStore (+ in-memory impls).
- webauthn_routes: POST /webauthn/register/options|register,
  /webauthn/authenticate/options, POST /login/webauthn — the login route rotates
  the session id and stores the security context (reused by
  SessionAuthenticationLayer), mirroring the OTT flow.
- WebAuthnProperties (rp-id / rp-name / allowed-origins), WebAuthnError.

Tests: +5 incl. a real end-to-end ceremony driven by a software authenticator
(register → authenticate → session context set) and an RP-level round-trip.
Verified: cargo test --features webauthn (96 lib + all suites green), default
build (feature off) compiles, clippy clean.
…erties (Batch 6)

JwtProperties gains clock_skew_seconds; verifier_from_config applies it to both
the JWKS and HMAC verifiers (0 keeps the Spring-faithful 60s default). The other
Tier 0 knobs are already serde-bound on their config structs
(SecurityHeadersConfig.hsts_include_insecure, WebAuthnProperties) or set at the
layer builder (CookieSecure on CsrfLayer, OTT ttl). +1 config test.
Following an adversarial review of the Tier 0 diff (5 dimensions, each finding
re-verified):
- [HIGH] In-process TLS termination now marks requests secure (SecureRequest
  extension, set by serve() when TLS is configured), so HSTS and the CSRF
  Secure-cookie flag are no longer silently dropped on direct-HTTPS deployments
  (request_is_secure previously only honoured X-Forwarded-Proto / URI scheme).
- [HIGH/MED] FilterChain role rules are now ROLE_-prefix aware (and match a
  ROLE_-prefixed authority), consistent with Authentication::has_role — a
  ROLE_ADMIN principal satisfies require(..., ["ADMIN"]) just as it does
  #[pre_authorize]. Closes the cross-surface H2 asymmetry.
- [MED] Postgres SessionRegistry pruning is now opt-in (default OFF): a fixed
  created_at+ttl expiry would wrongly evict still-active sliding sessions and
  under-count maximumSessions. with_ttl enables it for absolute-lifetime caps.
- Tests: ROLE_ cross-surface test, in-app-TLS HSTS test, default-no-prune test
  (real PG), OTT anti-fixation rotation assertion, de-flaked OTT expiry test,
  permit_all combinator test. Lower-severity findings documented as known
  limitations.

web 48+60+11+19 + security 94 + session-postgres 18 (real PG) green; clippy clean.
Bump the workspace to 26.6.29 and record the CHANGELOG for the Spring Security
parity increment: the Tier 0 hardening (H1–H14), one-time-token login, WebAuthn,
configurable clock-skew, the parity book appendix, the post-review fixes, and
the documented known limitations.
…DULES

- MODULES.md + crates/security/README.md: reflect ROLE_-aware/segment-safe
  FilterChain, RSA/EC/EdDSA JWKS, one-time-token + WebAuthn passwordless login,
  Argon2id, and the Spring Security 6-faithful hardening.
- Security chapter (EN + ES): cross-link the new Spring Security Parity appendix.
- Rebuild the designed book dist (EN + ES PDF/EPUB) so the release ships the
  appendix and cross-links.
@ancongui ancongui merged commit f64c998 into main Jun 18, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant