From 544d557a167dc67c737b27a34c405099fb99d79a Mon Sep 17 00:00:00 2001 From: snehar-nd Date: Thu, 14 May 2026 14:37:20 +0530 Subject: [PATCH] fix: concurrent session logout not invalidating JWT in first system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit logOutUserFromConcurrentSession only cleaned up old-style Redis session keys but never added the displaced user's JWT to the denylist. Because JwtUserIdValidationFilter validates solely via JWT signature and the denylist, System 1's token remained valid and all APIs returned 200 after System 2 forced a concurrent login. Fix: store a username→JTI mapping in Redis at login time; during concurrent-session logout, look up the JTI and add it to the denylist and evict the user_ cache so the next request from System 1 is rejected with 401 and the frontend shows the session-expiry message. Co-Authored-By: Claude Sonnet 4.6 --- .../controller/users/IEMRAdminController.java | 32 +++++++++++++++++++ .../java/com/iemr/common/utils/JwtUtil.java | 4 +++ 2 files changed, 36 insertions(+) diff --git a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java index 701add24..c76fc1df 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -197,6 +197,15 @@ public String userAuthenticate( user.setUserName(mUser.get(0).getUserName()); logger.info("UserAgentUtil isMobile : " + isMobile); + // Store username → JTI mapping so concurrent-session logout can denylist this token + String accessJti = jwtUtil.getJtiFromToken(jwtToken); + redisTemplate.opsForValue().set( + "jti:" + m_User.getUserName().trim().toLowerCase(), + accessJti + "|" + mUser.get(0).getUserID(), + jwtUtil.getAccessTokenExpiration(), + TimeUnit.MILLISECONDS + ); + if (isMobile) { refreshToken = jwtUtil.generateRefreshToken(m_User.getUserName(), user.getUserID().toString()); logger.debug("Refresh token generated successfully for user: {}", user.getUserName()); @@ -387,6 +396,20 @@ public String logOutUserFromConcurrentSession( if (previousTokenFromRedis != null) { deleteSessionObjectByGettingSessionDetails(previousTokenFromRedis); sessionObject.deleteSessionObject(previousTokenFromRedis); + + // Denylist the active JWT so the first system's requests are immediately rejected + String usernameKey = mUsers.get(0).getUserName().trim().toLowerCase(); + String jtiData = (String) redisTemplate.opsForValue().get("jti:" + usernameKey); + if (jtiData != null) { + String[] parts = jtiData.split("\\|", 2); + String jti = parts[0]; + tokenDenylist.addTokenToDenylist(jti, jwtUtil.getAccessTokenExpiration()); + if (parts.length > 1) { + redisTemplate.delete("user_" + parts[1]); + } + redisTemplate.delete("jti:" + usernameKey); + } + response.setResponse("User successfully logged out"); } else{ logger.error("Unable to fetch session from redis"); @@ -522,6 +545,15 @@ public String superUserAuthenticate( isMobile = UserAgentUtil.isMobileDevice(userAgent); logger.info("UserAgentUtil isMobile : " + isMobile); + // Store username → JTI mapping so concurrent-session logout can denylist this token + String accessJti = jwtUtil.getJtiFromToken(jwtToken); + redisTemplate.opsForValue().set( + "jti:" + m_User.getUserName().trim().toLowerCase(), + accessJti + "|" + mUser.getUserID(), + jwtUtil.getAccessTokenExpiration(), + TimeUnit.MILLISECONDS + ); + if (isMobile) { refreshToken = jwtUtil.generateRefreshToken(m_User.getUserName(), user.getUserID().toString()); logger.debug("Refresh token generated successfully for user: {}", user.getUserName()); diff --git a/src/main/java/com/iemr/common/utils/JwtUtil.java b/src/main/java/com/iemr/common/utils/JwtUtil.java index 5d37a990..d7f6c270 100644 --- a/src/main/java/com/iemr/common/utils/JwtUtil.java +++ b/src/main/java/com/iemr/common/utils/JwtUtil.java @@ -163,6 +163,10 @@ public long getRefreshTokenExpiration() { return REFRESH_EXPIRATION_TIME; } + public long getAccessTokenExpiration() { + return ACCESS_EXPIRATION_TIME; + } + /** * Extract user ID from JWT token in the request (checks header and cookie) * @param request the HTTP request