Unable to prevent user from login multiple time using spring security with custom UserDetail and AuthenticationProvider

I have implement the customs UserDetail and AuthenticationProvider to use with custom base login form which require more than 1 parameter. everything is working fine except the Concurrency Control which is not prevent the user from login multiple time or by using multiple devices.

Here is the configuration that I’m using:

web.xml

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  version="3.0"> 
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<listener>
    <listener-class>kh.com.gfam.rsos.listener.InitializeApplicationListner</listener-class>
</listener>

<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
    </init-param>
    <multipart-config>
        <max-file-size>10485760</max-file-size>
        <max-request-size>104857600</max-request-size>
        <file-size-threshold>20971520</file-size-threshold>
    </multipart-config>
</servlet>
<context-param>
    <param-name>log4jConfigLocation</param-name>
    <param-value>/WEB-INF/classes/log4j.properties</param-value>
</context-param>
<context-param>
<param-name>webAppRootKey</param-name>
<param-value>rsos.root</param-value>

<listener>
    <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>
<servlet-mapping>
    <servlet-name>appServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.css</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.js</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.png</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.jpg</url-pattern>
</servlet-mapping>
<servlet>
    <description></description>
    <display-name>GetImageController</display-name>
    <servlet-name>GetImageController</servlet-name>
    <servlet-class>kh.com.gfam.rsos.presentation.controller.GetImage.GetImageController</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>GetImageController</servlet-name>
    <url-pattern>/GetImageController</url-pattern>
</servlet-mapping>
<welcome-file-list>
    <welcome-file>home</welcome-file>
</welcome-file-list>
<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<listener>
    <listener-class>
        org.springframework.security.web.session.HttpSessionEventPublisher
    </listener-class>
</listener>

spring-security.xml

    <http auto-config="false" use-expressions="true"
        entry-point-ref="loginUrlAuthenticationEntryPoint">  
        <!-- omitted -->
        <intercept-url pattern="/Admin/**" access="hasRole('ROLE_ADMIN')" />
        <intercept-url pattern="/Concierge/**" access="hasRole('ROLE_USER')" />
        <intercept-url pattern="/Login" access="permitAll" />
        <intercept-url pattern="/**" access="permitAll" />
        <custom-filter position="FORM_LOGIN_FILTER"
            ref="companyIdUsernamePasswordAuthenticationFilter" />  
        <custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />

        <logout logout-url="/Logout" delete-cookies="true"
            invalidate-session="true" success-handler-ref="RsosLogoutSuccessHandler" />
        <csrf disabled="true" />
        <session-management invalid-session-url="/Login"
            session-authentication-strategy-ref="sas"/>

    </http>

    <global-method-security secured-annotations="enabled"/>

    <authentication-manager alias="authenticationManager">
        <authentication-provider
            ref="companyIdUsernamePasswordAuthenticationProvider" />  
    </authentication-manager>

spring-config.xml

