From b1d16670d56e19f7b35e7f2f12f3611fdb3fab58 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Thu, 9 Apr 2026 16:00:03 +0200 Subject: [PATCH] fix: set JWT iat after password_changed_at to prevent token rejection (#263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new token issued after password change had iat (integer seconds) that was <= password_changed_at (millisecond precision), causing the auth middleware's "iat < passwordChangedTime" check to reject it immediately. Set iat explicitly to 1 second after password_changed_at. E2E tested: login → mandatory password change → dashboard loads successfully with no redirect loop and no 401 errors. --- backend/src/routes/adminAuth.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/routes/adminAuth.js b/backend/src/routes/adminAuth.js index 9d03a809..d14860cb 100644 --- a/backend/src/routes/adminAuth.js +++ b/backend/src/routes/adminAuth.js @@ -135,12 +135,17 @@ router.post('/change-password', [ updated_at: now }); - // Issue a new token so the session remains valid after password_changed_at invalidated the old one + // Issue a new token so the session remains valid after password_changed_at invalidated the old one. + // Set iat to 1 second after password_changed_at to guarantee the token passes the + // "iat < password_changed_at" check in auth middleware (password_changed_at has ms precision + // but JWT iat is floored to seconds, which can cause the new token to be rejected). + const iatAfterPasswordChange = Math.floor(now.getTime() / 1000) + 1; const newToken = jwt.sign({ id: user.id, username: user.username, type: 'admin', role: user.role_name, + iat: iatAfterPasswordChange, loginTime: Date.now() }, process.env.JWT_SECRET, { expiresIn: '24h',