Nobody has started this yet — be first.
Business impact
This is a distinct defense-in-depth control from the rate limiting already implemented: rate limiting counts requests in a rolling window and resets on its own regardless of outcome. Lockout should count consecutive failures and reset immediately to zero the moment a login succeeds -- so a legitimate user who fat-fingers their password twice is never penalized, while a sustained guessing attempt, even one paced slowly enough to stay under the rate limit, still gets cut off.
Problem
app/blueprints/auth/lockout.py already has three stub functions (record_failed_login, clear_failed_logins, is_locked_out) wired into login() at the right points -- call sites exist, nothing crashes -- but every function is a no-op / hardcoded False, so no account can ever be locked out.
Current behavior
After LOCKOUT_THRESHOLD consecutive failed logins for the same account, the next attempt still returns 401 for a wrong password (or succeeds for the correct one), never the expected 423 Locked.
Expected behavior
After LOCKOUT_THRESHOLD (default 10; 4 in TestingConfig) consecutive failed logins for the same account, further login attempts for that account -- even with the correct password -- return 423 Locked for LOCKOUT_DURATION_SECONDS. A single successful login immediately resets the counter to zero.
Steps to reproduce
Why this matters
The interesting design constraint here is "consecutive," not "total in a window" -- which is exactly what distinguishes this from rate limiting and is why it cannot just reuse check_rate_limit(). Redis is the natural place to keep this counter (mirroring the pattern already used for TOTP replay tracking in totp.py).
Suggested approach
Look at how totp.py uses Redis for a small piece of per-user state with a TTL (_last_used_step_key / redis_client.setex) -- the shape is similar here, just keyed by email instead of user_id, and counting failures instead of tracking a step number. record_failed_login(), clear_failed_logins(), and is_locked_out() are already called from the right places in ; implement what is inside them.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_10.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b feat/account-lockoutFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/account-lockoutSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inlogin()http://localhost:5000make seedpyotpmake docker-prod-upYou do not need Docker running at all to work the tickets -- the test suite uses an in-memory SQLite database (via StaticPool, an isolated DB per test function) and fakeredis (TestingConfig.USE_FAKE_REDIS), so everything runs against a plain virtualenv:
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
pytest practice_tickets/tests/test_ticket_01.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 15, clean pass/fail summary
All 15 tests fail at the start; as you fix each ticket in app/, its test (and only its test) should flip to passing. make test (pytest -q) runs the project's own main suite -- as of writing it shows 9 pre-existing failures caused by ticket bugs 02, 03, 07, 11, and 15; fixing those tickets fixes those failures too, not a separate task. Do not look at git diff / version control history to find the bugs if this repo is ever put under source control -- the point is to find them the way you would find a real bug: from a failing test and a description of broken behavior, not a diff.
Level 2
Implement a feature
Extend the system within its own patterns.