Skip to content

Invalidate auth tokens on password change and reset#5128

Draft
RishabhJain2018 wants to merge 2 commits into
masterfrom
cursor/invalidate-sessions-on-password-change-0f5c
Draft

Invalidate auth tokens on password change and reset#5128
RishabhJain2018 wants to merge 2 commits into
masterfrom
cursor/invalidate-sessions-on-password-change-0f5c

Conversation

@RishabhJain2018

@RishabhJain2018 RishabhJain2018 commented Jun 29, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the reported Failure to Invalidate Session on Password Change vulnerability.

When a user changes or resets their password, EvalAI now revokes all outstanding credentials for that account:

  • Expiring API tokens (ExpiringToken)
  • Stored JWT records (JwtToken)
  • JWT refresh tokens (OutstandingToken blacklist)

Other browsers/devices can no longer keep using old tokens after a password change. The browser that performed the change is also logged out and redirected to the login page.

Changes

  • Added revoke_all_user_tokens() helper in apps/accounts/token_revocation.py
  • Added custom PasswordChangeSerializer and PasswordResetConfirmSerializer that call the helper after saving the new password
  • Wired serializers through REST_AUTH_SERIALIZERS in settings
  • Updated frontend password-change success handlers to clear localStorage and redirect to login
  • Added backend unit tests for token revocation and both password flows

Test plan

  • pytest tests/unit/accounts/test_token_revocation.py
  • Manual: log in from two browsers, change password in one, confirm the other receives 401 on next API call
  • Manual: confirm password reset via email link also invalidates existing sessions
  • Frontend unit tests for changePwdCtrl and profileCtrl

Slack Thread

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Password changes and password reset confirmations now invalidate all active sessions and tokens, ensuring prior credentials can’t be used again.
  • Bug Fixes

    • Improved password-change flow to fully clear saved sign-in state.
    • Updated success messaging and redirected users to the login page after a password update, requiring re-authentication.
  • Tests

    • Added automated coverage for token revocation and password-change/reset credential invalidation.

Revoke ExpiringToken, JwtToken, and blacklisted JWT refresh tokens on
password change and password reset confirm so other browsers and devices
cannot keep using old credentials.

Frontend redirects to login after a successful password change and clears
stored tokens. Adds unit tests for the revocation helper and API flows.

Co-authored-by: Rishabh Jain <rishabhjain2018@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 77a4b05d-1804-4579-9f9f-b9eacc20b133

📥 Commits

Reviewing files that changed from the base of the PR and between b1bc5dc and aeac32f.

📒 Files selected for processing (1)
  • apps/accounts/serializers.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/accounts/serializers.py

Walkthrough

Adds a revoke_all_user_tokens utility that deletes expiring auth tokens, JwtToken records, and blacklists outstanding JWT tokens. Custom serializers invoke it on password change and reset confirm, and the frontend now clears storage and redirects to login after password change success.

Changes

Token Revocation on Password Change/Reset

Layer / File(s) Summary
Token revocation utility
apps/accounts/token_revocation.py
New revoke_all_user_tokens(user) deletes expiring auth tokens and JwtToken rows, then iterates OutstandingToken records to blacklist each via RefreshToken.blacklist() or a direct BlacklistedToken create on TokenError.
Custom serializers and settings wiring
apps/accounts/serializers.py, settings/common.py
CustomPasswordChangeSerializer and CustomPasswordResetConfirmSerializer override save() to call the parent then revoke_all_user_tokens. REST_AUTH_SERIALIZERS in settings is updated to reference these classes.
Frontend forced logout
frontend/src/js/controllers/changePwdCtrl.js, frontend/src/js/controllers/profileCtrl.js
Success handlers now call utilities.resetStorage(), set $rootScope.isAuth = false, and redirect to auth.login with an updated notification message.
Backend and frontend tests
tests/unit/accounts/test_token_revocation.py, frontend/tests/controllers-test/changePwdCtrl.test.js, frontend/tests/controllers-test/profileCtrl.test.js
New backend test suite verifies direct revoke_all_user_tokens calls, password change endpoint token revocation, and password reset confirm endpoint token revocation. Frontend tests assert resetStorage, isAuth=false, and auth.login redirect.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: revoking authentication tokens when passwords are changed or reset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/invalidate-sessions-on-password-change-0f5c

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/accounts/token_revocation.py (1)

