Skip to content

Account Lockout & Cool-Down

Account lockout is a security feature that temporarily locks user accounts after a configurable number of failed login attempts, protecting against brute force attacks.

Overview

The account lockout mechanism helps prevent unauthorized access by locking user accounts after a certain number of failed login attempts. This makes brute force attacks impractical, as attackers would need to wait for the lockout period to expire before trying again.

How It Works

When a user enters incorrect credentials, the system records the failed login attempt. If the number of failed attempts exceeds the configured threshold within a specified time window, the account is locked for a configurable duration. During this lockout period, login attempts for the affected account will be rejected, even with correct credentials.

The lockout status is reset after a successful login or when an administrator manually unlocks the account.

Configuration

Account lockout can be configured via environment variables or by modifying the app/config/security_config.py file.

Environment Variables

# Account Lockout
ACCOUNT_LOCKOUT_ENABLED=true
ACCOUNT_LOCKOUT_MAX_FAILED_ATTEMPTS=5
ACCOUNT_LOCKOUT_DURATION_SECONDS=1800
ACCOUNT_LOCKOUT_RESET_AFTER_SECONDS=86400

Configuration Parameters

Parameter Description Default
ACCOUNT_LOCKOUT_ENABLED Enable/disable account lockout true
ACCOUNT_LOCKOUT_MAX_FAILED_ATTEMPTS Maximum number of failed login attempts before lockout 5
ACCOUNT_LOCKOUT_DURATION_SECONDS Duration in seconds to lock the account 1800 (30 minutes)
ACCOUNT_LOCKOUT_RESET_AFTER_SECONDS Time in seconds after which failed attempts are reset 86400 (24 hours)

Implementation

The account lockout feature is implemented in the app/security/account_lockout.py module. The module uses Redis to store lockout data, with an in-memory fallback if Redis is not available.

API Responses

When a user attempts to log in to a locked account, the API returns a 403 (Forbidden) status code with a JSON response:

{
  "detail": "Account is locked due to too many failed login attempts. Please try again later."
}

Managing Account Lockouts

Administrators can unlock user accounts through the Security API. This allows administrators to help users who have been locked out without waiting for the lockout period to expire.

Admin API Endpoints

The following endpoint is available for managing account lockouts:

  • POST /api/admin/security/account/unlock/{username}: Unlock a user account

This endpoint requires admin privileges and is protected by role-based access control.

Client Implementation Examples

Handling Account Lockout in Login Flow

import requests

def login(base_url, username, password):
    url = f"{base_url}/authentication/request-otp"
    data = {
        "username": username,
        "password": password
    }

    response = requests.post(url, json=data)

    if response.status_code == 200:
        result = response.json()
        if result.get("otpRequired", False):
            print("OTP required. Check your email.")
            # Handle OTP flow
            return {"status": "otp_required", "username": username}
        else:
            # Direct login successful
            return {"status": "success", "tokens": result}
    elif response.status_code == 403 and "Account is locked" in response.json().get("detail", ""):
        print("Account is locked due to too many failed login attempts. Please try again later.")
        return {"status": "locked", "message": response.json().get("detail")}
    else:
        print(f"Login failed: {response.json().get('detail', 'Unknown error')}")
        return {"status": "error", "message": response.json().get("detail", "Unknown error")}
async function login(baseUrl, username, password) {
  const url = `${baseUrl}/authentication/request-otp`;
  const data = {
    username: username,
    password: password
  };

  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    });

    const result = await response.json();

    if (response.ok) {
      if (result.otpRequired) {
        console.log("OTP required. Check your email.");
        // Handle OTP flow
        return { status: "otp_required", username: username };
      } else {
        // Direct login successful
        return { status: "success", tokens: result };
      }
    } else if (response.status === 403 && result.detail && result.detail.includes("Account is locked")) {
      console.log("Account is locked due to too many failed login attempts. Please try again later.");
      return { status: "locked", message: result.detail };
    } else {
      console.error(`Login failed: ${result.detail || 'Unknown error'}`);
      return { status: "error", message: result.detail || 'Unknown error' };
    }
  } catch (error) {
    console.error(`Error during login: ${error}`);
    return { status: "error", message: "Network or server error" };
  }
}
#!/bin/bash

