From d505e3124fbf3b3e40a1792d8e3c6079aec5e912 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 16 Dec 2025 18:42:00 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=84=20fix:=20Resolve=20Infinite=20Refr?= =?UTF-8?q?esh=20Loop=20on=20OpenID=20Provider/Database=20Switch=20(#11002?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The refreshController was not updating user openidId when found by email with a different openidId already set. This caused an infinite loop where: 1. JWT auth failed (openidId mismatch) 2. Refresh found user by email but didn't update openidId 3. Next JWT auth failed again → loop Now updates openidId when either migration is needed OR when the stored openidId doesn't match the token's sub claim. Added debug logging for visibility into auth state during refresh. --- api/server/controllers/AuthController.js | 32 ++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/api/server/controllers/AuthController.js b/api/server/controllers/AuthController.js index dfef2bbfa1..cb4e1a7eea 100644 --- a/api/server/controllers/AuthController.js +++ b/api/server/controllers/AuthController.js @@ -10,7 +10,13 @@ const { setAuthTokens, registerUser, } = require('~/server/services/AuthService'); -const { findUser, getUserById, deleteAllUserSessions, findSession } = require('~/models'); +const { + deleteAllUserSessions, + getUserById, + findSession, + updateUser, + findUser, +} = require('~/models'); const { getGraphApiToken } = require('~/server/services/GraphTokenService'); const { getOAuthReconnectionManager } = require('~/config'); const { getOpenIdConfig } = require('~/strategies'); @@ -72,16 +78,38 @@ const refreshController = async (req, res) => { const openIdConfig = getOpenIdConfig(); const tokenset = await openIdClient.refreshTokenGrant(openIdConfig, refreshToken); const claims = tokenset.claims(); - const { user, error } = await findOpenIDUser({ + const { user, error, migration } = await findOpenIDUser({ findUser, email: claims.email, openidId: claims.sub, idOnTheSource: claims.oid, strategyName: 'refreshController', }); + + logger.debug( + `[refreshController] findOpenIDUser result: user=${user?.email ?? 'null'}, error=${error ?? 'null'}, migration=${migration}, userOpenidId=${user?.openidId ?? 'null'}, claimsSub=${claims.sub}`, + ); + if (error || !user) { + logger.warn( + `[refreshController] Redirecting to /login: error=${error ?? 'null'}, user=${user ? 'exists' : 'null'}`, + ); return res.status(401).redirect('/login'); } + + // Handle migration: update user with openidId if found by email without openidId + // Also handle case where user has mismatched openidId (e.g., after database switch) + if (migration || user.openidId !== claims.sub) { + const reason = migration ? 'migration' : 'openidId mismatch'; + await updateUser(user._id.toString(), { + provider: 'openid', + openidId: claims.sub, + }); + logger.info( + `[refreshController] Updated user ${user.email} openidId (${reason}): ${user.openidId ?? 'null'} -> ${claims.sub}`, + ); + } + const token = setOpenIDAuthTokens(tokenset, res, user._id.toString(), refreshToken); user.federatedTokens = {