spring-bootspring-securitysession-management

Not able to implement session limiting in Spring Security with custom Filter


My requirement is to restrict multiple user login and allow only a single user to login at a time in the application. My Application is a spring boot application with JWT authentication implemented for user authorisation and authentication. I have read several posts, and understood that it can be achieved using spring security in spring boot.

package com.cellpointmobile.mconsole.security;
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private JWTAuthEntryPoint unauthorizedHandler;

    @Bean
    public JWTAuthenticationTokenFilter authenticationJwtTokenFilter() {
        return new JWTAuthenticationTokenFilter();
    }

    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception
    {
        authenticationManagerBuilder
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }


    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }

    @Bean
    public org.springframework.security.crypto.password.PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        http.cors().and().csrf().disable().
                 authorizeRequests()
                .antMatchers("/mconsole/app/login").permitAll()
                .antMatchers("/mconsole/**").authenticated()
                .and()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS).maximumSessions(1).
                maxSessionsPreventsLogin(true);

        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        http.apply(customConfigurer());

    }


    @Bean
    public CustomJwtAuthenticationTokenConfigurer customConfigurer() {
        return new CustomJwtAuthenticationTokenConfigurer();
    }

}

This is my configuration class, in which i have added the required code. Also added a new class as below:

public class CustomJwtAuthenticationTokenConfigurer extends
        AbstractHttpConfigurer<CustomJwtAuthenticationTokenConfigurer, HttpSecurity> {
    @Autowired
    private JWTAuthenticationTokenFilter myFilter;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        SessionAuthenticationStrategy sessionAuthenticationStrategy = http
                .getSharedObject(SessionAuthenticationStrategy.class);
        myFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
        http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

And have extended AbstractAuthenticationProcessingFilter in JWTAuthenticationTokenFilter.

public class JWTAuthenticationTokenFilter extends AbstractAuthenticationProcessingFilter
{
    @Autowired
    private JWTProvider tokenProvider;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    private static final Logger logger = LoggerFactory.getLogger(JWTAuthenticationTokenFilter.class);

    public JWTAuthenticationTokenFilter() {super("/mconsole/**");  }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws ServletException, IOException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        String header = request.getHeader("x-cpm-auth-token");
        if (header == null) {
            filterChain.doFilter(request, response);
            return;
        }
        String sToken = header.replace("x-cpm-auth-token", "");
        try {
            if (sToken != null && tokenProvider.validateJwtToken(sToken, userDetailsService)) {
                Authentication authResult;
                UsernamePasswordAuthenticationToken authentication;
                String sUserName = tokenProvider.getUserNameFromJwtToken(sToken, userDetailsService);
                UserDetails userDetails = userDetailsService.loadUserByUsername(sUserName);
                if (userDetails != null) {
                    authentication
                            = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    logger.info("entered in authentication>> " + authentication);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                } else {
                    throw new AuthenticationCredentialsNotFoundException("Could not createAuthentication user");
                }                try {

                    authResult = attemptAuthentication(request, response);
                    if (authResult == null) {
                        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                        return;
                    }
                } catch (AuthenticationException failed) {
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    return;
                }

            }
        } catch (Exception e) {
            logger.error("Access denied !! Unable to set authentication", e);
            SecurityContextHolder.clearContext();
        }
        filterChain.doFilter(request, response);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null)
            throw new AuthenticationServiceException(MessageFormat.format("Error | {0}", "Bad Token"));
        return authentication;
    }

    @Override
    @Autowired
    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }

Still I am able to login multiple times, I have been struggling to achieve this since 10 days, @Eleftheria Stein-Kousathana Can you please check what am I doing wrong now ?


Solution

  • First, let's consider how concurrent session control works in a simple case, without a custom filter, in an application using form login.

    A valid login request will arrive at the UsernamePasswordAuthenticationFilter.
    In this filter, that the session concurrency limit is checked, by calling SessionAuthenticationStrategy#onAuthentication.
    If the maximum limit is not exceeded then the user is logged in successfully. If the limit is exceeded then a SessionAuthenticationException is thrown which returns an error response to the user.

    To have the same behaviour in a custom filter, you need to make sure that SessionAuthenticationStrategy#onAuthentication is called in the doFilter method.

    One way to accomplish this is by creating a custom Configurer and applying it in the HttpSecurity configuration.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .apply(customConfigurer())
        // ...
    }
    
    public static class CustomJwtAuthenticationTokenConfigurer extends
            AbstractHttpConfigurer<CustomJwtAuthenticationTokenConfigurer, HttpSecurity> {
        @Override
        public void configure(HttpSecurity http) throws Exception {
            CustomJwtAuthenticationTokenFilter myFilter = new CustomJwtAuthenticationTokenFilter();
            SessionAuthenticationStrategy sessionAuthenticationStrategy = http
                    .getSharedObject(SessionAuthenticationStrategy.class);
            myFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
            http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class);
        }
    
        public static CustomJwtAuthenticationTokenConfigurer customConfigurer() {
            return new CustomJwtAuthenticationTokenConfigurer();
        }
    }
    

    This assumes that CustomJwtAuthenticationTokenFilter extends AbstractAuthenticationProcessingFilter, but the configuration will be similar even if it doesn't.