Skip to content

refactor: soft-refresh via SecurityContextRepository decorator instead of filter#24495

Closed
netroms wants to merge 2 commits into
feat/userdetails-soft-refresh-v2from
feat/userdetails-soft-refresh-v3
Closed

refactor: soft-refresh via SecurityContextRepository decorator instead of filter#24495
netroms wants to merge 2 commits into
feat/userdetails-soft-refresh-v2from
feat/userdetails-soft-refresh-v3

Conversation

@netroms

@netroms netroms commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #24493 (base is that PR's branch, so this diff shows only the restructure — same behavior, one commit).

  • Replace UserDetailsSoftRefreshFilter with SoftRefreshSecurityContextRepository, decorating the HttpSession delegate of the standard DelegatingSecurityContextRepository pair.
  • Session-only refresh becomes structural instead of gate-guarded: JWT/PAT/stateless authentications never load through HttpSessionSecurityContextRepository, so token-type conversion or accidental session creation is impossible by construction, and the filter-ordering concern disappears.
  • DeferredSecurityContext wrapping runs the freshness check only when a request actually accesses its authentication; under requireExplicitSave the refresh persists itself via the session attribute (SecurityContextHolderFilter never saves — verified against Spring Security 6.5.10 sources).
  • Decision flow, eligibility gates (impersonation skip), and OIDC principal rebuild are moved verbatim from the filter; tests ported to load-through-repository semantics plus new laziness and caching cases.

Test plan

  • Unit: SoftRefreshSecurityContextRepositoryTest (9: PAT/impersonation skip, epoch fast path, gen refresh + self-persistence, OIDC rebuild, deferred laziness, get() caching)
  • H2 web-api: UserControllerTest 52/52, AuthzSoftRefreshH2CompatTest 3/3, MeControllerTest 28/28
  • Live server (fresh DB): grant and revoke visible on the next request of the same session, JSESSIONID unchanged, disable still hard-kicks

AI Assisted

netroms added 2 commits July 18, 2026 21:13
Replace role/user-driven mass SessionRegistry.expireNow with a JDBC-backed
two-level invalidation scheme:

- authz_version table holds per-user ('user', <uid>) and per-role
  ('role', <uid>) generation stamps plus a single global epoch row; every
  bump advances the epoch in the same transaction as the entity change,
  so readers can never observe a stamp without its data. A schema-only
  JPA mapping (AuthzVersion) lets H2 test contexts generate the table;
  production schema stays Flyway-managed (V2_44_16) and validated at
  startup.
- UserDetailsSoftRefreshFilter (session-bound auth only: form + OIDC,
  never JWT/PAT/stateless, never during impersonation) checks one epoch
  read per request; on epoch movement it compares the principal's
  effective generation and lazily rebuilds an immutable UserDetails
  snapshot, replacing the Authentication with an explicit
  SecurityContextRepository save. No logout, no cookie change.
- AuthzService memoizes snapshots per username keyed by a pre-read epoch
  (stamp-before-build invariant) with striped single-flight rebuilds;
  JWT and PAT auth paths share the same epoch-validated cache.
- Role authority/restriction edits and role deletion bump one role key,
  O(1) regardless of member count. User role/org-unit changes and group
  membership deltas (import hook owning side + service paths) bump only
  affected users.
- Gen bumps are update-first with a dialect-branched first-insert
  (Postgres ON CONFLICT DO NOTHING; plain INSERT on H2, which has no
  ON CONFLICT support).
- Password change, disable, lock, and expiry keep hard session
  invalidation.
…d of filter

Decorating the HttpSession delegate of the standard repository pair makes
the refresh session-only by construction: JWT, PAT, and stateless
authentications never load through HttpSessionSecurityContextRepository,
so no token-type conversion or accidental session creation is possible,
and the filter-ordering concern disappears. Wrapping DeferredSecurityContext
runs the freshness check only when a request actually accesses its
authentication. Under requireExplicitSave the refresh persists itself via
the session attribute (SecurityContextHolderFilter never saves).

Same decision flow, gates, and OIDC rebuild as the removed filter; tests
ported to load-through-repository semantics plus laziness and caching
cases.
@netroms
netroms force-pushed the feat/userdetails-soft-refresh-v2 branch from df3f015 to 9dfed6e Compare July 18, 2026 20:07
@netroms

netroms commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Folded into #24493 per review preference for a single readable PR.

@netroms netroms closed this Jul 18, 2026
@netroms
netroms deleted the feat/userdetails-soft-refresh-v3 branch July 18, 2026 20:07
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