Prevent Brute Force Authentication Attempts with Spring Security

1. Overview

In this quick tutorial, we’ll implement a basic solution for preventing brute force authentication attempts using Spring Security.

Simply put – we’ll keep a record of the number of failed attempts originating from a single IP address. If that particular IP goes over a set number of requests – it will be blocked for 24 hours.

2. An AuthenticationFailureListener

Let’s start by defining an AuthenticationFailureListener – to listen to AuthenticationFailureBadCredentialsEvent events and notify us of an authentication failure:

@Component
public class AuthenticationFailureListener implements 
  ApplicationListener<AuthenticationFailureBadCredentialsEvent> {

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private LoginAttemptService loginAttemptService;

    @Override
    public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent e) {
        final String xfHeader = request.getHeader("X-Forwarded-For");
        if (xfHeader == null) {
            loginAttemptService.loginFailed(request.getRemoteAddr());
        } else {
            loginAttemptService.loginFailed(xfHeader.split(",")[0]);
        }
    }
}

Note how, when authentication fails, we inform the LoginAttemptService of the IP address from where the unsuccessful attempt originated. Here, we get the IP address from the HttpServletRequest bean, which also gives us the originating address in the X-Forwarded-For header for requests that are forwarded by e.g. a proxy server.

3. An AuthenticationSuccessEventListener

Let’s also define an AuthenticationSuccessEventListener – which listens for AuthenticationSuccessEvent events and notifies us of a successful authentication:

@Component
public class AuthenticationSuccessEventListener implements 
  ApplicationListener<AuthenticationSuccessEvent> {
    
    @Autowired
    private HttpServletRequest request;

    @Autowired
    private LoginAttemptService loginAttemptService;

    @Override
    public void onApplicationEvent(final AuthenticationSuccessEvent e) {
        final String xfHeader = request.getHeader("X-Forwarded-For");
        if (xfHeader == null) {
            loginAttemptService.loginSucceeded(request.getRemoteAddr());
        } else {
            loginAttemptService.loginSucceeded(xfHeader.split(",")[0]);
        }
    }
}

Note how – similar to the failure listener, we’re notifying the LoginAttemptService of the IP address from which the authentication request originated.

4. The LoginAttemptService

Now – let’s discuss our LoginAttemptService implementation; simply put – we keep the number of wrong attempts per IP address for 24 hours:

@Service
public class LoginAttemptService {

    private final int MAX_ATTEMPT = 10;
    private LoadingCache<String, Integer> attemptsCache;

    public LoginAttemptService() {
        super();
        attemptsCache = CacheBuilder.newBuilder().
          expireAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader<String, Integer>() {
            public Integer load(String key) {
                return 0;
            }
        });
    }

    public void loginSucceeded(String key) {
        attemptsCache.invalidate(key);
    }

    public void loginFailed(String key) {
        int attempts = 0;
        try {
            attempts = attemptsCache.get(key);
        } catch (ExecutionException e) {
            attempts = 0;
        }
        attempts++;
        attemptsCache.put(key, attempts);
    }

    public boolean isBlocked(String key) {
        try {
            return attemptsCache.get(key) >= MAX_ATTEMPT;
        } catch (ExecutionException e) {
            return false;
        }
    }
}

Notice how an unsuccessful authentication attempt increases the number of attempts for that IP, and the successful authentication resets that counter.

From this point, it’s simply a matter of checking the counter when we authenticate.

5. The UserDetailsService

Now, let’s add the extra check in our custom UserDetailsService implementation; when we load the UserDetailswe first need to check if this IP address is blocked:

