feat(security): Spring Security parity — Tier 2 web mechanisms (v26.6.31)#39
Merged
Merged
Conversation
added 8 commits
June 19, 2026 16:37
httpBasic() parity: HttpBasicLayer reads Authorization: Basic, authenticates the username/password through the Tier-1 AuthenticationManager spine, and scopes the resulting Authentication (extension + task-local) like BearerLayer. A present-but-invalid/malformed header is rejected 401 with a WWW-Authenticate: Basic challenge (new BasicAuthenticationEntryPoint); an absent header passes through (Spring's BasicAuthenticationFilter behaviour). +4 tests; security lib 113 -> 117 green; clippy + fmt clean.
formLogin() parity: form_login_routes mounts POST /login (url-encoded username/password), authenticates through the Tier-1 AuthenticationManager, and on success rotates the session id (anti-fixation) + persists the Authentication via a SecurityContextRepository (restored later by SessionAuthenticationLayer), then redirects. Configurable success/failure URLs and pluggable FormLoginSuccessHandler / FormLoginFailureHandler. +2 tests; security lib 117 -> 119 green; clippy + fmt clean.
Add hash-based remember-me — the Rust analog of Spring Security's rememberMe()/TokenBasedRememberMeServices: - RememberMeServices trait + TokenBasedRememberMeServices: mints a signed, expiring token (base64url(username:expiry:sig), sig = SHA-256 over username:expiry:password-hash:key) and validates it on auto_login against the UserDetailsService with a constant-time signature compare. - Password-bound + key-bound + TTL: a password change, a wrong key, an expired clock, a tampered token, or an unknown user all reject. - Trust levels on Authentication: is_remembered()/is_fully_authenticated() + REMEMBERED_CLAIM — auto_login marks the context remembered, so a sensitive route can demand a fresh login (Spring's isFullyAuthenticated()). 5 tests (roundtrip+remembered marking, expiry, tamper/wrong-key, password-change invalidation, unknown user). fmt + clippy clean.
Add the Rust analog of Spring Security's RequestCache/SavedRequest (HttpSessionRequestCache) and wire it into form login: - SavedRequest: a method+target snapshot of the page a user wanted before being redirected to log in; from_request() captures path+query, redirect_url() yields the post-login target (Spring's getRedirectUrl()). - RequestCache trait + HttpSessionRequestCache (session-attribute backed, default key firefly:savedRequest) + NullRequestCache (stateless). get_matching_request() consumes the saved request only on a method+target match (replay recognition). Methods take an owned SavedRequest because axum's Request is !Sync and can't cross an async-trait .await. - form_login: FormLoginState now consults a RequestCache on success and prefers the saved page over the configured success URL, then consumes it — Spring's SavedRequestAwareAuthenticationSuccessHandler. Configurable via .request_cache(...) (NullRequestCache to always use the success target). 6 tests (capture, roundtrip, survives session-id rotation, match-consumes- only-on-match, null cache, form-login returns-to-saved-request). 130 lib tests green; fmt + clippy clean.
Add the two remaining web-mechanism pieces from Spring's HttpSecurity: SessionCreationPolicy (sessionManagement().sessionCreationPolicy(...)): - enum Always/IfRequired(default)/Never/Stateless with uses_session(), allows_session_creation(), is_stateless() predicates. - security_context_repository() maps the policy to a SecurityContextRepository (Stateless -> NullSecurityContextRepository; others -> session-backed). - SessionAuthenticationLayer::session_creation_policy(policy) installs it; Stateless ignores any stored session context (proven by test), so a token API pairs it with anonymous_fallback(false) to let BearerLayer govern. Multiple filter chains (Spring's FilterChainProxy / several SecurityFilterChain): - RequestMatcher trait + AnyRequestMatcher + PathRequestMatcher (segment-aware prefix, optional method). - SecurityFilterChains: an ordered list of (matcher, FilterChain); the first matching chain handles each request (and only it runs); an unmatched request passes through untouched. layer()/try_layer() compile each chain, pre-applying its authorization layer to the inner service for cheap request-time dispatch. 10 tests (policy predicates + repo mapping + stateless-ignores-context; first-match-wins, earlier-chain-precedence, pass-through, method-scoped matcher, invalid-glob error). 139 lib tests green; fmt + clippy clean.
Adversarial multi-agent review of the Tier 2 web mechanisms surfaced six confirmed findings; fixes: - Open redirect (medium): the post-login saved-request redirect could emit an attacker-influenced protocol-relative (//evil.com) or backslash (/\evil.com) Location. form_login now honours a SavedRequest only when SavedRequest::is_safe_redirect() holds — a rooted same-origin path, not protocol-relative/backslash-tricked, and free of control chars — delivering the 'same-origin redirect only' guarantee the parity docs advertise. - Tower readiness contract (medium): SecurityFilterChains dispatched a matched chain's service via call() without driving it ready (poll_ready only readied the no-match inner). It now drives the chosen chain service ready() before call(), correct even for a backpressure-bearing inner service. Regression test uses a readiness-gated inner that panics if called unready. - HMAC remember-me (low hardening): the remember-me signature is now HMAC-SHA256(key, "user:expiry:password-hash") — a proper keyed MAC — instead of a bare SHA-256 over colon-joined fields with the key as a suffix, removing all length-extension / delimiter-injection reasoning. - now_secs() fail-closed (low): a pre-UNIX-EPOCH clock returned 0, which would disable the remember-me expiry check (fail-open). now_secs() now returns Option and auto_login rejects on a clock error. - redirect() panic (low): form_login's redirect() used .expect() on a dynamic Location, panicking on a control char (CR/LF). It now builds the header value fallibly and falls back to "/", preventing a request-thread panic and header splitting. 143 lib tests green (was 139); fmt + clippy clean.
- Spring Security Parity appendix (EN + ES): mark HTTP Basic, form login, remember-me, RequestCache/SavedRequest, SessionCreationPolicy, and multiple filter chains as supported; add a 'Form login, HTTP Basic, and remember-me' section; mark the Web-mechanisms tier done in the roadmap. - Replace the emoji status markers (✅/🚧/🧩) in the coverage matrix with professional inline-SVG status icons (supported / opt-in / roadmap), matching the book's existing no-emoji callout-icon system. New _STATUS_ICON set in build/md.py (token substitution, well-formed XHTML for EPUB) + .status-ico styling in theme/book.css + a legend above the matrix. The book is now emoji-free; typographic arrows and literal CLI ✓/✗ output are unchanged. - CHANGELOG: v26.6.31 entry (Tier 2). MODULES.md: firefly-security row now lists the auth spine + web mechanisms. - Rebuild and republish both editions (PDF + EPUB, EN + ES) in docs/book/dist.
Add crates/security/tests/web_mechanisms_test.rs — full-stack tests composed through a real axum Router (oneshot, no sockets), spanning auth + authz which the per-module tests cover only in isolation: - HTTP Basic -> scoped context -> FilterChain RBAC -> handler: valid creds reach the protected route (200 + principal), bad creds get a 401 Basic challenge, and an absent header is denied by deny-by-default. - SecurityFilterChains routing: /api/** (authenticated) vs a public web surface, via a real Router and matcher dispatch. - Remember-me auto-login yields a remembered (not fully authenticated) principal; a wrong-key token does not auto-login. 3 e2e tests; fmt + clippy clean.
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 parity — Tier 2: the web authentication mechanisms
Builds on the Tier 1 authentication spine to deliver the classic browser/login
surface from Spring's
HttpSecurity. All additive — no behaviour change toexisting code. Adversarially reviewed before release; the review's six confirmed
findings are fixed here.
Added
httpBasic()) —HttpBasicLayerover theAuthenticationManagerspine; absent header passes through, invalid/malformed →
401+WWW-Authenticate: Basic realm="…".formLogin()) —form_login_routes(POST /login), session-idrotation (anti-fixation) before persisting the context, pluggable
success/failure handlers, saved-request-aware redirect.
rememberMe()) —TokenBasedRememberMeServices: HMAC-SHA256token bound to the user's stored password hash + a server key; trust levels
is_remembered()/is_fully_authenticated()(+REMEMBERED_CLAIM).RequestCache/SavedRequest—HttpSessionRequestCache; same-origin-onlypost-login redirect (
is_safe_redirectrejects protocol-relative / backslash /control-char targets).
NullRequestCachefor stateless.SessionCreationPolicy—Always/IfRequired/Never/Stateless;SessionAuthenticationLayer::session_creation_policy(...).SecurityFilterChains(first matchingRequestMatcherwins, Spring'sFilterChainProxy); tower readiness-correct.Adversarial review fixes
SecurityFilterChainsdispatcher.now_secs()fails closed on a pre-epoch clock (was fail-open expiry).redirect()builds theLocationfallibly (no panic / header-splitting).Tests & docs
web_mechanisms_test.rs) +15 doctests — all green;
fmt+clippyclean; full workspacecargo checkgreen at v26.6.31.
markers replaced with professional inline-SVG icons (no emoji) and the book
republished (PDF + EPUB, both editions). CHANGELOG v26.6.31; MODULES.md.
Version bumped to 26.6.31.