22-28: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid one blacklist lookup per outstanding token.

Line 23 adds an N+1 read before the blacklist writes. Preload already-blacklisted token IDs once, then iterate only unblacklisted outstanding tokens.

As per path instructions, apps/**: “Django backend. Pay attention to: Database query efficiency (N+1 queries, missing select_related/prefetch_related)”.

♻️ Proposed query reduction
-    for outstanding_token in OutstandingToken.objects.filter(user=user):
-        if BlacklistedToken.objects.filter(token=outstanding_token).exists():
-            continue
+    blacklisted_token_ids = set(
+        BlacklistedToken.objects.filter(token__user=user).values_list(
+            "token_id", flat=True
+        )
+    )
+
+    for outstanding_token in OutstandingToken.objects.filter(
+        user=user
+    ).exclude(id__in=blacklisted_token_ids):
         try:
             RefreshToken(outstanding_token.token).blacklist()
         except TokenError:
             BlacklistedToken.objects.get_or_create(token=outstanding_token)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/accounts/token_revocation.py` around lines 22 - 28, The revocation loop
in outstanding token processing is doing an N+1 blacklist read via
BlacklistedToken.objects.filter(token=outstanding_token).exists() for every
token; update the token revocation flow in token_revocation.py to fetch the
already-blacklisted outstanding token IDs once up front, then iterate only over
unblacklisted OutstandingToken entries before calling
RefreshToken(...).blacklist() and the TokenError fallback. Keep the logic
centered around the existing OutstandingToken and BlacklistedToken queryset
usage so the loop no longer performs a per-token blacklist lookup.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/accounts/serializers.py`:
- Around line 350-352: The password update and token revocation are not atomic
in the serializer save path, so a failure in revocation can leave the new
password committed while old tokens still work. Update the relevant serializer
save methods in apps/accounts/serializers.py to wrap both
self.set_password_form.save() and revoke_all_user_tokens(self.user) in a single
transaction using the existing save method(s) as the entry point, ensuring both
operations succeed or fail together.

In `@tests/unit/accounts/test_token_revocation.py`:
- Around line 25-29: The test fixture user is being created with an unhashed
password via User.objects.create, which will break old_password validation in
the revocation flow. Update the setup in the test class to use
User.objects.create_user or create the User and call set_password before saving
so the password-change and token revocation checks operate on a valid hashed
password.

---

Nitpick comments:
In `@apps/accounts/token_revocation.py`:
- Around line 22-28: The revocation loop in outstanding token processing is
doing an N+1 blacklist read via
BlacklistedToken.objects.filter(token=outstanding_token).exists() for every
token; update the token revocation flow in token_revocation.py to fetch the
already-blacklisted outstanding token IDs once up front, then iterate only over
unblacklisted OutstandingToken entries before calling
RefreshToken(...).blacklist() and the TokenError fallback. Keep the logic
centered around the existing OutstandingToken and BlacklistedToken queryset
usage so the loop no longer performs a per-token blacklist lookup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2c399ed0-956c-4d55-8359-ac0a91af19b8

📥 Commits

Reviewing files that changed from the base of the PR and between 3257dd5 and b1bc5dc.

📒 Files selected for processing (8)
  • apps/accounts/serializers.py
  • apps/accounts/token_revocation.py
  • frontend/src/js/controllers/changePwdCtrl.js
  • frontend/src/js/controllers/profileCtrl.js
  • frontend/tests/controllers-test/changePwdCtrl.test.js
  • frontend/tests/controllers-test/profileCtrl.test.js
  • settings/common.py
  • tests/unit/accounts/test_token_revocation.py

Comment on lines +350 to +352
def save(self):
self.set_password_form.save()
revoke_all_user_tokens(self.user)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether request-level transactions already cover these password endpoints.
rg -n --glob 'settings/**' 'ATOMIC_REQUESTS|DATABASES'
rg -n --glob 'apps/**/*.py' 'transaction\.atomic|revoke_all_user_tokens|CustomPassword(Change|ResetConfirm)Serializer'

