The Master Class of "Learn Spring Security" is out:

>> CHECK OUT THE COURSE

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 AuthenticationFailureEventListener

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

@Component
public class AuthenticationFailureListener 
  implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> {

    @Autowired
    private LoginAttemptService loginAttemptService;

    public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent e) {
        WebAuthenticationDetails auth = (WebAuthenticationDetails) 
          e.getAuthentication().getDetails();
        
        loginAttemptService.loginFailed(auth.getRemoteAddress());
    }
}

Note how, when authentication fails, we inform the LoginAttemptService of the IP address from where the unsuccessful attempt originated.

3. An AuthenticationSuccessEventListener

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

@Component
public class AuthenticationSuccessEventListener 
  implements ApplicationListener<AuthenticationSuccessEvent> {

    @Autowired
    private LoginAttemptService loginAttemptService;

    public void onApplicationEvent(AuthenticationSuccessEvent e) {
        WebAuthenticationDetails auth = (WebAuthenticationDetails) 
          e.getAuthentication().getDetails();
        
        loginAttemptService.loginSucceeded(auth.getRemoteAddress());
    }
}

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 UserDetails, we first need to check if this IP address is blocked:

@Service("userDetailsService")
@Transactional
public class MyUserDetailsService implements UserDetailsService {
 
[email protected]
    private UserRepository userRepository;
 
[email protected]
    private RoleRepository roleRepository;
 
[email protected]
    private LoginAttemptService loginAttemptService;
 
[email protected]
    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 a room for improvement. A production grade brute-force prevention strategy may involve more than elements that an IP block.

The full implementation of this tutorial can be found in the github project – this is an Eclipse based project, so it should be easy to import and run as it is.

The Master Class "Learn Spring Security" is out:

>> CHECK OUT THE COURSE

  • Hi Eugen, I just implemented this for a project. Is it really necessary to throw a runtime exception when the user is blocked? There is a flag in the User class, that indicates if the user is locked or not. Shouldn’t this field be used either?

    When injecting the ServletRequest, makes this the class automatically request scoped?

    • It depends – using the exception ties into the ProviderManager mechanism of dealing with exceptions during authentication and it also propagates the message to the client. On the client side, we can then display a semantically rich message, such as “Your IP is blocked for 24 hours”. But no – you don’t have to use that mechanism and you can certainly just return a locked user.
      As for injecting the request – this process does happen in the scope of a request, but that does mean that you need to define everything as request scoped. Basically you can inject a request scoped bean into a singleton just fine – here’s a SO answer on the topic going into a bit more detail.
      Cheers,
      Eugen.

  • V.Virtanen

    If a lot of your users are behind a proxy this may block entire organizations at once as you only analyze the getRemoteAddr which returns the ip of the _last_ proxy in the chain. To get the originating ip you should also take into account the x-forwarded-for header and the individual ips in there.

    I have some experience with this, and in the end you probably end up also recording the usage patterns of the users in your system to detected anomalies. (To detect legitimately authenticated users that have lost their password bypassing your site all together using phishing sites and cold calling known users pretending to be it support etc.)

    • Thanks for the great feedback Virtanen – I’ll explore it and add it to the article. Cheers,
      Eugen.

      • Hey Virtanen – the article is updated, you can have a look at the way the IP resolution is now handled. Cheers,
        Eugen.

    • Matt Krevs

      If you are worried about this, perhaps you could store login failures by username and IP, rather than by IP.

      • That’s definitely a sensible way to go – and it does solve some valid issues. It does leave other scenarios open (such as an attacker that got access to the full list of usernames, or a system where usernames are open). But – overall, I think it’s worth doing – thanks for pointing it out. The best approach would be a hybrid approach. Cheers,
        Eugen.

  • Dwayne

    I’m getting a NPE as e.getAuthentication().getDetails() is null.

  • Hey Jocko – that’s definitely a fair point and I considered it, but I wanted to keep things simple. There are many things that make the implementation just the first step towards being production ready, and definitely not done. Like the fact that it’s not dealing with a distributed system, or the lack of persistence, or the lack of being able to manually remove a block, etc.
    What I hope the article does is provide a solid first step and a clear direction to go forward.
    Thanks for the feedback. Cheers,
    Eugen.

  • any way to avoid

    org.springframework.web.context.request.RequestContextListener

    like using anotation or something like that?

    • Hey Marc – yes, there are several ways to avoid XML – have a look at the new(ish) Java config that replaces the web.xml in Servlet 3.1, and also have a look at Spring Boot.
      Hope that gives you some ideas of replacing your web.xml. Cheers,
      Eugen.

      • i know about that… question was more about how … like maybe with spring boot we need to add

        @Bean
        public RequestContextListener requestContextListener(){
        RequestContextListener requestContextListener = new RequestContextListener();
        return requestContextListener;
        }

        • There are a couple of ways to do it – the simplest of which would be defining the listener as a Bean – Boot will auto-register it. Try to do that conditionally with @ConditionalOnBean.
          Hope that clears things up. Cheers,
          Eugen.

  • you write

    Note that we’re using the X-Forwarded-For header to identify client original IP address. The syntax of this header is:

    X-Forwarded-For: clientIpAddress, proxy1, proxy2

    So in javascript, you need to provide these informations?

    • No, definitely not. You can skip that header entirely in most cases – this is just to deal with more complex typologies where that header exists – but using it is optional – so if you don’t need to use it, don’t send it from the client side.
      Hope that clears things up. Cheers,
      Eugen.

  • Jim Clayson

    Having trouble getting my application listeners to receive events.

    I have the exact code from the example. Both listeners are annotated as components. And I have the listeners’ package specified in WebSecurityConfig’s @ComponentScan.

    Is there something I’m missing?

    (I know for sure the RequestContextListener is registered in the spring container because it is listed in ‘beans’ via the actuator.)

    • Jim Clayson

      Ah. The failed event, it appears, is only generated for valid usernames.

    • Well Jim, I’m not sure 🙂
      It’s hard to say from just a description.
      Have you tried to run the code associated with the article to see events working in that context? Cheers,
      Eugen.

  • Marco Lenzo

    First of all thank you for the article. I know the scope was to keep it simple. However I just wanted to suggest that the check might belong more to an AuthenticationProvider rather than the UserDetailService. (e.g. DaoAuthenticationProvider.additionalAuthenticationChecks).

    • Hey Marco – glad you like the article.
      I definitely agree that’s a good place to do the check.
      There are two (small) reasons why you might not want to do that.

      First – that framework hook is meant more to enable us to add extra auth checks – that involve actual data/fields determining the outcome of the authentication process. From the Javadoc:
      If custom logic is needed to compare additional properties of UserDetails and/or UsernamePasswordAuthenticationToken, these should also appear in this method.
      And this particular check isn’t involving any fields, it’s more of a meta-check.

      Second – if you do that, you do have to roll your own provider – which is fine, just more complex. If you need to do that anyways, cool – but if this is the only reason to do it, I’m not sure if it’s worth it just to move the check.
      Hope that helps.
      Cheers,
      Eugen.

      • Marco Lenzo

        Fair point. Thank you for the reply 🙂