2

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 ?

  • You said you want to allow only a single user to login at a time. `sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true)` doesn't restrict the number of users that can be logged in at a time, but rather the number of sessions a single user can have. For example, if you log in with user1 and in a different browser log in with user2, then you will be permitted to log in. But, if you log in with user1 and in a different browser try to log in with user1 again, then you will not be permitted to log in. – Eleftheria Stein-Kousathana Dec 07 '20 at 14:44
  • yes, so I logged in with user 1 in a browser, and in different browse I am again trying to login with user 1, and I am able to login successfully. According to my requirement, spring shall throw an error on second login with user 1 saying exceeded maximum limit for login. Can you please help ? – Apeksha Saxena Dec 08 '20 at 04:54
  • Thanks for the clarification. I will add an answer below. – Eleftheria Stein-Kousathana Dec 08 '20 at 11:11

1 Answers1

2

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.

  • Thanks @Eleftheria Stein-Kousathana for the more cleared view. Still I am facing some issues in implementing it as I am just a beginner in spring boot. As you have said, I have updated the code and checked, its still not working. Please check my updated code in the question above, Still I am able to login multiple times, Can you please help? – Apeksha Saxena Dec 10 '20 at 12:02
  • @ApekshaSaxena Try removing the line `http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);` in your class `SecurityConfiguration`. It may be adding the filter twice. You do not need to have that line there, since you have it in the custom Configurer. – Eleftheria Stein-Kousathana Dec 10 '20 at 12:27
  • Removed it, still able to login twice. Is JWTAuthenticationTokenFilter#doFilter code looks good to you? Is that what you wanted me to do inside that to achieve limit concurrency? I have put debugger on onAuthentication and attemptAuthentication method of AbstractAuthenticationProcessingFilter, but it doesn't go there. – Apeksha Saxena Dec 10 '20 at 12:57
  • @ApekshaSaxena Thanks for sharing the Filter code. The `SessionAuthenticationStrategy#onAuthentication` is called in the `doFilter` method of `AbstractAuthenticationProcessingFilter`. You are overriding that method in your `JWTAuthenticationTokenFilter` so it's not getting called. If you were not extending `AbstractAuthenticationProcessingFilter` before, then you don't need to. I only added that to my example because it looked like you were. If you call `sessionStrategy#onAuthentication` in your `doFilter` then it should work. You just need to add a setter for the `sessionStrategy`. – Eleftheria Stein-Kousathana Dec 10 '20 at 14:55
  • I am able to achieve it. Thanks a lot for your help. – Apeksha Saxena Dec 11 '20 at 08:56