Friday, January 28, 2011

Remember Me with LDAP Spring Security

Remember Me functionality in Spring Security can be as easy as implement as just using a checkbox in your HTML ...
<input type='checkbox' name='_spring_security_remember_me' value="on"/>

... plus a declaration in the spring application context xml file:
<remember-me/>

However that is not true when using LDAP

For LDAP a persistent token will be needed which is actually good news as it is more secure. Here are the important bits to ensure the system remembers the user for 4 hours:
<http auto-config="true" use-expressions="true" access-decision-manager-ref="accessDecisionManager">
...
   <remember-me key="_spring_security_remember_me" token-validity-seconds="14400" token-repository-ref="tokenRepository"/>
A UserService is needed:
<ldap-user-service id="ldapUserService" group-search-base="ou=groups,o=MyCompany" group-role-attribute="cn" group-search-filter="(uniquemember={0})" user-search-base="ou=people,o=MyCompany" user-search-filter="mail={0}"/>
And a token Repository:
<beans:bean id="tokenRepository"
      class="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl">
      <beans:property name="createTableOnStartup" value="false" />
      <beans:property name="dataSource" ref="myDataSource"/>
   </beans:bean>
Note createTableOnStartup can be true the first time you start your application so it creates the "persistent_logins" table automatically. Below is the schema creation statement for MySQL. You can just run the statement and leave createTableOnStartup=false.
CREATE TABLE `persistent_logins` (
  `username` varchar(64) NOT NULL,
  `series` varchar(64) NOT NULL,
  `token` varchar(64) NOT NULL,
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

Now spring will keep track of the last time the user logs in and will remember the user as long as the token-validity-seconds is not reached.

If you are protecting your site against CSRF attacks which you should of course then you will face an additional problem: The user will be remembered but the security token will be invalid.

To address that issue you could use a combination of before and after security filters or just a before filter. Basically you can put any code you want before (or after) the remember me functionality is triggered. Here the relevant xml bits:
<beans:bean id="customRememberMeFilter" class="com.nestorurquiza.web.filter.CustomRememberMeFilter" />
...
<http auto-config="true" use-expressions="true" access-decision-manager-ref="accessDecisionManager">
...
   <custom-filter  before="REMEMBER_ME_FILTER" ref="customRememberMeFilter" />

And here is the filter:
package com.nestorurquiza.web.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import com.nestorurquiza.web.ControllerContext;

public class CustomRememberMeFilter implements Filter 
{
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException 
    {
        Authentication authentication = (Authentication) SecurityContextHolder.getContext().getAuthentication();
        String ctoken = request.getParameter(ControllerContext.CSRF_TOKEN);
        HttpServletRequest req = (HttpServletRequest)request;
        if(authentication == null && ctoken != null && getRememberMeCookie(req.getCookies()) != null) {
            req.getSession().setAttribute(ControllerContext.CSRF_TOKEN, ctoken);
        }
        chain.doFilter(request, response);
    }

    public void destroy() 
    {   

    }

    public void init(FilterConfig config) throws ServletException 
    {
    
    }
    
    private Cookie getRememberMeCookie(Cookie[] cookies) {
        if (cookies != null)
          for (Cookie cookie : cookies) {
            if (AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY.equals(cookie.getName())) {
              return cookie;
            }
          }
        return null;
    }
}

While remember me functionality is very handy in some circumstances it is abused by some developers who ignore the vulnerabilities behind this feature.

AS you can see from the above code we are disabling CSRF protection for users opting to be remembered.

Here are some measures:
  1. Avoid if possible using Remember Me functionality
  2. Explain your users they will be more vulnerable
  3. Instruct your users to log-out if they opted to use remember-me functionality but have finished their job. Closing the browser is not enough to prevent a different user from gaining control over the original account

2 comments:

chatura sandaruwan said...

Please note anyone who try to do this as same , make sure implement isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired & isEnabled accordingly in your UserDetails, unless its not work as expected due to those checking done in UserDetailsChecker

Nestor Urquiza said...

@chatura. It has been a while since I went through this so it would help if you refer to the dependency versions you used.

Followers