Lock Account after “x” amount of attempts — Spring Security

By Benjamin P

A simple solution to a simple problem. I expected a concise answer to already be out there but I could not find it; so I wrote it.

Album Art for Crush by Floating Points

If you are interested in the details surrounding this solution, read on after the code snippets.

Config class extending WebSecurityConfigurerAdapter:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
public class ServerSecurityConfig extends WebSecurityConfigurerAdapter {....

@Bean
public DaoAuthenticationProvider authenticationProvider(final PreAuthPasswordChecker preAuthPasswordChecker,
final PostAuthPasswordChecker postAuthPasswordChecker) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPreAuthenticationChecks(preAuthPasswordChecker);
provider.setPostAuthenticationChecks(postAuthPasswordChecker);
return provider;
} ....
}

PreAuthPasswordChecker

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.stereotype.Component;
@Component
public class PreAuthPasswordChecker implements UserDetailsChecker {
@Override
public void check(UserDetails userDetails) {
String username = userDetails.getUsername(); // perform your logic here
}
}

PostAuthPasswordChecker

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.stereotype.Component;
@Component
public class PostAuthPasswordChecker implements UserDetailsChecker {
@Override
public void check(UserDetails userDetails) {
String username = userDetails.getUsername();

// perform your logic here }
}

Locking a user’s account after a certain amount of incorrect password attempts is a common requirement for an application. To my surprise — because Spring is what I consider to be magic — Spring Security does not possess out-of-the-box functionality to achieve this. There is, however (of course), a mechanism for developers to implement such behavior.

Oddly my Google searches did not turn up any surefire solutions. The answers I found were all spurious; either being outdated or behaving incorrectly.

The first attempt

@Component
public class BadListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> {
@Override
public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent authenticationFailureBadCredentialsEvent) {

Worked perfectly for the error scenario. The success scenario was where we realized that an ApplicationListener type solution was not viable.

@Component
public class GoodListener implements ApplicationListener<InteractiveAuthenticationSuccessEvent> {
@Override
public void onApplicationEvent(InteractiveAuthenticationSuccessEvent e) {

The Event above would not fire at all, and AuthenticationSuccessEvent would fire even when the password was incorrect. So, AuthenticationFailureBadCredentialsEvent would fire, followed by AuthenticationSuccessEvent which is obviously incorrect. Perhaps I do not understand how these events truly behave but regardless, their respective names are rather misleading.

Getting back to the decided upon a solution:

setPostAuthenticationChecks: runs only after a user has supplied valid credentials.

setPreAuthenticationChecks: runs before the supplied credentials are validated. NOTE: this checker is executed after loadUserByUsername which means that the username value present on the UserDetails argument will be valid — assuming you throw an exception in loadUserByUsername if the username is incorrect — so you just need to validate the password at this point.

The two methods above are the fundamental strategies required. The password logic you implement in the overridden check does not have to be identical to ours. For those of you interested in our implementation:

Pre Authentication

  1. Inject UserRepository into the Checker
  2. call findByUsername. NOTE: we throw an exception in loadUserByUsername, so at this point, the validity of the username is incontrovertible
  3. If the number of attempts ≥ 3, set status to AccountStatus.LOCKED and throw an AccountLockedException. If not, increment the passwordCounter, irrespective of whether the password is valid. We increment regardless of password validity because if the User is authenticated (the password is valid), the post-authentication checker will run and reset the counter.
  4. Create an ExceptionHandler for AccountLockedException so that a suitable error response may be returned to the client

Post Authentication

  1. At this point, we know that the supplied credentials are valid
  2. Inject UserRepository into the Checker
  3. Call findByUsername
  4. Set the passwordCounter field on UserDetails to 0, set the status field to UserStatus.ACTIVE, and update the entry

Please feel free to ask any questions. I am always open to suggestions pertaining to the improvement of my writing

Thanks for reading

Written By: Benj Power: Software Engineer | Neslo

Software & Creative Studio

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store