Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Security locks user out with concurrent login attempts

I am new to security and have run into a problem that is causing the user account to be locked in such a fashion that only a application restart fixes it.

I have a spring boot (1.3.0.BUILD-SNAPSHOT) with spring security (4.0.2.RELEASE) app that I am trying to control the concurrent session strategy so a user can only have a single login. It correctly detects subsequent login attempts from another browser and prevents that. However, I have noticed some odd behavior that i can't seem to track down:

  • A User can have two tabs authenticated in the same browser. I can't login with three tabs, but two works. Logging out of one seems to logout of both. I see the cookie values are the same, so I am guessing they are sharing a session:

tab 1 JSESSIONID: DA7C3EF29D55297183AF5A9BEBEF191F & 941135CEBFA92C3912ADDC1DE41CFE9A

tab 2 JSESSIONID: DA7C3EF29D55297183AF5A9BEBEF191F & 48C17A19B2560EAB8EC3FDF51B179AAE

A second login attempt presents the following log messages which seems to indicate a second login attempt (which I verified by stepping thru the Spring-Security source:

o.s.s.w.a.i.FilterSecurityInterceptor    : Secure object: FilterInvocation: URL: /loginPage; Attributes: [permitAll]
 o.s.s.w.a.i.FilterSecurityInterceptor    : Previously Authenticated: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@754041c8: Principal: User [[email protected], password=<somevalue> ]; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@43458: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 4708D404F64EE758662B2B308F36FFAC; Granted Authorities: Owner
 o.s.s.access.vote.AffirmativeBased       : Voter: org.springframework.security.web.access.expression.WebExpressionVoter@17527bbe, returned: 1
 o.s.s.w.a.i.FilterSecurityInterceptor    : Authorization successful
 o.s.s.w.a.i.FilterSecurityInterceptor    : RunAsManager did not change Authentication object
 o.s.security.web.FilterChainProxy        : /loginPage reached end of additional filter chain; proceeding with original chain
 org.apache.velocity                      : ResourceManager : unable to find resource 'loginPage.vm' in any resource loader.
 o.s.s.w.a.ExceptionTranslationFilter     : Chain processed normally
 s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
  • When I log in with two tabs and then logout, the user account becomes locked up and requires a server restart. There are no errors in the console and the user records in the db are unchanged.

Here is my security config:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

      @Autowired
      private CustomUserDetailsService customUserDetailsService;

      @Autowired
      private SessionRegistry sessionRegistry;

      @Autowired
      ServletContext servletContext;

      @Autowired
      private CustomLogoutHandler logoutHandler;

      @Autowired
      private MessageSource messageSource;


/**
 * Sets security configurations for the authentication manager
 */
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
                throws Exception {
    auth
                    .userDetailsService(customUserDetailsService)
                    .passwordEncoder(passwordEncoder());
    return;
}  
  protected void configure(HttpSecurity http) throws Exception {
        http
           .formLogin()
           .loginPage("/loginPage")
           .permitAll()
           .loginProcessingUrl("/login")
           .defaultSuccessUrl("/?tab=success")
           .and()
              .logout().addLogoutHandler(logoutHandler).logoutRequestMatcher( new AntPathRequestMatcher("/logout"))
              .deleteCookies("JSESSIONID")
               .invalidateHttpSession(true).permitAll().and().csrf()
           .and()  
              .sessionManagement().sessionAuthenticationStrategy(             concurrentSessionControlAuthenticationStrategy).sessionFixation().changeSessionId().maximumSessions(1)
              .maxSessionsPreventsLogin( true).expiredUrl("/login?expired" ).sessionRegistry(sessionRegistry )
          .and()
           .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
           .invalidSessionUrl("/")
           .and().authorizeRequests().anyRequest().authenticated();
        http.headers().contentTypeOptions();
        http.headers().xssProtection();
        http.headers().cacheControl();
        http.headers().httpStrictTransportSecurity();
        http.headers().frameOptions();
        servletContext.getSessionCookieConfig().setHttpOnly(true);
    }

  @Bean
  public ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlAuthenticationStrategy() {

      ConcurrentSessionControlAuthenticationStrategy strategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
      strategy.setExceptionIfMaximumExceeded(true);
      strategy.setMessageSource(messageSource);

      return strategy;
  }

  // Work around https://jira.spring.io/browse/SEC-2855
  @Bean
  public SessionRegistry sessionRegistry() {
      SessionRegistry sessionRegistry = new SessionRegistryImpl();
      return sessionRegistry;
  }
}

I also have the following methods to handle checking user:

@Entity
@Table(name = "USERS")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
                  property = "username")
public class User implements UserDetails {
...
 @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((username == null) ? 0 : username.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        User other = (User) obj;
        if (username == null) {
            if (other.username != null)
                return false;
        } else if (!username.equals(other.username))
            return false;
        return true;
    }
}

How do I prevent the account from locking up like that, or at least how do I unlock them programmatically?

Edit 1/5/16 I added the following to my WebSecurityConfig:

 @Bean
    public static ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
        return new ServletListenerRegistrationBean<HttpSessionEventPublisher>(new HttpSessionEventPublisher());
    }

and removed:

servletContext.addListener(httpSessionEventPublisher())

But I still see the behavior when I log in twice on the same browser - logging out locks the account until I restart.

like image 414
sonoerin Avatar asked Dec 21 '15 13:12

sonoerin


1 Answers

It turns out that the SessionRegistryImpl was not removing the user from the session. The first tab logout never actually called the server, so the second call does remove the one sessionid, leaving one in the principals.

I had to make several changes:

@Component
public class CustomLogoutHandler implements LogoutHandler {
    @Autowired
    private SessionRegistry sessionRegistry;

    @Override
    public void logout(HttpServletRequest httpServletRequest, httpServletResponse httpServletResponse, Authentication authentication) {
...
httpServletRequest.getSession().invalidate();
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
//redirect to login
httpServletResponse.sendRedirect("/");
    List<SessionInformation> userSessions = sessionRegistry.getAllSessions(user, true);

    for (SessionInformation session: userSessions) {
        sessionRegistry.removeSessionInformation(session.getSessionId());
    }
}

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
    public SessionRegistry sessionRegistry() {
        if (sessionRegistry == null) {
            sessionRegistry = new SessionRegistryImpl();
        }
        return sessionRegistry;
    }

    @Bean
    public static ServletListenerRegistrationBean httpSessionEventPublisher() {
        return new ServletListenerRegistrationBean(new HttpSessionEventPublisher());
    }

    @Bean
    public ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlAuthenticationStrategy() {

        ConcurrentSessionControlAuthenticationStrategy strategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
        strategy.setExceptionIfMaximumExceeded(true);
        strategy.setMessageSource(messageSource);

        return strategy;
    }
}
like image 107
sonoerin Avatar answered Oct 17 '22 18:10

sonoerin