diff --git a/.github/workflows/build-on-pull-request.yml b/.github/workflows/build-on-pull-request.yml index 744b905..695b179 100644 --- a/.github/workflows/build-on-pull-request.yml +++ b/.github/workflows/build-on-pull-request.yml @@ -17,6 +17,7 @@ jobs: uses: actions/setup-java@v2 with: java-version: 17 - distribution: 'adopt' + distribution: 'zulu' + - name: Build with Maven run: mvn clean install diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 5cc0ca0..769fce7 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -26,7 +26,7 @@ jobs: uses: actions/setup-java@v2 with: java-version: 17 - distribution: 'adopt' + distribution: 'zulu' - name: Build with Maven run: mvn clean install -DENV_VAR=${{ env.ENV_VAR }} @@ -35,7 +35,7 @@ jobs: run: mvn -B package --file pom.xml - name: Upload WAR file as artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: FHIR-API path: target/fhirapi-v1.0.war diff --git a/.github/workflows/sast.yml b/.github/workflows/sast.yml index eb8d6b3..a886904 100644 --- a/.github/workflows/sast.yml +++ b/.github/workflows/sast.yml @@ -41,10 +41,10 @@ jobs: uses: actions/setup-java@v2 with: java-version: 17 - distribution: 'adopt' + distribution: 'zulu' - name: Build with Maven - run: mvn clean install -DENV_VAR=test + run: mvn clean install - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 diff --git a/pom.xml b/pom.xml index a345c2c..22e8b75 100644 --- a/pom.xml +++ b/pom.xml @@ -58,10 +58,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 @@ -189,8 +189,7 @@ spring-boot-starter-mail - + ca.uhn.hapi.fhir hapi-fhir-structures-r4 @@ -198,8 +197,7 @@ - + ca.uhn.hapi.fhir org.hl7.fhir.utilities @@ -235,6 +233,27 @@ json-path 2.9.0 + + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + + @@ -250,7 +269,7 @@ HTML nvd - + org.apache.maven.plugins @@ -329,8 +348,7 @@ ${target-properties} and ${source-properties} - diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index ed70c81..f3137e0 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -110,4 +110,5 @@ logging.level.com.iemr=DEBUG logging.level.org.springframework=INFO #ELK logging file name -logging.file.name=@env.FHIR_API_LOGGING_FILE_NAME@ \ No newline at end of file +logging.file.name=@env.FHIR_API_LOGGING_FILE_NAME@ +jwt.secret=@env.JWT_SECRET_KEY@ \ No newline at end of file diff --git a/src/main/environment/common_dev.properties b/src/main/environment/common_dev.properties index 3e012e9..6210f9e 100644 --- a/src/main/environment/common_dev.properties +++ b/src/main/environment/common_dev.properties @@ -107,3 +107,4 @@ logging.level.org.springframework.web=INFO logging.level.org.hibernate=INFO logging.level.com.iemr=DEBUG logging.level.org.springframework=INFO +jwt.secret= diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index 803d225..0679bed 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -109,3 +109,4 @@ logging.level.org.springframework.web=INFO logging.level.org.hibernate=INFO logging.level.com.iemr=DEBUG logging.level.org.springframework=INFO +jwt.secret= diff --git a/src/main/environment/common_test.properties b/src/main/environment/common_test.properties index db18ad9..44e1523 100644 --- a/src/main/environment/common_test.properties +++ b/src/main/environment/common_test.properties @@ -108,3 +108,4 @@ logging.level.org.springframework.web=INFO logging.level.org.hibernate=INFO logging.level.com.iemr=DEBUG logging.level.org.springframework=INFO +jwt.secret= diff --git a/src/main/java/com/wipro/fhir/FhirApiApplication.java b/src/main/java/com/wipro/fhir/FhirApiApplication.java index 0395010..d732a3f 100644 --- a/src/main/java/com/wipro/fhir/FhirApiApplication.java +++ b/src/main/java/com/wipro/fhir/FhirApiApplication.java @@ -26,6 +26,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory; +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 com.wipro.fhir.data.users.User; @SpringBootApplication public class FhirApiApplication { @@ -33,4 +39,19 @@ public class FhirApiApplication { public static void main(String[] args) { SpringApplication.run(FhirApiApplication.class, args); } + + @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<>(User.class); + template.setValueSerializer(serializer); + + return template; + } } diff --git a/src/main/java/com/wipro/fhir/config/RedisConfig.java b/src/main/java/com/wipro/fhir/config/RedisConfig.java new file mode 100644 index 0000000..d29e7c3 --- /dev/null +++ b/src/main/java/com/wipro/fhir/config/RedisConfig.java @@ -0,0 +1,40 @@ +package com.wipro.fhir.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.wipro.fhir.data.users.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<>(User.class); + template.setValueSerializer(serializer); + + return template; + } + +} + + diff --git a/src/main/java/com/wipro/fhir/data/users/User.java b/src/main/java/com/wipro/fhir/data/users/User.java new file mode 100644 index 0000000..c31049e --- /dev/null +++ b/src/main/java/com/wipro/fhir/data/users/User.java @@ -0,0 +1,28 @@ +package com.wipro.fhir.data.users; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; + +@Entity +@Table(name = "m_user") +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class User implements Serializable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "UserID") + private Long userID; + @Column(name = "userName") + private String userName; + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; +} diff --git a/src/main/java/com/wipro/fhir/repo/user/UserLoginRepo.java b/src/main/java/com/wipro/fhir/repo/user/UserLoginRepo.java new file mode 100644 index 0000000..1267ab6 --- /dev/null +++ b/src/main/java/com/wipro/fhir/repo/user/UserLoginRepo.java @@ -0,0 +1,16 @@ +package com.wipro.fhir.repo.user; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.wipro.fhir.data.users.User; + +@Repository +public interface UserLoginRepo extends CrudRepository { + + @Query(" SELECT u FROM User u WHERE u.userID = :userID AND u.deleted = false ") + public User getUserByUserID(@Param("userID") Long userID); + +} diff --git a/src/main/java/com/wipro/fhir/service/common/CommonServiceImpl.java b/src/main/java/com/wipro/fhir/service/common/CommonServiceImpl.java index 0c87321..5fcf125 100644 --- a/src/main/java/com/wipro/fhir/service/common/CommonServiceImpl.java +++ b/src/main/java/com/wipro/fhir/service/common/CommonServiceImpl.java @@ -58,11 +58,13 @@ import com.wipro.fhir.data.mongo.care_context.NDHMResponse; import com.wipro.fhir.data.mongo.care_context.Notification; import com.wipro.fhir.data.mongo.care_context.PatientCareContexts; +import com.wipro.fhir.data.mongo.care_context.PatientCareContextsStringOBJ; import com.wipro.fhir.data.mongo.care_context.SMSNotify; import com.wipro.fhir.data.patient.PatientDemographic; import com.wipro.fhir.data.patient_data_handler.PatientDemographicModel_NDHM_Patient_Profile; import com.wipro.fhir.data.request_handler.PatientEligibleForResourceCreation; import com.wipro.fhir.data.request_handler.ResourceRequestHandler; +import com.wipro.fhir.data.users.User; import com.wipro.fhir.repo.common.PatientEligibleForResourceCreationRepo; import com.wipro.fhir.repo.healthID.BenHealthIDMappingRepo; import com.wipro.fhir.repo.mongo.amrit_resource.AMRIT_ResourceMongoRepo; @@ -101,6 +103,12 @@ public class CommonServiceImpl implements CommonService { private static String authKey; private UUID uuid; + + // public static String NDHM_AUTH_TOKEN; + // public static Long NDHM_TOKEN_EXP; + // public static String NDHM_OTP_TOKEN; + + @Value("${clientID}") private String clientID; @@ -161,7 +169,8 @@ public String processResourceOperation() throws FHIRException { String response = null; // list of patient eligible for resource creation List pList = getPatientListForResourceEligible(); - logger.info("No of records available to create FHIR in last 2 dagetPatientListForResourceEligibleys : " + pList.size()); + logger.info("No of records available to create FHIR in last 2 dagetPatientListForResourceEligibleys : " + + pList.size()); ResourceRequestHandler resourceRequestHandler; for (PatientEligibleForResourceCreation p : pList) { @@ -290,6 +299,38 @@ public void addCareContextToMongo(PatientDemographic pDemo, PatientEligibleForRe if (pDemo != null && pVisit != null) { + +// JsonObject jsnOBJ = new JsonObject(); +// JsonParser jsnParser = new JsonParser(); +// JsonElement jsnElmnt = jsnParser.parse(requestObj); +// jsnOBJ = jsnElmnt.getAsJsonObject(); + + PatientCareContextsStringOBJ patientCareContextsStringOBJ = new PatientCareContextsStringOBJ(); + + // wrong variable name in request obj for benregid, need to correct in main + // request obj first +// Long benID = null; +// Long benRegID = null; +// Long visitCode = null; +// +// if (jsnOBJ.has("beneficiaryID") && jsnOBJ.get("beneficiaryID") != null) +// benRegID = jsnOBJ.get("beneficiaryID").getAsLong(); +// if (jsnOBJ.has("visitCode") && jsnOBJ.get("visitCode") != null) +// visitCode = jsnOBJ.get("visitCode").getAsLong(); +// String healthID = jsnOBJ.get("healthID").getAsString(); +// String healthIDNumber = jsnOBJ.get("healthIdNumber").getAsString(); +// String visitCategory = jsnOBJ.get("visitCategory").getAsString(); +// String phoneNo; +// String gender; +// String yearOfBirth; +// String name; +// String email; + + // get benid +// if (benRegID != null) +// benID = benHealthIDMappingRepo.getBenID(benRegID); + + // fetch abdm facility id logger.info("********t_benvisistData fetch request pvisit data :", pVisit); @@ -299,6 +340,7 @@ public void addCareContextToMongo(PatientDemographic pDemo, PatientEligibleForRe ArrayList ccList = new ArrayList<>(); CareContexts cc = new CareContexts(); + logger.info("********t_benvisistData fetch response : {}", res); cc.setReferenceNumber(pVisit.getVisitCode() != null ? pVisit.getVisitCode().toString() : null); @@ -310,16 +352,22 @@ public void addCareContextToMongo(PatientDemographic pDemo, PatientEligibleForRe cc.setCareContextLinkedDate(resData[1] != null ? resData[1].toString() : null); } + logger.info("********data to be saved in mongo :", cc); PatientCareContexts pcc; + PatientCareContexts resultSet = null; + + + logger.info("********data to be saved in mongo :", cc); + PatientCareContexts pcc1; if (pDemo.getBeneficiaryID() != null) { - pcc = patientCareContextsMongoRepo.findByIdentifier(pDemo.getBeneficiaryID().toString()); + pcc1 = patientCareContextsMongoRepo.findByIdentifier(pDemo.getBeneficiaryID().toString()); - if (pcc != null && pcc.getIdentifier() != null) { + if (pcc1 != null && pcc1.getIdentifier() != null) { // Get the existing careContextsList - if (pcc.getCareContextsList() != null && pcc.getCareContextsList().size() > 0) { - ccList = pcc.getCareContextsList(); + if (pcc1.getCareContextsList() != null && pcc1.getCareContextsList().size() > 0) { + ccList = pcc1.getCareContextsList(); // Check if the visitCode is already in the careContextsList for (CareContexts existingContext : ccList) { @@ -330,8 +378,8 @@ public void addCareContextToMongo(PatientDemographic pDemo, PatientEligibleForRe } } ccList.add(cc); - pcc.setCareContextsList(ccList); - patientCareContextsMongoRepo.save(pcc); + pcc1.setCareContextsList(ccList); + patientCareContextsMongoRepo.save(pcc1); } // } // if (pcc != null && pcc.getIdentifier() != null) { @@ -341,20 +389,20 @@ public void addCareContextToMongo(PatientDemographic pDemo, PatientEligibleForRe // resultSet = patientCareContextsMongoRepo.save(pcc); // } else { - pcc = new PatientCareContexts(); - pcc.setCaseReferenceNumber(pDemo.getBeneficiaryID().toString()); - pcc.setIdentifier(pDemo.getBeneficiaryID().toString()); + pcc1 = new PatientCareContexts(); + pcc1.setCaseReferenceNumber(pDemo.getBeneficiaryID().toString()); + pcc1.setIdentifier(pDemo.getBeneficiaryID().toString()); if (pDemo.getGenderID() != null) { switch (pDemo.getGenderID()) { case 1: - pcc.setGender("M"); + pcc1.setGender("M"); break; case 2: - pcc.setGender("F"); + pcc1.setGender("F"); break; case 3: - pcc.setGender("O"); + pcc1.setGender("O"); break; default: @@ -362,19 +410,19 @@ public void addCareContextToMongo(PatientDemographic pDemo, PatientEligibleForRe } } if (pDemo.getName() != null) - pcc.setName(pDemo.getName()); + pcc1.setName(pDemo.getName()); if (pDemo.getDOB() != null) - pcc.setYearOfBirth(pDemo.getDOB().toString().split("-")[0]); + pcc1.setYearOfBirth(pDemo.getDOB().toString().split("-")[0]); if (pDemo.getPreferredPhoneNo() != null) - pcc.setPhoneNumber(pDemo.getPreferredPhoneNo()); + pcc1.setPhoneNumber(pDemo.getPreferredPhoneNo()); if (pDemo.getHealthID() != null) - pcc.setHealthId(pDemo.getHealthID()); + pcc1.setHealthId(pDemo.getHealthID()); if (pDemo.getHealthIdNo() != null) - pcc.setHealthNumber(pDemo.getHealthIdNo()); + pcc1.setHealthNumber(pDemo.getHealthIdNo()); ccList.add(cc); - pcc.setCareContextsList(ccList); + pcc1.setCareContextsList(ccList); // save carecontext back to mongo - patientCareContextsMongoRepo.save(pcc); + patientCareContextsMongoRepo.save(pcc1); } } diff --git a/src/main/java/com/wipro/fhir/utils/CookieUtil.java b/src/main/java/com/wipro/fhir/utils/CookieUtil.java new file mode 100644 index 0000000..3ccec9d --- /dev/null +++ b/src/main/java/com/wipro/fhir/utils/CookieUtil.java @@ -0,0 +1,31 @@ +package com.wipro.fhir.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/wipro/fhir/utils/FilterConfig.java b/src/main/java/com/wipro/fhir/utils/FilterConfig.java new file mode 100644 index 0000000..5a7ef36 --- /dev/null +++ b/src/main/java/com/wipro/fhir/utils/FilterConfig.java @@ -0,0 +1,19 @@ +package com.wipro.fhir.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/wipro/fhir/utils/JwtAuthenticationUtil.java b/src/main/java/com/wipro/fhir/utils/JwtAuthenticationUtil.java new file mode 100644 index 0000000..61cdc90 --- /dev/null +++ b/src/main/java/com/wipro/fhir/utils/JwtAuthenticationUtil.java @@ -0,0 +1,125 @@ +package com.wipro.fhir.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.wipro.fhir.data.users.User; +import com.wipro.fhir.repo.user.UserLoginRepo; + +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 UserLoginRepo userLoginRepo; + 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 Exception { + try { + // Validate JWT token and extract claims + Claims claims = jwtUtil.validateToken(jwtToken); + + if (claims == null) { + throw new Exception("Invalid JWT token."); + } + + String userId = claims.get("userId", String.class); + + // Check if user data is present in Redis + 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 Exception("Invalid User ID."); + } + + return true; // Valid userId and JWT token + } catch (Exception e) { + logger.error("Validation failed: " + e.getMessage(), e); + throw new Exception("Validation error: " + e.getMessage(), e); + } + } + + private User getUserFromCache(String userId) { + String redisKey = "user_" + userId; // The Redis key format + User user = (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 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 + User user = userLoginRepo.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/wipro/fhir/utils/JwtUserIdValidationFilter.java b/src/main/java/com/wipro/fhir/utils/JwtUserIdValidationFilter.java new file mode 100644 index 0000000..09ae81f --- /dev/null +++ b/src/main/java/com/wipro/fhir/utils/JwtUserIdValidationFilter.java @@ -0,0 +1,111 @@ +package com.wipro.fhir.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: "); + + // 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: "); + + // 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/wipro/fhir/utils/JwtUtil.java b/src/main/java/com/wipro/fhir/utils/JwtUtil.java new file mode 100644 index 0000000..6f22eb7 --- /dev/null +++ b/src/main/java/com/wipro/fhir/utils/JwtUtil.java @@ -0,0 +1,68 @@ +package com.wipro.fhir.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 { + // 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 + } + } + + 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(); + } +}