Skip to content

Enhance JWT Handling: Return Token in Response for Mobile, Add Secure Refresh API, and Update Middleware #181

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 101 additions & 28 deletions src/main/java/com/iemr/common/controller/users/IEMRAdminController.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,14 @@
*/
package com.iemr.common.controller.users;

import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.TimeUnit;

import javax.ws.rs.core.MediaType;

import com.iemr.common.utils.UserAgentUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
Expand All @@ -40,20 +37,13 @@
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.iemr.common.config.encryption.SecurePassword;
import com.iemr.common.data.directory.Directory;
import com.iemr.common.data.users.LoginSecurityQuestions;
import com.iemr.common.data.users.M_Role;
import com.iemr.common.data.users.ServiceRoleScreenMapping;
Expand All @@ -63,10 +53,8 @@
import com.iemr.common.model.user.ChangePasswordModel;
import com.iemr.common.model.user.ForceLogoutRequestModel;
import com.iemr.common.model.user.LoginRequestModel;
import com.iemr.common.model.user.LoginResponseModel;
import com.iemr.common.service.users.IEMRAdminUserService;
import com.iemr.common.utils.CookieUtil;
import com.iemr.common.utils.JwtAuthenticationUtil;
import com.iemr.common.utils.JwtUtil;
import com.iemr.common.utils.encryption.AESUtil;
import com.iemr.common.utils.exception.IEMRException;
Expand Down Expand Up @@ -94,8 +82,6 @@ public class IEMRAdminController {
@Autowired
private CookieUtil cookieUtil;
@Autowired
private JwtAuthenticationUtil jwtAuthenticationUtil;
@Autowired
private RedisTemplate<String, Object> redisTemplate;

private AESUtil aesUtil;
Expand Down Expand Up @@ -164,21 +150,39 @@ public String userAuthenticate(
} else if (m_User.getUserName() != null && m_User.getDoLogout() != null && m_User.getDoLogout() == true) {
deleteSessionObject(m_User.getUserName().trim().toLowerCase());
}

String jwtToken = null;
String refreshToken = null;
boolean isMobile = false;
if (mUser.size() == 1) {
String Jwttoken = jwtUtil.generateToken(m_User.getUserName(), mUser.get(0).getUserID().toString());
logger.info("jwt token is:" + Jwttoken);
jwtToken = jwtUtil.generateToken(m_User.getUserName(), mUser.get(0).getUserID().toString());

User user = new User(); // Assuming the Users class exists
user.setUserID(mUser.get(0).getUserID());
user.setUserName(mUser.get(0).getUserName());

String redisKey = "user_" + mUser.get(0).getUserID(); // Use user ID to create a unique key

// Store the user in Redis (set a TTL of 30 minutes)
redisTemplate.opsForValue().set(redisKey, user, 30, TimeUnit.MINUTES);
String userAgent = request.getHeader("User-Agent");
isMobile = UserAgentUtil.isMobileDevice(userAgent);
logger.info("UserAgentUtil isMobile : " + isMobile);

if (isMobile) {
refreshToken = jwtUtil.generateRefreshToken(m_User.getUserName(), user.getUserID().toString());
logger.debug("Refresh token generated successfully for user: {}", user.getUserName());
String jti = jwtUtil.getJtiFromToken(refreshToken);
redisTemplate.opsForValue().set(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the expiry of refresh token? Are we storing it in Redis?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jwt.access.expiration=900000 (in ms)
jwt.refresh.expiration=604800000(in ms)

can be modified from .properties files

"refresh:" + jti,
user.getUserID().toString(),
jwtUtil.getRefreshTokenExpiration(),
TimeUnit.MILLISECONDS
);
Comment on lines +172 to +177
Copy link
Contributor

@coderabbitai coderabbitai bot Apr 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Document the refresh token expiration strategy.

The code uses jwtUtil.getRefreshTokenExpiration() without any inline documentation explaining its value or the reasoning behind it.


🌐 Web query:

What is a typical expiration time for JWT refresh tokens?

💡 Result:

JWT refresh token expiration times vary based on security requirements and system design, but common configurations range from 7 days to 90 days, with some systems allowing configurations up to 1 year. Here's a breakdown of typical practices:

Common Expiration Periods

  • **Short

Citations:


Document JWT Refresh Token Expiration Strategy Inline

At src/main/java/com/iemr/common/controller/users/IEMRAdminController.java (lines 172-177), please add inline documentation explaining the refresh token expiration strategy. For example, clarify that jwtUtil.getRefreshTokenExpiration() returns the token's lifetime in milliseconds and mention that typical refresh token expirations usually range from 7 to 90 days (or more, depending on security requirements). This added context will help future maintainers understand the rationale behind selecting this expiration period.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

already added

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Examining JwtUtil class for documentation about refresh token expiration
cat src/main/java/com/iemr/common/utils/JwtUtil.java | grep -A 15 "getRefreshTokenExpiration"

# Also check application.properties for any expiration configuration
cat src/main/resources/application.properties | grep -i "jwt\|expiration\|token"

Length of output: 581


@kevalkanp1011 I see that you've configured the refresh token expiration in application.properties (jwt.refresh.expiration=604800000, which is 7 days). While the configuration value exists, adding a brief comment in the code where it's used would help future developers understand your token strategy without having to search through configuration files. This is especially helpful for explaining why you chose 7 days and any security considerations that went into that decision.


🧬 Code Graph Analysis Results

Relevant Code Snippets

1. UserAgentUtil.java

Filename: src/main/java/com/iemr/common/utils/UserAgentUtil.java
Line Range: 3-9

public class UserAgentUtil {
    public static boolean isMobileDevice(String userAgent) {
        if (userAgent == null) return false;
        String lowerUA = userAgent.toLowerCase();
        return lowerUA.contains("mobile") || lowerUA.contains("android") || lowerUA.contains("iphone");
    }
}

Description: This utility class contains a method to check if the user agent string corresponds to a mobile device. It returns true if the user agent indicates a mobile device, otherwise false.

2. IEMRAdminController.java (userAuthenticate method)

Filename: src/main/java/com/iemr/common/controller/users/IEMRAdminController.java
Line Range: 70-130

@CrossOrigin()
@Operation(summary = "User authentication")
@RequestMapping(value = "/userAuthenticate", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON)
public String userAuthenticate(
        @Param(value = "\"{\\\"userName\\\":\\\"String\\\",\\\"password\\\":\\\"String\\\"}\"") @RequestBody LoginRequestModel m_User,
        HttpServletRequest request, HttpServletResponse httpResponse) {
    OutputResponse response = new OutputResponse();
    logger.info("userAuthenticate request - " + m_User + " " + m_User.getUserName() + " " + m_User.getPassword());
    try {
        String decryptPassword = aesUtil.decrypt("Piramal12Piramal", m_User.getPassword());
        logger.info("decryptPassword : " + decryptPassword);
        List<User> mUser = iemrAdminUserServiceImpl.userAuthenticate(m_User.getUserName(), decryptPassword);
        // Additional logic for authentication...
    } catch (Exception e) {
        logger.error("userAuthenticate failed with error " + e.getMessage(), e);
        response.setError(e);
    }
    logger.info("userAuthenticate response " + response.toString());
    return response.toString();
}

Description: This method handles user authentication. It decrypts the provided password, checks the user credentials, and logs the authentication process. It returns a response indicating the success or failure of the authentication.

3. IEMRAdminController.java (userAuthenticateNew method)

Filename: src/main/java/com/iemr/common/controller/users/IEMRAdminController.java
Line Range: 50-68

@CrossOrigin()
@Operation(summary = "New user authentication")
@RequestMapping(value = "/userAuthenticateNew", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON)
public String userAuthenticateNew(
        @Param(value = "\"{\\\"userName\\\":\\\"String\\\",\\\"password\\\":\\\"String\\\"}\"") @RequestBody String jsonRequest) {
    OutputResponse response = new OutputResponse();
    try {
        User m_user = inputMapper.gson().fromJson(jsonRequest, User.class);
        response.setResponse("hello.....");
    } catch (Exception e) {
        response.setError(e);
    }
    return response.toString();
}

Description: This method is designed for new user authentication. It parses the incoming JSON request to extract user credentials and sets a response. The implementation is basic and primarily serves as a placeholder.

4. IEMRAdminController.java (logOutUserFromConcurrentSession method)

Filename: src/main/java/com/iemr/common/controller/users/IEMRAdminController.java
Line Range: 140-180

@CrossOrigin()
@Operation(summary = "Log out user from concurrent session")
@RequestMapping(value = "/logOutUserFromConcurrentSession", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON)
public String logOutUserFromConcurrentSession(
        @Param(value = "\"{\\\"userName\\\":\\\"String\\\"}\"") @RequestBody LoginRequestModel m_User,
        HttpServletRequest request) {
    OutputResponse response = new OutputResponse();
    logger.info("logOutUserFromConcurrentSession request - " + m_User);
    try {
        if (m_User != null && m_User.getUserName() != null) {
            List<User> mUsers = iemrAdminUserServiceImpl.userExitsCheck(m_User.getUserName());
            // Additional logic for logging out...
        }
    } catch (Exception e) {
        logger.error("logOutUserFromConcurrentSession failed with error " + e.getMessage(), e);
        response.setError(e);
    }
    logger.info("logOutUserFromConcurrentSession response " + response.toString());
    return response.toString();
}

Description: This method logs out a user from a concurrent session. It checks if the user exists and performs the necessary actions to log them out, handling any exceptions that may occur during the process.

5. IEMRAdminController.java (refreshToken method)

Filename: src/main/java/com/iemr/common/controller/users/IEMRAdminController.java
Line Range: 130-140

@Operation(summary = "generating a auth token with the refreshToken.")
@RequestMapping(value = "/refreshToken", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON)
public ResponseEntity<?> refreshToken(@RequestBody Map<String, String> request) {
    String refreshToken = request.get("refreshToken");
    // Logic for refreshing token...
}

Description: This method handles the generation of a new authentication token using a provided refresh token. It validates the refresh token and generates a new JWT if valid.

These snippets provide context on user authentication and session management, which may relate to the user's comment about something being "already added."

} else {
cookieUtil.addJwtTokenToCookie(jwtToken, httpResponse, request);
}

String redisKey = "user_" + mUser.get(0).getUserID(); // Use user ID to create a unique key

// Set Jwttoken in the response cookie
cookieUtil.addJwtTokenToCookie(Jwttoken, httpResponse, request);
// Store the user in Redis (set a TTL of 30 minutes)
redisTemplate.opsForValue().set(redisKey, user, 30, TimeUnit.MINUTES);

createUserMapping(mUser.get(0), resMap, serviceRoleMultiMap, serviceRoleMap, serviceRoleList,
previlegeObj);
Expand All @@ -199,6 +203,13 @@ public String userAuthenticate(
}
responseObj = iemrAdminUserServiceImpl.generateKeyAndValidateIP(responseObj, remoteAddress,
request.getRemoteHost());

// Add tokens to response for mobile
if (isMobile && !mUser.isEmpty()) {
responseObj.put("jwtToken", jwtToken);
responseObj.put("refreshToken", refreshToken);
}

response.setResponse(responseObj.toString());
} catch (Exception e) {
logger.error("userAuthenticate failed with error " + e.getMessage(), e);
Expand All @@ -208,6 +219,68 @@ public String userAuthenticate(
return response.toString();
}

@Operation(summary = "generating a auth token with the refreshToken.")
@RequestMapping(value = "/refreshToken", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON)
public ResponseEntity<?> refreshToken(@RequestBody Map<String, String> request) {
String refreshToken = request.get("refreshToken");

try {
if (jwtUtil.validateToken(refreshToken) == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid token");
}

Claims claims = jwtUtil.getAllClaimsFromToken(refreshToken);

// Verify token type
if (!"refresh".equals(claims.get("token_type", String.class))) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid token type");
}

// Check revocation using JTI
String jti = claims.getId();
if (!redisTemplate.hasKey("refresh:" + jti)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Token revoked");
}

// Get user details
// Get user details
String userId = claims.get("userId", String.class);
User user = iemrAdminUserServiceImpl.getUserById(Long.parseLong(userId));

// Validate that the user still exists and is active
if (user == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("User not found");
}

if (user.getM_status() == null || !"Active".equalsIgnoreCase(user.getM_status().getStatus())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("User account is inactive");
}
// Generate new tokens
String newJwt = jwtUtil.generateToken(user.getUserName(), userId);

Map<String, String> tokens = new HashMap<>();
tokens.put("jwtToken", newJwt);

// Generate and store a new refresh token (token rotation)
String newRefreshToken = jwtUtil.generateRefreshToken(user.getUserName(), userId);
String newJti = jwtUtil.getJtiFromToken(newRefreshToken);
redisTemplate.opsForValue().set(
"refresh:" + newJti,
userId,
jwtUtil.getRefreshTokenExpiration(),
TimeUnit.MILLISECONDS
);
tokens.put("refreshToken", newRefreshToken);

return ResponseEntity.ok(tokens);
} catch (ExpiredJwtException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Token expired");
} catch (Exception e) {
logger.error("Refresh failed: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Token refresh failed");
}
}

@CrossOrigin()
@Operation(summary = "Log out user from concurrent session")
@RequestMapping(value = "/logOutUserFromConcurrentSession", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON)
Expand Down Expand Up @@ -740,7 +813,7 @@ public String userLogout(HttpServletRequest request) {

/**
*
* @param request
* @param key
* @return
*/
private void deleteSessionObjectByGettingSessionDetails(String key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public List<ServiceRoleScreenMapping> getUserServiceRoleMappingForProvider(Integ

String generateTransactionIdForPasswordChange(User user) throws Exception;


User getUserById(Long userId) throws IEMRException;



Expand Down
27 changes: 17 additions & 10 deletions src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,9 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo
String jwtTokenFromHeader = request.getHeader("Jwttoken");
logger.info("JWT token from header: ");

// Skip login and public endpoints
if (path.equals(contextPath + "/user/userAuthenticate")
|| path.equalsIgnoreCase(contextPath + "/user/logOutUserFromConcurrentSession")
|| path.startsWith(contextPath + "/swagger-ui")
|| path.startsWith(contextPath + "/v3/api-docs")
|| path.startsWith(contextPath + "/public")) {
logger.info("Skipping filter for path: " + path);
// Skip authentication for public endpoints
if (shouldSkipAuthentication(path, contextPath)) {
logger.info("Skipping filter for path: {}", path);
filterChain.doFilter(servletRequest, servletResponse);
return;
}
Expand All @@ -71,17 +67,18 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo
// Determine which token (cookie or header) to validate
String jwtToken = jwtTokenFromCookie != null ? jwtTokenFromCookie : jwtTokenFromHeader;
if (jwtToken == null) {
logger.error("JWT token not found in cookies or headers");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT token not found in cookies or headers");
return;
}

// Validate JWT token and userId
boolean isValid = jwtAuthenticationUtil.validateUserIdAndJwtToken(jwtToken);

if (isValid) {
if (jwtAuthenticationUtil.validateUserIdAndJwtToken(jwtToken)) {
// If token is valid, allow the request to proceed
logger.info("Valid JWT token");
filterChain.doFilter(servletRequest, servletResponse);
} else {
logger.error("Invalid JWT token");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token");
}
} catch (Exception e) {
Expand All @@ -90,6 +87,16 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo
}
}

private boolean shouldSkipAuthentication(String path, String contextPath) {
return path.equals(contextPath + "/user/userAuthenticate")
|| path.equalsIgnoreCase(contextPath + "/user/logOutUserFromConcurrentSession")
|| path.startsWith(contextPath + "/swagger-ui")
|| path.startsWith(contextPath + "/v3/api-docs")
|| path.startsWith(contextPath + "/public")
|| path.equals(contextPath + "/user/refreshToken")
;
}

private String getJwtTokenFromCookies(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
Expand Down
87 changes: 60 additions & 27 deletions src/main/java/com/iemr/common/utils/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -1,68 +1,101 @@
package com.iemr.common.utils;

import java.security.Key;
import java.util.Date;
import java.util.UUID;
import java.util.function.Function;

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.SignatureAlgorithm;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;

import javax.crypto.SecretKey;

@Component
public class JwtUtil {

@Value("${jwt.secret}")
private String SECRET_KEY;

private static final long EXPIRATION_TIME = 24L * 60 * 60 * 1000; // 1 day in milliseconds
@Value("${jwt.access.expiration}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these new env variabes. If yes, please add them to application properties

Copy link
Contributor Author

@kevalkanp1011 kevalkanp1011 Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently its configured to change from environment specific .properties files. bcz jwt-secret was listed in these files . so i put just below of it to easily find out

private long ACCESS_EXPIRATION_TIME;

@Value("${jwt.refresh.expiration}")
private long REFRESH_EXPIRATION_TIME;

// Generate a key using the secret
private Key getSigningKey() {
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 JWT Token
public String generateToken(String username, String userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + EXPIRATION_TIME);
return buildToken(username, userId, "access", ACCESS_EXPIRATION_TIME);
}

// Include the userId in the JWT claims
return Jwts.builder().setSubject(username).claim("userId", userId) // Add userId as a claim
.setIssuedAt(now).setExpiration(expiryDate).signWith(getSigningKey(), SignatureAlgorithm.HS256)
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();
}

// Validate and parse JWT Token
public Claims validateToken(String token) {
try {
// Use the JwtParserBuilder correctly in version 0.12.6
return Jwts.parser() // Correct method in 0.12.6 to get JwtParserBuilder
.setSigningKey(getSigningKey()) // Set the signing key
.build() // Build the JwtParser
.parseClaimsJws(token) // Parse and validate the token
.getBody();
} catch (Exception e) {
return null; // Handle token parsing/validation errors
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;
}
Comment on lines 59 to 73
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve exception handling in validateToken method

While the updated validation logic using the new JWT API is good, the exception handling needs improvement.

The current implementation has empty catch blocks that silently ignore errors. This will make debugging difficult and hide potential security issues.

public Claims validateToken(String token) {
    try {
        return Jwts.parser()
                .verifyWith(getSigningKey())
                .build()
                .parseSignedClaims(token)
                .getPayload();

    } catch (ExpiredJwtException ex) {
-        // Handle expired token specifically if needed
+        // Log the expired token with specific error code
+        logger.warn("JWT token has expired: {}", ex.getMessage());
    } catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException ex) {
-        // Log specific error types
+        // Log the specific error type for security monitoring
+        logger.error("JWT validation error: {} - {}", ex.getClass().getName(), ex.getMessage());
    }
    return null;
}

Consider adding a logging mechanism (e.g., SLF4J) to record these exceptions for security monitoring and debugging purposes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public Claims validateToken(String token) {
try {
// Use the JwtParserBuilder correctly in version 0.12.6
return Jwts.parser() // Correct method in 0.12.6 to get JwtParserBuilder
.setSigningKey(getSigningKey()) // Set the signing key
.build() // Build the JwtParser
.parseClaimsJws(token) // Parse and validate the token
.getBody();
} catch (Exception e) {
return null; // Handle token parsing/validation errors
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 Claims validateToken(String token) {
try {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
} catch (ExpiredJwtException ex) {
// Log the expired token with specific error code
logger.warn("JWT token has expired: {}", ex.getMessage());
} catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException ex) {
// Log the specific error type for security monitoring
logger.error("JWT validation error: {} - {}", ex.getClass().getName(), ex.getMessage());
}
return null;
}


public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
Comment on lines +75 to 78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add exception handling to token claim extraction methods

The claim extraction methods are well-structured, but they lack exception handling.

When these methods are called with invalid tokens, they will throw uncaught exceptions that could crash the application. Add proper exception handling:

public Claims getAllClaimsFromToken(String token) {
+    try {
        return Jwts.parser()
                .verifyWith(getSigningKey())
                .build()
                .parseSignedClaims(token)
                .getPayload();
+    } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | 
+             SignatureException | IllegalArgumentException ex) {
+        // Log the specific error
+        logger.error("Error extracting claims from token: {}", ex.getMessage());
+        return null;
+    }
}

The same exception handling should be applied to getClaimFromToken or it should handle null returns from getAllClaimsFromToken.

Also applies to: 80-86


public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
public Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}


public long getRefreshTokenExpiration() {
return REFRESH_EXPIRATION_TIME;
}

// Additional helper methods
public String getJtiFromToken(String token) {
return getAllClaimsFromToken(token).getId();
}
Comment on lines +94 to 96
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add exception handling to token extraction helper methods

The helper methods for extracting specific claims from tokens should include exception handling.

These methods directly call getAllClaimsFromToken() which currently doesn't handle exceptions. If a method is called with an invalid token, it will propagate exceptions to the caller. Add try-catch blocks or ensure getAllClaimsFromToken() handles exceptions properly.

public String getJtiFromToken(String token) {
+    Claims claims = getAllClaimsFromToken(token);
+    return claims != null ? claims.getId() : null;
-    return getAllClaimsFromToken(token).getId();
}

public String getUsernameFromToken(String token) {
+    Claims claims = getAllClaimsFromToken(token);
+    return claims != null ? claims.getSubject() : null;
-    return getAllClaimsFromToken(token).getSubject();
}

Also applies to: 98-100


private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody();
public String getUsernameFromToken(String token) {
return getAllClaimsFromToken(token).getSubject();
}
}
Loading