feat(security): Spring Security 6 parity — Tier 0 hardening + passwordless (26.6.29)#37
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.mdfor the itemised entry + known limitations.Hardening (H1–H14, all TDD'd red→green)
SessionAuthenticationLayernow 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')matchesROLE_X(Spring's prefix) on both the method-securityguards and the URL
FilterChain(cross-surface consistency).permit("/api")no longer leaks to/api-internal.Securefollows the request scheme; HSTS is secure-request-only;in-process TLS termination is recognised as secure.
nbf, and tolerates 60 s clock skew.id_tokenis never trusted without validation; RFC 6750WWW-Authenticate: Bearerchallenge on rejection.
SessionRegistryrows expire (opt-in absolute TTL + pruning).permit_all()admits anonymous (SpringpermitAll()).New (Spring Security 6.4)
OneTimeTokenService+ delivery handler +/ott/generate&/login/ott.webauthnmodule overwebauthn-rs, with apluggable credential repository and a real end-to-end ceremony test (software authenticator).
SecurityProperties.Verification
firefly-security(94 lib + webauthn-feature 96 + all integrationsuites),
firefly-web,firefly-session-postgres(incl. real-Postgres pruning tests),firefly-idp-internal-db, and thereactive-bankingsample e2e all green;clippyclean;default build (webauthn off) unchanged.
ones (incl. 2 HIGH regressions) are fixed, the rest documented as known limitations.
Notes
--features webauthn); the default build is untouched.Secure, CORS*+creds,clock-skew); each has a config escape hatch and is flagged in the CHANGELOG.
🤖 Generated with Claude Code