Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 3550

Prevent Brute Force Authentication Attempts with Spring Security

$
0
0

I usually post about Security on Twitter - you can follow me there:

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. Modify the UserDetailsService

Now, let’s ad the extra check in our custom UserDetailsService implementation; when we load the UserDetails, we first 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 = request.getRemoteAddr();
        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);
        }
    }
}

The interesting part is is how we are autowiring in the HTTP request; we do this because we need to access the IP address and we cannot modify the signature of the loadUserByUsername method. In order to be able to do this, we need to add some additional logic into our web.xml.

6. The web.xml

We need to add RequestContextListener to our web.xml to be able to access the request from the UserDetailsService:

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

7. Modify login.jsp

Finally – let’s take care of the front-end and modify our login.jsp.

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:

<c:if test="${param.error != null}">
    <c:choose>
        <c:when test="${SPRING_SECURITY_LAST_EXCEPTION.message == 'User is disabled'}">
	    <div class="alert alert-error">
                <spring:message code="auth.message.disabled"></spring:message>
	    </div>
	</c:when>
	<c:when test="${SPRING_SECURITY_LAST_EXCEPTION.message == 'User account has expired'}">
	    <div class="alert alert-error">
	        <spring:message code="auth.message.expired"></spring:message>
	    </div>
	</c:when>
	<c:when test="${SPRING_SECURITY_LAST_EXCEPTION.message == 'blocked'}">
            <div class="alert alert-error">
                <spring:message code="auth.message.blocked"></spring:message>
            </div>
        </c:when>
	<c:otherwise>
	    <div class="alert alert-error">
	        <spring:message code="message.badCredentials"></spring:message>
            </div>
        </c:otherwise>
    </c:choose>
</c:if>

8. 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.

I usually post about Security on Twitter - you can follow me there:



Viewing all articles
Browse latest Browse all 3550

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>