langgenius/dify · error · InvalidTokenError
invalid_or_expired_token
invalid_or_expired_token
Error message
The token is invalid or has expired.
What it means
Raised by InvalidTokenError in ForgotPasswordCheckApi.post when AccountService.get_reset_password_data(req_data.token) returns None. The reset token is the handle embedded in the reset email link; None means it is unknown, expired, malformed, or already revoked. Symmetric to error 402 but for the password-reset flow.
Source
Thrown at api/controllers/console/auth/forgot_password.py:120
200,
"Code verified successfully",
console_ns.models[ForgotPasswordCheckResponse.__name__],
)
@console_ns.response(400, "Invalid code or token")
@setup_required
@email_password_login_enabled
@model_validate(ForgotPasswordCheckPayload)
def post(self, req_data: ForgotPasswordCheckPayload):
user_email = req_data.email.lower()
is_forgot_password_error_rate_limit = AccountService.is_forgot_password_error_rate_limit(user_email)
if is_forgot_password_error_rate_limit:
raise EmailPasswordResetLimitError()
token_data = AccountService.get_reset_password_data(req_data.token)
if token_data is None:
raise InvalidTokenError()
token_email = token_data.get("email")
if not isinstance(token_email, str):
raise InvalidEmailError()
normalized_token_email = token_email.lower()
if user_email != normalized_token_email:
raise InvalidEmailError()
if req_data.code != token_data.get("code"):
AccountService.add_forgot_password_error_rate_limit(user_email)
raise EmailCodeError()
# Verified, revoke the first token
AccountService.revoke_reset_password_token(req_data.token)
# Refresh token data by generating a new token
_, new_token = AccountService.generate_reset_password_token(View on GitHub (pinned to ef8544b173)
Solutions
- Request a new reset email via /forgot-password/email-send and use the fresh token.
- Ensure the full reset link is copied without truncation (watch for line-wrap in some email clients).
- Confirm the client targets the same Dify instance that sent the email.
- Verify reset-token TTL is configured long enough for email delivery + user read time.
Example fix
// before
checkValidity({ email, token: oldLinkToken, code });
// after
if (res.code === 'invalid_or_expired_token') {
const s = await sendReset({ email });
// user clicks new link, then call checkValidity with the new token
} Defensive patterns
Strategy: try-catch
Validate before calling
// Sanity check token shape before calling
if (!token || token.split('.').length < 2) {
await restartResetFlow(email);
return;
} Type guard
function isPlausibleResetToken(t) { return typeof t === 'string' && t.length > 16 && t.includes('.'); } Try / catch
try {
await checkValidity({ email, token, code });
} catch (e) {
if (e.code === 'invalid_or_expired_token') await restartResetFlow(email);
else throw e;
} Prevention
- Use the reset link promptly within the token TTL.
- Copy the full reset link; watch for email-client line wrapping.
- Target the same Dify instance that issued the token.
When it happens
Trigger: POST /console/api/forgot-password/validity with a token that does not resolve in the reset-token store — stale link, already-used token, truncated string, or fabricated value.
Common situations: User clicks an old reset link past its TTL; user already reset the password and clicks the same link again; email client truncated/wrapped the link; client lost the token and substituted garbage; cross-environment token (token issued by prod used against staging).
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of langgenius/dify@ef8544b173 (2026-08-12).
Data as JSON: /api/errors/de56ee1c3a7074be.
Report an issue: GitHub.