fix(auth): surface refresh-token revocation failure instead of silently reporting success - #50
Conversation
…ly reporting success revoke_all_user_refresh_tokens() caught any Redis exception and returned 0, indistinguishable from "this user genuinely had no active refresh tokens." Its only caller, password-reset confirm, relies on it to guarantee that any refresh token an attacker might hold becomes invalid the moment a user resets their password. On a Redis hiccup, the endpoint still returned 200 "ok" while silently leaving those tokens valid -- undermining the entire point of revoking on reset. - token_store.py: return type is now int | None. None means "could not complete the revocation," which callers must not treat as "0 tokens existed." Logs now include the user_id for easier correlation. - router.py: password_reset_confirm_endpoint raises 503 (not 200) when revocation returns None. The password change itself is not rolled back (it already succeeded and reverting it would be worse UX), but the response now honestly reflects that the "revoke other sessions" guarantee couldn't be confirmed. Added tests/test_token_store_revocation.py with a fake Redis, covering all three cases directly: 0 tokens existed, N tokens revoked, and a Redis failure returning None (explicitly asserted != 0, the exact distinction that was previously lost). Verified the unaffected happy path end-to-end against a live docker-compose stack: issued a real refresh token for a test user, requested+confirmed a password reset through the actual HTTP endpoints, and confirmed both that the password changed (bcrypt verify) and that the refresh token's Redis key was actually deleted. Fixes #27
|
Warning Review limit reached
Next review available in: 23 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary
revoke_all_user_refresh_tokens()caught any Redis exception and returned0, indistinguishable from "this user genuinely had no active refresh tokens." Its only caller, password-reset confirm, relies on it to guarantee that any refresh token an attacker might hold becomes invalid the moment a user resets their password specifically to lock out unauthorized access. On a Redis hiccup, the endpoint still returned200 okwhile silently leaving those tokens valid -- undermining the entire point of revoking on reset.Changes
token_store.py:revoke_all_user_refresh_tokens()return type is nowint | None.Nonemeans "could not complete the revocation" and callers must not treat it as "0 tokens existed." Failure logs now include theuser_idfor easier correlation, aterrorlevel (already was).router.py:password_reset_confirm_endpointraises503(not200) when revocation returnsNone. The password change itself is not rolled back -- it already succeeded, and reverting it on a Redis hiccup would be worse UX for no security benefit -- but the response now honestly reflects that the "revoke other sessions" guarantee couldn't be confirmed, with a message telling the user to sign out other devices manually.Test plan
api/app/tests/test_token_store_revocation.pywith a fake Redis, covering all three cases directly: 0 tokens existed, N tokens revoked, and a Redis failure returningNone(explicitly asserted!= 0, the exact distinction that was previously lost)ruff check .,mypy .,pytest-- all clean (48/48 backend tests)consume_password_resetalso needs Redis for the reset-token lookup itself -- the unit tests are the precise verification for the failure path): issued a real refresh token for a test user, requested+confirmed a password reset through the actual HTTP endpoints, and confirmed both that the password changed (bcrypt verify) and that the refresh token's Redis key was actually deleted afterward.Fixes #27