Account Lockout Mechanism
Problem Description
The OAuth2 system implements an account lockout mechanism to defend against brute-force attacks. After repeated failed logins, an account is temporarily locked.
Lockout Rules
Per the implementation in AuthService.cc, the lockout rules are:
| Failed attempts | Lockout duration |
|---|---|
| 5-9 | 1 minute |
| 10-14 | 5 minutes |
| 15-19 | 30 minutes |
| 20+ | 1 hour |
Common Scenarios
Scenario 1: lockout caused by repeated test-script runs
Symptoms:
- The first run of the test script succeeds
- The second run fails all tests
- Backend logs show:
Account locked for user: admin until 1779441748
Cause: A test case in the script failed to log in (e.g. wrong credentials), accumulating failed attempts up to the threshold.
Solution: The test script now automatically resets the account lockout state at the end. If the problem persists, reset manually.
Manually Resetting Account Lockout
Method 1: use the reset script (recommended)
Local PostgreSQL database
# 默认使用config.json中的配置(fulla_user/fulla_db/123456)
.\scripts\backend\reset-account-lockout.ps1
# 重置特定用户
.\scripts\backend\reset-account-lockout.ps1 -Username admin
# 自定义数据库连接
.\scripts\backend\reset-account-lockout.ps1 -DbHost localhost -DbUser fulla_user -DbPassword 123456
Docker database
# 脚本会自动检测Docker容器
.\scripts\backend\reset-account-lockout.ps1
# 重置特定用户
.\scripts\backend\reset-account-lockout.ps1 -Username admin
Method 2: reset the admin password
If the admin password was changed accidentally, or login fails after the upgrade to PBKDF2, use this script to reset it to the default password:
# 重置admin密码为默认值 'admin'
.\scripts\backend\reset-admin-password.ps1
Note: this script resets the admin password to the default in SHA-256 form (for development environments). On first login, the system automatically upgrades it to PBKDF2.
Method 3: direct SQL
Local PostgreSQL
# Windows PowerShell - 重置锁定状态
$env:PGPASSWORD = "123456"
psql -U fulla_user -d fulla_db -h localhost -c "UPDATE users SET failed_login_count = 0, locked_until = 0 WHERE username='admin';"
$env:PGPASSWORD = $null
# 如果密码也需要重置(重置为默认密码 'admin')
$env:PGPASSWORD = "123456"
psql -U fulla_user -d fulla_db -h localhost -c "UPDATE users SET password_hash = '892738161086b314334f88d661aa6e7bab7c825c34bf55222811dad46cdbf724', salt = 'admin_salt', failed_login_count = 0, locked_until = 0 WHERE username = 'admin';"
$env:PGPASSWORD = $null
# Linux/Mac - 重置锁定状态
PGPASSWORD=123456 psql -U fulla_user -d fulla_db -h localhost -c "UPDATE users SET failed_login_count = 0, locked_until = 0 WHERE username='admin';"
# 如果密码也需要重置
PGPASSWORD=123456 psql -U fulla_user -d fulla_db -h localhost -c "UPDATE users SET password_hash = '892738161086b314334f88d661aa6e7bab7c825c34bf55222811dad46cdbf724', salt = 'admin_salt', failed_login_count = 0, locked_until = 0 WHERE username = 'admin';"
Docker database
# 重置锁定状态
docker exec <container_name> psql -U fulla_user -d fulla_db -c "UPDATE users SET failed_login_count = 0, locked_until = 0 WHERE username='admin';"
# 如果密码也需要重置
docker exec <container_name> psql -U fulla_user -d fulla_db -c "UPDATE users SET password_hash = '892738161086b314334f88d661aa6e7bab7c825c34bf55222811dad46cdbf724', salt = 'admin_salt', failed_login_count = 0, locked_until = 0 WHERE username = 'admin';"
Method 4: inspect lockout state
-- 查看所有用户的锁定状态
SELECT
username,
failed_login_count,
locked_until,
CASE
WHEN locked_until > EXTRACT(EPOCH FROM NOW()) THEN 'LOCKED'
ELSE 'UNLOCKED'
END as status,
CASE
WHEN locked_until > EXTRACT(EPOCH FROM NOW())
THEN TO_TIMESTAMP(locked_until) - NOW()
ELSE INTERVAL '0'
END as remaining_time
FROM users
ORDER BY username;
Test Script Auto-Cleanup
test-admin-endpoints.ps1 already resets the admin account's lockout state when tests finish:
# 测试脚本会在结束时执行:
# 1. 尝试连接Docker容器
# 2. 如果没有Docker,尝试连接本地PostgreSQL
# 3. 重置admin账号的 failed_login_count 和 locked_until
Note: when using a local PostgreSQL, configure the database password in the script:
# 编辑 test-admin-endpoints.ps1,找到这一行:
$env:PGPASSWORD = "your_password" # 修改为你的数据库密码
Preventive Measures
1. Use a dedicated account for testing
Do not use the production admin account in tests. Create a dedicated test account:
INSERT INTO users (username, password_hash, salt, email, email_verified)
INSERT INTO user_roles (user_id, role_id)
SELECT u.id, r.id FROM users u, roles r
WHERE u.username = 'test_admin' AND r.name = 'admin';
2. Automatic cleanup after tests
Add cleanup code at the end of every test script:
# Cleanup
try {
# 重置测试账号
psql -U fulla_user -d fulla_db -h localhost -c "UPDATE users SET failed_login_count = 0, locked_until = 0 WHERE username='test_admin';"
} catch {
Write-Host "Warning: Failed to reset test account" -ForegroundColor Yellow
}
3. Use correct credentials
Make sure the usernames and passwords used in test scripts match the database:
# 检查数据库中的用户
psql -U fulla_user -d fulla_db -h localhost -c "SELECT username FROM users;"
# 如果需要重置密码(使用PBKDF2)
# 需要通过应用程序的注册接口或直接调用PasswordHasher
Production Recommendations
1. Monitor lockout events
Monitor account lockout events in production:
-- 查找最近被锁定的账号
SELECT
username,
failed_login_count,
TO_TIMESTAMP(locked_until) as locked_until_time,
TO_TIMESTAMP(last_failed_login) as last_failed_time
FROM users
WHERE locked_until > EXTRACT(EPOCH FROM NOW())
ORDER BY locked_until DESC;
2. Set up alerts
Raise an alert when critical accounts (such as admin) get locked:
-- 可以通过定时任务检查
SELECT COUNT(*) FROM users
WHERE username IN ('admin', 'superuser')
AND locked_until > EXTRACT(EPOCH FROM NOW());
3. Audit logs
The backend logs every lockout event:
WARN Account locked for user: admin until 1779441748
INFO [METRIC] oauth2_login_failures_total reason=bad_credentials
Consider shipping these logs to a centralized logging system (e.g. ELK, Grafana Loki) for analysis.
Security Considerations
- Do not disable the lockout mechanism: it is an important defense against brute-force attacks
- Do not hardcode database passwords in code: use environment variables or a secret management system
- Restrict reset permissions: only administrators should be able to reset account lockout state
- Record reset operations: in production, every reset operation should be audited
Related Files
libs/drogon/src/AuthService.cc- account lockout logic implementationscripts/backend/test-admin-endpoints.ps1- test script (with auto-cleanup)scripts/backend/reset-account-lockout.ps1- manual reset script- Database table:
users(columns:failed_login_count,locked_until,last_failed_login)