Nobody has started this yet — be first.
Business impact
This is the single most important security property in the entire service: if a refresh token is ever stolen, the system's only way of noticing is that the legitimate client eventually presents the token too, after the thief has already used (and rotated) it. That replay is supposed to be treated as a full compromise signal and kill every token in the session family, forcing a real re-login. Right now it does not: an attacker who steals a refresh token and rotates it once keeps a fully working, never-flagged session indefinitely -- the exact scenario this whole project's headline feature was built to prevent is currently silently disabled.
Problem
rotate_refresh_token() checks row.revoked_at is not None before row.replaced_by is not None. A rotated token always has both set together (written two lines apart, immediately before the same commit, in the rotation code path itself), so for any already-rotated token the revoked_at check is always true first and the function returns before ever reaching the replaced_by check -- meaning revoke_family() is never called on a replay.
Current behavior
Replaying an already-rotated refresh token (A, rotated into B) still returns 401 as expected, but token B -- which legitimately superseded A and was never itself reused -- remains valid afterward, when it should have been killed along with the rest of the family.
Expected behavior
Presenting an already-rotated refresh token again revokes every token in its family -- including the one that legitimately superseded it, which would otherwise still be perfectly valid.
Steps to reproduce
Why this matters
This is deliberately the hardest ticket in the set because the regression is invisible from the outside unless you specifically think to check the successor token, not just the replayed one. Every other observable signal -- the replay attempt itself getting rejected, every other 2FA/session/password-reset flow, every other existing test -- stays green. It is a strong example of how a refactor-looking, "just reordered two checks" change can silently disable a security control while every test that only checks "was the bad request rejected" keeps passing.
Suggested approach
Work out what row.revoked_at is not None and row.replaced_by is not None each mean independently -- is there a RefreshToken row where one is true and the other is false? Under what circumstance? Which of the two checks needs to run first for the revoke_family() branch to ever be reachable for a rotated token specifically (as opposed to a token revoked some other way, e.g. via DELETE /auth/sessions/<id>)?
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_15.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 fix/reuse-detection-check-orderFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/reuse-detection-check-orderSubmit 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 inhttp://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 1
Fix a bug
Read existing behaviour, correct it.