Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

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.

Beginner5 min readUpdated
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, Secure and SameSite on 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

RememberMeConfig.java
@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();
}
V3__persistent_logins.sql
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

A stale token for a live series means two parties hold cookies. The server cannot tell which is which, so it invalidates both.

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.

CookieConfig.java
@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

InvalidateTokens.java
@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

TokenCleanup.java
@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?
Persistent. The hash-based default encodes the username, an expiry and the password hash into a cookie that cannot be invalidated before it expires and gives no way to detect theft. Persistent tokens rotate on each use, which makes a stolen cookie detectable and revocable.
How does rotation detect theft?
Each use issues a new token for the same series and invalidates the old one. If an old token is presented afterwards, two parties hold cookies for that series — the legitimate user and a thief. You cannot tell which is which, so the correct response is to invalidate the entire series and require a fresh login.
Should remember-me grant full access?
No. Treat it as a weaker authentication. Spring distinguishes fully authenticated from remembered, so use isFullyAuthenticated for anything sensitive — changing a password, viewing payment details — and re-prompt for credentials.

Related tutorials