Lock Account after “x” amount of attempts — Spring Security
By: B A N J O Obe
Tags: Java, Spring Security, Spring Boot, Password Security, Spring

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

curity(prePostEnabled = true, proxyTargetClass = true)

public class ServerSecurityConfig extends WebSecurityConfigurerAdapter { @EnableGlobalMethodSe curity(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;

} ....public DaoAuthenticationProvider authenticationProvider(final PreAuthPasswordChecker preAuthPasswordChecker,final PostAuthPasswordChecker postAuthPasswordChecker) {DaoAuthenticationProvider provider = new DaoAuthenticationProvider();preAuthPasswordCheckerpostAuthPasswordCheckerreturn 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 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

Inject UserRepository into the Checker call findByUsername. NOTE: we throw an exception in loadUserByUsername, so at this point, the validity of the username is incontrovertible 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. Create an ExceptionHandler for AccountLockedException so that a suitable error response may be returned to the clien

Post Authentication

At this point we know that the supplied credentials are valid Inject UserRepository into the Checker Call findByUsername Set the passwordCounter field on UserDetails to 0, set 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