Comprehensive rate limiting and account lockout system for authentication security.
ObjRateLimit provides multi-layer protection against brute force attacks and authentication abuse with per-user and per-IP rate limiting, automatic account lockout, exponential backoff, and detailed audit tracking.
✅ Per-user rate limiting
✅ Per-IP address rate limiting
✅ Account lockout after threshold violations
✅ Exponential backoff for repeated failures
✅ Automatic unlock after cooldown period
✅ Detailed login attempt tracking
✅ Security audit trail
✅ Configurable thresholds
✅ Manual unlock capabilities
✅ Statistics and reporting
security:
rate_limiting:
enabled: true
# User-based limits
user_max_attempts: 5
user_window_minutes: 15
user_lockout_minutes: 30
# IP-based limits
ip_max_attempts: 20
ip_window_minutes: 5
ip_lockout_minutes: 15
# Exponential backoff
exponential_backoff: true
backoff_base_seconds: 30
Requires sys_user_login_attempts table:
python -c "from factory.core.ObjData import ObjData; \
ObjData().create_tables_from_yaml(\
'local.processing/schema/package.sync/tables/sys_user_login_attempts.yaml')"
from factory.core.ObjRateLimit import RateLimit
limiter = RateLimit()
# Check if login is allowed
allowed, reason = limiter.is_login_allowed(
username="john.doe",
package="homechoice",
ip_address="192.168.1.100"
)
if not allowed:
print(f"Login blocked: {reason}")
# Show error to user
else:
# Proceed with authentication
pass
# Record failed attempt
limiter.record_login_attempt(
username="john.doe",
package="homechoice",
ip_address="192.168.1.100",
success=False,
failure_reason="invalid_password",
user_agent="Mozilla/5.0..."
)
# Record successful attempt
limiter.record_login_attempt(
username="john.doe",
package="homechoice",
ip_address="192.168.1.100",
success=True,
session_id="abc-123-def",
user_agent="Mozilla/5.0..."
)
The Login method in factory.web/ObjUser.py automatically uses rate limiting:
from factory.web.ObjUser import User
user = User()
result = user.Login(
username="john.doe",
password="password",
package="homechoice",
ip_address="192.168.1.100", # Required for IP limiting
user_agent="Mozilla/5.0..." # Optional, for audit trail
)
if not result['success']:
if result.get('rate_limited'):
print(f"Rate limited: {result['reason']}")
else:
print(f"Authentication failed: {result['reason']}")
Default thresholds:
Example:
Default thresholds:
Example:
When enabled, delays increase exponentially:
Formula: delay = base_seconds * (2 ^ (attempt_count - 1))
locked, unlock_time = limiter.is_account_locked(
"john.doe",
"homechoice"
)
if locked:
print(f"Account locked until: {unlock_time}")
else:
print("Account not locked")
# Unlock user account (clears failed attempts)
success = limiter.unlock_account("john.doe", "homechoice")
if success:
print("Account unlocked successfully")
# Unblock IP address
success = limiter.unblock_ip("192.168.1.100")
if success:
print("IP unblocked successfully")
# Get user's failed attempts in last 15 minutes
count = limiter.get_failed_attempts(
username="john.doe",
package="homechoice"
)
print(f"Failed attempts: {count}")
# Custom time window
count = limiter.get_failed_attempts(
username="john.doe",
package="homechoice",
window_minutes=60 # Last hour
)
# Get IP's failed attempts
count = limiter.get_ip_failed_attempts("192.168.1.100")
# All recent attempts
attempts = limiter.get_recent_attempts(limit=100)
# Filter by username
attempts = limiter.get_recent_attempts(
username="john.doe",
limit=50
)
# Filter by IP
attempts = limiter.get_recent_attempts(
ip_address="192.168.1.100",
limit=50
)
# Filter by package and username
attempts = limiter.get_recent_attempts(
username="john.doe",
package="homechoice",
limit=50
)
for attempt in attempts:
print(f"{attempt['attempted_at']}: {attempt['username']} - "
f"{'Success' if attempt['success'] else 'Failed'}")
stats = limiter.get_statistics(package="homechoice", hours=24)
print(f"Total attempts: {stats['total_attempts']}")
print(f"Successful: {stats['successful']}")
print(f"Failed: {stats['failed']}")
print(f"Unique users: {stats['unique_users']}")
print(f"Unique IPs: {stats['unique_ips']}")
# Configuration
config = stats['configuration']
print(f"User max attempts: {config['user_max_attempts']}")
print(f"Exponential backoff: {config['exponential_backoff']}")
# Delete records older than 7 days (default)
deleted = limiter.cleanup_old_attempts()
print(f"Deleted {deleted} old attempt records")
# Custom retention period
deleted = limiter.cleanup_old_attempts(older_than_days=30)
# Daily cleanup at 3 AM
0 3 * * * cd /path/to/project && python -c "from factory.core.ObjRateLimit import RateLimit; RateLimit().cleanup_old_attempts()"
| Reason | Description |
|---|---|
account_locked_for_N_minutes |
User exceeded max attempts, locked for N minutes |
rate_limited_wait_N_seconds |
Exponential backoff, wait N seconds |
ip_blocked_for_N_minutes |
IP exceeded max attempts, blocked for N minutes |
reason_messages = {
'account_locked_for_30_minutes':
"Too many failed login attempts. Your account is locked for 30 minutes.",
'rate_limited_wait_60_seconds':
"Please wait 60 seconds before trying again.",
'ip_blocked_for_15_minutes':
"Too many failed attempts from your location. Access blocked for 15 minutes."
}
if not result['success'] and result.get('rate_limited'):
user_message = reason_messages.get(
result['reason'],
"Login temporarily unavailable. Please try again later."
)
print(user_message)
Admin CLI includes rate limiting management:
# View recent failed attempts
python factory.web/ObjKeycloakAdmin.py users homechoice
# Unlock account manually
python -c "from factory.core.ObjRateLimit import RateLimit; \
RateLimit().unlock_account('john.doe', 'homechoice')"
Conservative (high security):
user_max_attempts: 3
user_window_minutes: 10
user_lockout_minutes: 60
Balanced (recommended):
user_max_attempts: 5
user_window_minutes: 15
user_lockout_minutes: 30
Lenient (better UX):
user_max_attempts: 10
user_window_minutes: 30
user_lockout_minutes: 15
Already included in schema:
CREATE INDEX idx_user_package ON sys_user_login_attempts(User, Package, AttemptedAt);
CREATE INDEX idx_ip_address ON sys_user_login_attempts(IpAddress, AttemptedAt);
CREATE INDEX idx_attempted_at ON sys_user_login_attempts(AttemptedAt);
CREATE INDEX idx_success ON sys_user_login_attempts(Success, User, AttemptedAt);
from factory.core.ObjRateLimit import RateLimit
limiter = RateLimit()
# Check lock status
locked, unlock_time = limiter.is_account_locked("john.doe", "homechoice")
print(f"Locked: {locked}")
print(f"Unlocks at: {unlock_time}")
# Manual unlock
limiter.unlock_account("john.doe", "homechoice")
# Check IP failed attempts
count = limiter.get_ip_failed_attempts("192.168.1.100")
print(f"Failed attempts from IP: {count}")
# Unblock IP
limiter.unblock_ip("192.168.1.100")
Solutions:
user_max_attempts thresholduser_window_minutes time windowuser_lockout_minutes durationfactory.web/ObjUser.py - Uses rate limiting in Login()factory.web/ObjKeycloakAdmin.py - Admin toolslocal.processing/schema/.../sys_user_login_attempts.yaml - Database schemaconfig.yaml - Configuration settings