From 7ee43ac1b53e34832729d41ef3e22bc5c38b1f0e Mon Sep 17 00:00:00 2001 From: Keval Kanpariya Date: Wed, 2 Apr 2025 11:24:11 +0530 Subject: [PATCH 1/3] middleware added through FilterConfig for JWT Token Validation. --- pom.xml | 34 +++++ .../common/identity/domain/iemr/M_User.java | 142 ++++++++++++++++++ .../repo/iemr/EmployeeMasterRepo.java | 12 ++ .../common/identity/utils/CookieUtil.java | 41 +++++ .../common/identity/utils/FilterConfig.java | 19 +++ .../identity/utils/JwtAuthenticationUtil.java | 124 +++++++++++++++ .../utils/JwtUserIdValidationFilter.java | 106 +++++++++++++ .../iemr/common/identity/utils/JwtUtil.java | 85 +++++++++++ 8 files changed, 563 insertions(+) create mode 100644 src/main/java/com/iemr/common/identity/domain/iemr/M_User.java create mode 100644 src/main/java/com/iemr/common/identity/repo/iemr/EmployeeMasterRepo.java create mode 100644 src/main/java/com/iemr/common/identity/utils/CookieUtil.java create mode 100644 src/main/java/com/iemr/common/identity/utils/FilterConfig.java create mode 100644 src/main/java/com/iemr/common/identity/utils/JwtAuthenticationUtil.java create mode 100644 src/main/java/com/iemr/common/identity/utils/JwtUserIdValidationFilter.java create mode 100644 src/main/java/com/iemr/common/identity/utils/JwtUtil.java diff --git a/pom.xml b/pom.xml index c41d078a..e1f121cc 100644 --- a/pom.xml +++ b/pom.xml @@ -220,6 +220,40 @@ spring-web 6.1.12 + + + + org.glassfish.jersey.media + jersey-media-json-processing + 2.30.1 + + + + + org.glassfish.jaxb + jaxb-runtime + 2.3.1 + + + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + diff --git a/src/main/java/com/iemr/common/identity/domain/iemr/M_User.java b/src/main/java/com/iemr/common/identity/domain/iemr/M_User.java new file mode 100644 index 00000000..fa9a8281 --- /dev/null +++ b/src/main/java/com/iemr/common/identity/domain/iemr/M_User.java @@ -0,0 +1,142 @@ +package com.iemr.common.identity.domain.iemr; + +import com.google.gson.annotations.Expose; +import jakarta.persistence.*; +import lombok.Data; +import java.sql.Date; +import java.sql.Timestamp; +import java.time.LocalDate; + +@Entity +@Table(name = "m_User",schema = "db_iemr") +@Data +public class M_User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name="UserID") + private Integer userID; + @Expose + @Column(name="TitleID") + private Integer titleID; + @Expose + @Column(name="FirstName") + private String firstName; + @Expose + @Column(name="MiddleName") + private String middleName; + @Expose + @Column(name="LastName") + private String lastName; + @Expose + @Column(name="GenderID") + private Short genderID; + + @Expose + @Column(name="MaritalStatusID") + private Integer maritalStatusID; + @Expose + @Column(name="DesignationID") + private Integer designationID; + + @Expose + @Column(name="AadhaarNo") + private String aadhaarNo; + @Expose + @Column(name="PAN") + private String pAN; + @Expose + @Column(name="DOB") + private LocalDate dOB; + @Expose + @Column(name="DOJ") + private LocalDate dOJ; + @Expose + @Column(name="QualificationID") + private Integer qualificationID; + @Expose + @Column(name="HealthProfessionalID") + private String healthProfessionalID; + @Expose + @Column(name="UserName") + private String userName; + @Expose + @Column(name="Password") + private String password; + @Expose + @Column(name="IsExternal") + private Boolean isExternal; + @Expose + @Column(name="AgentID") + private String agentID; + @Expose + @Column(name="AgentPassword") + private String agentPassword; + @Expose + @Column(name="EmailID") + private String emailID; + @Expose + @Column(name="StatusID") + private Integer statusID; + @Expose + @Column(name="EmergencyContactPerson") + private String emergencyContactPerson; + @Expose + @Column(name="EmergencyContactNo") + private String emergencyContactNo; + @Expose + @Column(name="IsSupervisor") + private Boolean isSupervisor; + @Expose + @Column(name="Deleted",insertable = false, updatable = true) + private Boolean deleted; + @Expose + @Column(name="CreatedBy") + private String createdBy; + @Expose + @Column(name="EmployeeID") + private String employeeID; + @Expose + @Column(name="CreatedDate",insertable = false, updatable = false) + private Timestamp createdDate; + @Expose + @Column(name="ModifiedBy") + private String modifiedBy; + @Expose + @Column(name="LastModDate",insertable = false, updatable = false) + private Timestamp lastModDate; + + @Expose + @Column(name="Remarks") + private String remarks; + + @Expose + @Column(name="ContactNo") + private String contactNo; + + + @Expose + @Column(name="IsProviderAdmin") + private Boolean isProviderAdmin; + + @Expose + @Column(name="ServiceProviderID") + private Integer serviceProviderID; + + + + @Expose + @Column(name = "failed_attempt", insertable = false) + private Integer failedAttempt; + public M_User() { + // TODO Auto-generated constructor stub + } + + public M_User(Integer userID, String userName) { + // TODO Auto-generated constructor stub + this.userID=userID; + this.userName=userName; + } + +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/identity/repo/iemr/EmployeeMasterRepo.java b/src/main/java/com/iemr/common/identity/repo/iemr/EmployeeMasterRepo.java new file mode 100644 index 00000000..4bea5680 --- /dev/null +++ b/src/main/java/com/iemr/common/identity/repo/iemr/EmployeeMasterRepo.java @@ -0,0 +1,12 @@ +package com.iemr.common.identity.repo.iemr; + +import com.iemr.common.identity.domain.iemr.M_User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface EmployeeMasterRepo extends JpaRepository { + M_User findByUserID(Integer userID); + + M_User getUserByUserID(Integer parseLong); +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/identity/utils/CookieUtil.java b/src/main/java/com/iemr/common/identity/utils/CookieUtil.java new file mode 100644 index 00000000..67ee952c --- /dev/null +++ b/src/main/java/com/iemr/common/identity/utils/CookieUtil.java @@ -0,0 +1,41 @@ +package com.iemr.common.identity.utils; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Service; +import java.util.Arrays; +import java.util.Optional; + +@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 void addJwtTokenToCookie(String Jwttoken, HttpServletResponse response, HttpServletRequest request) { + // Create a new cookie with the JWT token + Cookie cookie = new Cookie("Jwttoken", Jwttoken); + cookie.setHttpOnly(true); // Prevent JavaScript access for security + cookie.setMaxAge(60 * 60 * 24); // 1 day expiration time + cookie.setPath("/"); // Make the cookie available for the entire application + if ("https".equalsIgnoreCase(request.getScheme())) { + cookie.setSecure(true); // Secure flag only on HTTPS + } + response.addCookie(cookie); // Add the cookie to the response + } + + public String getJwtTokenFromCookie(HttpServletRequest request) { + return Arrays.stream(request.getCookies()).filter(cookie -> "Jwttoken".equals(cookie.getName())) + .map(Cookie::getValue).findFirst().orElse(null); + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/identity/utils/FilterConfig.java b/src/main/java/com/iemr/common/identity/utils/FilterConfig.java new file mode 100644 index 00000000..8961d997 --- /dev/null +++ b/src/main/java/com/iemr/common/identity/utils/FilterConfig.java @@ -0,0 +1,19 @@ +package com.iemr.common.identity.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; + } + +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/identity/utils/JwtAuthenticationUtil.java b/src/main/java/com/iemr/common/identity/utils/JwtAuthenticationUtil.java new file mode 100644 index 00000000..aa1fefab --- /dev/null +++ b/src/main/java/com/iemr/common/identity/utils/JwtAuthenticationUtil.java @@ -0,0 +1,124 @@ +package com.iemr.common.identity.utils; + +import com.iemr.common.identity.domain.iemr.M_User; +import com.iemr.common.identity.repo.iemr.EmployeeMasterRepo; +import com.iemr.common.identity.utils.exception.IEMRException; +import io.jsonwebtoken.Claims; +import jakarta.servlet.http.HttpServletRequest; +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 java.util.Optional; +import java.util.concurrent.TimeUnit; + +@Component +public class JwtAuthenticationUtil { + + @Autowired + private CookieUtil cookieUtil; + @Autowired + private JwtUtil jwtUtil; + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private EmployeeMasterRepo 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 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 = userLoginRepo.getUserByUserID(Integer.parseInt(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; + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/identity/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/identity/utils/JwtUserIdValidationFilter.java new file mode 100644 index 00000000..7fa15269 --- /dev/null +++ b/src/main/java/com/iemr/common/identity/utils/JwtUserIdValidationFilter.java @@ -0,0 +1,106 @@ +package com.iemr.common.identity.utils; + +import jakarta.servlet.*; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@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: "); + } + } + + 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); + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/identity/utils/JwtUtil.java b/src/main/java/com/iemr/common/identity/utils/JwtUtil.java new file mode 100644 index 00000000..de2f5864 --- /dev/null +++ b/src/main/java/com/iemr/common/identity/utils/JwtUtil.java @@ -0,0 +1,85 @@ +package com.iemr.common.identity.utils; + +import com.iemr.common.identity.utils.exception.IEMRException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; +import java.util.function.Function; + +@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 Integer extractUserId(String jwtToken) throws IEMRException { + try { + // Validate JWT token and extract claims + Claims claims = validateToken(jwtToken); + + if (claims == null) { + throw new IEMRException("Invalid JWT token."); + } + + String userId = claims.get("userId", String.class); + + return Integer.parseInt(userId); + + } catch (Exception e) { + throw new IEMRException("Validation error: " + e.getMessage(), e); + } + + + } + + + 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(); + } +} \ No newline at end of file From c4328a4d4d53ee2be3c4442a2c8bfe9d0bcc92cf Mon Sep 17 00:00:00 2001 From: Keval Kanpariya Date: Sat, 17 May 2025 09:21:59 +0530 Subject: [PATCH 2/3] fix(cors): global cors config added --- src/main/environment/1097_ci.properties | 3 ++ src/main/environment/1097_example.properties | 3 +- src/main/environment/common_ci.properties | 3 ++ .../environment/common_example.properties | 3 +- .../common/identity/config/CorsConfig.java | 25 +++++++++++ .../controller/IdentityController.java | 43 +++++++++---------- .../FamilyTaggingController.java | 14 +++--- .../controller/version/VersionController.java | 2 +- .../identity/utils/DynamicCorsFilter.java | 37 ++++++++++++++++ 9 files changed, 101 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/iemr/common/identity/config/CorsConfig.java create mode 100644 src/main/java/com/iemr/common/identity/utils/DynamicCorsFilter.java diff --git a/src/main/environment/1097_ci.properties b/src/main/environment/1097_ci.properties index ea60930f..953a9b47 100644 --- a/src/main/environment/1097_ci.properties +++ b/src/main/environment/1097_ci.properties @@ -18,3 +18,6 @@ tm-url=@TM_API@ # FHIR Config fhir-url=@FHIR_API@ +logging.file.name=@env.IDENTITY_API_1097_LOGGING_FILE_NAME@ + +cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS diff --git a/src/main/environment/1097_example.properties b/src/main/environment/1097_example.properties index 6a7d8b0f..ebd80ecf 100644 --- a/src/main/environment/1097_example.properties +++ b/src/main/environment/1097_example.properties @@ -14,4 +14,5 @@ logging.file.name=logs/1097identity-api.log tm-url=http://localhost:8089/ # FHIR Config -fhir-url=http://localhost:8093/ \ No newline at end of file +fhir-url=http://localhost:8093/ +cors.allowed-origins= diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index d6b426ef..56485871 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -18,3 +18,6 @@ tm-url=@TM_API@ # FHIR Config fhir-url=@FHIR_API@ +logging.file.name=@env.IDENTITY_API_LOGGING_FILE_NAME@ + +cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index 79dfeb12..9a0fce00 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -13,4 +13,5 @@ logging.file.name=logs/identity-api.log tm-url=http://localhost:8089/ # FHIR Config -fhir-url=http://localhost:8093/ \ No newline at end of file +fhir-url=http://localhost:8093/ +cors.allowed-origins= diff --git a/src/main/java/com/iemr/common/identity/config/CorsConfig.java b/src/main/java/com/iemr/common/identity/config/CorsConfig.java new file mode 100644 index 00000000..c9a94b02 --- /dev/null +++ b/src/main/java/com/iemr/common/identity/config/CorsConfig.java @@ -0,0 +1,25 @@ +package com.iemr.common.identity.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Value("${cors.allowed-origins}") + private String allowedOrigins; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins(allowedOrigins.split(",")) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .exposedHeaders("Authorization", "Jwttoken") // Explicitly expose headers if needed + .allowCredentials(true) + .maxAge(3600) + ; + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/common/identity/controller/IdentityController.java b/src/main/java/com/iemr/common/identity/controller/IdentityController.java index c3658c66..d9bd308d 100644 --- a/src/main/java/com/iemr/common/identity/controller/IdentityController.java +++ b/src/main/java/com/iemr/common/identity/controller/IdentityController.java @@ -33,7 +33,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -83,7 +82,7 @@ public class IdentityController { @Autowired IdentityMapper mapper; - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Get beneficiaries by advance search") @PostMapping(path = "/advanceSearch", headers = "Authorization") public String getBeneficiaries( @@ -112,7 +111,7 @@ public String getBeneficiaries( return response; } - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Search beneficiary based on beneficiary registration id") @PostMapping(path = "/getByBenRegId", headers = "Authorization") public String getBeneficiariesByBeneficiaryRegId( @@ -137,7 +136,7 @@ public String getBeneficiariesByBeneficiaryRegId( return response; } - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Search identity based on beneficiary registration id") @PostMapping(path = "/getByBenId", headers = "Authorization") public String getBeneficiariesByBeneficiaryId( @@ -172,7 +171,7 @@ public String getBeneficiariesByBeneficiaryId( return response; } - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Search beneficiary based on phone number") @PostMapping(path = "/getByPhoneNum", headers = "Authorization") public String getBeneficiariesByPhoneNum( @@ -203,7 +202,7 @@ public String getBeneficiariesByPhoneNum( return response; } - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Search beneficiary based on health ID / ABHA Address") @PostMapping(path = "/getByAbhaAddress", headers = "Authorization") public String searhBeneficiaryByABHAAddress( @@ -235,7 +234,7 @@ public String searhBeneficiaryByABHAAddress( return response; } - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Search beneficiary based on health ID number / ABHA ID number") @PostMapping(path = "/getByAbhaIdNo", headers = "Authorization") public String searhBeneficiaryByABHAIdNo( @@ -267,7 +266,7 @@ public String searhBeneficiaryByABHAIdNo( return response; } - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Search beneficiary based on family id") @PostMapping(path = "/searchByFamilyId", headers = "Authorization") public String searhBeneficiaryByFamilyId( @@ -297,7 +296,7 @@ public String searhBeneficiaryByFamilyId( } // search beneficiary by lastModDate and districtID - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary ="Search beneficiary by villageId and last modified date-time") @PostMapping(path = "/searchByVillageIdAndLastModifiedDate") public String searchBeneficiaryByVillageIdAndLastModDate( @@ -322,7 +321,7 @@ public String searchBeneficiaryByVillageIdAndLastModDate( return response; } // search beneficiary by lastModDate and districtID - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary ="Get count of beneficiary by villageId and last modified date-time") @PostMapping(path = "/countBenByVillageIdAndLastModifiedDate") public String countBeneficiaryByVillageIdAndLastModDate( @@ -342,7 +341,7 @@ public String countBeneficiaryByVillageIdAndLastModDate( } return response; } - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Search beneficiary based on government identity number") @PostMapping(path = "/searhByGovIdentity", headers = "Authorization") public String searhBeneficiaryByGovIdentity( @@ -376,7 +375,7 @@ public String searhBeneficiaryByGovIdentity( * @param identityEditData * @return */ - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Edit identity by agent") @PostMapping(path = "/edit", headers = "Authorization") public String editIdentity(@Param(value = "{\r\n" + " \"eventTypeName\": \"String\",\r\n" @@ -508,7 +507,7 @@ public String editIdentity(@Param(value = "{\r\n" + " \"eventTypeName\": \"Stri * @param identityData * @return */ - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Create identity by agent") @PostMapping(path = "/create", headers = "Authorization") public String createIdentity(@Param(value = "{\r\n" + " \"eventTypeName\": \"String\",\r\n" @@ -615,7 +614,7 @@ public String createIdentity(@Param(value = "{\r\n" + " \"eventTypeName\": \"St return response; } - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Reserve identity by agent") @PostMapping(path = "/reserve", headers = "Authorization") public String reserveIdentity(@RequestBody String reserveIdentity) { @@ -635,7 +634,7 @@ public String reserveIdentity(@RequestBody String reserveIdentity) { return response; } - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Unreserve identity by agent") @PostMapping(path = "/unreserve", headers = "Authorization") public String unreserveIdentity(@RequestBody String unreserve) { @@ -661,7 +660,7 @@ public String unreserveIdentity(@RequestBody String unreserve) { * @param benRegIds * @return */ - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Get beneficiaries partial details by beneficiary registration id list") @PostMapping(path = "/getByPartialBenRegIdList", headers = "Authorization") public String getPartialBeneficiariesByBenRegIds( @@ -693,7 +692,7 @@ public String getPartialBeneficiariesByBenRegIds( * @param benRegIds * @return */ - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Get beneficiaries by beneficiary registration id") @PostMapping(path = "/getByBenRegIdList", headers = "Authorization") public String getBeneficiariesByBenRegIds( @@ -792,7 +791,7 @@ public String getJsonAsString(Object obj) { return sb.toString(); } - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Get finite beneficiaries") @PostMapping(path = "/finiteSearch", headers = "Authorization") public String getFiniteBeneficiaries(@RequestBody String searchFilter) { @@ -816,7 +815,7 @@ public String getFiniteBeneficiaries(@RequestBody String searchFilter) { } // New API for getting beneficiary image only. - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Get beneficiary image by beneficiary registration id") @PostMapping(path = "/benImageByBenRegID", headers = "Authorization") public String getBeneficiaryImageByBenRegID(@RequestBody String identityData) { @@ -830,7 +829,7 @@ public String getBeneficiaryImageByBenRegID(@RequestBody String identityData) { return benImage; } - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Edit education or community by agent") @PostMapping(path = "/editEducationOrCommunity", headers = "Authorization") public String editIdentityEducationOrCommunity(@Param(value = "{\r\n" @@ -957,7 +956,7 @@ public String editIdentityEducationOrCommunity(@Param(value = "{\r\n" } } - @CrossOrigin() + @Operation(summary = "Check available beneficary id in local server") @GetMapping(path = "/checkAvailablBenIDLocalServer", headers = "Authorization") public String checkAvailablBenIDLocalServer() { @@ -972,7 +971,7 @@ public String checkAvailablBenIDLocalServer() { return response.toString(); } - @CrossOrigin(origins = { "*commonapi*" }) + @Operation(summary = "Save server generated beneficiary ID & beneficiary registration ID to local server") @PostMapping(path = "/saveGeneratedBenIDToLocalServer", headers = "Authorization", consumes = "application/json", produces = "application/json") public String saveGeneratedBenIDToLocalServer( diff --git a/src/main/java/com/iemr/common/identity/controller/familyTagging/FamilyTaggingController.java b/src/main/java/com/iemr/common/identity/controller/familyTagging/FamilyTaggingController.java index b1f117c7..69074128 100644 --- a/src/main/java/com/iemr/common/identity/controller/familyTagging/FamilyTaggingController.java +++ b/src/main/java/com/iemr/common/identity/controller/familyTagging/FamilyTaggingController.java @@ -35,7 +35,7 @@ import io.swagger.v3.oas.annotations.Operation; -@CrossOrigin + @RestController @RequestMapping({ "/family" }) public class FamilyTaggingController { @@ -43,7 +43,7 @@ public class FamilyTaggingController { @Autowired private FamilyTagService familyTagService; - @CrossOrigin() + @Operation(summary = "Create and modify family tagging") @PostMapping(value = { "/addTag" }, consumes = "application/json", produces = "application/json") public String saveFamilyTagging(@RequestBody String comingReq) { @@ -59,7 +59,7 @@ public String saveFamilyTagging(@RequestBody String comingReq) { return response.toString(); } - @CrossOrigin() + @Operation(summary = "Create family") @PostMapping(value = { "/createFamily" }, consumes = "application/json", produces = "application/json") public String createFamily(@RequestBody String comingReq) { @@ -75,7 +75,7 @@ public String createFamily(@RequestBody String comingReq) { return response.toString(); } - @CrossOrigin() + @Operation(summary = "Search family") @PostMapping(value = { "/searchFamily" }, consumes = "application/json", produces = "application/json") public String searchFamily(@RequestBody String comingReq) { @@ -91,7 +91,7 @@ public String searchFamily(@RequestBody String comingReq) { return response.toString(); } - @CrossOrigin() + @Operation(summary = "Get family members details") @PostMapping(value = { "/getFamilyDetails" }, consumes = "application/json", produces = "application/json") public String getFamilyDatails(@RequestBody String comingReq) { @@ -107,7 +107,7 @@ public String getFamilyDatails(@RequestBody String comingReq) { return response.toString(); } - @CrossOrigin() + @Operation(summary = "Untag beneficiary from a family") @PostMapping(value = { "/untag" }, consumes = "application/json", produces = "application/json") public String untagFamily(@RequestBody String comingReq) { @@ -123,7 +123,7 @@ public String untagFamily(@RequestBody String comingReq) { return response.toString(); } - @CrossOrigin() + @Operation(summary = "Edit beneficiary family details") @PostMapping(value = { "/editFamilyTagging" }, consumes = "application/json", produces = "application/json") public String editFamilyDetails(@RequestBody String comingReq) { diff --git a/src/main/java/com/iemr/common/identity/controller/version/VersionController.java b/src/main/java/com/iemr/common/identity/controller/version/VersionController.java index fdf973cd..28980e42 100644 --- a/src/main/java/com/iemr/common/identity/controller/version/VersionController.java +++ b/src/main/java/com/iemr/common/identity/controller/version/VersionController.java @@ -41,7 +41,7 @@ public class VersionController { private Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); - @CrossOrigin() + @Operation(summary = "Get version information") @GetMapping(value = "/version",consumes = "application/json", produces = "application/json") public String versionInformation() { diff --git a/src/main/java/com/iemr/common/identity/utils/DynamicCorsFilter.java b/src/main/java/com/iemr/common/identity/utils/DynamicCorsFilter.java new file mode 100644 index 00000000..9d018511 --- /dev/null +++ b/src/main/java/com/iemr/common/identity/utils/DynamicCorsFilter.java @@ -0,0 +1,37 @@ +package com.iemr.common.identity.utils; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Arrays; + +@Component +public class DynamicCorsFilter extends OncePerRequestFilter { + + @Value("${cors.allowed-origins}") + private String[] allowedOrigins; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + String origin = request.getHeader("Origin"); + if (origin != null && Arrays.asList(allowedOrigins).contains(origin)) { + response.setHeader("Access-Control-Allow-Origin", origin); + } + + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + response.setStatus(HttpServletResponse.SC_OK); + } else { + filterChain.doFilter(request, response); + } + } +} From ced3d76150b1071b05d0b8a7d6e8d2a1767e5ae8 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 26 May 2025 10:50:29 +0530 Subject: [PATCH 3/3] fix(cors): added the cors user validation --- .../environment/common_example.properties | 2 +- .../common/identity/config/CorsConfig.java | 2 +- .../common/identity/utils/FilterConfig.java | 10 +++- .../utils/JwtUserIdValidationFilter.java | 51 ++++++++++++++++--- 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index 9a0fce00..92ccd6a4 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -14,4 +14,4 @@ tm-url=http://localhost:8089/ # FHIR Config fhir-url=http://localhost:8093/ -cors.allowed-origins= +cors.allowed-origins=* diff --git a/src/main/java/com/iemr/common/identity/config/CorsConfig.java b/src/main/java/com/iemr/common/identity/config/CorsConfig.java index c9a94b02..ed306e04 100644 --- a/src/main/java/com/iemr/common/identity/config/CorsConfig.java +++ b/src/main/java/com/iemr/common/identity/config/CorsConfig.java @@ -14,7 +14,7 @@ public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins(allowedOrigins.split(",")) + .allowedOriginPatterns(allowedOrigins.split(",")) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .exposedHeaders("Authorization", "Jwttoken") // Explicitly expose headers if needed diff --git a/src/main/java/com/iemr/common/identity/utils/FilterConfig.java b/src/main/java/com/iemr/common/identity/utils/FilterConfig.java index 8961d997..1404b043 100644 --- a/src/main/java/com/iemr/common/identity/utils/FilterConfig.java +++ b/src/main/java/com/iemr/common/identity/utils/FilterConfig.java @@ -7,13 +7,19 @@ @Configuration public class FilterConfig { + @Value("${cors.allowed-origins}") + private String allowedOrigins; + @Bean public FilterRegistrationBean jwtUserIdValidationFilter( JwtAuthenticationUtil jwtAuthenticationUtil) { FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new JwtUserIdValidationFilter(jwtAuthenticationUtil)); + + // Pass allowedOrigins explicitly to the filter constructor + JwtUserIdValidationFilter filter = new JwtUserIdValidationFilter(jwtAuthenticationUtil, allowedOrigins); + + registrationBean.setFilter(filter); registrationBean.addUrlPatterns("/*"); // Apply filter to all API endpoints return registrationBean; } - } \ No newline at end of file diff --git a/src/main/java/com/iemr/common/identity/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/identity/utils/JwtUserIdValidationFilter.java index 7fa15269..c27778d8 100644 --- a/src/main/java/com/iemr/common/identity/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/identity/utils/JwtUserIdValidationFilter.java @@ -1,14 +1,21 @@ package com.iemr.common.identity.utils; -import jakarta.servlet.*; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import java.io.IOException; +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 { @@ -16,9 +23,13 @@ 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; - } + private final String allowedOrigins; + + public JwtUserIdValidationFilter(JwtAuthenticationUtil jwtAuthenticationUtil, + @Value("${cors.allowed-origins}") String allowedOrigins) { + this.jwtAuthenticationUtil = jwtAuthenticationUtil; + this.allowedOrigins = allowedOrigins; + } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) @@ -30,6 +41,20 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo String contextPath = request.getContextPath(); logger.info("JwtUserIdValidationFilter invoked for path: " + path); + String origin = request.getHeader("Origin"); + if (origin != null && isOriginAllowed(origin)) { + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept, Jwttoken"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + } + + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + logger.info("OPTIONS request - skipping JWT validation"); + response.setStatus(HttpServletResponse.SC_OK); + return; + } + // Log cookies for debugging Cookie[] cookies = request.getCookies(); if (cookies != null) { @@ -83,6 +108,16 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo } } + private boolean isOriginAllowed(String origin) { + if (origin == null || allowedOrigins == null || allowedOrigins.trim().isEmpty()) { + logger.warn("No allowed origins configured or origin is null"); + return false; + } + + return Arrays.stream(allowedOrigins.split(",")).map(String::trim) + .anyMatch(pattern -> origin.matches(pattern.replace(".", "\\.").replace("*", ".*"))); + } + private String getJwtTokenFromCookies(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies != null) {