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 c05436e1..e26f15dd 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -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; @@ -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; @@ -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; @@ -94,8 +82,6 @@ public class IEMRAdminController { @Autowired private CookieUtil cookieUtil; @Autowired - private JwtAuthenticationUtil jwtAuthenticationUtil; - @Autowired private RedisTemplate redisTemplate; private AESUtil aesUtil; @@ -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( + "refresh:" + jti, + user.getUserID().toString(), + jwtUtil.getRefreshTokenExpiration(), + TimeUnit.MILLISECONDS + ); + } 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); @@ -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); @@ -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 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 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) @@ -740,7 +813,7 @@ public String userLogout(HttpServletRequest request) { /** * - * @param request + * @param key * @return */ private void deleteSessionObjectByGettingSessionDetails(String key) { diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java index 58ed2b00..3f1d8068 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java @@ -116,7 +116,7 @@ public List getUserServiceRoleMappingForProvider(Integ String generateTransactionIdForPasswordChange(User user) throws Exception; - + User getUserById(Long userId) throws IEMRException; diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index 2eb2ee2e..b83f4873 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -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; } @@ -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) { @@ -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) { diff --git a/src/main/java/com/iemr/common/utils/JwtUtil.java b/src/main/java/com/iemr/common/utils/JwtUtil.java index a6e8b942..c0241954 100644 --- a/src/main/java/com/iemr/common/utils/JwtUtil.java +++ b/src/main/java/com/iemr/common/utils/JwtUtil.java @@ -1,16 +1,21 @@ 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 { @@ -18,51 +23,79 @@ 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}") + 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; } - public String extractUsername(String token) { - return extractClaim(token, Claims::getSubject); + public T getClaimFromToken(String token, Function claimsResolver) { + final Claims claims = getAllClaimsFromToken(token); + return claimsResolver.apply(claims); } - public T extractClaim(String token, Function 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(); } - private Claims extractAllClaims(String token) { - return Jwts.parser().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody(); + public String getUsernameFromToken(String token) { + return getAllClaimsFromToken(token).getSubject(); } } \ No newline at end of file diff --git a/src/main/java/com/iemr/common/utils/UserAgentUtil.java b/src/main/java/com/iemr/common/utils/UserAgentUtil.java new file mode 100644 index 00000000..e6b0dbce --- /dev/null +++ b/src/main/java/com/iemr/common/utils/UserAgentUtil.java @@ -0,0 +1,9 @@ +package com.iemr.common.utils; + +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"); + } +} diff --git a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java index a31b2a2b..b8359fe6 100644 --- a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java +++ b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java @@ -101,13 +101,12 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons case "api-docs": case "updateBenCallIdsInPhoneBlock": case "userAuthenticateByEncryption": - case "sendOTP": case "validateOTP": case "resendOTP": case "validateSecurityQuestionAndAnswer": case "logOutUserFromConcurrentSession": - + case "refreshToken": break; case "error": status = false; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d645f273..9418e973 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -179,3 +179,7 @@ quality-Audit-PageSize=5 ## max no of failed login attempt failedLoginAttempt=5 + +#Jwt Token configuration +jwt.access.expiration=900000 +jwt.refresh.expiration=604800000