diff --git a/backend/internal/user.js b/backend/internal/user.js index d4080dd78..8dd86f461 100644 --- a/backend/internal/user.js +++ b/backend/internal/user.js @@ -388,11 +388,21 @@ const internalUser = { .andWhere("type", data.type) .first() .then((existing_auth) => { + // Stamped here rather than read off modified_on, because it is compared against a + // token's `iat` and the two only line up when the same clock writes both. The + // database clock is a different one: with the app on one timezone and the database + // on another, its timestamps come back hours away from where Node thinks it is. + const password_changed_at = Math.floor(Date.now() / 1000); + if (existing_auth) { // patch + const meta = existing_auth.meta || {}; + meta.password_changed_at = password_changed_at; + return authModel.query().where("user_id", user.id).andWhere("type", data.type).patch({ type: data.type, // This is required for the model to encrypt on save secret: data.secret, + meta, }); } // insert @@ -400,7 +410,7 @@ const internalUser = { user_id: user.id, type: data.type, secret: data.secret, - meta: {}, + meta: { password_changed_at }, }); }) .then(() => { diff --git a/backend/lib/access.js b/backend/lib/access.js index 9de1461bc..d119a4f92 100644 --- a/backend/lib/access.js +++ b/backend/lib/access.js @@ -90,18 +90,11 @@ export default function (tokenString) { .where("type", "=", "password") .first(); - if (auth && typeof tokenData.iat === "number") { - // SQLite gives this back as a local time string, the other drivers as a Date. - const changedAt = - auth.modified_on instanceof Date - ? auth.modified_on.getTime() - : Date.parse(String(auth.modified_on).replace(" ", "T")); - - // Whole seconds on both sides, which is all `iat` carries, so a token issued in the - // same second as the change is kept. Postgres stores this column to the microsecond. - if (!Number.isNaN(changedAt) && tokenData.iat < Math.floor(changedAt / 1000)) { - throw new errs.TokenRevokedError("Token was issued before the password was changed"); - } + // Both sides come from the same clock and in the same unit, whole seconds since + // the epoch: `setPassword` stamps the marker and `jsonwebtoken` stamps `iat`. + const changedAt = auth?.meta?.password_changed_at; + if (changedAt && typeof tokenData.iat === "number" && tokenData.iat < changedAt) { + throw new errs.TokenRevokedError("Token was issued before the password was changed"); } initialised = true;