From 80fa0e5e65d7357c441dda42a43ed70b2a37ea38 Mon Sep 17 00:00:00 2001 From: snehar-nd Date: Thu, 14 May 2026 15:54:47 +0530 Subject: [PATCH] fix: concurrent session logout not invalidating JWT on first system 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. The root serialization bug: redisTemplate value serializer is Jackson2JsonRedisSerializer, so storing a plain String JTI caused a deserialization failure on retrieval. Fixed by using the existing StringRedisTemplate bean for the jti: key operations. Fix: - Store username->JTI mapping via StringRedisTemplate at login (both userAuthenticate and superUserAuthenticate) - On concurrent-session logout, retrieve the JTI, add it to the denylist, evict user_ from User cache, and clean up jti: key - Add getAccessTokenExpiration() to JwtUtil to supply the TTL Co-Authored-By: Claude Sonnet 4.6 --- .../controller/users/IEMRAdminController.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) 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 c76fc1df..a96e70bc 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -36,6 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -95,6 +96,8 @@ public class IEMRAdminController { private CookieUtil cookieUtil; @Autowired private RedisTemplate redisTemplate; + @Autowired + private StringRedisTemplate stringRedisTemplate; private AESUtil aesUtil; @@ -197,11 +200,10 @@ 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( + // Store username -> JTI mapping so concurrent-session logout can denylist this token + stringRedisTemplate.opsForValue().set( "jti:" + m_User.getUserName().trim().toLowerCase(), - accessJti + "|" + mUser.get(0).getUserID(), + jwtUtil.getJtiFromToken(jwtToken) + "|" + mUser.get(0).getUserID(), jwtUtil.getAccessTokenExpiration(), TimeUnit.MILLISECONDS ); @@ -397,17 +399,16 @@ public String logOutUserFromConcurrentSession( deleteSessionObjectByGettingSessionDetails(previousTokenFromRedis); sessionObject.deleteSessionObject(previousTokenFromRedis); - // Denylist the active JWT so the first system's requests are immediately rejected + // Denylist the active JWT so System 1's requests are immediately rejected String usernameKey = mUsers.get(0).getUserName().trim().toLowerCase(); - String jtiData = (String) redisTemplate.opsForValue().get("jti:" + usernameKey); + String jtiData = stringRedisTemplate.opsForValue().get("jti:" + usernameKey); if (jtiData != null) { String[] parts = jtiData.split("\\|", 2); - String jti = parts[0]; - tokenDenylist.addTokenToDenylist(jti, jwtUtil.getAccessTokenExpiration()); + tokenDenylist.addTokenToDenylist(parts[0], jwtUtil.getAccessTokenExpiration()); if (parts.length > 1) { redisTemplate.delete("user_" + parts[1]); } - redisTemplate.delete("jti:" + usernameKey); + stringRedisTemplate.delete("jti:" + usernameKey); } response.setResponse("User successfully logged out"); @@ -545,11 +546,10 @@ 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( + // Store username -> JTI mapping so concurrent-session logout can denylist this token + stringRedisTemplate.opsForValue().set( "jti:" + m_User.getUserName().trim().toLowerCase(), - accessJti + "|" + mUser.getUserID(), + jwtUtil.getJtiFromToken(jwtToken) + "|" + mUser.getUserID(), jwtUtil.getAccessTokenExpiration(), TimeUnit.MILLISECONDS );