diff --git a/pom.xml b/pom.xml index 7a0ab6a..25ea26c 100644 --- a/pom.xml +++ b/pom.xml @@ -39,10 +39,10 @@ org.springframework.boot spring-boot-starter - - co.elastic.logging - logback-ecs-encoder - 1.3.2 + + co.elastic.logging + logback-ecs-encoder + 1.3.2 org.springframework.boot @@ -115,8 +115,7 @@ 1.5.5.Final - + org.springframework.boot spring-boot-starter-data-jpa @@ -146,7 +145,7 @@ poi-ooxml 5.3.0 - + jakarta.ws.rs @@ -154,7 +153,7 @@ 3.1.0 - + com.google.guava guava @@ -192,14 +191,13 @@ org.springframework.boot spring-boot-starter-mail - - + + org.springframework.boot spring-boot-starter-data-redis - + org.springframework.session spring-session-data-redis @@ -219,8 +217,7 @@ jackson-datatype-joda 2.17.0 - + com.fasterxml.jackson.core jackson-databind @@ -232,9 +229,30 @@ jackson-core 2.17.0-rc1 + + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + + - + 104api-v3.0.0 @@ -318,8 +336,8 @@ concatenating properties file ${target-properties} and ${source-properties} - + diff --git a/src/main/environment/104_ci.properties b/src/main/environment/104_ci.properties index 7c54000..441d38b 100644 --- a/src/main/environment/104_ci.properties +++ b/src/main/environment/104_ci.properties @@ -18,4 +18,5 @@ common-url=@env.COMMON_API_BASE_URL@ spring.redis.host=localhost #ELK logging file name -logging.file.name=@env.HELPLINE104_API_LOGGING_FILE_NAME@ \ No newline at end of file +logging.file.name=@env.HELPLINE104_API_LOGGING_FILE_NAME@ +jwt.secret=@env.JWT_SECRET_KEY@ \ No newline at end of file diff --git a/src/main/environment/104_dev.properties b/src/main/environment/104_dev.properties index 3e9aa56..308886a 100644 --- a/src/main/environment/104_dev.properties +++ b/src/main/environment/104_dev.properties @@ -17,4 +17,4 @@ common-url=/commonapi-v1.0 ### Redis IP spring.redis.host=localhost - +jwt.secret= diff --git a/src/main/environment/104_example.properties b/src/main/environment/104_example.properties index 3552f5c..d1fb345 100644 --- a/src/main/environment/104_example.properties +++ b/src/main/environment/104_example.properties @@ -16,4 +16,4 @@ common-url=http://localhost:8080/commonapi-v1.0 ### Redis IP spring.redis.host=localhost - +jwt.secret= diff --git a/src/main/environment/104_test.properties b/src/main/environment/104_test.properties index 2edfe2e..82c660c 100644 --- a/src/main/environment/104_test.properties +++ b/src/main/environment/104_test.properties @@ -17,7 +17,7 @@ common-url=/commonapi-v1.0 ### Redis IP spring.redis.host=localhost - +jwt.secret= diff --git a/src/main/environment/104_uat.properties b/src/main/environment/104_uat.properties index 74cf317..daf1885 100644 --- a/src/main/environment/104_uat.properties +++ b/src/main/environment/104_uat.properties @@ -16,4 +16,4 @@ common-url=/commonapi-v1.0 ### Redis IP spring.redis.host=localhost - +jwt.secret= diff --git a/src/main/java/com/iemr/helpline104/App.java b/src/main/java/com/iemr/helpline104/App.java index e24eac0..4dd7bfd 100644 --- a/src/main/java/com/iemr/helpline104/App.java +++ b/src/main/java/com/iemr/helpline104/App.java @@ -25,14 +25,16 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; -import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; +import com.iemr.helpline104.data.users.M_User; import com.iemr.helpline104.utils.IEMRApplBeans; import com.iemr.helpline104.utils.config.ConfigProperties; @@ -50,20 +52,18 @@ public class App extends SpringBootServletInitializer { @Bean public ConfigProperties configProperties() { - return new ConfigProperties(); - } - + return new ConfigProperties(); + } + public static void main(String[] args) { SpringApplication.run(applicationClass, args); } - + @Bean - public IEMRApplBeans instantiateBeans() - { + public IEMRApplBeans instantiateBeans() { return new IEMRApplBeans(); } - @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(applicationClass); @@ -75,12 +75,25 @@ protected SpringApplicationBuilder configure(SpringApplicationBuilder applicatio @RestController class HelloController { - @PostMapping(value ="/hello/{name}", produces = MediaType.APPLICATION_JSON) + @PostMapping(value = "/hello/{name}", produces = MediaType.APPLICATION_JSON) public String hello(@PathVariable String name) { return "Hi " + name + " !"; } -} + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(factory); + + // Use StringRedisSerializer for keys (userId) + template.setKeySerializer(new StringRedisSerializer()); + + // Use Jackson2JsonRedisSerializer for values (Users objects) + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(M_User.class); + template.setValueSerializer(serializer); + return template; + } +} diff --git a/src/main/java/com/iemr/helpline104/config/RedisConfig.java b/src/main/java/com/iemr/helpline104/config/RedisConfig.java new file mode 100644 index 0000000..755b966 --- /dev/null +++ b/src/main/java/com/iemr/helpline104/config/RedisConfig.java @@ -0,0 +1,38 @@ +package com.iemr.helpline104.config; + +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.session.data.redis.config.ConfigureRedisAction; + +import com.iemr.helpline104.data.users.M_User; + +@Configuration +@EnableCaching +public class RedisConfig { + + @Bean + public ConfigureRedisAction configureRedisAction() { + return ConfigureRedisAction.NO_OP; + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(factory); + + // Use StringRedisSerializer for keys (userId) + template.setKeySerializer(new StringRedisSerializer()); + + // Use Jackson2JsonRedisSerializer for values (Users objects) + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(M_User.class); + template.setValueSerializer(serializer); + + return template; + } + +} diff --git a/src/main/java/com/iemr/helpline104/data/users/M_User.java b/src/main/java/com/iemr/helpline104/data/users/M_User.java index 28d2313..853b147 100644 --- a/src/main/java/com/iemr/helpline104/data/users/M_User.java +++ b/src/main/java/com/iemr/helpline104/data/users/M_User.java @@ -21,6 +21,7 @@ */ package com.iemr.helpline104.data.users; +import java.io.Serializable; import java.sql.Timestamp; import java.util.Set; @@ -35,12 +36,14 @@ import jakarta.persistence.OrderBy; import jakarta.persistence.Transient; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.iemr.helpline104.data.userbeneficiarydata.M_Gender; import com.iemr.helpline104.data.userbeneficiarydata.M_MaritalStatus; import com.iemr.helpline104.data.userbeneficiarydata.M_Status; @Entity -public class M_User { +@JsonIgnoreProperties(ignoreUnknown = true) +public class M_User implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long UserID; diff --git a/src/main/java/com/iemr/helpline104/repository/users/IEMRUserRepositoryCustom.java b/src/main/java/com/iemr/helpline104/repository/users/IEMRUserRepositoryCustom.java index 3ace45a..84cd04c 100644 --- a/src/main/java/com/iemr/helpline104/repository/users/IEMRUserRepositoryCustom.java +++ b/src/main/java/com/iemr/helpline104/repository/users/IEMRUserRepositoryCustom.java @@ -59,4 +59,7 @@ public interface IEMRUserRepositoryCustom extends CrudRepository { @Query("UPDATE M_User u set u.StatusID = 2 where u.UserID = :userId") int updateSetUserStatusActive(@Param("userId") Long userId); + @Query(" SELECT u FROM M_User u WHERE u.UserID = :UserID AND u.Deleted = false ") + public M_User getUserByUserID(@Param("UserID") Long UserID); + } diff --git a/src/main/java/com/iemr/helpline104/utils/CookieUtil.java b/src/main/java/com/iemr/helpline104/utils/CookieUtil.java new file mode 100644 index 0000000..a5a19a3 --- /dev/null +++ b/src/main/java/com/iemr/helpline104/utils/CookieUtil.java @@ -0,0 +1,31 @@ +package com.iemr.helpline104.utils; + +import java.util.Arrays; +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Service +public class CookieUtil { + + public Optional getCookieValue(HttpServletRequest request, String cookieName) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookieName.equals(cookie.getName())) { + return Optional.of(cookie.getValue()); + } + } + } + return Optional.empty(); + } + + public String getJwtTokenFromCookie(HttpServletRequest request) { + return Arrays.stream(request.getCookies()).filter(cookie -> "Jwttoken".equals(cookie.getName())) + .map(Cookie::getValue).findFirst().orElse(null); + } +} diff --git a/src/main/java/com/iemr/helpline104/utils/FilterConfig.java b/src/main/java/com/iemr/helpline104/utils/FilterConfig.java new file mode 100644 index 0000000..1e669b7 --- /dev/null +++ b/src/main/java/com/iemr/helpline104/utils/FilterConfig.java @@ -0,0 +1,19 @@ +package com.iemr.helpline104.utils; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +@Configuration +public class FilterConfig { + + @Bean + public FilterRegistrationBean jwtUserIdValidationFilter(JwtAuthenticationUtil jwtAuthenticationUtil) { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new JwtUserIdValidationFilter(jwtAuthenticationUtil)); + registrationBean.addUrlPatterns("/*"); // Apply filter to all API endpoints + return registrationBean; + } + +} diff --git a/src/main/java/com/iemr/helpline104/utils/JwtAuthenticationUtil.java b/src/main/java/com/iemr/helpline104/utils/JwtAuthenticationUtil.java new file mode 100644 index 0000000..2f255a0 --- /dev/null +++ b/src/main/java/com/iemr/helpline104/utils/JwtAuthenticationUtil.java @@ -0,0 +1,126 @@ +package com.iemr.helpline104.utils; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +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.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import com.iemr.helpline104.data.users.M_User; +import com.iemr.helpline104.repository.users.IEMRUserRepositoryCustom; +import com.iemr.helpline104.utils.exception.IEMRException; + +import io.jsonwebtoken.Claims; +import jakarta.servlet.http.HttpServletRequest; + +@Component +public class JwtAuthenticationUtil { + + @Autowired + private CookieUtil cookieUtil; + @Autowired + private JwtUtil jwtUtil; + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private IEMRUserRepositoryCustom iEMRUserRepositoryCustom; + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + + public JwtAuthenticationUtil(CookieUtil cookieUtil, JwtUtil jwtUtil) { + this.cookieUtil = cookieUtil; + this.jwtUtil = jwtUtil; + } + + public ResponseEntity validateJwtToken(HttpServletRequest request) { + Optional jwtTokenOpt = cookieUtil.getCookieValue(request, "Jwttoken"); + + if (jwtTokenOpt.isEmpty()) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body("Error 401: Unauthorized - JWT Token is not set!"); + } + + String jwtToken = jwtTokenOpt.get(); + + // Validate the token + Claims claims = jwtUtil.validateToken(jwtToken); + if (claims == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Error 401: Unauthorized - Invalid JWT Token!"); + } + + // Extract username from token + String usernameFromToken = claims.getSubject(); + if (usernameFromToken == null || usernameFromToken.isEmpty()) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body("Error 401: Unauthorized - Username is missing!"); + } + + // Return the username if valid + return ResponseEntity.ok(usernameFromToken); + } + + public boolean validateUserIdAndJwtToken(String jwtToken) throws IEMRException { + try { + // Validate JWT token and extract claims + Claims claims = jwtUtil.validateToken(jwtToken); + + if (claims == null) { + throw new IEMRException("Invalid JWT token."); + } + + String userId = claims.get("userId", String.class); + + // Check if user data is present in Redis + M_User user = getUserFromCache(userId); + if (user == null) { + // If not in Redis, fetch from DB and cache the result + user = fetchUserFromDB(userId); + } + if (user == null) { + throw new IEMRException("Invalid User ID."); + } + + return true; // Valid userId and JWT token + } catch (Exception e) { + logger.error("Validation failed: " + e.getMessage(), e); + throw new IEMRException("Validation error: " + e.getMessage(), e); + } + } + + private M_User getUserFromCache(String userId) { + String redisKey = "user_" + userId; // The Redis key format + M_User user = (M_User) redisTemplate.opsForValue().get(redisKey); + + if (user == null) { + logger.warn("User not found in Redis. Will try to fetch from DB."); + } else { + logger.info("User fetched successfully from Redis."); + } + + return user; // Returns null if not found + } + + private M_User fetchUserFromDB(String userId) { + // This method will only be called if the user is not found in Redis. + String redisKey = "user_" + userId; // Redis key format + + // Fetch user from DB + M_User user = iEMRUserRepositoryCustom.getUserByUserID(Long.parseLong(userId)); + + if (user != null) { + // Cache the user in Redis for future requests (cache for 30 minutes) + redisTemplate.opsForValue().set(redisKey, user, 30, TimeUnit.MINUTES); + + // Log that the user has been stored in Redis + logger.info("User stored in Redis with key: " + redisKey); + } else { + logger.warn("User not found for userId: " + userId); + } + + return user; + } +} diff --git a/src/main/java/com/iemr/helpline104/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/helpline104/utils/JwtUserIdValidationFilter.java new file mode 100644 index 0000000..540eb6c --- /dev/null +++ b/src/main/java/com/iemr/helpline104/utils/JwtUserIdValidationFilter.java @@ -0,0 +1,114 @@ +package com.iemr.helpline104.utils; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class JwtUserIdValidationFilter implements Filter { + + private final JwtAuthenticationUtil jwtAuthenticationUtil; + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + + public JwtUserIdValidationFilter(JwtAuthenticationUtil jwtAuthenticationUtil) { + this.jwtAuthenticationUtil = jwtAuthenticationUtil; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + String path = request.getRequestURI(); + String contextPath = request.getContextPath(); + logger.info("JwtUserIdValidationFilter invoked for path: " + path); + + // Log cookies for debugging + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if ("userId".equals(cookie.getName())) { + logger.warn("userId found in cookies! Clearing it..."); + clearUserIdCookie(response); // Explicitly remove userId cookie + } + } + } else { + logger.info("No cookies found in the request"); + } + + + // Log headers for debugging + String jwtTokenFromHeader = request.getHeader("Jwttoken"); + logger.info("JWT token from header: " + jwtTokenFromHeader); + + // Skip login and public endpoints + if (path.equals(contextPath + "/user/userAuthenticate") || + path.equalsIgnoreCase(contextPath + "/user/logOutUserFromConcurrentSession") || + path.startsWith(contextPath + "/public")) { + logger.info("Skipping filter for path: " + path); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + try { + // Retrieve JWT token from cookies + String jwtTokenFromCookie = getJwtTokenFromCookies(request); + logger.info("JWT token from cookie: " + jwtTokenFromCookie); + + // Determine which token (cookie or header) to validate + String jwtToken = jwtTokenFromCookie != null ? jwtTokenFromCookie : jwtTokenFromHeader; + if (jwtToken == null) { + 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 token is valid, allow the request to proceed + filterChain.doFilter(servletRequest, servletResponse); + } else { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token"); + } + } catch (Exception e) { + logger.error("Authorization error: ", e); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization error: " + e.getMessage()); + } + } + + private String getJwtTokenFromCookies(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals("Jwttoken")) { + return cookie.getValue(); + } + } + } + return null; + } + + private void clearUserIdCookie(HttpServletResponse response) { + Cookie cookie = new Cookie("userId", null); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setMaxAge(0); // Invalidate the cookie + response.addCookie(cookie); + } +} + diff --git a/src/main/java/com/iemr/helpline104/utils/JwtUtil.java b/src/main/java/com/iemr/helpline104/utils/JwtUtil.java new file mode 100644 index 0000000..41f0b10 --- /dev/null +++ b/src/main/java/com/iemr/helpline104/utils/JwtUtil.java @@ -0,0 +1,63 @@ +package com.iemr.helpline104.utils; + +import java.security.Key; +import java.util.Date; +import java.util.function.Function; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +@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 + + // Generate a key using the secret + private Key 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); + + // 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) + .compact(); + } + + // Validate and parse JWT Token + public Claims validateToken(String token) { + try { + return Jwts.parser().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody(); + } catch (Exception e) { + return null; // Handle token parsing/validation errors + } + } + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody(); + } +}