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:
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
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.
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;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With