Remember-Me Authentication
Keeping users signed in safely: hash-based versus persistent tokens, series rotation and how it detects theft, cookie configuration, and invalidating on password change.
On this page
Remember-me trades security for convenience by keeping a long-lived credential on a device you do not control. Done with rotating persistent tokens the trade is reasonable; done with the default hash mechanism it is not.
Key Takeaways
- Use
PersistentTokenBasedRememberMeServices, not the hash-based default. - Rotation on each use is what makes theft detectable.
- Remember-me is weaker authentication — gate sensitive actions on
isFullyAuthenticated. - Invalidate every token on password change and on explicit sign-out everywhere.
- Set
HttpOnly,SecureandSameSiteon the cookie, exactly as for a session.
The two mechanisms
Hash-based (the default) puts username:expiry:MD5(username:expiry:passwordHash:key) in a cookie.
It requires no storage, and it has two disqualifying weaknesses: the token cannot be invalidated before
it expires, and there is no way to know a cookie was stolen.
Persistent stores a series identifier and a token server-side. The token rotates on every use, which gives you both revocation and theft detection.
Configuration
@Bean
PersistentTokenRepository persistentTokenRepository(DataSource dataSource) {
var repository = new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
return repository;
}
@Bean
SecurityFilterChain chain(HttpSecurity http,
PersistentTokenRepository tokens,
UserDetailsService userDetailsService) throws Exception {
http
.formLogin(Customizer.withDefaults())
.rememberMe(remember -> remember
// A stable secret. If it changes, every existing cookie is void.
.key(System.getenv("REMEMBER_ME_KEY"))
.tokenRepository(tokens)
.userDetailsService(userDetailsService)
.tokenValiditySeconds((int) Duration.ofDays(14).toSeconds())
.rememberMeParameter("remember-me")
.rememberMeCookieName("acme-remember")
.useSecureCookie(true))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/catalogue/**").permitAll()
// Remembered users can browse.
.requestMatchers("/orders/**").authenticated()
// Sensitive pages require a fresh login, not a cookie.
.requestMatchers("/account/security/**", "/account/payment/**")
.fullyAuthenticated());
return http.build();
}CREATE TABLE persistent_logins (
username VARCHAR(64) NOT NULL,
series VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL
);
CREATE INDEX ix_persistent_logins_username ON persistent_logins (username);The fullyAuthenticated() distinction is the part most often skipped. A remembered user should be
able to browse and see their orders; they should not be able to change their password or view stored
payment details without proving who they are again.
One weakness of JdbcTokenRepositoryImpl is worth knowing before you rely on it: that token column
holds the raw value rather than a hash. Anyone who gains read access to the table — through SQL
injection, a stale backup, an over-broad reporting grant — can mint working cookies for every
remembered user without needing a password. Hashing the token and comparing on lookup takes a custom
PersistentTokenRepository of perhaps forty lines, and it puts remember-me on the same footing as
every other credential you store.
How theft detection works
The legitimate user being logged out is not a flaw — it is the design. The server has no way to distinguish victim from thief, so ending both is the only safe response, and the user simply signs in again.
Log this event and notify the user. "We detected unusual activity and signed you out" is how they learn their device may be compromised.
Cookie configuration
@Bean
RememberMeServices rememberMeServices(UserDetailsService userDetailsService,
PersistentTokenRepository tokens) {
var services = new PersistentTokenBasedRememberMeServices(
System.getenv("REMEMBER_ME_KEY"), userDetailsService, tokens);
services.setCookieName("acme-remember");
services.setTokenValiditySeconds((int) Duration.ofDays(14).toSeconds());
services.setUseSecureCookie(true);
services.setAlwaysRemember(false); // opt-in, never automatic
return services;
}Spring sets HttpOnly on the remember-me cookie automatically. Add SameSite=Lax at the container or
proxy level, and always Secure — a remember-me cookie sent over plaintext is a long-lived credential
on the wire.
Fourteen days is a reasonable default. Thirty is common; anything beyond that is a credential sitting on a device for a month with no reauthentication, which is hard to justify outside a low-risk application.
Invalidating
@Service
public class AccountSecurityService {
private final PersistentTokenRepository tokens;
@Transactional
public void changePassword(String username, String newPassword) {
users.updatePassword(username, encoder.encode(newPassword));
// Every remember-me cookie must die with the old password, or an
// attacker who has one keeps their access despite the reset.
tokens.removeUserTokens(username);
sessionRegistry.getAllSessions(username, false).forEach(SessionInformation::expireNow);
notifications.send(username,
"Your password was changed. All devices have been signed out.");
}
/** A user-facing "sign out everywhere" action. */
@Transactional
public void signOutEverywhere(String username) {
tokens.removeUserTokens(username);
sessionRegistry.getAllSessions(username, false).forEach(SessionInformation::expireNow);
}
}Both actions must clear tokens and sessions. Clearing only sessions leaves the remember-me cookie working, which means the "sign out everywhere" button does not.
Housekeeping
@Scheduled(cron = "0 0 3 * * *")
@SchedulerLock(name = "rememberMeCleanup", lockAtMostFor = "PT10M")
public void purgeExpiredTokens() {
int removed = jdbc.update(
"DELETE FROM persistent_logins WHERE last_used < ?",
Timestamp.from(Instant.now().minus(Duration.ofDays(14))));
log.info("purged {} expired remember-me tokens", removed);
}Spring does not clean the table itself, so without a job it grows for the life of the application. Not a security problem, but a slowly degrading query.
Whether to offer it at all
For a consumer application — a shop, a media site, a forum — remember-me is worth it. Users abandon sites that make them log in constantly, and the risk is proportionate.
For an administrative console, a banking application or anything handling regulated data, a long-lived credential on an uncontrolled device is hard to justify. A shorter session with a generous idle timeout gives most of the convenience with far less exposure.
If you offer it, make it opt-in with a clear label — "Keep me signed in on this device" — and
never alwaysRemember(true). Users on shared machines need to be able to decline.
What to take away
Use persistent rotating tokens so theft is detectable and revocation is possible. Treat remembered users as less authenticated and require a fresh login for sensitive pages. Clear tokens on password change and on sign-out everywhere, run a cleanup job, and make the option opt-in.
Frequently Asked Questions
Hash-based or persistent tokens?
How does rotation detect theft?
Should remember-me grant full access?
Related tutorials
- Session Management & SecuritySessions done safely: creation policies, session fixation defence, concurrent session limits, cookie flags that matter, and distributed sessions with Spring Session and Redis.
- Single Sign-On (SSO)Designing single sign-on across several applications: the trust model, choosing SAML or OIDC per tenant, silent authentication, single logout, and running Keycloak as the broker.
- Multi-Factor Authentication (MFA)Implementing a second factor: TOTP enrolment and verification, recovery codes, trusted-device handling, and why WebAuthn is the endpoint worth aiming at.
- Kerberos & SPNEGOSeamless Windows domain authentication: how Kerberos tickets work, SPNEGO negotiation over HTTP, keytab and SPN setup, Spring configuration, and diagnosing the usual failures.