Repository: Cloud-CV/EvalAI

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files | rg '(^|/)(apps/accounts/serializers\.py|.*settings.*\.py|.*settings/.*\.py|.*database.*\.py)$'

printf '\n== serializers outline ==\n'
ast-grep outline apps/accounts/serializers.py --view expanded || true

printf '\n== relevant settings hits ==\n'
rg -n 'ATOMIC_REQUESTS|DATABASES|AUTOCOMMIT|atomic\(' -g '*.py' .

printf '\n== revocation helper hits ==\n'
rg -n 'def revoke_all_user_tokens|revoke_all_user_tokens\(' -g '*.py' apps .

Repository: Cloud-CV/EvalAI

Length of output: 4480


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== serializers section ==\n'
sed -n '340,370p' apps/accounts/serializers.py

printf '\n== token revocation helper ==\n'
sed -n '1,220p' apps/accounts/token_revocation.py

printf '\n== settings common ==\n'
sed -n '1,220p' settings/common.py

printf '\n== settings env files atomics ==\n'
for f in settings/dev.py settings/prod.py settings/staging.py settings/test.py; do
  printf '\n--- %s ---\n' "$f"
  rg -n 'ATOMIC_REQUESTS|DATABASES|AUTOCOMMIT|transaction' "$f" || true
done

Repository: Cloud-CV/EvalAI

Length of output: 8491


Wrap password save/revocation in a transaction.
apps/accounts/serializers.py:350-362 Both serializers commit the new password before revoke_all_user_tokens(). If any revocation write fails, the password change can persist while old credentials remain usable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/accounts/serializers.py` around lines 350 - 352, The password update and
token revocation are not atomic in the serializer save path, so a failure in
revocation can leave the new password committed while old tokens still work.
Update the relevant serializer save methods in apps/accounts/serializers.py to
wrap both self.set_password_form.save() and revoke_all_user_tokens(self.user) in
a single transaction using the existing save method(s) as the entry point,
ensuring both operations succeed or fail together.

Comment on lines +25 to +29
self.user = User.objects.create(
username="someuser",
email="user@test.com",
password="secret_password",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant test file and nearby context
git ls-files tests/unit/accounts/test_token_revocation.py
wc -l tests/unit/accounts/test_token_revocation.py
sed -n '1,220p' tests/unit/accounts/test_token_revocation.py

# Find the user model / auth helpers used in this area
rg -n "create_user|set_password|check_password|old_password|token revocation|revocation" tests src . -g '!**/node_modules/**'

Repository: Cloud-CV/EvalAI

Length of output: 21870


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the password-change / reset serializers and any helper they use.
sed -n '300,390p' apps/accounts/serializers.py

# Inspect the token revocation helper for the side effects the tests assert.
sed -n '1,220p' apps/accounts/token_revocation.py

# Inspect the model used to store JWT tokens.
sed -n '1,220p' apps/accounts/models.py

Repository: Cloud-CV/EvalAI

Length of output: 7547


Create the fixture user with a hashed password. User.objects.create(...) leaves password unusable for old_password checks, so these password-change tests will fail before the revocation assertions run. Use create_user(...) or call set_password().

🧰 Tools
🪛 Ruff (0.15.18)

[error] 28-28: Possible hardcoded password assigned to argument: "password"

(S106)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/accounts/test_token_revocation.py` around lines 25 - 29, The test
fixture user is being created with an unhashed password via User.objects.create,
which will break old_password validation in the revocation flow. Update the
setup in the test class to use User.objects.create_user or create the User and
call set_password before saving so the password-change and token revocation
checks operate on a valid hashed password.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants