Skip to content

Complete remember-me support: persistent token store, config, docs, and token revocation #351

Description

@devondragon

Summary

user.security.rememberMe.enabled exists and is wired into the filter chain, but the surrounding support is missing to the point that enabling it does nothing. There is no persistent token store, no cookie configuration, no documentation, no test coverage, and no remember-me field in the reference login form — so a consumer who sets the flag gets no cookie and no error telling them why.

This proposes completing it. The security interaction in step 3 is the substantive part; the rest is wiring.

Current state

Wired in security/WebSecurityConfig.java:

// lines 101-105
@Value("${user.security.rememberMe.enabled:false}")
private boolean rememberMeEnabled;

@Value("${user.security.rememberMe.key:#{null}}")
private String rememberMeKey;

// lines 155-156
if (rememberMeEnabled && rememberMeKey != null && !rememberMeKey.trim().isEmpty()) {
    http.rememberMe(rememberMe -> rememberMe.key(rememberMeKey).userDetailsService(userDetailsService));
}

What's missing:

  • No PersistentTokenRepository or RememberMeServices bean anywhere in src/ (verified by grep). Passing only key + userDetailsService leaves Spring's hash-based TokenBasedRememberMeServices.
  • No persistent_logins table in db-scripts/, so the JDBC token-repository path isn't provisioned either way.
  • No cookie configuration. Only two properties exist (enabled, key) — no token validity, secure-cookie flag, cookie name, or request-parameter name.
  • Undocumented. Absent from src/main/resources/config/dsspringuserconfig.properties and from CONFIG.md. The only traces are additional-spring-configuration-metadata.json:308-316 and a bare README.md bullet under Local Authentication.
  • Zero test coverage. grep -rn "rememberMe\|RememberMe" src/test returns nothing.
  • Unreachable via the reference login form. The demo app's templates/user/login.html posts only username and password. With no remember-me parameter and no alwaysRemember, AbstractRememberMeServices never issues a cookie. Enabling the flag today is a no-op for the documented login path. (Form change tracked separately: Add remember-me checkbox to the reference login form SpringUserFrameworkDemoApp#79.)

Why this is worth doing here

DSUserDetails implements UserDetails, OAuth2User, and OidcUser simultaneously (service/DSUserDetails.java:43), and every authentication path converges on it — form login, OAuth2, OIDC, and WebAuthn. That means a RememberMeAuthenticationToken carrying a DSUserDetails principal is type-compatible with every @AuthenticationPrincipal DSUserDetails controller signature in the library. The usual blocker for adding remember-me to an OAuth2-flavoured app — that remember-me mints a different principal type than the OAuth2 filters do — does not exist in this codebase.

Two known behavioural caveats, both acceptable and worth documenting rather than fixing:

  • On a remember-me login, getIdToken() / getUserInfo() return null and getAttributes() falls back to the map synthesized from the User entity (DSUserDetails.java:153-178, 269-282). A consumer reading id-token claims degrades gracefully but loses provider claims.
  • AuthenticationEventListener.java:59-62 has a principal instanceof String branch commented "Basic authentication or remember-me". That branch is unreachable for this codebase's remember-me, which lands in the DSUserDetails branch. Stale comment, no functional impact.

Proposed work

1. Configuration surfacesecurity/WebSecurityConfig.java:101-105, 155-156

Add four properties, defaulting to Spring Security's own defaults:

Property Default
user.security.rememberMe.tokenValiditySeconds 1209600 (14 days)
user.security.rememberMe.rememberMeParameter remember-me
user.security.rememberMe.rememberMeCookieName remember-me
user.security.rememberMe.useSecureCookie unset — defer to Spring's behaviour (secure flag set when the request is HTTPS)

Inject an optional PersistentTokenRepository and call .tokenRepository(...) when one is present, so consumers can choose hash-based or persistent without editing the chain.

2. Optional persistent token storesecurity/UserSecurityBeansAutoConfiguration.java + db-scripts/

A @ConditionalOnMissingBean(PersistentTokenRepository.class) JdbcTokenRepositoryImpl, gated on a property so it is never created without the table existing. Add persistent_logins to the schema scripts.

3. Token revocation on session invalidation

SessionInvalidationService.invalidateUserSessions (line 67) and invalidateSessionsAfterPasswordChange (line 127) both work purely through SessionInformation.expireNow() (lines 93, 156). Neither touches remember-me state, because there is none today.

Decided semantics, by token type:

  • Persistent tokens (JDBC store): both invalidateUserSessions and invalidateSessionsAfterPasswordChange call PersistentTokenRepository.removeUserTokens(username) when a PersistentTokenRepository bean is present (inject it as optional). This must land in the same change as step 2, because TokenBasedRememberMeServices signs the cookie with the password hash — persistent tokens lose that incidental password-change protection, so shipping the store without revocation would be a regression relative to the hash-based default.
  • Hash-based tokens (default): admin-initiated revocation ("sign this user out everywhere") is not implementableTokenBasedRememberMeServices keeps no server-side state; the cookie is a self-contained signature over username, expiry, password hash, and key. Nothing to delete. Password change already invalidates these cookies (signature mismatch). Document this as an explicit limitation of hash-based mode: consumers who need admin revocation must use the persistent store. Do not attempt a workaround (key rotation would log out all users).

Worth checking against the fix in #329, which addressed this class of gap for sessions.

To be precise about what is not affected: disabled and locked accounts are already rejected on remember-me auto-login, because AbstractRememberMeServices runs an AccountStatusUserDetailsChecker and DSUserDetails.isEnabled() / isAccountNonLocked() return real values from the User entity (DSUserDetails.java:226-228, 246-248). This is not a lockout-bypass issue.

4. Documentationdsspringuserconfig.properties, CONFIG.md, README.md

Document the properties with defaults, the persistent_logins requirement when using the JDBC store, the hash-based admin-revocation limitation from step 3, and — most importantly — that the consumer's login form must post the remember-me parameter. The current bare README bullet implies the feature works out of the box.

5. Tests — currently none

  • Cookie issuance on login with the parameter present.
  • Cookie-based re-authentication producing a DSUserDetails principal.
  • SessionInvalidationService calls removeUserTokens for persistent tokens on both invalidation paths (and is a no-op without a PersistentTokenRepository bean).
  • Hash-based cookie is rejected after a password change (signature mismatch) — this is the only revocation guarantee for hash-based mode; there is deliberately no test for admin revocation of hash-based tokens, per step 3.
  • A remember-me login still populates session profiles via InteractiveAuthenticationSuccessEventBaseAuthenticationListener listens on that event (line 41) and Spring's RememberMeAuthenticationFilter publishes it, but nothing in this repo verifies it.

Out of scope

The reference login form checkbox lives in the demo app and is tracked as devondragon/SpringUserFrameworkDemoApp#79.

Note on priority

No issue in this repo, open or closed, reports idle logout or asks for a stay-signed-in option. This is a latent gap in a half-wired feature rather than a response to reported pain — reasonable to schedule accordingly. The argument for doing it as one unit rather than incrementally is step 3: the persistent token store and its revocation path should not ship in separate releases.

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions