From 053c28997819bbe71c47c688b4133ae7aa9509fe Mon Sep 17 00:00:00 2001 From: ravishanigarapu <133210792+ravishanigarapu@users.noreply.github.com> Date: Thu, 29 May 2025 15:33:23 +0530 Subject: [PATCH 01/13] Force Logout Jwttoken changes (#210) * null check and logger added * Redis issue * Jwttoken added for superUserAuthenticate * Null check * If mobile returned refresh token * Force logout jwttoken expiry * Method corrected * coderabbit changes --- .../com/iemr/common/constant/Constants.java | 1 + .../controller/users/IEMRAdminController.java | 26 ++++++++++-- .../java/com/iemr/common/utils/JwtUtil.java | 9 ++-- .../com/iemr/common/utils/TokenBlacklist.java | 42 +++++++++++++++++++ 4 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/iemr/common/utils/TokenBlacklist.java diff --git a/src/main/java/com/iemr/common/constant/Constants.java b/src/main/java/com/iemr/common/constant/Constants.java index c6f98e02..ebe7d772 100644 --- a/src/main/java/com/iemr/common/constant/Constants.java +++ b/src/main/java/com/iemr/common/constant/Constants.java @@ -11,5 +11,6 @@ public class Constants { public static final String HOLD = "Hold"; public static final String NOT_READY = "Not Ready"; public static final String AUX = "Aux"; + public static final String JWT_TOKEN = "Jwttoken"; } 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 072bf88d..9eee79fd 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -34,6 +34,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -44,6 +45,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.iemr.common.config.encryption.SecurePassword; +import com.iemr.common.constant.Constants; import com.iemr.common.data.users.LoginSecurityQuestions; import com.iemr.common.data.users.M_Role; import com.iemr.common.data.users.ServiceRoleScreenMapping; @@ -56,6 +58,7 @@ import com.iemr.common.service.users.IEMRAdminUserService; import com.iemr.common.utils.CookieUtil; import com.iemr.common.utils.JwtUtil; +import com.iemr.common.utils.TokenBlacklist; import com.iemr.common.utils.encryption.AESUtil; import com.iemr.common.utils.exception.IEMRException; import com.iemr.common.utils.mapper.InputMapper; @@ -83,6 +86,8 @@ public class IEMRAdminController { private CookieUtil cookieUtil; @Autowired private RedisTemplate redisTemplate; + @Value("${jwt.blacklist.expiration}") + private static long BLACK_LIST_EXPIRATION_TIME; private AESUtil aesUtil; @@ -933,9 +938,14 @@ public String forceLogout(@RequestBody ForceLogoutRequestModel request, HttpServ try { // Perform the force logout logic iemrAdminUserServiceImpl.forceLogout(request); - + String token = null; + token = getJwtTokenFromCookies(httpRequest); + if(null == token) { + token = httpRequest.getHeader(Constants.JWT_TOKEN); + } + TokenBlacklist.blacklistToken(token,BLACK_LIST_EXPIRATION_TIME); // Extract and invalidate JWT token cookie dynamically from the request - invalidateJwtCookie(httpRequest, response); + // invalidateJwtCookie(httpRequest, response); // Set the response message outputResponse.setResponse("Success"); @@ -944,7 +954,17 @@ public String forceLogout(@RequestBody ForceLogoutRequestModel request, HttpServ } return outputResponse.toString(); } - + private String getJwtTokenFromCookies(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equalsIgnoreCase("Jwttoken")) { + return cookie.getValue(); + } + } + } + return null; + } private void invalidateJwtCookie(HttpServletRequest request, HttpServletResponse response) { // Get the cookies from the incoming request Cookie[] cookies = request.getCookies(); diff --git a/src/main/java/com/iemr/common/utils/JwtUtil.java b/src/main/java/com/iemr/common/utils/JwtUtil.java index 56e49549..940b6959 100644 --- a/src/main/java/com/iemr/common/utils/JwtUtil.java +++ b/src/main/java/com/iemr/common/utils/JwtUtil.java @@ -57,12 +57,11 @@ private String buildToken(String username, String userId, String tokenType, long } public Claims validateToken(String token) { + if (TokenBlacklist.isTokenBlacklisted(token)) { + return null; + } try { - return Jwts.parser() - .verifyWith(getSigningKey()) - .build() - .parseSignedClaims(token) - .getPayload(); + return Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(token).getPayload(); } catch (ExpiredJwtException ex) { // Handle expired token specifically if needed diff --git a/src/main/java/com/iemr/common/utils/TokenBlacklist.java b/src/main/java/com/iemr/common/utils/TokenBlacklist.java new file mode 100644 index 00000000..c60f177a --- /dev/null +++ b/src/main/java/com/iemr/common/utils/TokenBlacklist.java @@ -0,0 +1,42 @@ +package com.iemr.common.utils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.factory.annotation.Value; + +public class TokenBlacklist { + + + // Store blacklisted tokens (in-memory) + private static final Map blacklistedTokens = new ConcurrentHashMap<>(); + + + // Add a token to the blacklist + public static void blacklistToken(String token ,Long blackListExpirationTime) { + if(token == null || token.trim().isEmpty()) { + return; + } + blacklistedTokens.put(token, System.currentTimeMillis()+ blackListExpirationTime); + } + + // Check if a token is blacklisted + + public static boolean isTokenBlacklisted(String token) { + if(token == null || token.trim().isEmpty()) { + return false; + } + Long expiry = blacklistedTokens.get(token); + if (expiry == null) return false; + // If token is expired, remove it from blacklist and treat as not blacklisted + if (System.currentTimeMillis() > expiry) { + blacklistedTokens.remove(token); + return false; + } + return true; + } + +} From dc9c5a5c04677adbfe40fb7909d9821d3de13adf Mon Sep 17 00:00:00 2001 From: SR20290919 Date: Fri, 30 May 2025 12:48:13 +0530 Subject: [PATCH 02/13] adding token denylisting changes to forceLogout API --- .../controller/users/IEMRAdminController.java | 127 ++++++--- .../java/com/iemr/common/utils/JwtUtil.java | 253 ++++++++++++------ .../com/iemr/common/utils/TokenBlacklist.java | 42 --- .../com/iemr/common/utils/TokenDenylist.java | 38 +++ 4 files changed, 293 insertions(+), 167 deletions(-) delete mode 100644 src/main/java/com/iemr/common/utils/TokenBlacklist.java create mode 100644 src/main/java/com/iemr/common/utils/TokenDenylist.java 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 9eee79fd..19f968e6 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -58,7 +58,7 @@ import com.iemr.common.service.users.IEMRAdminUserService; import com.iemr.common.utils.CookieUtil; import com.iemr.common.utils.JwtUtil; -import com.iemr.common.utils.TokenBlacklist; +import com.iemr.common.utils.TokenDenylist; import com.iemr.common.utils.encryption.AESUtil; import com.iemr.common.utils.exception.IEMRException; import com.iemr.common.utils.mapper.InputMapper; @@ -83,6 +83,8 @@ public class IEMRAdminController { @Autowired private JwtUtil jwtUtil; @Autowired + private TokenDenylist tokenDenylist; + @Autowired private CookieUtil cookieUtil; @Autowired private RedisTemplate redisTemplate; @@ -928,8 +930,6 @@ private void deleteSessionObject(String key) { } } - - @CrossOrigin() @Operation(summary = "Force log out") @RequestMapping(value = "/forceLogout", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON, headers = "Authorization") @@ -938,14 +938,28 @@ public String forceLogout(@RequestBody ForceLogoutRequestModel request, HttpServ try { // Perform the force logout logic iemrAdminUserServiceImpl.forceLogout(request); - String token = null; - token = getJwtTokenFromCookies(httpRequest); - if(null == token) { - token = httpRequest.getHeader(Constants.JWT_TOKEN); + + // Extract token from cookies or headers + String token = getJwtTokenFromCookies(httpRequest); + if (token == null) { + token = httpRequest.getHeader(Constants.JWT_TOKEN); } - TokenBlacklist.blacklistToken(token,BLACK_LIST_EXPIRATION_TIME); - // Extract and invalidate JWT token cookie dynamically from the request - // invalidateJwtCookie(httpRequest, response); + + // Validate the token: Check if it is expired or in the deny list + Claims claims = jwtUtil.validateToken(token); + if (claims == null) { + // If token is either expired or in the deny list, return 401 Unauthorized + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + outputResponse.setError(new RuntimeException("Token is expired or has been logged out")); + return outputResponse.toString(); + } + + // Extract the jti (JWT ID) and expiration time from the JWT token + String jti = jwtUtil.getJtiFromToken(token); + long expirationTime = jwtUtil.getAllClaimsFromToken(token).getExpiration().getTime(); + + // Denylist the token's jti in Redis with its expiration time + tokenDenylist.denylistToken(jti, expirationTime); // Set the response message outputResponse.setResponse("Success"); @@ -954,43 +968,82 @@ public String forceLogout(@RequestBody ForceLogoutRequestModel request, HttpServ } return outputResponse.toString(); } + private String getJwtTokenFromCookies(HttpServletRequest request) { - Cookie[] cookies = request.getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - if (cookie.getName().equalsIgnoreCase("Jwttoken")) { - return cookie.getValue(); - } - } - } - return null; - } - private void invalidateJwtCookie(HttpServletRequest request, HttpServletResponse response) { - // Get the cookies from the incoming request Cookie[] cookies = request.getCookies(); - if (cookies != null) { for (Cookie cookie : cookies) { - // Check if the cookie name matches "Jwttoken" (case-sensitive) if (cookie.getName().equalsIgnoreCase("Jwttoken")) { - // Invalidate the JWT token cookie by setting the value to null and max age to 0 - cookie.setValue(null); - cookie.setMaxAge(0); // Expire the cookie immediately - cookie.setPath(cookie.getPath()); // Ensure the path matches the cookie's original path - cookie.setHttpOnly(true); // Secure the cookie so it can't be accessed via JS - cookie.setSecure(true); // Only send over HTTPS if you're using secure connections - cookie.setAttribute("SameSite", "Strict"); - // Add the invalidated cookie back to the response - response.addCookie(cookie); - break; // If we found the JWT cookie, no need to continue looping + return cookie.getValue(); } } - } else { - // Log or handle the case when no cookies are found in the request - logger.warn("No cookies found in the request."); } + return null; } + + +// @CrossOrigin() +// @Operation(summary = "Force log out") +// @RequestMapping(value = "/forceLogout", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON, headers = "Authorization") +// public String forceLogout(@RequestBody ForceLogoutRequestModel request, HttpServletRequest httpRequest, HttpServletResponse response) { +// OutputResponse outputResponse = new OutputResponse(); +// try { +// // Perform the force logout logic +// iemrAdminUserServiceImpl.forceLogout(request); +// String token = null; +// token = getJwtTokenFromCookies(httpRequest); +// if(null == token) { +// token = httpRequest.getHeader(Constants.JWT_TOKEN); +// } +// TokenBlacklist.blacklistToken(token,BLACK_LIST_EXPIRATION_TIME); +// // Extract and invalidate JWT token cookie dynamically from the request +// // invalidateJwtCookie(httpRequest, response); +// +// // Set the response message +// outputResponse.setResponse("Success"); +// } catch (Exception e) { +// outputResponse.setError(e); +// } +// return outputResponse.toString(); +// } +// private String getJwtTokenFromCookies(HttpServletRequest request) { +// Cookie[] cookies = request.getCookies(); +// if (cookies != null) { +// for (Cookie cookie : cookies) { +// if (cookie.getName().equalsIgnoreCase("Jwttoken")) { +// return cookie.getValue(); +// } +// } +// } +// return null; +// } +// private void invalidateJwtCookie(HttpServletRequest request, HttpServletResponse response) { +// // Get the cookies from the incoming request +// Cookie[] cookies = request.getCookies(); +// +// if (cookies != null) { +// for (Cookie cookie : cookies) { +// // Check if the cookie name matches "Jwttoken" (case-sensitive) +// if (cookie.getName().equalsIgnoreCase("Jwttoken")) { +// // Invalidate the JWT token cookie by setting the value to null and max age to 0 +// cookie.setValue(null); +// cookie.setMaxAge(0); // Expire the cookie immediately +// cookie.setPath(cookie.getPath()); // Ensure the path matches the cookie's original path +// cookie.setHttpOnly(true); // Secure the cookie so it can't be accessed via JS +// cookie.setSecure(true); // Only send over HTTPS if you're using secure connections +// cookie.setAttribute("SameSite", "Strict"); +// // Add the invalidated cookie back to the response +// response.addCookie(cookie); +// break; // If we found the JWT cookie, no need to continue looping +// } +// } +// } else { +// // Log or handle the case when no cookies are found in the request +// logger.warn("No cookies found in the request."); +// } +// } + @CrossOrigin() @Operation(summary = "User force log out") diff --git a/src/main/java/com/iemr/common/utils/JwtUtil.java b/src/main/java/com/iemr/common/utils/JwtUtil.java index 940b6959..6be3af57 100644 --- a/src/main/java/com/iemr/common/utils/JwtUtil.java +++ b/src/main/java/com/iemr/common/utils/JwtUtil.java @@ -1,104 +1,181 @@ package com.iemr.common.utils; -import java.util.Date; -import java.util.UUID; -import java.util.function.Function; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.UnsupportedJwtException; -import io.jsonwebtoken.security.Keys; -import io.jsonwebtoken.security.SignatureException; - import javax.crypto.SecretKey; +import java.util.Date; +import java.util.UUID; +import java.util.function.Function; @Component public class JwtUtil { - @Value("${jwt.secret}") - private String SECRET_KEY; - - @Value("${jwt.access.expiration}") - private long ACCESS_EXPIRATION_TIME; - - @Value("${jwt.refresh.expiration}") - private long REFRESH_EXPIRATION_TIME; - - private SecretKey getSigningKey() { - if (SECRET_KEY == null || SECRET_KEY.isEmpty()) { - throw new IllegalStateException("JWT secret key is not set in application.properties"); - } - return Keys.hmacShaKeyFor(SECRET_KEY.getBytes()); - } - - public String generateToken(String username, String userId) { - return buildToken(username, userId, "access", ACCESS_EXPIRATION_TIME); - } - - public String generateRefreshToken(String username, String userId) { - return buildToken(username, userId, "refresh", REFRESH_EXPIRATION_TIME); - } - - private String buildToken(String username, String userId, String tokenType, long expiration) { - return Jwts.builder() - .subject(username) - .claim("userId", userId) - .claim("token_type", tokenType) - .id(UUID.randomUUID().toString()) - .issuedAt(new Date()) - .expiration(new Date(System.currentTimeMillis() + expiration)) - .signWith(getSigningKey()) - .compact(); - } - - public Claims validateToken(String token) { - if (TokenBlacklist.isTokenBlacklisted(token)) { - return null; - } - try { - return Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(token).getPayload(); - - } catch (ExpiredJwtException ex) { - // Handle expired token specifically if needed - } catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException ex) { - // Log specific error types - } - return null; - } - - public T getClaimFromToken(String token, Function claimsResolver) { - final Claims claims = getAllClaimsFromToken(token); - return claimsResolver.apply(claims); - } - - public Claims getAllClaimsFromToken(String token) { - return Jwts.parser() + @Value("${jwt.secret}") + private String SECRET_KEY; + + @Value("${jwt.access.expiration}") + private long ACCESS_EXPIRATION_TIME; + + @Value("${jwt.refresh.expiration}") + private long REFRESH_EXPIRATION_TIME; + + @Autowired + private TokenDenylist tokenDenylist; + + private SecretKey getSigningKey() { + if (SECRET_KEY == null || SECRET_KEY.isEmpty()) { + throw new IllegalStateException("JWT secret key is not set in application.properties"); + } + return Keys.hmacShaKeyFor(SECRET_KEY.getBytes()); + } + + /** + * Generate an access token. + * + * @param username the username of the user + * @param userId the user ID + * @return the generated JWT access token + */ + public String generateToken(String username, String userId) { + return buildToken(username, userId, "access", ACCESS_EXPIRATION_TIME); + } + + /** + * Generate a refresh token. + * + * @param username the username of the user + * @param userId the user ID + * @return the generated JWT refresh token + */ + public String generateRefreshToken(String username, String userId) { + return buildToken(username, userId, "refresh", REFRESH_EXPIRATION_TIME); + } + + /** + * Build a JWT token with the specified parameters. + * + * @param username the username of the user + * @param userId the user ID + * @param tokenType the type of the token (access or refresh) + * @param expiration the expiration time of the token in milliseconds + * @return the generated JWT token + */ + private String buildToken(String username, String userId, String tokenType, long expiration) { + return Jwts.builder() + .subject(username) + .claim("userId", userId) + .claim("token_type", tokenType) + .id(UUID.randomUUID().toString()) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey()) + .compact(); + } + + /** + * Validate the JWT token, checking if it is expired and if it's blacklisted + * @param token the JWT token + * @return Claims if valid, null if invalid (expired or denylisted) + */ + public Claims validateToken(String token) { + // Check if the token is blacklisted (invalidated by force logout) + if (tokenDenylist.isTokenDenylisted(getJtiFromToken(token))) { + return null; // Token is denylisted, so return null + } + + // Check if the token is expired + if (isTokenExpired(token)) { + return null; // Token is expired, so return null + } + + // If token is not blacklisted and not expired, verify the token signature and return claims + try { + return Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(token).getPayload(); + } catch (ExpiredJwtException ex) { + // Handle expired token specifically + // Optionally, log the exception or return a custom message if required + return null; // Token is expired, so return null + } catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException ex) { + // Log specific error types for debugging + // Optionally, you can log each exception for specific handling or debugging + // Example: logger.error("JWT validation failed: " + ex.getMessage()); + return null; // Return null for any other JWT-related issue (invalid format, bad signature, etc.) + } + } + + /** + * Check if the JWT token is expired + * @param token the JWT token + * @return true if expired, false otherwise + */ + private boolean isTokenExpired(String token) { + Date expirationDate = getAllClaimsFromToken(token).getExpiration(); + return expirationDate.before(new Date()); + } + + /** + * Extract claims from the token + * @param token the JWT token + * @return all claims from the token + */ + public Claims getAllClaimsFromToken(String token) { + return Jwts.parser() .verifyWith(getSigningKey()) .build() .parseSignedClaims(token) .getPayload(); - } - - - public long getRefreshTokenExpiration() { - return REFRESH_EXPIRATION_TIME; - } - - public String getUserIdFromToken(String token) { - return getAllClaimsFromToken(token).get("userId", String.class); - } - - // Additional helper methods - public String getJtiFromToken(String token) { - return getAllClaimsFromToken(token).getId(); - } - public String getUsernameFromToken(String token) { - return getAllClaimsFromToken(token).getSubject(); - } -} \ No newline at end of file + } + + /** + * Extract a specific claim from the token using a function + * @param token the JWT token + * @param claimsResolver the function to extract the claim + * @param the type of the claim + * @return the extracted claim + */ + public T getClaimFromToken(String token, Function claimsResolver) { + final Claims claims = getAllClaimsFromToken(token); + return claimsResolver.apply(claims); + } + + /** + * Get the JWT ID (JTI) from the token + * @param token the JWT token + * @return the JWT ID + */ + public String getJtiFromToken(String token) { + return getAllClaimsFromToken(token).getId(); + } + + /** + * Get the username from the token + * @param token the JWT token + * @return the username + */ + public String getUsernameFromToken(String token) { + return getAllClaimsFromToken(token).getSubject(); + } + + /** + * Get the user ID from the token + * @param token the JWT token + * @return the user ID + */ + public String getUserIdFromToken(String token) { + return getAllClaimsFromToken(token).get("userId", String.class); + } + + /** + * Get the expiration time of the refresh token + * @return the expiration time in milliseconds + */ + public long getRefreshTokenExpiration() { + return REFRESH_EXPIRATION_TIME; + } +} diff --git a/src/main/java/com/iemr/common/utils/TokenBlacklist.java b/src/main/java/com/iemr/common/utils/TokenBlacklist.java deleted file mode 100644 index c60f177a..00000000 --- a/src/main/java/com/iemr/common/utils/TokenBlacklist.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.iemr.common.utils; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import org.springframework.beans.factory.annotation.Value; - -public class TokenBlacklist { - - - // Store blacklisted tokens (in-memory) - private static final Map blacklistedTokens = new ConcurrentHashMap<>(); - - - // Add a token to the blacklist - public static void blacklistToken(String token ,Long blackListExpirationTime) { - if(token == null || token.trim().isEmpty()) { - return; - } - blacklistedTokens.put(token, System.currentTimeMillis()+ blackListExpirationTime); - } - - // Check if a token is blacklisted - - public static boolean isTokenBlacklisted(String token) { - if(token == null || token.trim().isEmpty()) { - return false; - } - Long expiry = blacklistedTokens.get(token); - if (expiry == null) return false; - // If token is expired, remove it from blacklist and treat as not blacklisted - if (System.currentTimeMillis() > expiry) { - blacklistedTokens.remove(token); - return false; - } - return true; - } - -} diff --git a/src/main/java/com/iemr/common/utils/TokenDenylist.java b/src/main/java/com/iemr/common/utils/TokenDenylist.java new file mode 100644 index 00000000..8c62bc50 --- /dev/null +++ b/src/main/java/com/iemr/common/utils/TokenDenylist.java @@ -0,0 +1,38 @@ +package com.iemr.common.utils; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class TokenDenylist { + + @Autowired + private RedisTemplate redisTemplate; + + // Add a token's jti to the denylist with expiration time + public void denylistToken(String jti, Long expirationTime) { + if (jti == null || jti.trim().isEmpty()) { + return; + } + // Store the jti in Redis with expiration time set to the token's exp time (in milliseconds) + redisTemplate.opsForValue().set(jti, "denied", expirationTime, TimeUnit.MILLISECONDS); + } + + // Check if a token's jti is in the denylist + public boolean isTokenDenylisted(String jti) { + if (jti == null || jti.trim().isEmpty()) { + return false; + } + return redisTemplate.hasKey(jti); + } + + // Remove a token's jti from the denylist (Redis) + public void removeTokenFromDenylist(String jti) { + if (jti != null && !jti.trim().isEmpty()) { + redisTemplate.delete(jti); + } + } +} From e867131b2875468a3097d0e7b0e0c00f20ace5bf Mon Sep 17 00:00:00 2001 From: SR20290919 Date: Fri, 30 May 2025 12:51:22 +0530 Subject: [PATCH 03/13] removing commented code --- .../controller/users/IEMRAdminController.java | 63 ------------------- 1 file changed, 63 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 19f968e6..c0bbce2a 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -981,69 +981,6 @@ private String getJwtTokenFromCookies(HttpServletRequest request) { return null; } - - -// @CrossOrigin() -// @Operation(summary = "Force log out") -// @RequestMapping(value = "/forceLogout", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON, headers = "Authorization") -// public String forceLogout(@RequestBody ForceLogoutRequestModel request, HttpServletRequest httpRequest, HttpServletResponse response) { -// OutputResponse outputResponse = new OutputResponse(); -// try { -// // Perform the force logout logic -// iemrAdminUserServiceImpl.forceLogout(request); -// String token = null; -// token = getJwtTokenFromCookies(httpRequest); -// if(null == token) { -// token = httpRequest.getHeader(Constants.JWT_TOKEN); -// } -// TokenBlacklist.blacklistToken(token,BLACK_LIST_EXPIRATION_TIME); -// // Extract and invalidate JWT token cookie dynamically from the request -// // invalidateJwtCookie(httpRequest, response); -// -// // Set the response message -// outputResponse.setResponse("Success"); -// } catch (Exception e) { -// outputResponse.setError(e); -// } -// return outputResponse.toString(); -// } -// private String getJwtTokenFromCookies(HttpServletRequest request) { -// Cookie[] cookies = request.getCookies(); -// if (cookies != null) { -// for (Cookie cookie : cookies) { -// if (cookie.getName().equalsIgnoreCase("Jwttoken")) { -// return cookie.getValue(); -// } -// } -// } -// return null; -// } -// private void invalidateJwtCookie(HttpServletRequest request, HttpServletResponse response) { -// // Get the cookies from the incoming request -// Cookie[] cookies = request.getCookies(); -// -// if (cookies != null) { -// for (Cookie cookie : cookies) { -// // Check if the cookie name matches "Jwttoken" (case-sensitive) -// if (cookie.getName().equalsIgnoreCase("Jwttoken")) { -// // Invalidate the JWT token cookie by setting the value to null and max age to 0 -// cookie.setValue(null); -// cookie.setMaxAge(0); // Expire the cookie immediately -// cookie.setPath(cookie.getPath()); // Ensure the path matches the cookie's original path -// cookie.setHttpOnly(true); // Secure the cookie so it can't be accessed via JS -// cookie.setSecure(true); // Only send over HTTPS if you're using secure connections -// cookie.setAttribute("SameSite", "Strict"); -// // Add the invalidated cookie back to the response -// response.addCookie(cookie); -// break; // If we found the JWT cookie, no need to continue looping -// } -// } -// } else { -// // Log or handle the case when no cookies are found in the request -// logger.warn("No cookies found in the request."); -// } -// } - @CrossOrigin() @Operation(summary = "User force log out") From 0661080b71aa103e5b436d04256932327bb2a0a6 Mon Sep 17 00:00:00 2001 From: SR20290919 Date: Fri, 30 May 2025 12:55:52 +0530 Subject: [PATCH 04/13] remove unnecessary comments --- src/main/java/com/iemr/common/utils/JwtUtil.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/com/iemr/common/utils/JwtUtil.java b/src/main/java/com/iemr/common/utils/JwtUtil.java index 6be3af57..0a8829dc 100644 --- a/src/main/java/com/iemr/common/utils/JwtUtil.java +++ b/src/main/java/com/iemr/common/utils/JwtUtil.java @@ -97,13 +97,9 @@ public Claims validateToken(String token) { try { return Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(token).getPayload(); } catch (ExpiredJwtException ex) { - // Handle expired token specifically - // Optionally, log the exception or return a custom message if required + return null; // Token is expired, so return null } catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException ex) { - // Log specific error types for debugging - // Optionally, you can log each exception for specific handling or debugging - // Example: logger.error("JWT validation failed: " + ex.getMessage()); return null; // Return null for any other JWT-related issue (invalid format, bad signature, etc.) } } From 1bca9d5ec258de3b2ae514737f9a2b9068e30f08 Mon Sep 17 00:00:00 2001 From: SR20290919 Date: Fri, 30 May 2025 12:57:09 +0530 Subject: [PATCH 05/13] removing unused values --- .../com/iemr/common/controller/users/IEMRAdminController.java | 2 -- 1 file changed, 2 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 c0bbce2a..d8ebbcc3 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -88,8 +88,6 @@ public class IEMRAdminController { private CookieUtil cookieUtil; @Autowired private RedisTemplate redisTemplate; - @Value("${jwt.blacklist.expiration}") - private static long BLACK_LIST_EXPIRATION_TIME; private AESUtil aesUtil; From 5db845890150b78d4d15e7c973e81e2d7942f451 Mon Sep 17 00:00:00 2001 From: SR20290919 Date: Fri, 30 May 2025 13:07:58 +0530 Subject: [PATCH 06/13] adding code rabbit suggestions --- .../controller/users/IEMRAdminController.java | 4 ++-- .../com/iemr/common/utils/TokenDenylist.java | 24 +++++++++++++++---- 2 files changed, 22 insertions(+), 6 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 d8ebbcc3..6838981c 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -971,8 +971,8 @@ private String getJwtTokenFromCookies(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { - if (cookie.getName().equalsIgnoreCase("Jwttoken")) { - return cookie.getValue(); + if (cookie.getName().equalsIgnoreCase(Constants.JWT_TOKEN)) { + return cookie.getValue(); } } } diff --git a/src/main/java/com/iemr/common/utils/TokenDenylist.java b/src/main/java/com/iemr/common/utils/TokenDenylist.java index 8c62bc50..dd8c62fc 100644 --- a/src/main/java/com/iemr/common/utils/TokenDenylist.java +++ b/src/main/java/com/iemr/common/utils/TokenDenylist.java @@ -1,5 +1,7 @@ package com.iemr.common.utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @@ -8,6 +10,7 @@ @Component public class TokenDenylist { + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); @Autowired private RedisTemplate redisTemplate; @@ -17,17 +20,30 @@ public void denylistToken(String jti, Long expirationTime) { if (jti == null || jti.trim().isEmpty()) { return; } + if (expirationTime == null || expirationTime <= 0) { + throw new IllegalArgumentException("Expiration time must be positive"); + } // Store the jti in Redis with expiration time set to the token's exp time (in milliseconds) - redisTemplate.opsForValue().set(jti, "denied", expirationTime, TimeUnit.MILLISECONDS); - } + try { + redisTemplate.opsForValue().set(jti, "denied", expirationTime, TimeUnit.MILLISECONDS); + } catch (Exception e) { + logger.error("Failed to denylist token with jti: " + jti, e); + throw new RuntimeException("Failed to denylist token", e); + } + } // Check if a token's jti is in the denylist public boolean isTokenDenylisted(String jti) { if (jti == null || jti.trim().isEmpty()) { return false; } - return redisTemplate.hasKey(jti); - } + try { + return Boolean.TRUE.equals(redisTemplate.hasKey(jti)); + } catch (Exception e) { + logger.error("Failed to check denylist status for jti: " + jti, e); + // In case of Redis failure, consider the token as not denylisted to avoid blocking all requests + return false; + } } // Remove a token's jti from the denylist (Redis) public void removeTokenFromDenylist(String jti) { From 8d5d3fa12d95ab1377406001a80c718ab2f7a957 Mon Sep 17 00:00:00 2001 From: SR20290919 Date: Fri, 30 May 2025 13:16:21 +0530 Subject: [PATCH 07/13] applying code rabbit suggestions to code --- .../iemr/common/controller/users/IEMRAdminController.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 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 6838981c..489f939a 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -940,8 +940,10 @@ public String forceLogout(@RequestBody ForceLogoutRequestModel request, HttpServ // Extract token from cookies or headers String token = getJwtTokenFromCookies(httpRequest); if (token == null) { - token = httpRequest.getHeader(Constants.JWT_TOKEN); - } + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + outputResponse.setError(new RuntimeException("No JWT token found in request")); + return outputResponse.toString(); + } // Validate the token: Check if it is expired or in the deny list Claims claims = jwtUtil.validateToken(token); From c0e7bfb759dad5ee23c6e0166ec5640242830873 Mon Sep 17 00:00:00 2001 From: SR20290919 Date: Fri, 30 May 2025 14:25:10 +0530 Subject: [PATCH 08/13] fixing issues highlighted by Mithun --- .../controller/users/IEMRAdminController.java | 8 ++--- .../com/iemr/common/utils/TokenDenylist.java | 33 +++++++++++-------- 2 files changed, 23 insertions(+), 18 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 489f939a..bb15d008 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -954,12 +954,12 @@ public String forceLogout(@RequestBody ForceLogoutRequestModel request, HttpServ return outputResponse.toString(); } - // Extract the jti (JWT ID) and expiration time from the JWT token - String jti = jwtUtil.getJtiFromToken(token); - long expirationTime = jwtUtil.getAllClaimsFromToken(token).getExpiration().getTime(); + // Extract the jti (JWT ID) and expiration time from the validated claims + String jti = claims.getId(); // jti is in the 'id' field of claims + long expirationTime = claims.getExpiration().getTime(); // Use expiration from claims // Denylist the token's jti in Redis with its expiration time - tokenDenylist.denylistToken(jti, expirationTime); + tokenDenylist.addTokenToDenylist(jti, expirationTime); // Set the response message outputResponse.setResponse("Success"); diff --git a/src/main/java/com/iemr/common/utils/TokenDenylist.java b/src/main/java/com/iemr/common/utils/TokenDenylist.java index dd8c62fc..d286b39c 100644 --- a/src/main/java/com/iemr/common/utils/TokenDenylist.java +++ b/src/main/java/com/iemr/common/utils/TokenDenylist.java @@ -10,27 +10,31 @@ @Component public class TokenDenylist { - private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + + private static final String PREFIX = "denied_"; @Autowired private RedisTemplate redisTemplate; // Add a token's jti to the denylist with expiration time - public void denylistToken(String jti, Long expirationTime) { + public void addTokenToDenylist(String jti, Long expirationTime) { if (jti == null || jti.trim().isEmpty()) { return; } if (expirationTime == null || expirationTime <= 0) { - throw new IllegalArgumentException("Expiration time must be positive"); - } + throw new IllegalArgumentException("Expiration time must be positive"); + } + // Store the jti in Redis with expiration time set to the token's exp time (in milliseconds) try { - redisTemplate.opsForValue().set(jti, "denied", expirationTime, TimeUnit.MILLISECONDS); - } catch (Exception e) { - logger.error("Failed to denylist token with jti: " + jti, e); - throw new RuntimeException("Failed to denylist token", e); - } + String key = PREFIX + jti; + redisTemplate.opsForValue().set(key, " ", expirationTime * 1000, TimeUnit.MILLISECONDS); + } catch (Exception e) { + logger.error("Failed to denylist token with jti: " + jti, e); + throw new RuntimeException("Failed to denylist token", e); } + } // Check if a token's jti is in the denylist public boolean isTokenDenylisted(String jti) { @@ -39,11 +43,12 @@ public boolean isTokenDenylisted(String jti) { } try { return Boolean.TRUE.equals(redisTemplate.hasKey(jti)); - } catch (Exception e) { - logger.error("Failed to check denylist status for jti: " + jti, e); - // In case of Redis failure, consider the token as not denylisted to avoid blocking all requests - return false; - } } + } catch (Exception e) { + logger.error("Failed to check denylist status for jti: " + jti, e); + // In case of Redis failure, consider the token as not denylisted to avoid blocking all requests + return false; + } + } // Remove a token's jti from the denylist (Redis) public void removeTokenFromDenylist(String jti) { From a43afd6f0696b8540ef91fb4f33c11e583c4b9ad Mon Sep 17 00:00:00 2001 From: SR20290919 Date: Fri, 30 May 2025 14:56:01 +0530 Subject: [PATCH 09/13] adding sonar quality suggested changes --- .../com/iemr/common/controller/users/IEMRAdminController.java | 3 +-- src/main/java/com/iemr/common/utils/TokenDenylist.java | 1 - 2 files changed, 1 insertion(+), 3 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 bb15d008..60f73ca9 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -947,8 +947,7 @@ public String forceLogout(@RequestBody ForceLogoutRequestModel request, HttpServ // Validate the token: Check if it is expired or in the deny list Claims claims = jwtUtil.validateToken(token); - if (claims == null) { - // If token is either expired or in the deny list, return 401 Unauthorized + if (claims.isEmpty() || claims.getExpiration() == null || claims.getId() == null) { // If token is either expired or in the deny list, return 401 Unauthorized response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); outputResponse.setError(new RuntimeException("Token is expired or has been logged out")); return outputResponse.toString(); diff --git a/src/main/java/com/iemr/common/utils/TokenDenylist.java b/src/main/java/com/iemr/common/utils/TokenDenylist.java index d286b39c..6ef35958 100644 --- a/src/main/java/com/iemr/common/utils/TokenDenylist.java +++ b/src/main/java/com/iemr/common/utils/TokenDenylist.java @@ -31,7 +31,6 @@ public void addTokenToDenylist(String jti, Long expirationTime) { String key = PREFIX + jti; redisTemplate.opsForValue().set(key, " ", expirationTime * 1000, TimeUnit.MILLISECONDS); } catch (Exception e) { - logger.error("Failed to denylist token with jti: " + jti, e); throw new RuntimeException("Failed to denylist token", e); } } From 04ebe645a61918d9ad174cb52c0cfbb2740dd5f8 Mon Sep 17 00:00:00 2001 From: SR20290919 Date: Fri, 30 May 2025 16:21:01 +0530 Subject: [PATCH 10/13] logic change for calculating expiration time --- .../iemr/common/controller/users/IEMRAdminController.java | 2 ++ src/main/java/com/iemr/common/utils/TokenDenylist.java | 8 +++++--- 2 files changed, 7 insertions(+), 3 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 60f73ca9..cd9eafcf 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -956,6 +956,8 @@ public String forceLogout(@RequestBody ForceLogoutRequestModel request, HttpServ // Extract the jti (JWT ID) and expiration time from the validated claims String jti = claims.getId(); // jti is in the 'id' field of claims long expirationTime = claims.getExpiration().getTime(); // Use expiration from claims + long ttlMillis = expirationTime - System.currentTimeMillis(); + tokenDenylist.addTokenToDenylist(jti, ttlMillis); // Denylist the token's jti in Redis with its expiration time tokenDenylist.addTokenToDenylist(jti, expirationTime); diff --git a/src/main/java/com/iemr/common/utils/TokenDenylist.java b/src/main/java/com/iemr/common/utils/TokenDenylist.java index 6ef35958..ffcafa91 100644 --- a/src/main/java/com/iemr/common/utils/TokenDenylist.java +++ b/src/main/java/com/iemr/common/utils/TokenDenylist.java @@ -29,7 +29,7 @@ public void addTokenToDenylist(String jti, Long expirationTime) { // Store the jti in Redis with expiration time set to the token's exp time (in milliseconds) try { String key = PREFIX + jti; - redisTemplate.opsForValue().set(key, " ", expirationTime * 1000, TimeUnit.MILLISECONDS); + redisTemplate.opsForValue().set(key, " ", expirationTime, TimeUnit.MILLISECONDS); } catch (Exception e) { throw new RuntimeException("Failed to denylist token", e); } @@ -41,7 +41,8 @@ public boolean isTokenDenylisted(String jti) { return false; } try { - return Boolean.TRUE.equals(redisTemplate.hasKey(jti)); + String key = PREFIX + jti; + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); } catch (Exception e) { logger.error("Failed to check denylist status for jti: " + jti, e); // In case of Redis failure, consider the token as not denylisted to avoid blocking all requests @@ -52,7 +53,8 @@ public boolean isTokenDenylisted(String jti) { // Remove a token's jti from the denylist (Redis) public void removeTokenFromDenylist(String jti) { if (jti != null && !jti.trim().isEmpty()) { - redisTemplate.delete(jti); + String key = PREFIX + jti; + redisTemplate.delete(key); } } } From 25e60e61809547036823ba7eb1a5059183d95c31 Mon Sep 17 00:00:00 2001 From: SR20290919 Date: Fri, 30 May 2025 16:27:01 +0530 Subject: [PATCH 11/13] adding code rabbit suggested changes --- src/main/java/com/iemr/common/utils/TokenDenylist.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/iemr/common/utils/TokenDenylist.java b/src/main/java/com/iemr/common/utils/TokenDenylist.java index ffcafa91..dd5ba8cb 100644 --- a/src/main/java/com/iemr/common/utils/TokenDenylist.java +++ b/src/main/java/com/iemr/common/utils/TokenDenylist.java @@ -17,6 +17,10 @@ public class TokenDenylist { @Autowired private RedisTemplate redisTemplate; + private String getKey(String jti) { + return PREFIX + jti; + } + // Add a token's jti to the denylist with expiration time public void addTokenToDenylist(String jti, Long expirationTime) { if (jti == null || jti.trim().isEmpty()) { @@ -28,7 +32,7 @@ public void addTokenToDenylist(String jti, Long expirationTime) { // Store the jti in Redis with expiration time set to the token's exp time (in milliseconds) try { - String key = PREFIX + jti; + String key = getKey(jti); // Use helper method to get the key redisTemplate.opsForValue().set(key, " ", expirationTime, TimeUnit.MILLISECONDS); } catch (Exception e) { throw new RuntimeException("Failed to denylist token", e); @@ -41,7 +45,7 @@ public boolean isTokenDenylisted(String jti) { return false; } try { - String key = PREFIX + jti; + String key = getKey(jti); // Use helper method to get the key return Boolean.TRUE.equals(redisTemplate.hasKey(key)); } catch (Exception e) { logger.error("Failed to check denylist status for jti: " + jti, e); @@ -53,7 +57,7 @@ public boolean isTokenDenylisted(String jti) { // Remove a token's jti from the denylist (Redis) public void removeTokenFromDenylist(String jti) { if (jti != null && !jti.trim().isEmpty()) { - String key = PREFIX + jti; + String key = getKey(jti); // Use helper method to get the key redisTemplate.delete(key); } } From 9efda6dda4ca4e9bfb3f7c0473b22bd9495ff3f4 Mon Sep 17 00:00:00 2001 From: SR20290919 Date: Fri, 30 May 2025 16:33:10 +0530 Subject: [PATCH 12/13] removing duplicate line --- .../com/iemr/common/controller/users/IEMRAdminController.java | 3 --- 1 file changed, 3 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 cd9eafcf..f46b9653 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -959,9 +959,6 @@ public String forceLogout(@RequestBody ForceLogoutRequestModel request, HttpServ long ttlMillis = expirationTime - System.currentTimeMillis(); tokenDenylist.addTokenToDenylist(jti, ttlMillis); - // Denylist the token's jti in Redis with its expiration time - tokenDenylist.addTokenToDenylist(jti, expirationTime); - // Set the response message outputResponse.setResponse("Success"); } catch (Exception e) { From c4d83c65c5ab02d4391fe0100d3d6879e42e194c Mon Sep 17 00:00:00 2001 From: SR20290919 Date: Fri, 30 May 2025 16:40:58 +0530 Subject: [PATCH 13/13] removed comment --- src/main/java/com/iemr/common/utils/TokenDenylist.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/iemr/common/utils/TokenDenylist.java b/src/main/java/com/iemr/common/utils/TokenDenylist.java index dd5ba8cb..660499af 100644 --- a/src/main/java/com/iemr/common/utils/TokenDenylist.java +++ b/src/main/java/com/iemr/common/utils/TokenDenylist.java @@ -30,7 +30,6 @@ public void addTokenToDenylist(String jti, Long expirationTime) { throw new IllegalArgumentException("Expiration time must be positive"); } - // Store the jti in Redis with expiration time set to the token's exp time (in milliseconds) try { String key = getKey(jti); // Use helper method to get the key redisTemplate.opsForValue().set(key, " ", expirationTime, TimeUnit.MILLISECONDS);