Spring security 3 remember-me with LDAP authentication

Hi,

 

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:

<remember-me/>

 

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:security="http://www.springframework.org/schema/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/security
                        http://www.springframework.org/schema/security/spring-security-3.0.xsd
                        http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context.xsd">

 

 

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"/>
   </bean>


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;

@Entity()
@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;

    @Id
    @NotNull
    private String series;

    @NotNull
    private String token;

    @Column(name = "last_used", columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP")
    @NotNull
    @Temporal(TemporalType.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)
     */
    @Override
    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)
                                  .isEquals();
    }

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

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



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_AUTHENTICATED_REMEMBERED or IS_AUTHENTICATED_ANONYMOUSLY is present. This list -->
<!-- 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">               
    </bean>


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">
            <list>
                <ref bean="roleVoter"  />
                <ref bean="authVoter" />
            </list>
        </property>
    </bean>
 

       
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" />                                   
    </security:http>


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"
     group-search-base="${spring.ldap.groupSearchBase}"
     group-role-attribute="${spring.ldap.groupRoleAttribute}"
     group-search-filter="${spring.ldap.groupSearchFilter}"
     user-search-base="${spring.ldap.userSearchBase}"
     user-search-filter="${spring.ldap.userSearchFilter}"/>
    
        
    <security:authentication-manager>
        <security:ldap-authentication-provider
            server-ref="contextSource"
            group-search-base="${spring.ldap.groupSearchBase}"
            user-search-base="${spring.ldap.userSearchBase}"
            user-search-filter="${spring.ldap.userSearchFilter}"
            group-role-attribute="${spring.ldap.groupRoleAttribute}"
            group-search-filter="${spring.ldap.groupSearchFilter}"
            role-prefix="${spring.ldap.rolePrefix}" />
    </security:authentication-manager>

    <security:ldap-server id="contextSource"
                        url="${spring.ldap.url}"
                        port="389"
                        manager-dn="${spring.ldap.managerDn}"
                        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" />
    </bean>

    <!-- LDAP Security Service -->
    <bean id="securityService"
        class="com.xxx.LdapUserManagementServiceImpl">
        <constructor-arg ref="ldapTemplate" />
    </bean>


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,


 
 

Thank you for your interest!

We will contact you as soon as possible.

Want to Know More?

Oops, something went wrong
Please try again or contact us by email at info@tikalk.com
Thank you for your interest!

We will contact you as soon as possible.

Let's talk

Oops, something went wrong
Please try again or contact us by email at info@tikalk.com