login() {
  local base_url=$1
  local username=$2
  local password=$3

  response=$(curl -s -w "%{http_code}" \
    -X POST \
    -H "Content-Type: application/json" \
    -d "{\"username\":\"$username\",\"password\":\"$password\"}" \
    "$base_url/authentication/request-otp")

  http_code=${response: -3}
  content=${response:0:${#response}-3}

  if [ "$http_code" == "200" ]; then
    if [[ $content == *"\"otpRequired\":true"* ]]; then
      echo "OTP required. Check your email."
      # Handle OTP flow
      echo "{ \"status\": \"otp_required\", \"username\": \"$username\" }"
    else
      # Direct login successful
      echo "{ \"status\": \"success\", \"tokens\": $content }"
    fi
  elif [ "$http_code" == "403" ] && [[ $content == *"Account is locked"* ]]; then
    echo "Account is locked due to too many failed login attempts. Please try again later."
    echo "{ \"status\": \"locked\", \"message\": $(echo $content | jq -r '.detail') }"
  else
    echo "Login failed: $(echo $content | jq -r '.detail // "Unknown error"')"
    echo "{ \"status\": \"error\", \"message\": $(echo $content | jq -r '.detail // "Unknown error"') }"
  fi
}
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

public class AuthClient
{
    private readonly HttpClient _client;
    private readonly string _baseUrl;

    public AuthClient(string baseUrl)
    {
        _client = new HttpClient();
        _baseUrl = baseUrl;
    }

    public class LoginRequest
    {
        public string username { get; set; }
        public string password { get; set; }
    }

    public class LoginResponse
    {
        public string status { get; set; }
        public string username { get; set; }
        public string message { get; set; }
        public object tokens { get; set; }
    }

    public async Task<LoginResponse> LoginAsync(string username, string password)
    {
        string url = $"{_baseUrl}/authentication/request-otp";

        var request = new LoginRequest
        {
            username = username,
            password = password
        };

        var content = new StringContent(
            JsonConvert.SerializeObject(request),
            Encoding.UTF8,
            "application/json");

        try
        {
            HttpResponseMessage response = await _client.PostAsync(url, content);
            string responseContent = await response.Content.ReadAsStringAsync();
            dynamic result = JsonConvert.DeserializeObject(responseContent);

            if (response.IsSuccessStatusCode)
            {
                bool otpRequired = result.otpRequired != null && (bool)result.otpRequired;

                if (otpRequired)
                {
                    Console.WriteLine("OTP required. Check your email.");
                    // Handle OTP flow
                    return new LoginResponse
                    {
                        status = "otp_required",
                        username = username
                    };
                }
                else
                {
                    // Direct login successful
                    return new LoginResponse
                    {
                        status = "success",
                        tokens = result
                    };
                }
            }
            else if ((int)response.StatusCode == 403 && 
                     result.detail != null && 
                     result.detail.ToString().Contains("Account is locked"))
            {
                Console.WriteLine("Account is locked due to too many failed login attempts. Please try again later.");
                return new LoginResponse
                {
                    status = "locked",
                    message = result.detail
                };
            }
            else
            {
                string errorMessage = result.detail != null ? result.detail.ToString() : "Unknown error";
                Console.WriteLine($"Login failed: {errorMessage}");
                return new LoginResponse
                {
                    status = "error",
                    message = errorMessage
                };
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error during login: {ex.Message}");
            return new LoginResponse
            {
                status = "error",
                message = "Network or server error"
            };
        }
    }
}

Unlocking an Account (Admin Only)

import requests

def unlock_account(base_url, token, username):
    url = f"{base_url}/api/admin/security/account/unlock/{username}"
    headers = {
        "Authorization": f"Bearer {token}"
    }

    response = requests.post(url, headers=headers)

    if response.status_code == 200:
        print(f"Successfully unlocked account for {username}")
        return True
    else:
        print(f"Failed to unlock account: {response.json()}")
        return False
async function unlockAccount(baseUrl, token, username) {
  const url = `${baseUrl}/api/admin/security/account/unlock/${username}`;
  const headers = {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  };

  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: headers
    });

    const data = await response.json();

    if (response.ok) {
      console.log(`Successfully unlocked account for ${username}`);
      return true;
    } else {
      console.error(`Failed to unlock account: ${data.detail}`);
      return false;
    }
  } catch (error) {
    console.error(`Error unlocking account: ${error}`);
    return false;
  }
}
#!/bin/bash

unlock_account() {
  local base_url=$1
  local token=$2
  local username=$3

  response=$(curl -s -w "%{http_code}" \
    -X POST \
    -H "Authorization: Bearer $token" \
    "$base_url/api/admin/security/account/unlock/$username")

  http_code=${response: -3}
  content=${response:0:${#response}-3}

  if [ "$http_code" == "200" ]; then
    echo "Successfully unlocked account for $username"
    return 0
  else
    echo "Failed to unlock account: $content"
    return 1
  fi
}
using System;
using System.Net.Http;
using System.Threading.Tasks;

public class AccountManager
{
    private readonly HttpClient _client;
    private readonly string _baseUrl;

    public AccountManager(string baseUrl)
    {
        _client = new HttpClient();
        _baseUrl = baseUrl;
    }

    public async Task<bool> UnlockAccountAsync(string token, string username)
    {
        string url = $"{_baseUrl}/api/admin/security/account/unlock/{username}";

        _client.DefaultRequestHeaders.Authorization = 
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);

        try
        {
            HttpResponseMessage response = await _client.PostAsync(url, null);

            if (response.IsSuccessStatusCode)
            {
                Console.WriteLine($"Successfully unlocked account for {username}");
                return true;
            }
            else
            {
                string content = await response.Content.ReadAsStringAsync();
                Console.WriteLine($"Failed to unlock account: {content}");
                return false;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error unlocking account: {ex.Message}");
            return false;
        }
    }
}

Best Practices

  1. Balance Security and Usability: Set lockout thresholds and durations that provide security without causing excessive user frustration.
  2. Notify Users: Inform users when their account is locked and provide instructions for unlocking it.
  3. Monitor Lockouts: Monitor account lockouts to identify potential attack patterns or issues with legitimate users.
  4. Provide Self-Service Options: Consider implementing self-service account unlock options, such as email verification or security questions.
  5. Progressive Delays: Consider implementing progressive delays between login attempts instead of immediate lockouts.
  6. Separate Rate Limiting: Apply separate rate limiting for login attempts to prevent denial of service attacks.
  7. Log All Lockout Events: Maintain detailed logs of all account lockout events for security auditing.