{"record":{"id":"65751c101c701a6e","repo":"payloadcms/payload","slug":"token-is-either-invalid-or-has-expired","errorCode":null,"errorMessage":"Token is either invalid or has expired.","messagePattern":"Token is either invalid or has expired\\.","errorType":"exception","errorClass":"APIError","httpStatus":403,"severity":"error","filePath":"packages/payload/src/auth/operations/resetPassword.ts","lineNumber":95,"sourceCode":"    // /////////////////////////////////////\n\n    const where = appendNonTrashedFilter({\n      enableTrash: Boolean(collectionConfig.trash),\n      trash: false,\n      where: {\n        resetPasswordExpiration: { greater_than: new Date().toISOString() },\n        resetPasswordToken: { equals: data.token },\n      },\n    })\n\n    user = await payload.db.findOne<User>({\n      collection: collectionConfig.slug,\n      req,\n      where,\n    })\n\n    if (!user) {\n      throw new APIError('Token is either invalid or has expired.', httpStatus.FORBIDDEN)\n    }\n\n    // TODO: replace this method\n    const { hash, salt } = await generatePasswordSaltHash({\n      collection: collectionConfig,\n      password: data.password,\n      req,\n    })\n\n    user.salt = salt\n    user.hash = hash\n\n    user.resetPasswordExpiration = new Date().toISOString()\n\n    if (collectionConfig.auth.verify) {\n      user._verified = Boolean(user._verified)\n    }\n","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/payloadcms/payload/blob/00c58b35c0ed348ddc22daabf467b139727214fd/packages/payload/src/auth/operations/resetPassword.ts#L77-L113","documentation":"Thrown in `resetPassword` after `findOne` with `where: { resetPasswordToken: { equals }, resetPasswordExpiration: { greater_than: now } }` returns no user. The token does not match any document or has exceeded its validity window. `APIError` with HTTP 403 (FORBIDDEN).","triggerScenarios":"The user clicks an expired reset link (`resetPasswordExpiration` elapsed, default 10 minutes unless configured); the token was already consumed by a previous reset (token cleared after use); the token is malformed/copy-pasted incompletely; the token never existed.","commonSituations":"Slow email delivery pushes the click past the expiration; user requests multiple resets and clicks an old link; URL encoding mangles the token; the previous successful reset nulled `resetPasswordToken` so the same token is now invalid.","solutions":["Request a fresh reset email and use the newest link promptly.","Increase `auth.forgotPassword.expiration` if delivery latency is high.","Ensure the token is passed verbatim (no URL-decoding issues) from the email link to the POST body.","If expired tokens are common, review email delivery latency and the expiration default."],"exampleFix":"// before — calling with a possibly-stale token\nawait payload.resetPassword({ collection, data: { token, password }, req })\n// after — handle expiry by re-issuing\ntry {\n  await payload.resetPassword({ collection, data: { token, password }, req })\n} catch (e) {\n  if (e.message.includes('invalid or has expired')) {\n    await payload.forgotPassword({ collection, data: { email }, req })\n  }\n}","handlingStrategy":"try-catch","validationCode":"// Best-effort: check token freshness against expiration config before posting\nconst expMs = (collectionConfig.auth.forgotPassword?.expiration ?? 600) * 1000\nif (Date.now() - issuedAt > expMs) { return requestNewResetEmail() }","typeGuard":"function isTokenInvalidError(e: unknown): e is APIError {\n  return e instanceof APIError && e.status === 403\n    && /invalid or has expired/.test(e.message)\n}","tryCatchPattern":"try {\n  await payload.resetPassword({ collection, data: { token, password }, req })\n} catch (e) {\n  if (isTokenInvalidError(e)) {\n    await payload.forgotPassword({ collection, data: { email }, req })\n  } else throw e\n}","preventionTips":["Use the most recent reset link; old ones are invalidated after use.","Tune `auth.forgotPassword.expiration` if email delivery is slow.","Forward the token verbatim, watching for URL-encoding issues."],"tags":["auth","reset-password","token","expired"],"backgroundTag":null,"analyzedSha":"00c58b35c0ed348ddc22daabf467b139727214fd","analyzedAt":"2026-08-12T20:45:03.758Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}