'Token expired' while resetting password

Description

I have Rocket Chat deployed on a self-hosted server, and generally things are working fine. However, if a user goes through the password reset workflow, they eventually get a ‘token expired’ error and can’t finish the process.

  1. User clicks the ‘Forgot your password?’ link on the login screen
  2. User enters email in the field and clicks ‘send instructions’
  3. User finds the Password Recovery email and clicks the ‘Reset’ button
  4. User enters a new password and is prompted for an authentication code in a new email
  5. User finds the authentication code from the email and enters it
  6. The Authentication Code prompt disappears and the password field now has an error message underneath it that says ‘Token expired’:

This happens even when the entire process proceeds quickly without any delays.

Server Setup Information

  • Version of Rocket.Chat Server: 5.4.0
  • Operating System: Ubuntu server 22.04 LTS
  • Deployment Method: snap
  • Number of Running Instances: 1
  • DB Replicaset Oplog: ?
  • NodeJS Version: v14.19.3
  • MongoDB Version: 4.4.15 / wiredTiger (oplog Enabled)
  • Proxy: caddy
  • Firewalls involved:

Any additional Information

None; pretty much a vanilla setup.

Checking my logs, I see the following error:

{
    "level": 50,
    "time": "2023-01-20T20:42:35.212Z",
    "pid": 1091,
    "hostname": "*redacted*",
    "name": "System",
    "msg": "Exception while invoking method resetPassword",
    "err": {
        "type": "errorClass",
        "message": "Token expired [403]",
        "stack": "Error: Token expired [403]<br>    at packages/accounts-password/password_server.js:598:15<br>    at tryLoginMethod (packages/accounts-base/accounts_server.js:1518:14)<br>    at AccountsServer._loginMethod (packages/accounts-base/accounts_server.js:508:7)<br>    at MethodInvocation.resetPassword (packages/accounts-password/password_server.js:566:19)<br>    at maybeAuditArgumentChecks (packages/ddp-server/livedata_server.js:1885:12)<br>    at packages/ddp-server/livedata_server.js:1803:15<br>    at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1257:12)<br>    at packages/ddp-server/livedata_server.js:1801:36<br>    at new Promise (<anonymous>)<br>    at Server.applyAsync (packages/ddp-server/livedata_server.js:1800:12)<br>    at Server.apply (packages/ddp-server/livedata_server.js:1739:26)<br>    at Server.call (packages/ddp-server/livedata_server.js:1721:17)<br>    at Object.post (app/api/server/v1/misc.ts:612:27)<br>    at app/api/server/api.js:463:96<br>    at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1257:12)<br>    at Object._internalRouteActionHandler [as action] (app/api/server/api.js:463:39)<br>    at Route._callEndpoint (packages/rocketchat_restivus/lib/route.coffee:150:32)<br>    at packages/rocketchat_restivus/lib/route.coffee:59:33<br>    at packages/simple_json-routes.js:100:9<br> => awaited here:<br>    at Promise.await (/snap/rocketchat-server/1536/programs/server/npm/node_modules/meteor/promise/node_modules/meteor-promise/promise_server.js:60:12)<br>    at Server.apply (packages/ddp-server/livedata_server.js:1752:22)<br>    at Server.call (packages/ddp-server/livedata_server.js:1721:17)<br>    at Object.post (app/api/server/v1/misc.ts:612:27)<br>    at app/api/server/api.js:463:96<br>    at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1257:12)<br>    at Object._internalRouteActionHandler [as action] (app/api/server/api.js:463:39)<br>    at Route._callEndpoint (packages/rocketchat_restivus/lib/route.coffee:150:32)<br>    at packages/rocketchat_restivus/lib/route.coffee:59:33<br>    at packages/simple_json-routes.js:100:9",
        "isClientSafe": true,
        "error": 403,
        "reason": "Token expired",
        "errorType": "Meteor.Error"
    },
    "msg": "Token expired [403]"
}

Reformatting that stack trace for easier readability:

Error: Token expired [403]
    at packages/accounts-password/password_server.js:598:15
    at tryLoginMethod (packages/accounts-base/accounts_server.js:1518:14)
    at AccountsServer._loginMethod (packages/accounts-base/accounts_server.js:508:7)
    at MethodInvocation.resetPassword (packages/accounts-password/password_server.js:566:19)
    at maybeAuditArgumentChecks (packages/ddp-server/livedata_server.js:1885:12)
    at packages/ddp-server/livedata_server.js:1803:15
    at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1257:12)
    at packages/ddp-server/livedata_server.js:1801:36
    at new Promise (<anonymous>)
    at Server.applyAsync (packages/ddp-server/livedata_server.js:1800:12)
    at Server.apply (packages/ddp-server/livedata_server.js:1739:26)
    at Server.call (packages/ddp-server/livedata_server.js:1721:17)
    at Object.post (app/api/server/v1/misc.ts:612:27)
    at app/api/server/api.js:463:96
    at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1257:12)
    at Object._internalRouteActionHandler [as action] (app/api/server/api.js:463:39)
    at Route._callEndpoint (packages/rocketchat_restivus/lib/route.coffee:150:32)
    at packages/rocketchat_restivus/lib/route.coffee:59:33
    at packages/simple_json-routes.js:100:9
 => awaited here:
    at Promise.await (/snap/rocketchat-server/1536/programs/server/npm/node_modules/meteor/promise/node_modules/meteor-promise/promise_server.js:60:12)
    at Server.apply (packages/ddp-server/livedata_server.js:1752:22)
    at Server.call (packages/ddp-server/livedata_server.js:1721:17)
    at Object.post (app/api/server/v1/misc.ts:612:27)
    at app/api/server/api.js:463:96
    at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1257:12)
    at Object._internalRouteActionHandler [as action] (app/api/server/api.js:463:39)
    at Route._callEndpoint (packages/rocketchat_restivus/lib/route.coffee:150:32)
    at packages/rocketchat_restivus/lib/route.coffee:59:33
    at packages/simple_json-routes.js:100:9

Finally added the proper terms to my Google searches and found the relevant Github issue: