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.
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.
/**
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,
