Skip to content

feat(security): Spring Security parity — Tier 2 web mechanisms (v26.6.31)#39

Merged
ancongui merged 8 commits into
mainfrom
feat/spring-security-tier2-web-mechanisms
Jun 19, 2026
Merged

feat(security): Spring Security parity — Tier 2 web mechanisms (v26.6.31)#39
ancongui merged 8 commits into
mainfrom
feat/spring-security-tier2-web-mechanisms

Conversation

@ancongui

Copy link
Copy Markdown
Contributor

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 to
existing code. Adversarially reviewed before release; the review's six confirmed
findings are fixed here.

Added

  • HTTP Basic (httpBasic()) — HttpBasicLayer over the AuthenticationManager
    spine; absent header passes through, invalid/malformed → 401 +
    WWW-Authenticate: Basic realm="…".
  • Form login (formLogin()) — form_login_routes (POST /login), session-id
    rotation (anti-fixation) before persisting the context, pluggable
    success/failure handlers, saved-request-aware redirect.
  • Remember-me (rememberMe()) — TokenBasedRememberMeServices: HMAC-SHA256
    token bound to the user's stored password hash + a server key; trust levels
    is_remembered() / is_fully_authenticated() (+ REMEMBERED_CLAIM).
  • RequestCache / SavedRequestHttpSessionRequestCache; same-origin-only
    post-login redirect (is_safe_redirect rejects protocol-relative / backslash /
    control-char targets). NullRequestCache for stateless.
  • SessionCreationPolicyAlways/IfRequired/Never/Stateless;
    SessionAuthenticationLayer::session_creation_policy(...).
  • Multiple filter chainsSecurityFilterChains (first matching
    RequestMatcher wins, Spring's FilterChainProxy); tower readiness-correct.

Adversarial review fixes

  • Open-redirect guard on the saved-request redirect (same-origin only).
  • Tower readiness contract honoured in the SecurityFilterChains dispatcher.
  • HMAC-SHA256 remember-me signature (was bare SHA-256 with key as a suffix).
  • now_secs() fails closed on a pre-epoch clock (was fail-open expiry).
  • redirect() builds the Location fallibly (no panic / header-splitting).

Tests & docs

  • 148 lib + 113 integration (incl. new end-to-end web_mechanisms_test.rs) +
    15 doctests — all green; fmt + clippy clean; full workspace cargo check
    green at v26.6.31.
  • Book Spring Security Parity appendix updated (EN + ES); emoji status
    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.

Andres Contreras 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.
@ancongui ancongui merged commit ce1311a into main Jun 19, 2026
4 checks passed
@ancongui ancongui deleted the feat/spring-security-tier2-web-mechanisms branch June 19, 2026 16:59
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