From 88a6c6a7fba7e1419a021f4870518f0b76ac6494 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Fri, 1 May 2026 23:12:20 +0200 Subject: [PATCH] fix(auth): make /auth/session verify the issuer claim like adminAuth (#350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Asymmetric JWT verification was causing a /admin/login → /admin/dashboard → /admin/login redirect loop for users carrying admin cookies issued before the iss: 'picpeak-auth' claim was added (commit 23cd9cb, "address Shannon security assessment findings (37 vulnerabilities)"). The frontend uses GET /auth/session as the source of truth for "is the user authenticated?". That endpoint called jwt.verify(token, JWT_SECRET) with no issuer option, so it accepted pre-issuer tokens and reported valid: true. AdminLoginPage then redirected to /admin/dashboard, every protected endpoint went through adminAuth which DOES verify the issuer, each one rejected the token with 401, the response interceptor window.location.href'd back to /admin/login, and the loop closed. Fix: pass { issuer: 'picpeak-auth' } to /auth/session's jwt.verify so it matches adminAuth and galleryAuth. Tokens without the claim now correctly return valid: false from the session check, AdminLoginPage shows the login form, and a fresh login mints a properly-issued cookie. The other intentionally-lax verify call sites (logout-flow logging, photoAuth, sessionTimeout, rateLimit) are unrelated to the loop and stay lax — their callers don't gate "authenticated?" decisions on the result. Reproducer: open a removed/archived gallery URL with a stale admin cookie from before the issuer claim was added, click "Back to home" on the gallery-not-found page → loop. --- backend/src/routes/auth.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index a427a8c6..aab00b23 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -483,7 +483,14 @@ router.get('/session', async (req, res) => { } try { - const decoded = jwt.verify(token, process.env.JWT_SECRET); + // Verify with the same `issuer` claim that adminAuth/galleryAuth + // require (#350 — without this, /auth/session accepted pre-issuer + // tokens and the frontend thought the user was authenticated, but + // every protected endpoint rejected them with 401, producing a + // /admin/login → /admin/dashboard → /admin/login redirect loop). + const decoded = jwt.verify(token, process.env.JWT_SECRET, { + issuer: 'picpeak-auth' + }); // Check if token has been revoked (e.g. after logout) const { isTokenRevoked } = require('../utils/tokenRevocation');