Skip to content

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
langgenius:mainfrom
vuko:fix/token-data-phase-stripped
Open

fix(auth): preserve phase field in _TokenData so reset-password / change-email phase-bound checks don't 400 (#36116)#36117
vuko wants to merge 1 commit into
langgenius:mainfrom
vuko:fix/token-data-phase-stripped

Conversation

@vuko
Copy link
Copy Markdown

@vuko vuko commented May 13, 2026

Fixes #36116

Summary

POST /console/api/forgot-password/resets (and equivalent change-email flows) always return 400 invalid_or_expired_token on 1.14.0, 1.14.1, and current main — even with a fresh, valid token that's still in redis. Two PRs that shipped together in 1.14.0 interact badly:

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: str to _TokenData. Since the TypedDict is declared total=False, every field is already optional — no callers need to change.

I also added api/tests/unit_tests/libs/test_token_manager.py which round-trips both the reset_password and change_email payloads through the _token_data_adapter. Without the one-line fix, both tests fail with the same KeyError/None symptom seen in production; with the fix, both pass.

Verified end-to-end on a 1.14.0 deployment after hot-patching the same line: /forgot-password/resets returns 200 {"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

  • This change requires a documentation update — N/A, internal contract.
  • I understand that this PR may be closed in case there was no previous discussion or issues. Issue Password reset always returns 400 invalid_or_expired_token in 1.14.x — _TokenData TypedDict strips the phase field #36116 was filed first; this PR links to it.
  • I've added a test for each change that was introduced (api/tests/unit_tests/libs/test_token_manager.py) and tried to keep it a single atomic change.
  • I've updated the documentation accordingly. (No docs touched the field list.)
  • I ran ruff check + ruff format --check on the changed files locally (using ruff 0.15.12, matching api/pyproject.toml's pin); both clean.

From Claude Code

…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>
@dosubot dosubot Bot added the size:XS This PR changes 0-9 lines, ignoring generated files. label May 13, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Pyrefly Type Coverage

Metric Base PR Delta
Type coverage 0.00% 43.70% +43.70%
Strict coverage 0.00% 43.23% +43.23%
Typed symbols 0 22,023 +22,023
Untyped symbols 0 28,684 +28,684
Modules 0 2552 +2,552

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XS This PR changes 0-9 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Password reset always returns 400 invalid_or_expired_token in 1.14.x — _TokenData TypedDict strips the phase field

1 participant