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