Spring Security Authentication with Persistent Remember Me example

Overview


This article will show how to configure Remember Me functionality in Spring Security – using the standard cookie approach with persistence token.

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.Please read this article  for Remember me using Token Based Approach.
2. Persistent Token Approach : It uses a database or other persistent storage mechanism to store the generated tokens


In this example we will see spring security using Persistent Token Approach with a simple login form.

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.



DataBase script : 


create table ROLE (
    ROLE_ID integer not null auto_increment, 
    NAME varchar(32) not null, 
    primary key (ROLE_ID))

create table USER (
    USER_ID varchar(255) not null, 
    ACTIVE integer not null, 
    NAME varchar(32) not null, 
    PASSWORD varchar(64) not null, 
    primary key (USER_ID))
create table USER_ROLE (
    USER_ID varchar(255) not null, 
    ROLE_ID integer not null, 
    primary key (USER_ID, ROLE_ID))

alter table USER_ROLE add constraint FK_oqmdk7xj0ainhxpvi79fkaq3y foreign key (ROLE_ID) references ROLE (ROLE_ID)
alter table USER_ROLE add constraint FK_j2j8kpywaghe8i36swcxv8784 foreign key (USER_ID) references USER (USER_ID)

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


persistent_logins table is used to persist remember-me token details in the database.

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>

    <security:http pattern="/services/login" security="none"/>
    <security:http pattern="/services/accessdenied" security="none"/>
    <security:http auto-config="true" 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:form-login login-page="/services/login"
                    username-parameter="username"
                    password-parameter="password"
                    login-processing-url="/j_spring_security_check"
                    authentication-success-handler-ref="successHandler"
                    authentication-failure-handler-ref="failureHandler"
                />

        <!-- enable remember me -->
        <security:remember-me
                token-validity-seconds="300"
                remember-me-parameter="remember-me"
                data-source-ref="dataSource"
                />

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

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’.
data-source-ref : If this is specified, “Persistent Token Approach” will be used.If we did not provide data source then InMemoryTokenRepositoryImpl will be used to persist token.You can test same application be removing datasource.

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>
            <!-- removed css here,you can see in the 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="username" placeholder="Username"  />
        <input type="password" id="password" name="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 with Remember Me :



Remember Me Token After Login :


In the response headers you can Observe SPRING_SECURITY_REMEMBER_ME token generated in the backend for loged in user.

With Only Remember Me Token :

Let us request any resource in the application with out providing JSession token.


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.You can start adding this feature in your login page.
Show Comments: OR

0 comments: