fix: floor password_changed_at when comparing against JWT iat

JWT `iat` has 1-second resolution; `password_changed_at` is stored with
sub-second precision. The previous comparison rejected tokens whose iat
fell in the same wall-clock second as a password change — e.g. a token
issued by an immediate re-login after a password reset, or by any
script-driven flow that resets and logs in in quick succession. Floor
the stored timestamp to whole seconds before comparing.

Caught while wiring up the local E2E suite: the seeder needed a
"set password_changed_at 10 s in the past" hack to avoid this race;
with the fix in place that hack is gone and the suite is naturally
deterministic.
This commit is contained in:
Paul Nothaft
2026-04-27 16:13:22 +02:00
parent 8d0fb8e157
commit 793e410554
+9 -3
View File
@@ -91,10 +91,16 @@ async function adminAuth(req, res, next) {
return res.status(401).json({ error: 'Invalid token' });
}
// Check if password was changed after token was issued
// Check if password was changed after token was issued. JWT `iat` has
// 1-second resolution; `password_changed_at` is sub-second. Floor the
// comparison so a token issued in the *same* second as the password
// change isn't incorrectly rejected — that race used to bite anyone
// logging in immediately after a password reset/change.
if (admin.password_changed_at) {
const passwordChangedTime = new Date(admin.password_changed_at).getTime() / 1000;
if (decoded.iat < passwordChangedTime) {
const passwordChangedSeconds = Math.floor(
new Date(admin.password_changed_at).getTime() / 1000
);
if (decoded.iat < passwordChangedSeconds) {
logger.warn('Token used after password change', { userId: decoded.id });
return res.status(401).json({
error: 'Token invalid due to password change',