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 ?
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.