fix(auth): preserve phase field in _TokenData so reset-password / change-email phase-bound checks don't 400 (#36116)#36117
Open
vuko wants to merge 1 commit into
Open
Conversation
…don't 400 `POST /console/api/forgot-password/resets` (and the equivalent change-email flows) always return 400 `invalid_or_expired_token` in 1.14.0 / 1.14.1 / main because the `phase` field stored on the token is silently dropped on read. Two PRs in 1.14.0 interact: - langgenius#35425 (`fix(auth): enforce phase-bound change-email token flow`, GHSA-4q3w-q5mc-45rq) added a `phase` field to reset / change-email tokens and a `if data.get("phase", "") != "reset": raise InvalidTokenError()` gate in `controllers/console/auth/forgot_password.py`. - langgenius#34380 (`refactor(api): replace json.loads with Pydantic validation`) replaced `json.loads(...)` in `TokenManager.get_token_data` with a `_TokenData` `TypeAdapter`. The TypedDict listed `account_id`, `email`, `token_type`, `code`, `old_email` — but not `phase` — so the adapter silently strips it. Net effect: the security gate from langgenius#35425 always sees `phase == ""` and the reset flow is 100% broken. Repro: curl -X POST https://<host>/console/api/forgot-password/resets \ -H 'Content-Type: application/json' \ -d '{"token":"<fresh-token>","new_password":"Aa12345678","password_confirm":"Aa12345678"}' # => 400 {"code":"invalid_or_expired_token", ...} even when the token is still in redis with `phase: "reset"`. Fix: add `phase: str` to `_TokenData`. Since the TypedDict is declared `total=False`, every field is already optional — no callers need to change. Tests: `api/tests/unit_tests/libs/test_token_manager.py` asserts the TypeAdapter round-trips `phase` for both `reset_password` and `change_email` payloads, so this regression can't recur. Signed-off-by: vuko <alexander.vukovic@seqis.com>
Contributor
Pyrefly Type Coverage
|
5 tasks
hualong1009
added a commit
to hualong1009/dify
that referenced
this pull request
May 14, 2026
This commit addresses the same root cause as PR langgenius#36117 (langgenius#36117), where the 'phase' field was missing in the email registration token, causing token validation to fail for new user registration. In addition to the changes in PR langgenius#36117, this patch ensures the 'phase' field is always included when generating the email registration token, fully resolving the issue for both password reset and new user registration flows.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #36116
Summary
POST /console/api/forgot-password/resets(and equivalent change-email flows) always return400 invalid_or_expired_tokenon1.14.0,1.14.1, and currentmain— even with a fresh, valid token that's still in redis. Two PRs that shipped together in1.14.0interact badly:fix(auth): enforce phase-bound change-email token flow, GHSA-4q3w-q5mc-45rq) added aphasefield to reset / change-email tokens and aif data.get("phase", "") != "reset"gate incontrollers/console/auth/forgot_password.py:174.refactor(api): replace json.loads with Pydantic validation in security and tools layers) replacedjson.loads(...)inTokenManager.get_token_data(libs/helper.py:473) withdict(_token_data_adapter.validate_json(...)). The_TokenDataTypedDict onlibs/helper.py:36listsaccount_id, email, token_type, code, old_email— but notphase, so the TypeAdapter silently strips it.Net effect: the gate from #35425 always sees
phase == ""and the reset flow is 100% broken. Issue #36116 has the full repro.This PR adds
phase: strto_TokenData. Since the TypedDict is declaredtotal=False, every field is already optional — no callers need to change.I also added
api/tests/unit_tests/libs/test_token_manager.pywhich round-trips both thereset_passwordandchange_emailpayloads through the_token_data_adapter. Without the one-line fix, both tests fail with the sameKeyError/Nonesymptom seen in production; with the fix, both pass.Verified end-to-end on a
1.14.0deployment after hot-patching the same line:/forgot-password/resetsreturns200 {"result":"success"}and the password is updated.Screenshots
N/A — backend-only change. The user-visible symptom is the 400 alert on the password-reset form; after this fix the form succeeds and routes to the sign-in page as before
1.14.0.Checklist
invalid_or_expired_tokenin 1.14.x —_TokenDataTypedDict strips thephasefield #36116 was filed first; this PR links to it.api/tests/unit_tests/libs/test_token_manager.py) and tried to keep it a single atomic change.ruff check+ruff format --checkon the changed files locally (usingruff 0.15.12, matchingapi/pyproject.toml's pin); both clean.From Claude Code