Spring security 3 remember-me with LDAP authentication



For a client I had to implement Springs "remember-me" functionality.Remember me allows a user to login, then close the browser and re open it and access a secured application without the need to re-enter the login details.

To implement the feature, I read many resources freely available on the internet such as: http://static.springsource.org/spring-security/site/docs/3.0.x/reference/remember-me.html, http://www.jeviathon.com/2009/09/spring-security-30-with-active.html, http://jamwiki.org/wiki/en/Permissions, http://code.google.com/p/zk-sample-gui/

The application is web enabled application with a Flex client utilizing Spring security 3.0.5 and Springs LDAP template to authenticate and authorize against an OpenLdap server.

What started (as I wrongly assumed) as a one liner inside the applicationContext.xml file:



Turned out to be much more involved in term of the required configuration when LDAP is involved. So here are the details of how to do that, I am assuming you got the LDAP part working (with OpenLdap, active directory or whatever) and hence will not touch that here.


Step 1:


Add the necessary schema definitions to the applicationContext.xml  file. 


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"



Step 2:


Add a Spring token repository (c) , this repository will enable Spring to persist a token which identifies a user that has been already logged in, and assign him with a IS_AUTHENTICATED_REMEMBERED rule, or one of a predefined rules (see below such as RULE_APPLICATION) .


<bean id="tokenRepository" class="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl">
      <property name="createTableOnStartup" value="false" />
      <property name="dataSource" ref="inMemDataSource"/>

You may allow Spring to create the RDBMS schema for you automatically (createTableOnStartup=true), you may inspect the DDL :


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

Refer to JdbcTokenRepositoryImpl.CREATE_TABLE_SQL by yourself  and run it, or you can opt for a read only Hibernate entity that will be used solely for the creation of the table:


import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.hibernate.validator.NotNull;

@Table(name = "persistent_logins")
@org.hibernate.annotations.Entity(mutable = false)
 * A Hibernate class used to create the table for the "remember-me"
 * persistent token mechanism.
 * @author skashani
public class PersistentLogin implements java.io.Serializable {

    private static final long serialVersionUID = 1L;

    @Column(name = "username")
    private String userName;

    private String series;

    private String token;

