dify - ✅(Solved) Fix Change-email verification rejects valid tokens because shared token decoding trims change-email state [1 pull requests, 2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
langgenius/dify#36411Fetched 2026-05-20 04:00:07
View on GitHub
Comments
2
Participants
3
Timeline
6
Reactions
1
Assignees
Timeline (top)
commented ×2assigned ×1cross-referenced ×1labeled ×1

The change-email verification flow can reject fresh, valid tokens with:

The token is invalid or has expired.

The failing request is POST /console/api/account/change-email/validity.

Root Cause

This is a follow-up design bug in the phase-bound token flow introduced by PR #35425.

PR #35425 correctly introduced a dedicated change-email state machine:

old_email -> old_email_verified -> new_email -> new_email_verified

However, the shared TokenManager.get_token_data() path was still validating Redis payloads through a partial _TokenData schema that trimmed undeclared fields. That made business-specific auth metadata vulnerable to being silently dropped during the Redis -> Python round-trip.

PR #36117 fixed the adjacent generic phase field for forgot-password / email-register, but change-email does not actually depend on that key. The change-email flow depends on its dedicated email_change_phase state and, more broadly, on the ability to preserve business-specific token metadata without the storage layer stripping fields.

The old flow also relied on loose dict/string checks and did not keep the stable account_id binding explicit across every change-email transition.

Fix Action

Fixed

PR fix notes

PR #36412: refactor(auth): type and bind the change-email token state machine

Description (problem / solution / changelog)

AI disclosure: This PR was primarily drafted with Codex using GPT-5.4. I have reviewed and edited the final diff, and I am responsible for the content.

Summary

  • keep TokenManager on a shared _TokenData + TypeAdapter baseline, but allow and preserve undeclared extra fields instead of trimming them
  • model the change-email token state machine with Pydantic discriminated unions at the AccountService boundary
  • bind account_id through the entire change-email flow and validate it at every transition
  • expand regression coverage to include shared token-adapter behavior, typed change-email parsing, and account-bound controller transitions

Root cause

PR #35425 introduced the right security model for change-email, but the runtime contract underneath it was still too weak.

The change-email flow depends on a dedicated token state machine:

old_email -> old_email_verified -> new_email -> new_email_verified

However, the shared TokenManager.get_token_data() path was still decoding Redis payloads through a partial _TokenData schema that trimmed undeclared fields. That meant business-specific auth metadata could be silently dropped on readback.

PR #36117 fixed the adjacent generic phase field for forgot-password / email-register, but change-email does not actually use that field at runtime. Change-email needs its own state discriminator plus a stable account_id binding across all transitions.

This PR addresses the problem at the right boundary:

  • TokenManager still performs shared baseline validation, but _TokenData now preserves undeclared extra fields instead of stripping them
  • change-email converts the raw token dict into typed Pydantic models at the AccountService boundary
  • controller transitions now operate on concrete token types and reject tokens whose account_id does not match the logged-in account

Testing

  • uv run --project api pytest -o addopts='' api/tests/unit_tests/libs/test_token_manager.py api/tests/unit_tests/controllers/console/test_workspace_account.py -q
  • uv run --project api python -m py_compile api/libs/helper.py api/tests/unit_tests/libs/test_token_manager.py api/services/entities/auth_entities.py api/services/account_service.py api/controllers/console/workspace/account.py api/tests/unit_tests/controllers/console/test_workspace_account.py

Fixes #36411

Linear: ENG-426

Changed files

  • api/controllers/console/workspace/account.py (modified, +30/-50)
  • api/libs/helper.py (modified, +19/-4)
  • api/services/account_service.py (modified, +51/-32)
  • api/services/entities/auth_entities.py (modified, +139/-1)
  • api/tests/unit_tests/controllers/console/test_workspace_account.py (modified, +293/-82)
  • api/tests/unit_tests/libs/test_token_manager.py (modified, +123/-43)

Code Example

The token is invalid or has expired.

---

old_email -> old_email_verified -> new_email -> new_email_verified
RAW_BUFFERClick to expand / collapse

AI disclosure: This issue was drafted and analyzed with Codex using GPT-5.4. I have reviewed the analysis, and I am responsible for the content.

Summary

The change-email verification flow can reject fresh, valid tokens with:

The token is invalid or has expired.

The failing request is POST /console/api/account/change-email/validity.

Reproduction

  1. Open the account settings page.
  2. Start the change-email flow.
  3. Enter the verification code sent to the current email address.
  4. Submit the code.

Actual result

The backend returns 400 invalid_or_expired_token even though the code and token are still fresh.

Expected result

A valid code should advance the flow and return the refreshed token for the next phase.

Root cause

This is a follow-up design bug in the phase-bound token flow introduced by PR #35425.

PR #35425 correctly introduced a dedicated change-email state machine:

old_email -> old_email_verified -> new_email -> new_email_verified

However, the shared TokenManager.get_token_data() path was still validating Redis payloads through a partial _TokenData schema that trimmed undeclared fields. That made business-specific auth metadata vulnerable to being silently dropped during the Redis -> Python round-trip.

PR #36117 fixed the adjacent generic phase field for forgot-password / email-register, but change-email does not actually depend on that key. The change-email flow depends on its dedicated email_change_phase state and, more broadly, on the ability to preserve business-specific token metadata without the storage layer stripping fields.

The old flow also relied on loose dict/string checks and did not keep the stable account_id binding explicit across every change-email transition.

Proposed fix

  • keep TokenManager on a shared _TokenData + TypeAdapter baseline, but configure it to allow and preserve undeclared extra fields
  • model change-email tokens at the AccountService boundary with Pydantic discriminated union types
  • bind account_id through the entire change-email flow and validate it at every transition
  • add regression tests for both the shared token adapter behavior and the typed change-email state machine

Notes

  • This issue is about the protected happy path failing after #35425; it does not reintroduce the original bypass from GHSA-4q3w-q5mc-45rq.
  • Reported internally in Linear as ENG-426.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

dify - ✅(Solved) Fix Change-email verification rejects valid tokens because shared token decoding trims change-email state [1 pull requests, 2 comments, 3 participants]