<bean id="companyIdUsernamePasswordAuthenticationProvider"
        class="kh.com.gfam.rsos.common.security.RsosAuthenticationProvider" />

    <bean id="companyIdUsernamePasswordAuthenticationFilter"
        class="kh.com.gfam.rsos.common.security.RsosUsernamePasswordAuthenticationFilter">
        <property name="authenticationManager" ref="authenticationManager" />
        <property name="sessionAuthenticationStrategy" ref="sas" />
        <property name="authenticationFailureHandler" ref="authenticationFailureHandler" />
        <property name="authenticationSuccessHandler" ref="authenticationSuccessHandler" />
        <property name="filterProcessesUrl" value="/Authenticate" />
    </bean>

    <bean id="loginUrlAuthenticationEntryPoint"
        class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
        <constructor-arg value="/Login" />
    </bean>

    <bean id="authenticationFailureHandler"
        class="org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler">
        <property name="defaultFailureUrl" value="/Login/defaultError" />
        <property name="exceptionMappings">
            <props>
                <prop
                    key="org.springframework.security.authentication.BadCredentialsException">
                    /Login/badCredentials
                </prop>
                <prop
                    key="org.springframework.security.core.userdetails.UsernameNotFoundException">
                    /Login/usernameNotFound
                </prop>
                <prop
                    key="org.springframework.security.authentication.DisabledException">
                    /Login/disabled
                </prop>
                <prop
                    key="org.springframework.security.authentication.ProviderNotFoundException">
                    /Login/providerNotFound
                </prop>
                <prop
                    key="org.springframework.security.authentication.AuthenticationServiceException">
                    /Login/authenticationService
                </prop>
            </props>
        </property>
    </bean>

    <bean id="authenticationSuccessHandler"
        class="kh.com.gfam.rsos.common.security.RsosAuthenticationSuccessHandler">
    </bean>

    <bean id="RsosLogoutSuccessHandler"
        class="kh.com.gfam.rsos.common.security.RsosLogoutSucessHandler"></bean>

    <bean id="concurrencyFilter"
        class="org.springframework.security.web.session.ConcurrentSessionFilter">
        <constructor-arg name="sessionRegistry" ref="sessionRegistry" />
        <constructor-arg name="expiredUrl" value="/session-expired.jsp" />
    </bean>

    <bean id="sas"
        class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
        <constructor-arg name="sessionRegistry" ref="sessionRegistry" />
        <property name="maximumSessions" value="1" />
        <property name="exceptionIfMaximumExceeded" value="true" />
    </bean>

    <bean id="sessionRegistry"
        class="org.springframework.security.core.session.SessionRegistryImpl"/>

root-context.xml

        <import resource="classpath:springConfig.xml" />
        <import resource="appServlet/servlet-context.xml" />
        <import resource="appServlet/spring-security.xml" />

        <bean id="InitializationService"
            class="kh.com.gfam.rsos.businesslogic.initialization.impl.InitializationServiceImpl">
        </bean>

custom AuthenticationProvider class

public class RsosAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    LoginService service;

    @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        RsosUsernamePasswordAuthenticationToken auth = (RsosUsernamePasswordAuthenticationToken) authentication;
        String user_id = String.valueOf(auth.getName());
        String password = String.valueOf(auth.getCredentials());
        int hotel_code = auth.getHotel_code();
        int user_type = auth.getUser_type();

        UserDTO user = null;
        user = service.authenicate(hotel_code, user_id, password, user_type);

        if (user.getUser_type() == 1) {
            return new UsernamePasswordAuthenticationToken(user, authentication.getCredentials(),
                    Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
        } else {
            return new UsernamePasswordAuthenticationToken(user, authentication.getCredentials(),
                    Collections.singletonList(new SimpleGrantedAuthority("ROLE_ADMIN")));
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return RsosUsernamePasswordAuthenticationToken.class
                .equals(authentication);
    }
}

Custom UsernamePasswordAuthenticationToken

public class RsosUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
    private static final long serialVersionUID = 9103166337373681531L;
    private final int hotel_code;
    private final int user_type;

    public RsosUsernamePasswordAuthenticationToken(Object principal, Object credentials,
            int hotel_code, int user_type) {
        super(principal, credentials);
        this.hotel_code = hotel_code;
        this.user_type = user_type;
    }

    public RsosUsernamePasswordAuthenticationToken(Object principal, Object credentials,
            int hotel_code, int user_type,
            Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
        this.hotel_code = hotel_code;
        this.user_type = user_type;
    }

    public int getHotel_code() {
        return hotel_code;
    }

    public int getUser_type() {
        return user_type;
    }
}

Custom UsernamePasswordAuthenticationFilter

