Remember me in Spring Security

The article about basic security filter in Spring Security introduces us to particularity of handle remember-me authentication. This time, we'll focus on this aspect more in details.

This time we'll describe remember-me authentication more in details. It's an interesting topic because Spring Security uses two methods to handle this feature. In the first part of this article we'll see the first method, based on standard cookie mechanism. After that, we'll present the second method of remember-me management, based on cookie and persistent mechanism technologies. The last part of this article will show how to setup this second method, by definition more complicated than the first one.

Cookie-based remember-me mechanism in Spring Security

All elements needed to handle remember-me mechanism are placed inside org.springframework.security.web.authentication.rememberme package. The most important component is RememberMeAuthenticationFilter which detects if remember-me cookie is set and if it can be resolved into Authentication object. This second operation is made with one of two possible remember-me services. The first one, considered as the basic one, is TokenBasedRememberMeServices. This service uses cookie composed by following parts:
- username: the login of remembered user.
- token's expiration time: the validity of remember-me cookie.
- user's password: password of remembered user.
- salt (key): used to prevent manual modifications of remember-me cookie.

The two first attributes are present as normal, plaintext strings. The third string presented in the cookie is a security token composed by all listed elements. Password in token looks a little bit scary. However, it isn't presented as plaintext version. Instead, it's used, with other attributes, to construct remember-me cookie signature. This signature is a hexadecimal representation of md5 algorithm's execution on text composed by presented attributes (username, expiration time, password, salt). All attributes are separated by the character of double point (:). What does Spring do with this token ? Before working on it, TokenBasedRememberMeServices checks if cookie is still valid. If its expiration time is outdated, it invalidates cookie by throwing InvalidCookieException:

long tokenExpiryTime;

try {
  tokenExpiryTime = new Long(cookieTokens[1]).longValue();
}
catch (NumberFormatException nfe) {
  throw new InvalidCookieException("Cookie token[1] did not contain a valid number (contained '" +
      cookieTokens[1] + "')");
}

if (isTokenExpired(tokenExpiryTime)) {
  throw new InvalidCookieException("Cookie token[1] has expired (expired on '"
      + new Date(tokenExpiryTime) + "'; current time is '" + new Date() + "')");
}

When cookie is still valid, TokenBasedRememberMeService retrieves an instance of UserDetails through login stored in clear in the cookie. After that, a local security token is constructed with protected String makeTokenSignature(long tokenExpiryTime, String username, String password) method. The result of this method is compared with security token included in the cookie. If both match, UserDetails instance is returned and authentication is made by converting it into Authentication object. The conversion is made by public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) method of AbstractRememberMeServices. Otherwise, InvalidCookieException is thrown and analyzed remember-me cookie is deleted.

This solution is simple but has a big drawback. When user changes his password, remember-me cookie won't work anymore because local security token won't be composed by the same elements as security token stored in the cookie.

Remember-me with persistent storage in Spring Security

Another solution is based on the mix of cookie and persistent (as database) storage. Remember me cookie doesn't store anymore the username. It stores only an identifier which will be used to get corresponding user from persistent storage (database table for example). If we use database as persistent storage, we need to keep in mind that identifier column must be unique. Otherwise, one token can correspond to more than one user. To understand this mechanism better, let's suppose that we use database as storage for remember me cookies. Spring Security defines the way to access to it through an implementation of PersistentTokenRepository interface. Implementation used for database is JdbcTokenRepositoryImpl. Default structure of table which stores mapping with cookies is:

create table persistent_logins (
  series varchar(64) primary key,
  username varchar(64) not null, 
  token varchar(64) not null, 
  last_used timestamp not null
);

You can find there username field. However, there're no username stored in remember me cookie. Only the values belonging to the first (series) and the third (token) columns are. Details of matching cookie with database entry are placed in PersistentTokenBasedRememberMeServices class and its protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) method. First of all, persistent service checks if two expected elements are present in the cookie: series and token.

The series value is used to retrieve corresponding row from persistents_logins table. This entry is represented by PersistentRememberMeToken class. If found, the service checks if database's token field equals to cookie's token value. If not, CookieTheftException is thrown. Otherwise, a second check is made for the cookie validity, exactly as in expiration time in the case of unique cookie-based remember me service. In the other side, if everything works as expected, we update remember me cookie information by generating new token and new expiration time. These values are saved in the database and in cookie through below code:

PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(),
   token.getSeries(), generateTokenData(), new Date());

try {
  tokenRepository.updateToken(newToken.getSeries(), 
    newToken.getTokenValue(), newToken.getDate());
  addCookie(newToken, request, response);
} catch (DataAccessException e) {
  logger.error("Failed to update token: ", e);
  throw new RememberMeAuthenticationException
   ("Autologin failed due to data access problem");
}
// some other stuff here