    @Column(name = "last_used", columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP")
    private Date lastUsed;

    public String getUserName() {
        return userName;

    public void setUserName(final String userName) {
        this.userName = userName;

    public String getSeries() {
        return series;

    public void setSeries(final String series) {
        this.series = series;

    public String getToken() {
        return token;

    public void setToken(final String token) {
        this.token = token;

    public Date getLastUsed() {
        return lastUsed;

    public void setLastUsed(final Date lastUsed) {
        this.lastUsed = lastUsed;

     * (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
    public boolean equals(final Object other) {
        if (!(other instanceof PersistentLogin)) {
            return false;
        final PersistentLogin castOther = (PersistentLogin) other;
        return new EqualsBuilder().append(userName, castOther.userName)
                                  .append(token, castOther.token)
                                  .append(lastUsed, castOther.lastUsed)

     * (non-Javadoc)
     * @see java.lang.Object#hashCode()
    public int hashCode() {
        return new HashCodeBuilder().append(userName).append(token).append(lastUsed).toHashCode();

     * (non-Javadoc)
     * @see java.lang.Object#toString()
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE).append("userName",
                                                                        .append("series", series)
                                                                        .append("token", token)

The dataSource inMemDataSource must be a valid Spring datasource.

Step 3:
Add two Spring "voters" (please consult the full documentation for more details), namely, a role and authorization voters:


<!-- Votes if any ConfigAttribute.getAttribute() starts with a prefix indicating that it is a role. The -->
<!-- default prefix string is ROLE_, but this may be overridden to any value. It may also be set to -->
<!-- empty, which means that essentially any attribute will be voted on. As described further -->
<!-- below, the effect of an empty prefix may not be quite desirable. -->
<!-- Abstains from voting if no configuration attribute commences with the role prefix. Votes to -->
<!-- grant access if there is an exact matching -->
<!-- org.springframework.security.core.GrantedAuthority to a ConfigAttribute starting with the role -->
<!-- prefix. Votes to deny access if there is no exact matching GrantedAuthority to a -->
<!-- ConfigAttribute starting with the role prefix. -->
<!-- An empty role prefix means that the voter will vote for every ConfigAttribute. When there are -->
<!-- different categories of ConfigAttributes used, this will not be optimal since the voter will be -->
<!-- voting for attributes which do not represent roles. However, this option may be of some use -->
<!-- when using pre-existing role names without a prefix, and no ability exists to prefix them with -->
<!-- a role prefix on reading them in, such as provided for example in -->
<!-- org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl. -->
<!-- All comparisons and prefixes are case sensitive. --> 
<bean id="roleVoter" class="org.springframework.security.access.vote.RoleVoter"
           p:rolePrefix=""    />  

<!-- Votes if a ConfigAttribute.getAttribute() of IS_AUTHENTICATED_FULLY or -->
<!-- is in order of most strict checking to least strict checking. -->
<!-- The current Authentication will be inspected to determine if the principal has a particular -->
<!-- level of authentication. The "FULLY" authenticated option means the user is authenticated -->
<!-- fully (i.e. -->
<!-- org.springframework.security.authentication.AuthenticationTrustResolver.isAnonymous-->
<!-- (Authentication) is false and -->
<!-- org.springframework.security.authentication.AuthenticationTrustResolver.isRememberMe-->
<!-- (Authentication) is false). The "REMEMBERED" will grant access if the principal was either -->
<!-- authenticated via remember-me OR is fully authenticated. The "ANONYMOUSLY" will grant -->
<!-- access if the principal was authenticated via remember-me, OR anonymously, OR via full -->
<!-- authentication. -->
<!-- All comparisons and prefixes are case sensitive. -->
    <bean id="authVoter" class="org.springframework.security.access.vote.AuthenticatedVoter">               

Step 4:
You must now configure a Spring AccessDecisionManager which utilizes the two voters above, in order to determine if a user was granted the correct authority to access a resource.


<!--    Simple concrete implementation of -->
<!--     org.springframework.security.access.AccessDecisionManager that uses a consensus-based -->
<!--     approach. -->
<!--    "Consensus" here means majority-rule (ignoring abstains) rather than unanimous agreement -->
<!--     (ignoring abstains). If you require unanimity, please see UnanimousBased.-->
   <bean id="accessDecisionManager" class="org.springframework.security.access.vote.ConsensusBased">
        <property name="allowIfAllAbstainDecisions" value="false" />
        <property name="decisionVoters">
                <ref bean="roleVoter"  />
                <ref bean="authVoter" />

Step 5:
This is the standard Spring security configuration,  I commented the places in which setup is needed specifically for the remember-me feature.

<!-- HTTP security configuration -->
    <security:http auto-config="false" use-expressions="false" access-decision-manager-ref="accessDecisionManager">
<!--        token-repository-ref-->
<!--        Configures a PersistentTokenBasedRememberMeServices but allows the use of a custom PersistentTokenRepository bean. -->
<!--        token-validity-seconds-->
<!--        Maps to the tokenValiditySeconds property of AbstractRememberMeServices. -->
<!--        Specifies the period in seconds for which the remember-me cookie should be valid. By -->
<!--        default it will be valid for 14 days. -->
        <security:remember-me key="_spring_security_remember_me" token-validity-seconds="864000" token-repository-ref="tokenRepository" />

        <!--  login page related urls - allow anonymous access -->
        <security:intercept-url pattern="/security/login.html" access="IS_AUTHENTICATED_ANONYMOUSLY" />                   
        <security:intercept-url pattern="/admin-stuff/**" access="ROLE_ADMINISTRATOR" />               
        <security:intercept-url pattern="/**" access="IS_AUTHENTICATED_REMEMBERED, IS_AUTHENTICATED_FULLY" />
        <!-- login & logout redirection configuration -->
        <security:form-login login-page="/security/login.html" default-target-url="/Main.html"/>       
        <security:anonymous />   
        <security:logout logout-success-url="/security/login.html" />                                   

Note the reference to the accessDecisionManager defined above, it is mandatory to add it to get this feature working.


    <security:http auto-config="false" use-expressions="false" access-decision-manager-ref="accessDecisionManager">

LDAP configuration:

(it is mandatory to add  to get this feature working)


<security:ldap-user-service id="ldapUserService"
            role-prefix="${spring.ldap.rolePrefix}" />

    <security:ldap-server id="contextSource"
                        manager-password="${spring.ldap.managerPassword}" />

    <!-- LDAP Template used to execute core LDAP functionality -->
    <bean id="ldapTemplate" class="org.springframework.ldap.core.LdapTemplate">
        <constructor-arg ref="contextSource" />

    <!-- LDAP Security Service -->
    <bean id="securityService"
        <constructor-arg ref="ldapTemplate" />

Step 6:
From your HTML/Flex application you must send the key to instruct Spring to use the remember-me feature:


      <p><input type='checkbox' name='_spring_security_remember_me'/> Remember me on this computer.</p>

Step 7:
Once you login and check the check box,  a new row will be inserted for you automatically by spring into the persistent_logins table:


mysql> describe persistent_logins;
| Field     | Type         | Null | Key | Default           | Extra                       |
| series    | varchar(255) | NO   | PRI | NULL              |                             |
| last_used | timestamp    | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
| token     | varchar(255) | NO   |     | NULL              |                             |
| username  | varchar(255) | YES  |     | NULL              |                             |
4 rows in set (0.01 sec)


And after login:

mysql> select * from  persistent_logins;
| series                   | last_used           | token                    | username |
| uLbBshezy3jsMBvZxgMmuw== | 2011-03-17 09:06:54 | bGZfz2+9by+ks+ZaDH3hhQ== | shlomo   |
1 row in set (0.00 sec)

If you now close the browser and re-visit the secured application, there will be no need to enter your credentials.

Questions welcomed,