@Service("userDetailsService")
@Transactional
public class MyUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserRepository userRepository;
 
    @Autowired
    private RoleRepository roleRepository;
 
    @Autowired
    private LoginAttemptService loginAttemptService;
 
    @Autowired
    private HttpServletRequest request;
 
    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        String ip = getClientIP();
        if (loginAttemptService.isBlocked(ip)) {
            throw new RuntimeException("blocked");
        }
 
        try {
            User user = userRepository.findByEmail(email);
            if (user == null) {
                return new org.springframework.security.core.userdetails.User(
                  " ", " ", true, true, true, true, 
                  getAuthorities(Arrays.asList(roleRepository.findByName("ROLE_USER"))));
            }
 
            return new org.springframework.security.core.userdetails.User(
              user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, 
              getAuthorities(user.getRoles()));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

And here is getClientIP() method:

private String getClientIP() {
    String xfHeader = request.getHeader("X-Forwarded-For");
    if (xfHeader == null){
        return request.getRemoteAddr();
    }
    return xfHeader.split(",")[0];
}

Notice that we have some extra logic to identify the original IP address of the Client. In most cases, that’s not going to be necessary, but in some network scenarios, it is.

For these rare scenarios, we’re using the X-Forwarded-For header to get to the original IP; here’s the syntax for this header:

X-Forwarded-For: clientIpAddress, proxy1, proxy2

Also, notice another super-interesting capability that Spring has – we need the HTTP request, so we’re simply wiring it in.

Now, that’s cool. We’ll have to add a quick listener into our web.xml for that to work, and it makes things a whole lot easier.

<listener>
    <listener-class>
        org.springframework.web.context.request.RequestContextListener
    </listener-class>
</listener>

That’s about it – we’ve defined this new RequestContextListener in our web.xml to be able to access the request from the UserDetailsService.

6. Modify AuthenticationFailureHandler

Finally – let’s modify our CustomAuthenticationFailureHandler to customize our new error message.

We’re handling the situation when the user actually does get blocked for 24 hours – and we’re informing the user that his IP is blocked because he exceeded the maximum allowed wrong authentication attempts:

@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private MessageSource messages;

    @Override
    public void onAuthenticationFailure(...) {
        ...

        String errorMessage = messages.getMessage("message.badCredentials", null, locale);
        if (exception.getMessage().equalsIgnoreCase("blocked")) {
            errorMessage = messages.getMessage("auth.message.blocked", null, locale);
        }

        ...
    }
}

7. Conclusion

It’s important to understand that this is a good first step in dealing with brute-force password attempts, but also that there’s room for improvement. A production-grade brute-force prevention strategy may involve more elements than an IP block.

The full implementation of this tutorial can be found in the github project.

Related posts:

Thực thi nhiều tác vụ cùng lúc như thế nào trong Java?
Java Program to Implement Find all Forward Edges in a Graph
Java Program to Check if a Directed Graph is a Tree or Not Using DFS
Removing all Nulls from a List in Java
Java – Random Long, Float, Integer and Double
Unsatisfied Dependency in Spring
Java Program to Implement CopyOnWriteArraySet API
Guide to WeakHashMap in Java
Constructor Injection in Spring with Lombok
Returning Custom Status Codes from Spring Controllers
Java Program to Evaluate an Expression using Stacks
Giới thiệu Google Guice – Aspect Oriented Programming (AOP)
Một số ký tự đặc biệt trong Java
The Difference Between Collection.stream().forEach() and Collection.forEach()
Lớp Arrarys trong Java (Arrays Utility Class)
Guide to Apache Commons CircularFifoQueue
Java String to InputStream
Using Spring ResponseEntity to Manipulate the HTTP Response
Use Liquibase to Safely Evolve Your Database Schema
Java Program to Compute the Area of a Triangle Using Determinants
Easy Ways to Write a Java InputStream to an OutputStream
Giới thiệu Swagger – Công cụ document cho RESTfull APIs
Hướng dẫn sử dụng biểu thức chính quy (Regular Expression) trong Java
Java Program to Check if it is a Sparse Matrix
Spring Boot - Logging
Debug a JavaMail Program
Giới thiệu Design Patterns
Chương trình Java đầu tiên
Object cloning trong java
JUnit5 @RunWith
Converting Between Byte Arrays and Hexadecimal Strings in Java
Java Program to Describe the Representation of Graph using Adjacency List