Skip to content

Security improvements for email tokens#4202

Merged
hlbmtc merged 5 commits intomainfrom
feat/email-token-improvements
Feb 6, 2026
Merged

Security improvements for email tokens#4202
hlbmtc merged 5 commits intomainfrom
feat/email-token-improvements

Conversation

@hlbmtc
Copy link
Contributor

@hlbmtc hlbmtc commented Jan 30, 2026

https://www.notion.so/metaculus/Expire-Password-Reset-Links-on-Email-Change-f1613821497f4e33bbbe503b9a059dad?v=2f76aaf4f1018099baa0000cdc0a6471&source=copy_link

Summary by CodeRabbit

  • Tests

    • Added comprehensive unit tests for token flows: email-change and password-reset success, expiry/invalid/mismatch cases, multi-token and cross-token invalidation, and login-triggered invalidation.
  • Refactor

    • Redesigned email-change token generation/validation for more robust, signed tokens and unified error handling.
  • New Features

    • Email-change confirmation flow moved to client-side with a dedicated confirmation page, action, layout, and success toast that refreshes auth state and redirects to settings.
  • Localization

    • Added localized strings for email-change success, error, and "Back to Settings" in multiple languages.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 30, 2026

📝 Walkthrough

Walkthrough

Refactors email-change token generation to use a signed payload via a new EmailChangeTokenGenerator (subclassing PasswordResetTokenGenerator) and adds comprehensive unit tests plus frontend email-change confirmation UI and translations.

Changes

Cohort / File(s) Summary
Backend tests
tests/unit/test_auth/test_token_invalidation.py
Adds comprehensive unit tests for email-change and password-reset token lifecycles, expiration, malformed tokens, user/token mismatches, email-in-use races, multi-token and cross-token invalidation, and login-driven invalidation.
Token generation & validation
users/services/common.py
EmailChangeTokenGenerator now subclasses PasswordResetTokenGenerator; adds key_salt, make_token(user,new_email), and check_token(user,token,max_age) using TimestampSigner and embedded Django token; generate_email_change_token and change_email_from_token updated to use these methods and raise ValidationError on invalid/expired tokens.
Frontend: email-change confirmation flow
front_end/src/app/(main)/accounts/change-email/actions.ts, .../client.tsx, .../layout.tsx, .../page.tsx, .../route.ts (removed)
Moves server-side confirm flow into client actions/components: adds confirmEmailChange, ChangeEmailClient, ChangeEmailLayout, and ChangeEmailPage; removes the previous Next.js route handler route.ts.
Frontend: settings UI
front_end/src/app/(main)/accounts/settings/account/components/email_change_toast.tsx, .../page.tsx
Adds EmailChangeToast to show success toast when query emailChanged=true and integrates it into the account settings page.
i18n
front_end/messages/en.json, .../cs.json, .../es.json, .../pt.json, .../zh-TW.json, .../zh.json
Adds three translation keys: emailChangeErrorMessage, emailChangeSuccessMessage, and backToSettings across multiple locales.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client
participant Frontend as FrontendApp
participant Server as ProfileAPI
participant Signer as TimestampSigner
participant DjangoToken as PasswordResetTokenGenerator
participant DB as UserStore

Client->>Frontend: open /change-email?token=...
Frontend->>Server: confirmEmailChange(token)
Server->>Signer: unsign token => {user_id, new_email, validation_token}
Signer-->>Server: payload
Server->>DB: fetch user by id
DB-->>Server: user
Server->>DjangoToken: check_token(user, validation_token)
DjangoToken-->>Server: valid / invalid
alt valid
    Server->>DB: update user.email to new_email
    DB-->>Server: updated user
    Server-->>Frontend: success + new auth tokens
    Frontend->>Client: navigate to settings?emailChanged=true
else invalid
    Server-->>Frontend: error (ValidationError)
    Frontend->>Client: show error UI
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Global tokens invalidation #4199 — Modifies users/services/common.py and touches email-change/password-reset token handling, directly related to the refactor here.
  • Auth Integration V1 #4087 — Changes authentication/token flows and frontend auth manager usage; related to token/refresh behavior introduced here.

Suggested reviewers

  • cemreinanc
  • lsabor
  • elisescu

Poem

🐇 I signed a note with carrot ink,
A token hop, a tiny wink.
Emails move and passwords sleep,
Tests chase tokens in a joyful leap.
🥕 Hooray for code that hops so quick!

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Security improvements for email tokens' is directly related to the main changes, which implement enhanced token validation, expiration handling, and cross-invalidation logic for email change and password reset tokens.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/email-token-improvements

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 and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 30, 2026

🧹 Preview Environment Cleaned Up

The preview environment for this PR has been destroyed.

Resource Status
🌐 Preview App ✅ Deleted
🗄️ PostgreSQL Branch ✅ Deleted
⚡ Redis Database ✅ Deleted
🔧 GitHub Deployments ✅ Removed
📦 Docker Image ⚠️ Retained (auto-cleanup via GHCR policies)

Cleanup triggered by PR close at 2026-02-06T13:16:29Z

@hlbmtc hlbmtc marked this pull request as draft January 30, 2026 15:51
# Conflicts:
#	users/services/common.py
@hlbmtc hlbmtc marked this pull request as ready for review February 2, 2026 17:51
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@front_end/src/app/`(main)/accounts/change-email/layout.tsx:
- Around line 15-18: The UI currently renders raw error.message in layout.tsx
which can leak internals and isn't localized; replace the visible text with a
localized, generic message via the i18n function t (e.g.,
t("emailChangeErrorDetails") or t("unknownError")) and stop outputting
error.message directly in the <p> element, and if you still need the raw error
for debugging, log it conditionally (only in development) using
console.debug/console.error when NODE_ENV === "development" or similar,
referencing the same error variable and the t function to locate where to
change.

In `@front_end/src/app/`(main)/accounts/change-email/page.tsx:
- Around line 5-13: In ChangeEmailPage, avoid throwing during server render:
normalize props.searchParams.token (which may be string | string[] | undefined)
by resolving props.searchParams, then coerce token to a single string (e.g., if
Array take the first element) or null/undefined if absent, remove the throw new
Error("Missing token parameter"), and pass the normalized token down to the
client component so the client-side component can handle missing/invalid tokens
gracefully; update references in ChangeEmailPage to use the normalizedToken
variable (or similar) instead of token.
🧹 Nitpick comments (1)
front_end/src/app/(main)/accounts/change-email/client.tsx (1)

18-22: Use router.replace() instead of router.push() to prevent back-button returns to the token URL.

replace() replaces the current history entry rather than adding a new one, which improves UX by preventing the user from navigating back to this intermediate confirmation page. Note: while replace() avoids a history entry, it does not prevent the token from being exposed via server logs, caches, or referrer headers. For stronger security, consider consuming the token server-side and redirecting to a clean URL.

Suggested change
-      .then(() => router.push("/accounts/settings/account?emailChanged=true"))
+      .then(() => router.replace("/accounts/settings/account?emailChanged=true"))

@hlbmtc hlbmtc merged commit a421c22 into main Feb 6, 2026
23 of 24 checks passed
@hlbmtc hlbmtc deleted the feat/email-token-improvements branch February 6, 2026 13:16
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