Conversation
📝 WalkthroughWalkthroughAdds per-user JWT revocation and session-aware authentication: introduces SessionJWTAuthentication and user.auth_revoked_at, adds revoke_all_user_tokens/is_user_global_token_revoked and updates refresh/grace logic; password/email flows now rotate and return tokens which frontend persists; migration and tests added. Changes
Sequence DiagramsequenceDiagram
actor User
participant Frontend as Frontend
participant AuthMgr as Auth Manager
participant Server as Backend API
participant DB as Database
User->>Frontend: Submit password change / email-confirm
Frontend->>Server: POST /change_password or /confirm_email
Server->>DB: Update user and set auth_revoked_at = now()
Server->>Server: Issue new JWT AuthTokens
Server-->>Frontend: 200 OK + AuthTokens
Frontend->>AuthMgr: setAuthTokens(tokens)
AuthMgr->>AuthMgr: Persist tokens (cookies/session)
rect rgba(200,100,150,0.5)
Frontend->>Server: Subsequent request with old token
Server->>DB: Fetch user and auth_revoked_at
Server->>Server: Check token.iat < auth_revoked_at?
Server-->>Frontend: 401 Unauthorized
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Organization UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used🧠 Learnings (2)📓 Common learnings📚 Learning: 2026-01-15T19:29:58.940ZApplied to files:
🧬 Code graph analysis (1)tests/unit/test_auth/test_jwt_session.py (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
🔇 Additional comments (6)
✏️ Tip: You can disable this entire section by setting 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 |
🧹 Preview Environment Cleaned UpThe preview environment for this PR has been destroyed.
Cleanup triggered by PR close at 2026-01-30T20:22:37Z |
# Conflicts: # authentication/jwt_session.py
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@authentication/jwt_session.py`:
- Around line 101-110: The function is_user_global_token_revoked (and its caller
that reads old_token_iat from refresh.get("iat")) must guard against a missing
iat to avoid TypeError; change the refresh token code path to default the iat
(e.g. old_token_iat = refresh.get("iat", 0)) or validate/convert the value
before calling is_user_global_token_revoked, and update
is_user_global_token_revoked to handle None/invalid types (coerce to int or
return True/False safely) so comparisons like token_iat < revoked_at_ts never
raise.
🧹 Nitpick comments (1)
tests/unit/test_auth/test_jwt_session.py (1)
232-242: Make the revocation timestamp test deterministic.
Wrapping the assertions infreeze_timeremoves any wall‑clock flakiness.♻️ Suggested tweak
- revoke_all_user_tokens(user) - user.refresh_from_db() - - assert user.auth_revoked_at is not None - # Should be recent (within last minute) - assert (timezone.now() - user.auth_revoked_at).total_seconds() < 60 + with freeze_time("2024-01-01 12:00:00"): + revoke_all_user_tokens(user) + user.refresh_from_db() + + assert user.auth_revoked_at is not None + # Should be recent (within last minute) + assert (timezone.now() - user.auth_revoked_at).total_seconds() < 60
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
authentication/auth.pyauthentication/jwt_session.pyauthentication/views/common.pyfront_end/src/app/(main)/accounts/change-email/route.tsfront_end/src/app/(main)/accounts/settings/actions.tsxfront_end/src/services/api/profile/profile.server.tsmetaculus_web/settings.pytests/unit/test_auth/test_jwt_session.pyusers/migrations/0018_add_auth_revoked_at.pyusers/models.pyusers/services/common.pyusers/views.py
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: hlbmtc
Repo: Metaculus/metaculus PR: 4198
File: front_end/src/app/(main)/accounts/social/[provider]/page.tsx:0-0
Timestamp: 2026-01-29T18:15:08.473Z
Learning: In the OAuth flow implementation for this codebase, CSRF protection uses token rotation (generating a new token after each use) rather than implementing separate state parameter expiration, and relies on Session cookie expiration for timeout protection.
📚 Learning: 2026-01-15T19:29:58.940Z
Learnt from: hlbmtc
Repo: Metaculus/metaculus PR: 4075
File: authentication/urls.py:24-26
Timestamp: 2026-01-15T19:29:58.940Z
Learning: In this codebase, DRF is configured to use IsAuthenticated as the default in REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] within metaculus_web/settings.py. Therefore, explicit permission_classes([IsAuthenticated]) decorators are unnecessary on DRF views unless a view needs to override the default. When reviewing Python files, verify that views relying on the default are not redundantly decorated, and flag cases where permissions are being over-specified or when a non-default permission is explicitly required.
Applied to files:
authentication/jwt_session.pyusers/migrations/0018_add_auth_revoked_at.pyusers/models.pymetaculus_web/settings.pyusers/services/common.pyauthentication/views/common.pytests/unit/test_auth/test_jwt_session.pyusers/views.pyauthentication/auth.py
📚 Learning: 2026-01-19T16:13:59.519Z
Learnt from: hlbmtc
Repo: Metaculus/metaculus PR: 4087
File: metaculus_web/settings.py:150-155
Timestamp: 2026-01-19T16:13:59.519Z
Learning: In the Metaculus codebase `metaculus_web/settings.py`, `SessionAuthentication` is intentionally listed before `JWTAuthentication` in `REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"]` because session auth is expected to be used specifically for the Django admin dashboard, while JWT handles the primary web user authentication.
Applied to files:
metaculus_web/settings.pyauthentication/auth.py
📚 Learning: 2026-01-16T20:30:29.385Z
Learnt from: hlbmtc
Repo: Metaculus/metaculus PR: 4087
File: authentication/views/social.py:47-55
Timestamp: 2026-01-16T20:30:29.385Z
Learning: In the Metaculus codebase, `SocialTokenOnlyAuthView` from the `rest_social_auth` library uses `AllowAny` permission by default, so views inheriting from it (like `SocialCodeAuth` in `authentication/views/social.py`) do not need to explicitly set `permission_classes = (AllowAny,)` for OAuth code exchange to work with unauthenticated requests.
Applied to files:
metaculus_web/settings.py
🧬 Code graph analysis (9)
front_end/src/app/(main)/accounts/settings/actions.tsx (1)
front_end/src/services/auth_tokens.ts (1)
getAuthCookieManager(146-149)
front_end/src/services/api/profile/profile.server.ts (1)
front_end/src/types/auth.ts (1)
AuthTokens(9-12)
authentication/jwt_session.py (1)
users/models.py (1)
User(21-285)
front_end/src/app/(main)/accounts/change-email/route.ts (1)
front_end/src/services/auth_tokens.ts (1)
getAuthCookieManager(146-149)
users/services/common.py (5)
authentication/serializers.py (1)
validate_password(77-80)authentication/jwt_session.py (1)
revoke_all_user_tokens(225-236)users/models.py (2)
User(21-285)UserCampaignRegistration(288-311)utils/email.py (1)
send_email_with_template(13-50)utils/frontend.py (1)
build_frontend_email_change_url(79-80)
authentication/views/common.py (1)
users/services/common.py (2)
register_user_to_campaign(156-189)change_user_password(68-77)
tests/unit/test_auth/test_jwt_session.py (2)
authentication/jwt_session.py (2)
revoke_all_user_tokens(225-236)refresh_tokens_with_grace_period(148-214)authentication/services.py (1)
get_tokens_for_user(130-139)
users/views.py (2)
users/services/common.py (1)
change_user_password(68-77)authentication/services.py (1)
get_tokens_for_user(130-139)
authentication/auth.py (2)
authentication/models.py (1)
ApiKey(8-34)authentication/jwt_session.py (1)
is_user_global_token_revoked(101-110)
🪛 Ruff (0.14.14)
authentication/jwt_session.py
[warning] 174-174: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
[warning] 174-174: Avoid specifying long messages outside the exception class
(TRY003)
[warning] 177-177: Avoid specifying long messages outside the exception class
(TRY003)
[warning] 179-179: Avoid specifying long messages outside the exception class
(TRY003)
users/migrations/0018_add_auth_revoked_at.py
[warning] 8-10: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
[warning] 12-22: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
authentication/auth.py
[warning] 29-32: Avoid specifying long messages outside the exception class
(TRY003)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Build Docker Image / Build Docker Image
- GitHub Check: integration-tests
- GitHub Check: Backend Checks
- GitHub Check: Frontend Checks
🔇 Additional comments (18)
users/models.py (1)
145-150: Looks good: user-level revocation timestamp.
Adds the needed metadata for global JWT invalidation without impacting existing users.front_end/src/services/api/profile/profile.server.ts (2)
68-72: LGTM: changePassword now returns AuthTokens.
86-89: LGTM: changeEmailConfirm now returns AuthTokens.authentication/auth.py (1)
12-34: LGTM: user-level revocation enforced during authentication.metaculus_web/settings.py (1)
149-155: LGTM: custom SessionJWTAuthentication is now the default JWT auth class.front_end/src/app/(main)/accounts/change-email/route.ts (1)
12-15: No action needed —setAuthTokensis synchronous.The method signature is
setAuthTokens(tokens: AuthTokens): void, which is synchronous and does not return a Promise. The missingawaitis not a concern.Likely an incorrect or invalid review comment.
authentication/views/common.py (2)
40-40: LGTM — centralized password helper import.This keeps the reset flow aligned with the shared password-change logic.
255-257: LGTM — password reset now uses the centralized helper.Good consolidation for validation + token revocation in one place.
users/services/common.py (3)
3-21: LGTM — imports align with new password/email flows.No issues with the expanded import surface.
68-77: LGTM — consolidated password change + revocation.Nice centralization of validation, update, and token invalidation.
136-136: LGTM — revoke tokens after email change.This matches the global revocation behavior.
users/migrations/0018_add_auth_revoked_at.py (1)
1-22: LGTM — migration adds auth_revoked_at for revocation tracking.Looks correct and consistent with the intended auth flow.
users/views.py (3)
29-36: LGTM — shared password helper import.Clean wiring for the new flow.
170-173: LGTM — password change returns fresh tokens.Consistent with the global revocation model.
206-207: LGTM — email change confirmation returns fresh tokens.Keeps clients in sync after revocation.
authentication/jwt_session.py (3)
3-7: LGTM — time utilities for revocation timestamps.Imports are appropriate for the new logic.
168-179: LGTM — active-user + global revocation checks before grace logic.This sequencing looks solid.
225-236: LGTM — user-wide revocation timestamp setter.Matches the intended “logout everywhere” semantics.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@tests/unit/test_auth/test_jwt_session.py`:
- Around line 304-329: The test
test_revoked_access_token_rejected_by_api_endpoint incorrectly asserts a 403;
when SessionJWTAuthentication (which raises
JWTAuthenticationFailed/rest_framework_simplejwt.exceptions.AuthenticationFailed)
rejects a revoked token the response should be 401, so change the assertion to
assert response.status_code == 401 (or to assert response.status_code in (401,
403) if you want to accept both behaviors); also add a brief comment near the
nested freeze_time calls clarifying that tokens are created at 12:00:03,
revoke_all_user_tokens sets auth_revoked_at to 12:00:04, and the final check at
12:00:05 verifies the token is considered revoked.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
tests/unit/test_auth/test_jwt_session.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: hlbmtc
Repo: Metaculus/metaculus PR: 4087
File: metaculus_web/settings.py:150-155
Timestamp: 2026-01-19T16:13:59.519Z
Learning: In the Metaculus codebase `metaculus_web/settings.py`, `SessionAuthentication` is intentionally listed before `JWTAuthentication` in `REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"]` because session auth is expected to be used specifically for the Django admin dashboard, while JWT handles the primary web user authentication.
📚 Learning: 2026-01-15T19:29:58.940Z
Learnt from: hlbmtc
Repo: Metaculus/metaculus PR: 4075
File: authentication/urls.py:24-26
Timestamp: 2026-01-15T19:29:58.940Z
Learning: In this codebase, DRF is configured to use IsAuthenticated as the default in REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] within metaculus_web/settings.py. Therefore, explicit permission_classes([IsAuthenticated]) decorators are unnecessary on DRF views unless a view needs to override the default. When reviewing Python files, verify that views relying on the default are not redundantly decorated, and flag cases where permissions are being over-specified or when a non-default permission is explicitly required.
Applied to files:
tests/unit/test_auth/test_jwt_session.py
🧬 Code graph analysis (1)
tests/unit/test_auth/test_jwt_session.py (2)
authentication/jwt_session.py (5)
revoke_all_user_tokens(225-236)get_session_enforce_at(50-54)_get_whitelist_key(33-35)_get_grace_key(24-26)refresh_tokens_with_grace_period(148-214)authentication/services.py (1)
get_tokens_for_user(130-139)
🔇 Additional comments (5)
tests/unit/test_auth/test_jwt_session.py (5)
7-17: LGTM on imports.The new imports properly support the user-level revocation test cases with
revoke_all_user_tokens, time utilities, and API testing capabilities.
232-242: LGTM.Good test structure with proper
refresh_from_db()call to verify the database state change.
244-260: LGTM.Clear timeline testing the core revocation behavior.
262-279: LGTM.Good test verifying that new tokens work after revocation.
281-302: LGTM.Good coverage for the multi-session revocation scenario.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@front_end/src/app/`(main)/accounts/settings/components/change_email.tsx:
- Around line 51-63: The onSubmit callback calls handleClose(true) after a
successful email change which contradicts the modal's onClose semantics; update
the success branch in the onSubmit function to call handleClose(false) instead
of handleClose(true) so the modal closes correctly (locate the onSubmit function
in change_email.tsx and replace the handleClose(true) call).
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
front_end/src/app/(main)/accounts/settings/components/change_email.tsx
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: hlbmtc
Repo: Metaculus/metaculus PR: 4087
File: metaculus_web/settings.py:150-155
Timestamp: 2026-01-19T16:13:59.519Z
Learning: In the Metaculus codebase `metaculus_web/settings.py`, `SessionAuthentication` is intentionally listed before `JWTAuthentication` in `REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"]` because session auth is expected to be used specifically for the Django admin dashboard, while JWT handles the primary web user authentication.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build Docker Image / Build Docker Image
- GitHub Check: integration-tests
🔇 Additional comments (3)
front_end/src/app/(main)/accounts/settings/components/change_email.tsx (3)
33-38: Good addition ofresetfor form cleanup.
This aligns with the new close behavior and keeps the modal state consistent.
42-49: Solid close handler cleanup.
Resetting the form and clearing submit errors on close is the right UX for this modal.
67-71: LGTM: BaseModal now closes via handleClose.
Ensures form reset + error cleanup on all close paths.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
https://www.notion.so/metaculus/2f76aaf4f1018041a6d6cdc06ea4b9f1?v=2f76aaf4f1018099baa0000cdc0a6471
Summary by CodeRabbit
New Features
Security Improvements
Frontend
Tests
✏️ Tip: You can customize this high-level summary in your review settings.