Spring Security Remember Me Authentication with Login Filter

Overview :


This article will show how to set up Remember Me functionality in Spring Security – using standard cookie approach.
Also we will configure login form with UsernamePasswordAuthenticationFilter custom filter.

Before reading this article I would recommend to go through my previous blog on Spring Security Remember me Web Flow.

As discussed, Spring Security provides two implementations for Remember-Me :

1. Simple Hash-Based Token Approach : It uses hashing to preserve the security of cookie-based tokens
2. Persistent Token Approach : It uses a database or other persistent storage mechanism to store the generated tokens.Please read this article for example of Remember me using Persistent Token Approach.

In this example we will see spring security using Token Hash-Based Approach with custom login filter.

Example Workflow :

1. If user login with a “remember me” checked, the system will store a “remember me” cookie in the requested browser.

2. If user’s browser provides a valid “remember me” cookie, the system will perform automatic login.

3. If user is login via “remember me” cookies, to update the user detail, user need to type the username and password again.

Token Preparation :


Here we are using TokenBasedRememberMeServices to prepare token,this is how token is prepared

base64(username + ":" + expirationTime + ":" + md5Hex(username + ":" + expirationTime + ":" password + ":" + key))

here key is A private key to prevent modification of the remember-me token.

Spring Configuration Xml :

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:security="http://www.springframework.org/schema/security"

       xsi:schemaLocation="http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/mvc
                http://www.springframework.org/schema/mvc/spring-mvc.xsd
                http://www.springframework.org/schema/context
                http://www.springframework.org/schema/context/spring-context.xsd
                http://www.springframework.org/schema/security
                http://www.springframework.org/schema/security/spring-security.xsd">

    <context:component-scan base-package="com.spring.security.example"/>

    <bean
            class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix">
            <value>/WEB-INF/pages/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

    <!-- Application entry point which will redirect to login if user is not authenticated -->
    <bean id="appAuthenticationEntryPoint" class="com.spring.security.example.entrypoint.AppAuthenticationEntryPoint">
        <constructor-arg name="loginFormUrl" value="/services/login"/>
    </bean>

    <!-- if user authentication is successful then AppSuccessHandler will redirect to page based on role-->
    <bean id="successHandler" class="com.spring.security.example.handler.AppSuccessHandler"/>

    <!-- if user authentication is unsuccessful then failureHandler will redirect to access denied page-->
    <bean id="failureHandler"
          class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
        <constructor-arg name="defaultFailureUrl" value="/services/accessdenied"/>
     </bean>

    <bean id="SecurityAuthFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
        <property name="authenticationSuccessHandler" ref="successHandler"/>
        <property name="authenticationFailureHandler" ref="failureHandler"/>
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="filterProcessesUrl" value="/j_spring_security_check"/>
        <property name="rememberMeServices" ref="rememberMeServices"/>
    </bean>

    <bean id="rememberMeServices"
          class="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
        <constructor-arg name="key" value="TOKEN_KEY" />
        <constructor-arg name="userDetailsService" ref="loginService"/>
        <property name="tokenValiditySeconds" value="300"></property>
        <property name="parameter" value="remember-me"/>
     </bean>


    <security:http pattern="/services/login" security="none"/>
    <security:http pattern="/services/accessdenied" security="none"/>
    <security:http auto-config="false" use-expressions="true" entry-point-ref="appAuthenticationEntryPoint" >
        <!-- Interceptor urls -->
        <security:intercept-url pattern="/" access="isAuthenticated()"/>
        <security:intercept-url pattern="/**" access="isAuthenticated()"/>
        <security:intercept-url pattern="/userpage" access="hasRole('USER')"/>
        <security:intercept-url pattern="/admin**" access="hasRole('ADMIN')"/>
        <security:intercept-url pattern="/j_spring_security_check" access="permitAll"/>
        <security:custom-filter position="FORM_LOGIN_FILTER" ref="SecurityAuthFilter"/>
        <security:remember-me key="TOKEN_KEY" user-service-ref="loginService" />


    </security:http>

    <security:authentication-manager alias="authenticationManager">
        <security:authentication-provider user-service-ref="loginService"/>
    </security:authentication-manager>


    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/LoginDB"></property>
        <property name="username" value="root"></property>
        <property name="password" value="pramati"></property>
        <property name="connectionProperties">
            <props>
                <prop key="hibernate.show_sql">
                    true
                </prop>
                <prop key="hibernate.enable_lazy_load_no_trans">
                    true
                </prop>
            </props>
        </property>
    </bean>

    <bean id="loginSessionFactory"
          class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="packagesToScan" value="com.spring.security.example"/>
    </bean>

    <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
        <property name="sessionFactory" ref="loginSessionFactory"/>
    </bean>
    <bean id="hibernateTemplate" class="org.springframework.orm.hibernate4.HibernateTemplate">
        <property name="sessionFactory" ref="loginSessionFactory"/>
    </bean>
