skip to content

Compare PersistentTokenBasedRememberMeServices with TokenBasedRememberMeServices. When would you choose persistent tokens?

level: seniorimportance: must knowfreq 50%

answer

  1. stateless hash vs series/token table
  2. persistent_logins: username, series(PK), token, last_used
  3. token rotates each use; series stable
  4. series valid + token mismatch => CookieTheftException
  5. persistent = per-device revoke + theft detection

basics

~20 s

TokenBased is a stateless signed cookie with no storage but no per-device revocation. Persistent stores a series/token row per login in a database, rotates the token on each use, allows revoking single devices, and can detect a stolen (cloned) cookie. Choose persistent when you need revocation and theft detection.

solid answer

~50 s

Both implement RememberMeServices. TokenBasedRememberMeServices is fully stateless: the cookie is a self-signed hash of username+expiry+password+key, needing no storage, but a stolen cookie stays valid until expiry and can't be individually revoked (only a password change kills all tokens). PersistentTokenBasedRememberMeServices keeps a row per remembered login in a persistent_logins table (via a PersistentTokenRepository such as JdbcTokenRepositoryImpl): each row has a stable 'series' id, a rotating 'token', the username, and last-used time. On each successful auto-login the token is regenerated (same series) and the cookie updated. This enables per-device logout (delete the row) and stolen-cookie detection: if a request presents a valid series but a mismatched token, the service assumes the cookie was cloned, deletes the series, and throws CookieTheftException, forcing everyone on that series to re-authenticate. Choose persistent when you need revocation, theft detection, or work with passwordless identities.

code

java · 20 lines
java
@Bean
PersistentTokenRepository tokenRepository(DataSource ds) {
    var repo = new JdbcTokenRepositoryImpl();
    repo.setDataSource(ds);
    // repo.setCreateTableOnStartup(true); // dev only; prod uses a migration
    return repo;
}

@Bean
SecurityFilterChain security(HttpSecurity http,
                             PersistentTokenRepository repo,
                             UserDetailsService uds) throws Exception {
    http.formLogin(Customizer.withDefaults())
        .rememberMe(rm -> rm
            .key("change-me-in-prod")
            .tokenRepository(repo)   // presence of a repo => PersistentTokenBasedRememberMeServices
            .userDetailsService(uds)
            .tokenValiditySeconds(1209600));
    return http.build();
}

go deeper

for a junior

Know one uses a database table and one doesn't.

for a middle

Name the persistent_logins columns and that the token rotates while the series stays.

for a senior

Explain theft detection via series-valid/token-mismatch and per-device revocation trade-offs.

for a principal

Reason about clustering the token repository, false-positive theft under concurrency/prefetch, and schema ownership via migrations.

Both classes implement the `RememberMeServices` interface (`autoLogin`, `loginSuccess`, `loginFail`) and are selected via `http.rememberMe(...)`. The difference is *where the trust lives*. **TokenBasedRememberMeServices (stateless).** - Cookie = Base64(`username:expiry:algorithm:signature`), signature = hash(`username:expiry:password:key`). - No server storage. Validation recomputes the hash from the loaded user's password + key. - Pros: trivial to deploy, no table, scales horizontally with no shared state. - Cons: no per-device revocation (only a password change invalidates, and it invalidates *all* tokens); a stolen cookie is replayable until expiry; no theft detection; needs a local password so unsuitable for pure OAuth/OIDC. **PersistentTokenBasedRememberMeServices (stateful).** - Backed by a `PersistentTokenRepository`. The JDBC impl (`JdbcTokenRepositoryImpl`) uses a table: ```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 ); ``` - The cookie carries a **series** identifier and a **token** value (Base64). The series is a stable, per-login-instance random id; the token rotates. - **On each auto-login**: look up the row by series. If found and the presented token matches the stored token, authentication succeeds and the service **generates a new token**, updates the row and the cookie (token rotation / single-use tokens). If the series is unknown or expired, it's simply an invalid/expired cookie. - **Theft detection**: if the series exists but the presented **token does not match** the stored one, that means two clients used the same series — the legitimate user rotated the token, and someone with an *old* copy of the cookie is now presenting a stale token (or vice versa). The service interprets this as a **cloned/stolen cookie**, deletes all tokens for that user's series, and throws `CookieTheftException`. Both the attacker and the victim are forced to log in again, and the victim is alerted something happened. - **Per-device revocation**: deleting a specific `series` row (e.g. a 'log out other devices' feature) invalidates just that device's cookie. **Trade-offs / gotchas.** - Persistent needs a datastore and its own migration; in a cluster the table must be shared (or use a distributed `PersistentTokenRepository`). - Token rotation means concurrent requests with the same cookie (e.g. parallel tab loads, or a client that doesn't update the cookie) can trip false theft detection. This is a real-world gotcha with aggressive prefetching or when the Set-Cookie response is lost. - Persistent still doesn't strictly require the password for validation (it matches series/token), so it composes better with external identity — though the DSL still wires a `UserDetailsService` to load authorities. - `JdbcTokenRepositoryImpl.setCreateTableOnStartup(true)` can auto-create the table in dev; in prod you own the schema (e.g. via Liquibase). **Choosing.** - Prefer **TokenBased** for simple apps that accept 'password change logs out everywhere' and want zero storage. - Prefer **Persistent** when you need per-device logout, stolen-cookie detection, an audit of remembered devices, or a smaller replay window via rotation. Either way, serve over HTTPS with `Secure`/`HttpOnly` cookies and set a fixed `key`.

  • What exactly triggers a CookieTheftException?
    A request presents a cookie whose series exists in persistent_logins but whose token does not match the stored token. That mismatch implies two parties share the series (the token was rotated for one but not the other), so the service treats it as theft, deletes the user's series tokens, and forces re-login.
  • Why can token rotation cause false theft detection?
    The token is single-use; each auto-login rotates it. If a client makes concurrent requests with the same cookie, or the Set-Cookie updating the token is lost/blocked, a later request presents the now-stale token against a series whose stored token already changed — indistinguishable from a clone.

saying these in an interview costs you the question

  • Saying persistent tokens don't rotate
  • Claiming TokenBased can detect cookie theft
  • Thinking the 'series' rotates and the 'token' is stable (it's the reverse)
  • Believing per-device logout is possible with TokenBased

context