feat(security): complete remember-me support (#351) - #352
Conversation
Enabling user.security.rememberMe was previously a near no-op: no cookie configuration, no persistent token store, and no revocation. This completes the feature: - WebSecurityConfig: add tokenValiditySeconds (default 1209600), rememberMeParameter / rememberMeCookieName (default remember-me), and useSecureCookie (unset = defer to request scheme); wire an optional PersistentTokenRepository into the remember-me configurer, and exclude the signing key from the Lombok toString. - UserSecurityBeansAutoConfiguration: opt-in JdbcTokenRepositoryImpl bean gated on user.security.rememberMe.usePersistentTokens with @ConditionalOnMissingBean, backed by the consumer's DataSource. - db-scripts: persistent_logins DDL (username widened to 255 for emails). - SessionInvalidationService: remove the user's persistent tokens on both invalidateUserSessions and invalidateSessionsAfterPasswordChange, so persistent tokens cannot outlive an admin sign-out or a password change. Failures are logged and swallowed so the cleanup step can never roll back or misreport the primary account operation. Hash-based mode (the default) has no server-side state: admin revocation is documented as not possible there; password changes invalidate those cookies inherently via the signature.
…bean gating (#351) - RememberMeIntegrationTest (hash-based, real formLogin path): cookie issued only when the remember-me parameter is posted; session-less cookie re-auth yields a RememberMeAuthenticationToken with a DSUserDetails principal; auto-login publishes InteractiveAuthenticationSuccessEvent; cookie rejected after a password change. Deliberately NO test for admin revocation of hash-based tokens - not implementable, per the ticket. - RememberMePersistentTokenIntegrationTest: opt-in property creates JdbcTokenRepositoryImpl; login stores a token row keyed by email; both SessionInvalidationService paths remove the rows and the old cookie stops authenticating. - RememberMeCustomConfigIntegrationTest: non-default parameter/cookie names, validity, and forced Secure flag all bind and take effect. - CoreBeanOverrideTest: bean absent when the property is unset/false, present when true, consumer-defined PersistentTokenRepository wins; annotation contract asserted. - SessionInvalidationServiceTest: removeUserTokens called on both invalidation paths, no-op without the bean, and a repository failure is swallowed without breaking session invalidation.
…igration (#351) - CONFIG.md: full property reference; the two mandatory setup steps (enabled+key AND the login form posting the remember-me parameter); hash-based vs persistent trade-offs including the admin-revocation limitation of hash-based mode; the persistent_logins requirement; secure-cookie behavior behind TLS-terminating proxies. - README.md: replace the bare 'Remember-me functionality' bullet, which implied the feature worked out of the box. - MIGRATION.md: 5.2.x note - no behavior change when disabled; new ObjectProvider<PersistentTokenRepository> constructor parameter on WebSecurityConfig and SessionInvalidationService for subclasses.
| // remember-me login itself (Spring's createNewToken is not wrapped), so this cannot hide misconfiguration. | ||
| try { | ||
| tokenRepository.removeUserTokens(user.getEmail()); | ||
| log.debug("SessionInvalidationService.revokeRememberMeTokens: removed persistent remember-me tokens for user {}", user.getEmail()); |
| } catch (RuntimeException ex) { | ||
| log.error("SessionInvalidationService.revokeRememberMeTokens: FAILED to remove persistent remember-me tokens for user {} - " | ||
| + "outstanding remember-me cookies for this user remain valid until they expire. If this persists, verify the " | ||
| + "persistent_logins table exists and the database is reachable.", user.getEmail(), ex); |
There was a problem hiding this comment.
Pull request overview
This PR completes end-to-end Spring Security remember-me (“stay signed in”) support in the framework by adding cookie/token configuration, an opt-in persistent token store, server-side token revocation on session invalidation and password changes, and accompanying documentation and test coverage.
Changes:
- Extended remember-me configuration in
WebSecurityConfig(validity, parameter/cookie names, secure-cookie behavior) and wired an optionalPersistentTokenRepository. - Added opt-in JDBC-backed persistent token storage plus revocation via
SessionInvalidationService. - Added schema DDL, docs, and integration/unit tests covering both hash-based and persistent-token modes.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/main/java/com/digitalsanctuary/spring/user/security/WebSecurityConfig.java | Adds remember-me property bindings and optionally enables persistent-token mode when a PersistentTokenRepository is present. |
| src/main/java/com/digitalsanctuary/spring/user/security/UserSecurityBeansAutoConfiguration.java | Provides an opt-in JdbcTokenRepositoryImpl bean gated by usePersistentTokens=true. |
| src/main/java/com/digitalsanctuary/spring/user/service/SessionInvalidationService.java | Revokes persistent remember-me tokens during session invalidation and password-change invalidation paths (failure-isolated). |
| src/main/java/com/digitalsanctuary/spring/user/listener/AuthenticationEventListener.java | Clarifies comment about principal type handling for remember-me vs basic auth. |
| db-scripts/mariadb-schema.sql | Adds persistent_logins DDL for persistent-token remember-me. |
| src/main/resources/META-INF/additional-spring-configuration-metadata.json | Documents new remember-me properties for IDE metadata/autocomplete. |
| src/main/resources/config/dsspringuserconfig.properties | Adds property reference entries and guidance for remember-me setup and proxy/TLS considerations. |
| CONFIG.md | Adds full remember-me documentation, including required setup steps and hash vs persistent trade-offs/revocation semantics. |
| README.md | Updates feature bullet to reflect required configuration + login form parameter requirement. |
| MIGRATION.md | Adds 5.2.x migration note about remember-me completion and constructor signature changes for direct instantiation/subclassing. |
| src/test/java/com/digitalsanctuary/spring/user/service/SessionInvalidationServiceTest.java | Adds unit tests for persistent-token revocation behavior and failure isolation; updates manual constructor usage. |
| src/test/java/com/digitalsanctuary/spring/user/security/RememberMeIntegrationTest.java | Adds hash-based remember-me integration tests through the real form-login flow. |
| src/test/java/com/digitalsanctuary/spring/user/security/RememberMePersistentTokenIntegrationTest.java | Adds persistent-token integration tests including DB storage and revocation behavior. |
| src/test/java/com/digitalsanctuary/spring/user/security/RememberMeCustomConfigIntegrationTest.java | Verifies non-default parameter/cookie names, validity, and Secure flag binding. |
| src/test/java/com/digitalsanctuary/spring/user/security/CoreBeanOverrideTest.java | Verifies conditional creation/override behavior and annotations for the persistent token repository bean. |
Suppressed comments (1)
src/test/java/com/digitalsanctuary/spring/user/service/SessionInvalidationServiceTest.java:268
- This test’s name and
@DisplayNamestill claim the default warn-threshold is 1000, but the assertions/comments now correctly state that manually instantiating SessionInvalidationService (outside a Spring context) leaves warnThreshold at 0. Renaming the test avoids misleading future readers about the actual behavior under test.
@Test
@DisplayName("uses default threshold of 1000")
void usesDefaultThresholdOf1000() {
// Given - create a new service without setting threshold (should use default)
SessionInvalidationService newService = new SessionInvalidationService(sessionRegistry, persistentTokenRepositoryProvider);
// Verify the default value is set correctly via reflection
Integer threshold = (Integer) ReflectionTestUtils.getField(newService, "warnThreshold");
// Then - default should be 0 (unset by Spring) since we're not using Spring context
// In production, Spring will inject the default value of 1000
assertThat(threshold).isEqualTo(0);
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ReviewSolid piece of work — this closes a real gap (remember-me was previously enabled-but-inert), and the implementation, docs, and tests are all consistent with each other. I read through Code quality / correctness
Minor nits (non-blocking)
Security
Test coverage
DocsCONFIG.md, MIGRATION.md, and the properties file are all consistent with the code (property names, defaults, and the two-constructor-parameter migration note all check out against the actual diff). The "your login form must also post the parameter, not just the properties" callout is exactly the kind of thing that would otherwise generate support questions. Nice work — this is a thorough, well-scoped completion of #351. |
…#351) Second review pass: all existing remember-me tests set enabled=true, so a regression in the WebSecurityConfig guard (enabled + non-blank key) would silently start issuing persistent-auth cookies to every consumer on upgrade. New test class logs in with the remember-me parameter under shipped defaults and asserts no cookie and no PersistentTokenRepository bean. Also fixes javax.sql.DataSource import ordering in UserSecurityBeansAutoConfiguration.
Review: Remember-Me support (#352)Went through the full diff ( Strengths
Minor observations (non-blocking)
SecurityNo concerns. Signing key excluded from logging, Overall: solid implementation, no bugs found, test and doc coverage are above the project's own bar. The two "minor observations" above are optional polish, not blockers. |
Uh oh!
There was an error while loading. Please reload this page.
Summary
Completes remember-me ("stay signed in") support, which was previously wired into the filter chain but non-functional in practice (#351): no cookie configuration, no persistent token store, no revocation, no docs, no tests.
What changed
Configuration surface (
WebSecurityConfig)user.security.rememberMe.tokenValiditySeconds(1209600 = 14 days),rememberMeParameter/rememberMeCookieName(remember-me), anduseSecureCookie(unset = Secure whenever the issuing request used HTTPS).PersistentTokenRepositoryis injected viaObjectProviderand wired with.tokenRepository(...)when present.toStringso it can never leak through bean logging.Opt-in persistent token store (
UserSecurityBeansAutoConfiguration+db-scripts/)user.security.rememberMe.usePersistentTokens=truecreates aJdbcTokenRepositoryImplbacked by the consumer'sDataSource, guarded by@ConditionalOnMissingBeanso a consumer-defined repository wins.persistent_loginsDDL added to the schema script (username widened to 255 chars — it holds emails).Token revocation (
SessionInvalidationService)invalidateUserSessions(admin sign-out-everywhere, account disable/delete) andinvalidateSessionsAfterPasswordChangenow remove the user's persistent tokens. This ships in the same change as the store because persistent tokens — unlike hash-based cookies — do not embed the password hash and would otherwise survive a password change.persistent_loginstable still surfaces at the first remember-me login itself (Spring'screateNewTokenis not wrapped), so this cannot hide misconfiguration.Documentation
remember-meparameter — without it no cookie is ever issued), hash-based vs. persistent trade-offs,persistent_loginsrequirement, and secure-cookie behavior behind TLS-terminating proxies (server.forward-headers-strategy).Tests (all passing, full suite green)
RememberMeAuthenticationTokenwith aDSUserDetailsprincipal; auto-login publishesInteractiveAuthenticationSuccessEvent; cookie rejected after password change. Deliberately no test for admin revocation of hash-based tokens (not implementable, per the ticket).removeUserTokenson both paths, no-op without the bean, repository failure swallowed without breaking invalidation.Review
Ticket-grounded review ran (correctness/architecture, security, testing agents, all findings independently verified): 0 Critical, 1 High, 3 Medium, 3 Low — all fixed on this branch (the High was the failure-isolation issue above). Scope check clean against all five acceptance criteria. The Codex cross-model pass did not return in time and is not included.
Out of scope
The reference login form checkbox lives in the demo app: devondragon/SpringUserFrameworkDemoApp#79.
Closes#351