protected String generateSeriesData() {
  byte[] newSeries = new byte[seriesLength];
  random.nextBytes(newSeries);
  return new String(Base64.encode(newSeries));
}

protected String generateTokenData() {
  byte[] newToken = new byte[tokenLength];
  random.nextBytes(newToken);
  return new String(Base64.encode(newToken));
}

private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
  setCookie(new String[] {
    token.getSeries(), token.getTokenValue()
  }, getTokenValiditySeconds(), request, response);
}

This data refresh protects user against dangers of login from different computers or browsers. Imagine that he logs to our system from Chrome installed in his friend's PC. But when he comes back to home, he needs to log from home PC. Now, user's friends won't be able to login as user because database's token field changes meanwhile. But in the other side, this connection could be possible in the case of only cookie-based remember me solution, presented in the first part of this article.

You should also note that logout means the removing of remember me cookie. In the case of persistent-based solution, not only cookie is removed but also database row corresponding to it.

Setup remember-me with persistant storage in Spring Security

In our example we'll try to implement remember me mechanism based on cookie-database mix. First of all, we'll setup the database by creating appropriate context file:

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
  <property name="driverClass" value="com.mysql.jdbc.Driver" />
  <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/webapp" />
  <property name="user" value="root" />
  <property name="password" value="" />
</bean>

Once created, we need to create tables handling our authentication objects:

create table persistent_logins (username varchar(64) not null,
  series varchar(64) primary key,
  token varchar(64) not null,
  last_used timestamp not null)

Now we can configure our Spring Security filter chain in separate context file:

<security:http authentication-manager-ref="frontend" auto-config="true" use-expressions="true">
  <security:form-login login-page="/login" default-target-url="/secret/data"
    authentication-failure-url="/login?error=true" password-parameter="password"
    username-parameter="login" login-processing-url="/do-login" />
    <security:remember-me data-source-ref="dataSource" key="secret_remember_me" />
    <security:logout logout-url="/logout" logout-success-url="/login" invalidate-session="true" delete-cookies="JSESSIONID" />
</security:http>

<security:authentication-manager id="frontend">
  <security:authentication-provider>
    <security:user-service>
      <security:user name="bartosz" password="bartosz" authorities="ROLE_ADMIN,ROLE_USER" />
    </security:user-service>
  </security:authentication-provider>
</security:authentication-manager>

The fragment which interests us is because it represents the configuration. We can observe there the presence of data-source-ref attribute which refers to database connection information. The second attribute, key, is used as a salt for value stored in cookie. We need also to create simple login page and observe what does happen in our browser. Let's begin by login page:

<form method="post" action="/do-login">
  <input type="text" name="login" placeholder="Your login" />
  <input type="password" name="password" placeholder="Your password" />
  remember me <input type="checkbox" name="_spring_security_remember_me" />
  <input type="submit" name="doLogin" value="Login" />
</form>

Now, after deploying and logging, SPRING_SECURITY_REMEMBER_ME_COOKIE should be put into cookie. An example of it can be:

SPRING_SECURITY_REMEMBER_ME_COOKIE=Wnh2V21CcCsxNk5SZUhrZ2VQQzZ0Zz09OmRVSi9jYTdlNlF6Z1Q0VmtYRUZvVHc9PQ

This cookie is encoded with base64. It will be decoded with the same algorithm at remember-me authentication. If we try to decode it manually, we should receive below text:

ZxvWmBp+16NReHkgePC6tg==:dUJ/ca7e6QzgT4VkXEFoTw==

As we saw previously, two strings are present here, separated by :. They should represent series and token value stored in the persisents_logins table. We can check it with simple SQL query :

mysql> SELECT * FROM persistent_logins WHERE series = "ZxvWmBp+16NReHkgePC6tg==" AND token = "dUJ/ca7e6QzgT4VkXEFoTw==";
+----------+--------------------------+--------------------------+---------------------+
| username | series                   | token                    | last_used           |
+----------+--------------------------+--------------------------+---------------------+
| bartosz  | ZxvWmBp+16NReHkgePC6tg== | dUJ/ca7e6QzgT4VkXEFoTw== | 2014-08-26 13:26:40 |
+----------+--------------------------+--------------------------+---------------------+
1 row in set (0.00 sec)

When we logout, we should observe remember me cookie should be removed as well from cookies as from persistent_logins table:

mysql> select * from persistent_logins;
Empty set (0.00 sec)

Through this article we discovered two ways to handle remember me authentication in Spring Security. The first part presented the simplest method, with a cookie containing all user credentials. But this solution is less safer than the second one which stores nothing associated with user credentials in the cookie. It stores only an intermediate value which helps to retrieve remembered user. At the last part of this article we saw how to put this second solution in place. We also checked what did happen in cookies and database when user logged-in and logged-out.

If you liked it, you should read:

The comments are moderated. I publish them when I answer, so don't worry if you don't see yours immediately :)

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!