Spring Security - Prevent Brute Force



Brute force attacks are one of the most common and simple types of attacks against web applications. They occur when an attacker tries every possible combination of inputs to guess credentials, such as usernames and passwords. For modern web applications, its crucial to implement security measures that prevent or mitigate brute force attempts.

Spring Security, a powerful and flexible framework for securing Spring-based applications, can be used to prevent brute force attacks. In this article, we'll explore various techniques for mitigating brute force attacks using Spring Security, providing you with code examples and best practices.

Introduction to Brute Force Attacks

What are Brute Force Attacks?

A brute force attack occurs when an attacker attempts to gain unauthorized access to a system by systematically trying all possible combinations of passwords or other credentials. The attacker might use automated tools to speed up the process. These attacks can target login pages, account creation forms, or API endpoints.

Why Preventing Brute Force Attacks is Critical

Brute force attacks can lead to data breaches, service disruptions, and even account hijacking. If a users credentials are compromised, attackers can gain unauthorized access to sensitive information, potentially harming both individuals and organizations.

Overview of Spring Security

What is Spring Security?

Spring Security is a comprehensive security framework for Java applications, providing authentication, authorization, and protection against common vulnerabilities. It is a powerful tool for securing web applications, and one of its key features is flexibility in configuring security protocols.

Key Concepts in Spring Security

  • Authentication− The process of verifying the identity of a user (e.g., via username and password).

  • Authorization− Determining whether a user has permission to perform an action (e.g., accessing a resource).

  • Session Management− Tracking user sessions to ensure that users are properly authenticated during their interaction with the application.

  • Filters− Components used to process incoming requests (e.g., for authentication, authorization, etc.).

Basic Brute Force Protection with Spring Security

Using Spring Security's Default Authentication Mechanism

Spring Security offers default authentication mechanisms such as form-based login and HTTP Basic authentication. While it provides a basic level of security, it doesnt directly protect against brute force attacks. To enhance security, we need to add additional mechanisms.

Adding Login Attempt Limiting

One of the simplest ways to prevent brute force attacks is to limit the number of failed login attempts. This can be achieved by tracking failed login attempts and temporarily locking accounts after a specified number of failed attempts. Heres a simple implementation−

SecurityConfig.java

package com.tutorialspoint.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      http
         .formLogin(form -> form
            .loginPage("/login")
			// Custom failure handler. See code below
            .failureHandler(new CustomAuthenticationFailureHandler()) 
            .permitAll()
         )
         .authorizeHttpRequests(auth -> auth
            .requestMatchers("/login", "/register").permitAll()
            .anyRequest().authenticated()
         );

      return http.build();
   }
}

In this example, we customize the login page and handle authentication failures with a custom failure handler. To track failed login attempts, we can create a custom failure handler. This handler will record the number of failed attempts for a user and lock the account if the number exceeds a certain threshold.

CustomAuthenticationFailureHandler.java

package com.tutorialspoint.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import java.io.IOException;
import com.tutorialspoint.service.LoginAttemptService;

public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

   private static final int MAX_FAILED_ATTEMPTS = 5;

   @Autowired
   private LoginAttemptService loginAttemptService;

   @Override
   public void onAuthenticationFailure(HttpServletRequest request, 
      HttpServletResponse response, 
      AuthenticationException exception) 
         throws IOException, ServletException {
      String username = request.getParameter("username");

      // Track failed attempts for this username
      loginAttemptService.recordFailedAttempt(username);

      if (loginAttemptService.isAccountLocked(username)) {
         response.sendRedirect("/account-locked");
      } else {
         response.sendRedirect("/login?error=true");
      }
   }
}

LoginAttemptService.java

package com.tutorialspoint.service;

