> ## Documentation Index
> Fetch the complete documentation index at: https://guide.codepure.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Improper Restriction of Excessive Authentication Attempts

> Mitigation for missing rate limiting or lockouts on login attempts, allowing attackers to brute-force credentials.

## Overview

This vulnerability occurs when an application **fails to limit the number of repeated authentication attempts** from a single user or IP address within a short period. Without such restrictions, attackers can use automated tools to rapidly submit thousands or millions of password guesses (**brute-force attack**) or common passwords (**dictionary attack**) against valid usernames. This significantly increases the likelihood of an attacker guessing a correct password and gaining unauthorized access. 🔑➡️💥

## Business Impact

Failure to restrict excessive authentication attempts directly leads to:

* **Account Takeover:** Attackers can systematically guess weak or common passwords, compromising user accounts.
* **Denial of Service (Account Lockout):** If the *only* defense is permanent account lockout after a few tries, attackers can intentionally lock out legitimate users. A better approach often involves temporary lockouts or CAPTCHAs.
* **Resource Consumption:** Brute-force attacks can consume server CPU and network bandwidth.
* **Detection Evasion:** Slow, distributed brute-force attacks might go unnoticed if basic attempt logging is the only defense.

<Card title="Reference Details" icon="book-open" iconType="solid">
  **CWE ID:** [CWE-307](https://cwe.mitre.org/data/definitions/307.html)
  **Related CWEs:** CWE-799 (Improper Control of Interaction Frequency), CWE-1216 (Lockout Errors)
  **OWASP Top 10 (2021):** A07:2021 - Identification and Authentication Failures
  **Severity:** High
</Card>

## Framework-Specific Analysis and Remediation

Brute-force protection is usually implemented via **rate limiting** (temporary blocking) or **account lockout** (temporary or requiring admin intervention). This often involves tracking failed login attempts per username and/or IP address in a cache (like Redis) or database. Many frameworks have plugins or built-in features, but custom logic is also common.

**Key Remediation Principles:**

1. **Limit Attempts per IP:** Track failed login attempts by IP address and temporarily block IPs with excessive failures (e.g., block for 5 minutes after 20 attempts).
2. **Limit Attempts per Username:** Track failed login attempts *per username* and implement stronger protections like temporary account lockout (e.g., lock for 30 minutes after 5 failed attempts) or requiring a CAPTCHA.
3. **Use Exponential Backoff:** Increase the lockout duration after repeated lockout events for the same account or IP.
4. **Secure Lockout Mechanism:** Ensure the lockout mechanism itself cannot be abused for Denial of Service (e.g., avoid permanent lockouts triggered solely by failed attempts).
5. **Logging and Monitoring:** Log failed login attempts and lockout events to detect attacks. Alert administrators to high rates of failures.

<Tabs>
  <Tab title="Python">
    #### Framework Context

    Using libraries like `django-ratelimit`, `django-axes`, `Flask-Limiter` to apply limits specifically to login views.

    #### Vulnerable Scenario 1: No Login Attempt Limit (Django)

    ```python theme={null}
    # accounts/views.py
    from django.contrib.auth.views import LoginView

    class UserLoginView(LoginView):
        template_name = 'accounts/login.html'
        # DANGEROUS: No mechanism to prevent an attacker from submitting
        # thousands of password guesses per minute for a given username.
    ```

    #### Vulnerable Scenario 2: Unprotected API Endpoint (Flask)

    ```python theme={null}
    # app.py (Flask)
    @app.route('/api/search', methods=['GET'])
    def api_search():
        query = request.args.get('q')
        # DANGEROUS: This search might be resource-intensive.
        # An attacker can flood this endpoint, causing DoS.
        # No rate limiting is applied.
        results = perform_complex_search(query)
        return jsonify(results)
    ```

    #### Mitigation and Best Practices

    Integrate a rate-limiting library. Apply decorators or middleware to the specific views/routes that need protection. Use Redis or Memcached for distributed rate limiting in multi-server environments.

    #### Secure Code Example

    ```python theme={null}
    # views.py (Django with django-axes - Configuration in settings.py)
    # settings.py:
    # INSTALLED_APPS = [..., 'axes', ...]
    # AUTHENTICATION_BACKENDS = ['axes.backends.AxesStandaloneBackend', ...] # Or AxesModelBackend
    # AXES_FAILURE_LIMIT = 5 # Lockout after 5 attempts
    # AXES_COOLOFF_TIME = timedelta(minutes=15) # Lockout duration
    # AXES_LOCKOUT_TEMPLATE = 'accounts/lockout.html' # Custom lockout page
    # AXES_HANDLER = 'axes.handlers.database.AxesDatabaseHandler' # Store attempts in DB
    # --- views.py needs no specific axes code if using backend ---
    # LoginView will automatically use the Axes backend if configured.

    # views.py (Django with django-ratelimit)
    from django.contrib.auth.views import LoginView
    from ratelimit.decorators import ratelimit
    from django.utils.decorators import method_decorator

    class UserLoginViewSecureRL(LoginView):
        template_name = 'accounts/login.html'
        # SECURE: Rate limit based on username in POST data + IP
        @method_decorator(ratelimit(key='post:username', rate='5/m', block=True, method='POST'), name='dispatch')
        def dispatch(self, *args, **kwargs):
            return super().dispatch(*args, **kwargs)
    ```

    ```python theme={null}
    # app.py (Flask with Flask-Limiter)
    from flask import Flask, request, jsonify # Added request, jsonify
    from flask_limiter import Limiter
    from flask_limiter.util import get_remote_address

    app = Flask(__name__) # Assume app exists
    limiter = Limiter(
        get_remote_address, # Use IP address for tracking
        app=app,
        default_limits=["200 per day", "50 per hour"],
        storage_uri="memory://" # Use "redis://localhost:6379" etc. for production
    )

    @app.route('/api/search-secure')
    @limiter.limit("10 per minute") # SECURE: Specific limit for this endpoint
    def api_search_secure():
        query = request.args.get('q')
        results = perform_complex_search(query) # Assume this exists
        return jsonify(results)

    @app.route('/login-secure', methods=['GET', 'POST'])
    # SECURE: Limit POST requests based on IP AND username form field
    @limiter.limit("5 per minute", key_func=lambda: f"{get_remote_address()}:{request.form.get('username')}", methods=['POST'])
    def login_secure():
        if request.method == 'POST':
            # ... login logic ...
            if login_successful: # Assume this variable is set
                 limiter.reset() # Optional: Reset limit on success for this key
                 # ...
            else:
                 # Limit applied automatically by decorator
                 flash('Invalid credentials') # Assume flash exists
        return render_template('login.html') # Assume render_template exists
    ```

    #### Testing Strategy

    Use automated tools (like `wfuzz`, `ffuf`, Burp Intruder) or simple scripts (e.g., `curl` in a loop) to send rapid, repeated requests to login endpoints, password reset forms, and resource-intensive API endpoints. Verify that after exceeding the configured limit, the server responds with an appropriate error (e.g., `429 Too Many Requests`) and blocks further requests for a period. Test different keys (IP vs. user vs. form field).
  </Tab>

  <Tab title="Java">
    #### Framework Context

    Implementing custom listeners or providers in Spring Security to track failed attempts, or using libraries like Bucket4j for rate limiting on the login endpoint.

    #### Vulnerable Scenario 1: Default Spring Security Form Login

    The basic `formLogin()` configuration doesn't include brute-force protection out of the box.

    ```java theme={null}
    // config/SecurityConfig.java
    // Assume WebSecurityConfigurerAdapter extension
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests().anyRequest().authenticated().and()
            // DANGEROUS: No protection against repeated failed login attempts.
            .formLogin().permitAll();
    }
    ```

    #### Vulnerable Scenario 2: Custom Authentication Logic without Tracking

    A custom authentication method doesn't track or limit failures.

    ```java theme={null}
    // service/AuthService.java - Assume UserDetails, UserDetailsService, PasswordEncoder exist
    public boolean authenticate(String username, String password) {
        UserDetails user = userDetailsService.loadUserByUsername(username);
        // DANGEROUS: Failure count is not tracked or limited.
        if (user != null && passwordEncoder.matches(password, user.getPassword())) {
             // Success logic
             return true;
        } else {
             // Failure logic - no tracking/blocking
             return false;
        }
    }
    ```

    #### Mitigation and Best Practices

    * **Spring Security:** Implement an `AuthenticationFailureHandler` and/or `AuthenticationSuccessHandler` to track failed/successful attempts per username/IP (e.g., storing counts in a cache like Redis or Guava). Before attempting authentication, check the failure count and throw a `LockedException` or similar if the threshold is exceeded.
    * **Rate Limiting:** Apply a rate-limiting filter (like the Bucket4j example in `CWE-799`) specifically to the login processing URL, keyed by IP address and potentially the username parameter.

    #### Secure Code Example

    ```java theme={null}
    // config/LoginAttemptService.java (Example using Guava Cache)
    import com.google.common.cache.CacheBuilder;
    import com.google.common.cache.CacheLoader;
    import com.google.common.cache.LoadingCache;
    import org.springframework.stereotype.Service;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.TimeUnit;

    @Service
    public class LoginAttemptService {
        private final int MAX_ATTEMPTS = 5;
        private LoadingCache<String, Integer> attemptsCache; // Key = IP Address or Username

        public LoginAttemptService() {
            attemptsCache = CacheBuilder.newBuilder()
                .expireAfterWrite(15, TimeUnit.MINUTES) // Cache expiry / lockout duration
                .build(new CacheLoader<String, Integer>() {
                    public Integer load(String key) { return 0; } // Default attempts = 0
                });
        }

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

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

        public boolean isBlocked(String key) {
            try {
                return attemptsCache.get(key) >= MAX_ATTEMPTS;
            } catch (ExecutionException e) { return false; /* Fail open on cache error? Or block? */ }
        }
    }

    // config/AuthenticationFailureListener.java (Example Listener)
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationListener;
    import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
    import org.springframework.stereotype.Component;
    import javax.servlet.http.HttpServletRequest; // Need request for IP

    @Component
    public class AuthenticationFailureListener
           implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> {

        @Autowired(required = false) // Make request optional if not always in context
        private HttpServletRequest request; // Inject request for IP
        @Autowired private LoginAttemptService loginAttemptService;

        @Override
        public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent e) {
            String key = "unknown_ip"; // Default
            if (request != null) {
                 final String xfHeader = request.getHeader("X-Forwarded-For");
                 if (xfHeader == null) {
                    key = request.getRemoteAddr();
                 } else {
                    key = xfHeader.split(",")[0]; // Get client IP from proxy header
                 }
            }
            // Could also extract username: e.getAuthentication().getName();
            loginAttemptService.loginFailed(key); // Track by IP
        }
    }
    // Need a similar AuthenticationSuccess Listener to call loginSucceeded.
    // Need to check isBlocked() *before* attempting auth (e.g., in a custom filter or provider).
    ```

    #### Testing Strategy

    Use automated tools to send repeated invalid login attempts. Monitor failure counts (if exposed via logs or an endpoint). Verify that after the threshold, login attempts from that IP/username are blocked with an appropriate error message or status code (e.g., 401/403 with lockout message, or 429 if using rate limiting) for the specified duration.
  </Tab>

  <Tab title=".NET(C#)">
    #### Framework Context

    ASP.NET Core Identity includes **account lockout** features configurable in `Startup.cs`. Rate limiting can be added via `AspNetCoreRateLimit`.

    #### Vulnerable Scenario 1: Account Lockout Disabled

    The default Identity configuration might have lockout disabled or set with very high thresholds.

    ```csharp theme={null}
    // Startup.cs (ConfigureServices)
    using Microsoft.AspNetCore.Identity; // Added namespace
    using Microsoft.Extensions.DependencyInjection; // Added namespace
    // Assume ApplicationDbContext, IdentityUser exist
    public void ConfigureServices(IServiceCollection services) // Example signature
    {
        services.AddDefaultIdentity<IdentityUser>(options => {
            // ... other options ...
            // DANGEROUS: Lockout might be disabled by default or configured weakly.
            options.Lockout.AllowedForNewUsers = false; // Example: Disabled
            options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(1); // Too short?
            options.Lockout.MaxFailedAccessAttempts = 100; // Too high!
        })
        .AddEntityFrameworkStores<ApplicationDbContext>();
        // ...
    }
    ```

    #### Vulnerable Scenario 2: No General Rate Limiting

    Even with account lockout, attackers can still try many usernames from one IP or target APIs without lockout. Missing general rate limiting (like `AspNetCoreRateLimit`) allows this. (See CWE-799 example).

    #### Mitigation and Best Practices

    * **Enable Identity Lockout:** Configure `IdentityOptions.Lockout` with sensible values (e.g., `MaxFailedAccessAttempts = 5`, `DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15)`). Ensure `AllowedForNewUsers = true`.
    * **Implement Rate Limiting:** Use `AspNetCoreRateLimit` (see `CWE-799`) to throttle requests to the login endpoint based on IP address as a first line of defense before Identity's user-specific lockout engages.

    #### Secure Code Example

    ```csharp theme={null}
    // Startup.cs (ConfigureServices - Secure Identity Lockout)
    using Microsoft.AspNetCore.Identity;
    using Microsoft.Extensions.DependencyInjection;
    using System; // Added for TimeSpan
    // ...
    public void ConfigureServices(IServiceCollection services) // Example signature
    {
        services.AddDefaultIdentity<IdentityUser>(options => {
            options.SignIn.RequireConfirmedAccount = true; // Good practice
            // SECURE: Configure sensible lockout settings.
            options.Lockout.AllowedForNewUsers = true;
            options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15); // 15-minute lockout
            options.Lockout.MaxFailedAccessAttempts = 5; // Lock after 5 failed attempts
        })
        .AddEntityFrameworkStores<ApplicationDbContext>(); // Assuming ApplicationDbContext
        // ...
        // Add AspNetCoreRateLimit configuration as shown in CWE-799 example,
        // ensuring the login endpoint has appropriate IP-based rules, e.g.:
        // { "Endpoint": "*:/Account/Login", "Period": "1m", "Limit": 10 } // Limit per IP before user lockout
    }

    // Controllers/AccountController.cs (Ensure lockoutOnFailure is true)
    // Assume _signInManager, _logger, LoginViewModel exist
    public async Task<IActionResult> Login(LoginViewModel model)
    {
        // ... model state check ...
        // SECURE: Ensure lockoutOnFailure is true (default is often false).
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe,
            lockoutOnFailure: true);

        if (result.Succeeded) { /* ... */ }
        if (result.IsLockedOut) {
             _logger.LogWarning("User account locked out: {Email}", model.Email);
             return RedirectToAction("Lockout"); // Show lockout page
        }
        // ... handle other failures ...
    }
    ```

    #### Testing Strategy

    Submit multiple (e.g., 6) incorrect passwords for a valid user. Verify the account becomes locked (user cannot log in even with correct password) and the `LockoutEnd` date is set in the `AspNetUsers` table. Check the lockout duration. Use tools to send rapid requests from one IP to the login endpoint and verify rate limiting (429 responses) occurs before account lockout if both are configured.
  </Tab>

  <Tab title="PHP">
    #### Framework Context

    Laravel provides login throttling out of the box (`Illuminate\Foundation\Auth\ThrottlesLogins` trait in `LoginController`). Plain PHP requires manual implementation.

    #### Vulnerable Scenario 1: Login Throttling Disabled (Laravel)

    A developer removes or overrides the `ThrottlesLogins` trait or related methods in `app/Http/Controllers/Auth/LoginController.php`.

    ```php theme={null}
    // app/Http/Controllers/Auth/LoginController.php (Modified)
    namespace App\Http\Controllers\Auth;
    use App\Http\Controllers\Controller; // Added base controller
    use Illuminate\Foundation\Auth\AuthenticatesUsers; // Added trait
    // ... use statements ...
    class LoginController extends Controller
    {
        // DANGEROUS: The ThrottlesLogins trait is removed or commented out.
        // use ThrottlesLogins;
        use AuthenticatesUsers; // Only has auth logic, not throttling

        // ... constructor, redirectTo etc. ...

        // If the trait is missing, the default login action won't throttle.
    }
    ```

    #### Vulnerable Scenario 2: Plain PHP without Attempt Tracking

    ```php theme={null}
    <?php
    // login.php (Plain PHP)
    session_start();
    // ... get username, password from POST ...
    // Assume get_password_hash_from_db, get_user_id exist
    $stored_hash = get_password_hash_from_db($username);

    // DANGEROUS: No tracking of failed attempts.
    if ($stored_hash && password_verify($password, $stored_hash)) {
        session_regenerate_id(true);
        $_SESSION['user_id'] = get_user_id($username);
        header('Location: /dashboard.php');
        exit;
    } else {
        // Failure, but no limit implemented
        header('Location: /login.php?error=1');
        exit;
    }
    ?>
    ```

    #### Mitigation and Best Practices

    * **Laravel:** Ensure the `ThrottlesLogins` trait is used in your `LoginController`. Configure the number of attempts (`maxAttempts`) and decay minutes (`decayMinutes`) via methods in the controller if you need to override defaults (defaults are usually reasonable: 5 attempts, 1 minute decay).
    * **Plain PHP:** Implement failed attempt tracking. Store failure counts and timestamps per username and/or IP address (e.g., in session, database, or cache like Redis/Memcached). Before checking the password, check the attempt count/timestamp; if exceeded, display an error and deny the attempt until the lockout period expires.

    #### Secure Code Example

    ```php theme={null}
    // app/Http/Controllers/Auth/LoginController.php (Laravel - Secure Default)
    namespace App\Http\Controllers\Auth;
    use App\Http\Controllers\Controller;
    use Illuminate\Foundation\Auth\AuthenticatesUsers;
    // SECURE: Include the ThrottlesLogins trait.
    use Illuminate\Foundation\Auth\ThrottlesLogins;

    class LoginController extends Controller
    {
        // Use AuthenticatesUsers for standard login logic
        use AuthenticatesUsers, ThrottlesLogins; // Ensure ThrottlesLogins is present

        // Optional: Override defaults if needed
        // protected $maxAttempts = 3; // Default is 5
        // protected $decayMinutes = 2; // Default is 1

        protected $redirectTo = '/home'; // Define redirect path

        public function __construct() {
             $this->middleware('guest')->except('logout');
        }

        // Override username() if not using 'email'
        // public function username() { return 'username'; }
    }
    ```

    ```php theme={null}
    <?php
    // login_secure.php (Plain PHP - Conceptual Example)
    session_start();
    define('MAX_LOGIN_ATTEMPTS', 5);
    define('LOCKOUT_TIME_SECONDS', 15 * 60); // 15 minutes

    // Assume helper functions using a persistent store (DB, Redis) exist:
    // get_failed_attempts($key), is_lockout_expired($key),
    // reset_failed_attempts($key), record_failed_attempt($key)

    $username = $_POST['username'] ?? '';
    $password = $_POST['password'] ?? '';
    $ip_address = $_SERVER['REMOTE_ADDR'];

    $ip_key = "login_attempt_ip:$ip_address";
    $user_key = "login_attempt_user:$username";

    // Check lockout
    if (get_failed_attempts($ip_key) >= (MAX_LOGIN_ATTEMPTS * 10) || // Stricter IP limit
        ($username && get_failed_attempts($user_key) >= MAX_LOGIN_ATTEMPTS)) {
        
        // Check if lockout time has passed (needs timestamp storage)
        if ($username && !is_lockout_expired($user_key)) { // Check user lockout first
             die("Too many failed attempts for this user. Please try again later.");
        } else if (!is_lockout_expired($ip_key)) {
             die("Too many failed attempts from this IP. Please try again later.");
        } else {
             // Reset if expired
             if ($username) reset_failed_attempts($user_key);
             reset_failed_attempts($ip_key);
        }
    }

    $stored_hash = get_password_hash_from_db($username);

    if ($stored_hash && password_verify($password, $stored_hash)) {
        reset_failed_attempts($ip_key);
        if ($username) reset_failed_attempts($user_key);
        session_regenerate_id(true);
        $_SESSION['user_id'] = get_user_id($username);
        header('Location: /dashboard.php');
        exit;
    } else {
        // Record failed attempt
        record_failed_attempt($ip_key);
        if ($username) record_failed_attempt($user_key);
        header('Location: /login.php?error=1');
        exit;
    }
    ?>
    ```

    #### Testing Strategy

    Use automated tools (Burp Intruder, Hydra) to send multiple invalid password attempts for a valid username. Verify that after the threshold (e.g., 5 in Laravel default), subsequent attempts are blocked with a specific error message and a `429 Too Many Requests` status code. Check the lockout duration. Test attacks against different users from the same IP and the same user from different IPs.
  </Tab>

  <Tab title="Node.js">
    #### Framework Context

    Using rate-limiting middleware (`express-rate-limit`, `rate-limiter-flexible`) specifically configured for the login route, potentially keyed by IP and username.

    #### Vulnerable Scenario 1: Login Route without Rate Limiting

    ```javascript theme={null}
    // app.js
    const express = require('express'); // Added imports for context
    const app = express();
    // Assume User model, bcrypt, session middleware exist
    app.use(express.urlencoded({ extended: true })); // Parse form body

    app.post('/login', async (req, res) => {
        const { username, password } = req.body;
        // DANGEROUS: No limit on login attempts.
        const user = await User.findOne({ username });
        if (user && await bcrypt.compare(password, user.passwordHash)) {
            // ... login success ...
            req.session.userId = user._id;
            res.redirect('/dashboard');
        } else {
            // Failure, but no tracking/limiting
            res.status(401).send('Invalid credentials');
        }
    });

    app.listen(3000);
    ```

    #### Vulnerable Scenario 2: Rate Limiter Only By IP

    While better than nothing, only limiting by IP allows an attacker to brute-force *many different usernames* from the same IP address without hitting the limit for any single user.

    ```javascript theme={null}
    // app.js
    const rateLimit = require('express-rate-limit');
    // ... setup app ...
    const loginLimiter = rateLimit({
        windowMs: 15 * 60 * 1000, // 15 minutes
        max: 100, // Limit each IP to 100 requests per windowMs
        // DANGEROUS: Doesn't consider username. Attacker tries 100 different users.
    });
    app.post('/login-ip-limited', loginLimiter, async (req, res) => { /* ... */ });
    ```

    #### Mitigation and Best Practices

    Apply rate-limiting middleware (like `express-rate-limit`) to the login route. Configure it with a strict limit (`max`) and reasonable window (`windowMs`). Crucially, use a `keyGenerator` function that includes **both the IP address and the username** from the request body to prevent attackers from cycling through usernames.

    #### Secure Code Example

    ```javascript theme={null}
    // app.js (Secure Login Limiter)
    const express = require('express');
    const rateLimit = require('express-rate-limit');
    const app = express();
    app.use(express.json()); // Middleware to parse JSON body
    app.use(express.urlencoded({ extended: true })); // Middleware to parse form body
    app.set('trust proxy', 1); // Trust proxy if behind one for req.ip

    const loginLimiter = rateLimit({
        windowMs: 15 * 60 * 1000, // 15 minutes
        max: 5, // SECURE: Limit each user/IP combination to 5 attempts per window
        message: 'Too many login attempts from this IP/user, please try again after 15 minutes',
        standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
        legacyHeaders: false, // Disable the `X-RateLimit-*` headers
        // SECURE: Key includes both IP and username from form body
        keyGenerator: (req, res) => {
             // Ensure username exists
             const username = req.body.username || 'unknown_user';
             // Use req.ip (requires `app.set('trust proxy', 1)` if behind proxy)
             return req.ip + ':' + username.toLowerCase();
        },
        // Optional: Handler to log blocked requests
        handler: (req, res, next, options) => {
           // Assume logger exists
           // logger.warn(`Rate limit exceeded for key: ${options.keyGenerator(req, res)}`);
           res.status(options.statusCode).send(options.message);
        }
    });

    // Apply the limiter ONLY to the login POST route
    app.post('/login-secure', loginLimiter, async (req, res) => {
        const { username, password } = req.body;
        // ... authentication logic (find user, bcrypt.compare) ...
        if (login_successful) { // Assume variable exists
            // Optional: Reset limiter count on success (requires external store like Redis)
            // await loginLimiter.resetKey(req.ip + ':' + username.toLowerCase());
            // ... login success (regenerate session etc.) ...
        } else {
             // Rate limiter handles blocking if needed (or increments count)
             res.status(401).send('Invalid credentials');
        }
    });
    // ... other routes ...
    ```

    #### Testing Strategy

    Use automated tools to send rapid, invalid password attempts for a *single valid username*. Verify that requests are blocked (429 response) after the `max` limit. Wait for `windowMs` and verify login is possible again. Repeat the test, but cycle through *different valid usernames* from the *same IP address*. Verify the IP-based limit (if separate) or the combined key limit functions correctly.
  </Tab>

  <Tab title="Ruby">
    #### Framework Context

    Using gems like `rack-attack` as middleware. Devise has some built-in tracking (`:lockable` module), but `rack-attack` is often used for IP/request-based throttling.

    #### Vulnerable Scenario 1: Devise Login Without `rack-attack` or `:lockable`

    A standard Devise setup without `rack-attack` configured and without the `:lockable` module enabled on the User model.

    ```ruby theme={null}
    # config/routes.rb
    devise_for :users # Standard Devise routes
    # DANGEROUS: No rack-attack initializer configured.

    # app/models/user.rb
    class User < ApplicationRecord
      # DANGEROUS: :lockable module is not included.
      devise :database_authenticatable, :registerable,
             :recoverable, :rememberable, :validatable
    end
    ```

    #### Vulnerable Scenario 2: `rack-attack` Configured but Missing Login Path

    ```ruby theme={null}
    # config/initializers/rack_attack.rb
    class Rack::Attack
      # DANGEROUS: General request limit exists, but no specific,
      # stricter limit for the sensitive POST /users/sign_in path.
      throttle('req/ip', limit: 100, period: 5.minutes, &:ip)
      # Missing: throttle('logins/ip', ...)
    end
    ```

    #### Mitigation and Best Practices

    * **Devise `:lockable`:** Add the `:lockable` module to your User model in Devise. Configure it in `config/initializers/devise.rb` (e.g., `config.maximum_attempts = 5`, `config.lock_strategy = :failed_attempts`, `config.unlock_strategy = :time`, `config.unlock_in = 15.minutes`). This provides *user-specific* lockout.
    * **`rack-attack`:** Implement `rack-attack` (as a first line of defense) to throttle login attempts by **IP** and/or **email parameter** to prevent user enumeration and protect against distributed attacks.

    #### Secure Code Example

    ```ruby theme={null}
    # app/models/user.rb (Secure - Devise :lockable)
    class User < ApplicationRecord
      devise :database_authenticatable, :registerable,
             :recoverable, :rememberable, :validatable,
             # SECURE: Enable lockable module for user-specific lockout
             :lockable
    end

    # config/initializers/devise.rb (Secure - Configure :lockable)
    Devise.setup do |config|
      # ...
      # ==> Configuration for :lockable
      config.lock_strategy = :failed_attempts # Lock based on failed attempts
      config.maximum_attempts = 5 # SECURE: Lock after 5 attempts
      config.unlock_strategy = :time # Unlock after a time period
      config.unlock_in = 15.minutes # SECURE: Lockout duration
      config.lock_strategy = :both # Or track by both IP and email
      # ...
    end

    # config/initializers/rack_attack.rb (Secure - IP/Email Throttling)
    class Rack::Attack
      Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # Use Redis in prod

      # Throttle POST requests to /users/sign_in by IP address
      # Limit: 5 requests per minute per IP (stricter than general)
      throttle('logins/ip', limit: 5, period: 60.seconds) do |req|
        if (req.path == '/users/sign_in' || req.path == '/login') && req.post?
          req.ip
        end
      end

      # Throttle POST requests to /users/sign_in by email param
      # Limit: 5 requests per 15 minutes per email address
      throttle('logins/email', limit: 5, period: 15.minutes) do |req|
         if (req.path == '/users/sign_in' || req.path == '/login') && req.post?
           # Assumes Devise params: req.params['user']['email']
           # Adjust key if params structure is different
           req.params['user']['email'].to_s.downcase.strip.presence if req.params['user'].is_a?(Hash)
         end
      end

      # Customize throttled response
       self.throttled_response = lambda do |env|
         retry_after = (env['rack.attack.match_data'] || {})[:period]
         headers = { 'Content-Type' => 'text/html', 'Retry-After' => retry_after.to_s }
         [ 429, headers, ["Too Many Requests. Retry later.\n"] ] # Example response
       end
    end
    # config/application.rb (Ensure middleware is used)
    # config.middleware.use Rack::Attack
    ```

    #### Testing Strategy

    Test both lockout (`:lockable`) and throttling (`rack-attack`):

    * **Lockout:** Submit 6 invalid passwords for one user. Verify the account is locked (DB flag `locked_at` set, login fails even with correct password). Wait 15 minutes and verify login succeeds.
    * **Throttling:** Submit 6 invalid passwords (e.g., for *different* users) from *one IP* within a minute. Verify the 6th+ requests receive a `429 Too Many Requests` response.
  </Tab>
</Tabs>