public class RsosUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {

        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported:"
                    + request.getMethod());
        }

        String username = super.obtainUsername(request);
        String password = super.obtainPassword(request);
        int hotel_code = Integer.parseInt(ObtainHotelCode(request));
        int user_type = Integer.parseInt(ObtainUserType(request));

        if (!StringUtils.hasText(hotel_code + "")) {
            throw new AuthenticationServiceException("Hotel Code is require");
        }

        RsosUsernamePasswordAuthenticationToken authrequest =
                new RsosUsernamePasswordAuthenticationToken(username, password, hotel_code, user_type); // (1)

        setDetails(request, authrequest);

        return  this.getAuthenticationManager().authenticate(authrequest);
    }

    protected String ObtainHotelCode(HttpServletRequest request) {
        return request.getParameter("hotel_code");
    }

    protected String ObtainUserType(HttpServletRequest request) {
        return request.getParameter("user_type");
    }
}

Custom AuthenticationSuccessHandler

public class RsosAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {

        UserDTO authUser = (UserDTO) SecurityContextHolder.getContext().getAuthentication()
                .getPrincipal();
        request.getSession().setAttribute("userData", authUser);

        response.setStatus(HttpServletResponse.SC_OK);

        if (authUser.getUser_type() == 1) {
            response.sendRedirect(request.getContextPath() + "/Concierge/New_Arrival");
        } else {
            response.sendRedirect(request.getContextPath() + "/Admin/Main_Info");
        }
    }

}

Custom LogoutSucessHandler

public class RsosLogoutSucessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request,
            HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        if (authentication != null && authentication.getDetails() != null) {
            try {
                request.getSession().invalidate();
            } catch (Exception e) {
                e.printStackTrace();
                e = null;
            }
        }

        response.setStatus(HttpServletResponse.SC_OK);

        response.sendRedirect(request.getContextPath() + "/Login");

    }
}

Custom UserDetail class

public class UserDTO implements UserDetails {

    /**  */
    private static final long serialVersionUID = -2228367483835088451L;

    /** Hotel Code */
    private int hotel_code;
    /** UserID */
    // @Size(min=4, max=4) @Pattern(regexp = "[0-9]")
    private String user_id;
    /** User Name */
    private String user_name;
    /** Password */
    @NotNull
    @Size(min = 8, max = 30)
    private String password;
    /** User Type */
    @NotNull
    private int user_type;

    //set() .... get()//

    public void setUser_name(String user_name) {
        this.user_name = user_name;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getUsername() {
        return user_id;
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }

    @Override
    public boolean equals(Object rhs) {
        if (rhs instanceof UserDTO) {
            return (user_id.equals(((UserDTO) rhs).user_id));
        }
        return false;
    }

    /**
     * Returns the hashcode of the {@code username}.
     */
    @Override
    public int hashCode() {
        return user_id.hashCode();
    }
}

and this is what the login form look like

enter image description here

I’m new to spring security, so what could possibly goes wrong here? can someone point out the mistake please. Thanks.

Answer

I have solved this problem after reading the Spring document on how to migration from Spring 3.2 to 4.x in here http://docs.spring.io/spring-security/site/migrate/current/3-to-4/html5/migrate-3-to-4-xml.html#m3to4-deprecations-web-cscs

I changed from

<bean id="sas"
    class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
    <constructor-arg name="sessionRegistry" ref="sessionRegistry" />
    <property name="maximumSessions" value="1" />
    <property name="exceptionIfMaximumExceeded" value="true" />
</bean>

to

<bean id="sas"
    class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
    <constructor-arg>
        <list>
            <bean
                class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
                <constructor-arg ref="sessionRegistry" />
                <property name="maximumSessions" value="1" />
                <property name="exceptionIfMaximumExceeded" value="true" />
            </bean>
            <bean
                class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />
            <bean
                class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
                <constructor-arg ref="sessionRegistry" />
            </bean>
        </list>
    </constructor-arg>
</bean>

for those who use Spring 4.x and have faced the same problem, you try read the whole document in the link above, it probably solve your problem too.

Leave a Reply

Your email address will not be published. Required fields are marked *