import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class LoginAttemptService {

   private static final int MAX_FAILED_ATTEMPTS = 5;
   private static final long LOCK_TIME_DURATION = 24 * 60 * 60 * 1000; // 24 hours

   private Map<String, Integer> attemptsCache = new ConcurrentHashMap<>();
   private Map<String, Long> lockTimeCache = new HashMap<>();

   public void recordFailedAttempt(String username) {
      int attempts = attemptsCache.getOrDefault(username, 0);
      attempts++;
      attemptsCache.put(username, attempts);

      if (attempts >= MAX_FAILED_ATTEMPTS) {
         lockTimeCache.put(username, System.currentTimeMillis());
      }
   }

   public boolean isAccountLocked(String username) {
      if (!lockTimeCache.containsKey(username)) {
         return false;
      }

      long lockTime = lockTimeCache.get(username);
      if (System.currentTimeMillis() - lockTime < LOCK_TIME_DURATION) {
         return true;
      } else {
         // Unlock the account after the lock time duration
         lockTimeCache.remove(username);
         attemptsCache.remove(username);
         return false;
      }
   }

   public void resetAttempts(String username) {
      attemptsCache.remove(username);
      lockTimeCache.remove(username);
   }
}

Rate Limiting

Rate limiting is a key strategy to prevent brute force attacks. By limiting the number of requests a user can make within a specific period, we can prevent an attacker from trying every possible password combination.

SecurityConfigRateLimit.java

package com.tutorialspoint.config;

import com.example.RateLimitingFilter;
import com.example.RateLimiter;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfigRateLimit {

   private final RateLimiter rateLimiter = new RateLimiter();

   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      http
         .addFilterBefore(new RateLimitingFilter(rateLimiter), 
            UsernamePasswordAuthenticationFilter.class)
         .formLogin(form -> form.permitAll());

      return http.build();
   }
}

RateLimitingFilter.java

package com.tutorialspoint.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;

public class RateLimitingFilter extends OncePerRequestFilter {

   private final RateLimiter rateLimiter;

   public RateLimitingFilter(RateLimiter rateLimiter) {
      this.rateLimiter = rateLimiter;
   }

   @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
      FilterChain filterChain) throws ServletException, IOException {
      String ipAddress = request.getRemoteAddr();
      if (!rateLimiter.consume(ipAddress)) {
         response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "Too many requests");
         return;
      }
      filterChain.doFilter(request, response);
   }
}

RateLimiter.java

package com.tutorialspoint.config;

import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

@Service
public class RateLimiter {

   private static final int MAX_REQUESTS_PER_MINUTE = 60;
   private static final long ONE_MINUTE_IN_MILLIS = TimeUnit.MINUTES.toMillis(1);

   private final ConcurrentHashMap<String, RequestInfo> requestCounts = new ConcurrentHashMap<>();

   public boolean consume(String ipAddress) {
      long currentTime = System.currentTimeMillis();
      RequestInfo requestInfo = requestCounts.computeIfAbsent(ipAddress, key -> new RequestInfo());

      synchronized (requestInfo) {
         if (currentTime - requestInfo.timestamp > ONE_MINUTE_IN_MILLIS) {
            requestInfo.timestamp = currentTime;
            requestInfo.requestCount.set(0);
         }

         if (requestInfo.requestCount.incrementAndGet() > MAX_REQUESTS_PER_MINUTE) {
            return false;
         }
      }
      return true;
   }

   private static class RequestInfo {
      long timestamp;
      AtomicInteger requestCount = new AtomicInteger(0);
   }
}

Output

Login Page

  • Username− user

  • Password− password

Login page

On Successful Login

Success page

On Failed Attempts

After 5 failed login attempts ( for user, type user / password, anything but password)

Failed page

Conclusion

Preventing brute force attacks is crucial for ensuring the security of your web applications. By leveraging Spring Securitys powerful features such as rate limiting, account lockout, custom filters you can effectively mitigate brute force attempts. Additionally, integrating two-factor authentication (2FA) and strong session management can provide further layers of protection against unauthorized access.

By adopting these strategies, you'll help protect your users from the dangers of brute force attacks and enhance the overall security of your application.
Advertisements