</beans>

TokenBasedRememberMeServices requires Key and user details service.
Key is a private key to prevent modification of the remember-me token.
UserDetailsService from which it can retrieve the username and password for signature comparison purposes, and generate theRememberMeAuthenticationToken to contain the correct GrantedAuthorities.

User defined properties for remember me service
token-validity-seconds : The expire date of “remember-me” cookie, in seconds. Here I kept 300 seconds( 5 mins).
remember-me-parameter :The name of the “check box”. Defaults to ‘_spring_security_remember_me’.

Here we used custom login filter UsernamePasswordAuthenticationFilter, and configured success handler,failure handler and remember service to authentication filter.This filter will use to Processes an authentication from form  submission.This filter by default responds to the URL /j_spring_security_check.

Controller :

@Controller
public class AppController {

    @RequestMapping(value = {"/userpage"}, method = RequestMethod.GET)
    public ModelAndView userPage() {

        if (isAdminPage())
            return adminPage();
        ModelAndView model = new ModelAndView();
        model.addObject("title", "Spring Security Hello World");
        model.addObject("user", getUser());
        model.setViewName("user");
        return model;
    }

    @RequestMapping(value = "/adminpage", method = RequestMethod.GET)
    public ModelAndView adminPage() {
            ModelAndView model = new ModelAndView();
            model.addObject("title", "Spring Security Hello World");
            model.addObject("user", getUser());
            model.setViewName("admin");
            return model;
    }

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public ModelAndView login(HttpServletRequest request, HttpServletResponse response) throws ServletException,IOException{
        ModelAndView model = new ModelAndView();
        model.addObject("title", "Login Page");
        model.setViewName("login");
        return model;
    }

    @RequestMapping(value = "/logout", method = RequestMethod.GET)
    public ModelAndView logout(HttpServletRequest request, HttpServletResponse response) throws ServletException,IOException{
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null) {
            new SecurityContextLogoutHandler().logout(request, response, auth);
        }
        return login(request,response);
    }

    @RequestMapping(value = {"/accessdenied"}, method = RequestMethod.GET)
    public ModelAndView accessDeniedPage() {
        ModelAndView model = new ModelAndView();
        model.addObject("message", "Either username or password is incorrect.");
        model.setViewName("accessdenied");
        return model;
    }

    private String getUser() {
        String userName = null;
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (principal instanceof UserDetails) {
            userName = ((UserDetails) principal).getUsername();
        } else {
            userName = principal.toString();
        }
        return userName;
    }


    private boolean isAdminPage() {
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (principal instanceof UserDetails) {
            Collection<? extends GrantedAuthority> authorities = ((UserDetails) principal).getAuthorities();
            if (authorities.size() == 1) {
                final Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
                GrantedAuthority grantedAuthority = iterator.next();
                if (grantedAuthority.getAuthority().equals("ADMIN")) {
                    return true;
                }
            }
        }
        return false;
    }
}

Views :

<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
    <title>${title}</title>
        <style>
            <!-- removing css,you can find them in attached source -->
        </style>
</head>
<body>
<br/><br/><br/><br/><br/><br/><br/>
<div class="login-block">
    <form name='loginForm'
                  action="<c:url value='../j_spring_security_check' />" method='POST'>
        <h1>Login</h1>
        <input type="text" id="username" name="j_username" placeholder="Username"  />
        <input type="password" id="password" name="j_password" placeholder="Password" />
        Remember Me: <input type="checkbox" name="remember-me" placeholder="remember-me"/></td>
        <button>Submit</button>

    </form>


</div>
</body>
</html>

Screens :

Login Page :



Remember Me Token in reponse :


Remember Me Token With out Jsession in Request :


If you observe,We did not sent Jsession id in the request headers,only remember me token is sent in the request header.But still page is accessible because remember-me token is valid for this user.It means remember-me is working.

Source :

You can download source of this example from GitHub.

Hope you understand remember-me implementation.
Show Comments: OR

0 comments: