diff --git a/pom.xml b/pom.xml
index 171ab162..6a821d7d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.iemr.common-API
common-api
- 3.6.1
+ 3.8.0
war
Common-API
diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties
index 9f54a35d..f2b774a3 100644
--- a/src/main/environment/common_ci.properties
+++ b/src/main/environment/common_ci.properties
@@ -203,5 +203,9 @@ platform.feedback.ratelimit.day-limit=@env.PLATFORM_FEEDBACK_RATELIMIT_DAY_LIMIT
platform.feedback.ratelimit.user-day-limit=@env.PLATFORM_FEEDBACK_RATELIMIT_USER_DAY_LIMIT@
platform.feedback.ratelimit.fail-window-minutes=@env.PLATFORM_FEEDBACK_RATELIMIT_FAIL_WINDOW_MINUTES@
platform.feedback.ratelimit.backoff-minutes=@env.PLATFORM_FEEDBACK_RATELIMIT_BACKOFF_MINUTES@
+otp.ratelimit.enabled=@env.OTP_RATELIMIT_ENABLED@
+otp.ratelimit.minute-limit=@env.OTP_RATELIMIT_MINUTE_LIMIT@
+otp.ratelimit.hour-limit=@env.OTP_RATELIMIT_HOUR_LIMIT@
+otp.ratelimit.day-limit=@env.OTP_RATELIMIT_DAY_LIMIT@
generateBeneficiaryIDs-api-url=@env.GEN_BENEFICIARY_IDS_API_URL@
diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties
index 59cb580d..a5c633e4 100644
--- a/src/main/environment/common_docker.properties
+++ b/src/main/environment/common_docker.properties
@@ -206,4 +206,8 @@ platform.feedback.ratelimit.day-limit=${PLATFORM_FEEDBACK_RATELIMIT_DAY_LIMIT}
platform.feedback.ratelimit.user-day-limit=${PLATFORM_FEEDBACK_RATELIMIT_USER_DAY_LIMIT}
platform.feedback.ratelimit.fail-window-minutes=${PLATFORM_FEEDBACK_RATELIMIT_FAIL_WINDOW_MINUTES}
platform.feedback.ratelimit.backoff-minutes=${PLATFORM_FEEDBACK_RATELIMIT_BACKOFF_MINUTES}
+otp.ratelimit.enabled=${OTP_RATELIMIT_ENABLED}
+otp.ratelimit.minute-limit=${OTP_RATELIMIT_MINUTE_LIMIT}
+otp.ratelimit.hour-limit=${OTP_RATELIMIT_HOUR_LIMIT}
+otp.ratelimit.day-limit=${OTP_RATELIMIT_DAY_LIMIT}
generateBeneficiaryIDs-api-url={GEN_BENEFICIARY_IDS_API_URL}
diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties
index 03f0d915..e3b5c031 100644
--- a/src/main/environment/common_example.properties
+++ b/src/main/environment/common_example.properties
@@ -226,5 +226,10 @@ platform.feedback.ratelimit.user-day-limit=50
platform.feedback.ratelimit.fail-window-minutes=5
platform.feedback.ratelimit.backoff-minutes=15
+# --- OTP Rate Limiting (per mobile number) ---
+otp.ratelimit.minute-limit=3
+otp.ratelimit.hour-limit=10
+otp.ratelimit.day-limit=20
+
### generate Beneficiary IDs URL
generateBeneficiaryIDs-api-url=/generateBeneficiaryController/generateBeneficiaryIDs
diff --git a/src/main/java/com/iemr/common/controller/beneficiaryConsent/BeneficiaryConsentController.java b/src/main/java/com/iemr/common/controller/beneficiaryConsent/BeneficiaryConsentController.java
index 77492d89..8750c0a1 100644
--- a/src/main/java/com/iemr/common/controller/beneficiaryConsent/BeneficiaryConsentController.java
+++ b/src/main/java/com/iemr/common/controller/beneficiaryConsent/BeneficiaryConsentController.java
@@ -22,6 +22,7 @@
package com.iemr.common.controller.beneficiaryConsent;
import com.iemr.common.data.beneficiaryConsent.BeneficiaryConsentRequest;
+import com.iemr.common.exception.OtpRateLimitException;
import com.iemr.common.service.beneficiaryOTPHandler.BeneficiaryOTPHandler;
import com.iemr.common.utils.mapper.InputMapper;
import com.iemr.common.utils.response.OutputResponse;
@@ -58,7 +59,9 @@ public String sendConsent(@Param(value = "{\"mobNo\":\"String\"}") @RequestBody
logger.info(success.toString());
response.setResponse(success);
-
+ } catch (OtpRateLimitException e) {
+ logger.warn("OTP rate limit hit for sendConsent: " + e.getMessage());
+ response.setError(429, e.getMessage());
} catch (Exception e) {
response.setError(500, "error : " + e);
}
@@ -105,6 +108,9 @@ public String resendConsent(@Param(value = "{\"mobNo\":\"String\"}") @RequestBod
else
response.setError(500, "failure");
+ } catch (OtpRateLimitException e) {
+ logger.warn("OTP rate limit hit for resendConsent: " + e.getMessage());
+ response.setError(429, e.getMessage());
} catch (Exception e) {
logger.error("error in re-sending Consent : " + e);
response.setError(500, "error : " + e);
diff --git a/src/main/java/com/iemr/common/exception/OtpRateLimitException.java b/src/main/java/com/iemr/common/exception/OtpRateLimitException.java
new file mode 100644
index 00000000..a0f3b53f
--- /dev/null
+++ b/src/main/java/com/iemr/common/exception/OtpRateLimitException.java
@@ -0,0 +1,30 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.common.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
+public class OtpRateLimitException extends RuntimeException {
+ public OtpRateLimitException(String message) { super(message); }
+}
diff --git a/src/main/java/com/iemr/common/service/beneficiaryOTPHandler/BeneficiaryOTPHandlerImpl.java b/src/main/java/com/iemr/common/service/beneficiaryOTPHandler/BeneficiaryOTPHandlerImpl.java
index 42e0acfe..3cc0a709 100644
--- a/src/main/java/com/iemr/common/service/beneficiaryOTPHandler/BeneficiaryOTPHandlerImpl.java
+++ b/src/main/java/com/iemr/common/service/beneficiaryOTPHandler/BeneficiaryOTPHandlerImpl.java
@@ -32,6 +32,7 @@
import com.iemr.common.repository.sms.SMSTemplateRepository;
import com.iemr.common.repository.sms.SMSTypeRepository;
import com.iemr.common.service.otp.OTPHandler;
+import com.iemr.common.service.otp.OtpRateLimiterService;
import com.iemr.common.service.users.IEMRAdminUserServiceImpl;
import com.iemr.common.utils.config.ConfigProperties;
import com.iemr.common.utils.http.HttpUtils;
@@ -59,6 +60,8 @@ public class BeneficiaryOTPHandlerImpl implements BeneficiaryOTPHandler {
HttpUtils httpUtils;
@Autowired
private IEMRAdminUserServiceImpl iEMRAdminUserServiceImpl;
+ @Autowired
+ private OtpRateLimiterService otpRateLimiterService;
final Logger logger = LoggerFactory.getLogger(this.getClass().getName());
@Autowired
@@ -107,6 +110,7 @@ public String load(String key) {
*/
@Override
public String sendOTP(BeneficiaryConsentRequest obj) throws Exception {
+ otpRateLimiterService.checkRateLimit(obj.getMobNo());
int otp = generateOTP(obj.getMobNo());
return sendSMS(otp, obj);
}
@@ -141,6 +145,7 @@ public JSONObject validateOTP(BeneficiaryConsentRequest obj) throws Exception {
*/
@Override
public String resendOTP(BeneficiaryConsentRequest obj) throws Exception {
+ otpRateLimiterService.checkRateLimit(obj.getMobNo());
int otp = generateOTP(obj.getMobNo());
return sendSMS(otp, obj);
}
diff --git a/src/main/java/com/iemr/common/service/otp/OtpRateLimiterService.java b/src/main/java/com/iemr/common/service/otp/OtpRateLimiterService.java
new file mode 100644
index 00000000..da06a64b
--- /dev/null
+++ b/src/main/java/com/iemr/common/service/otp/OtpRateLimiterService.java
@@ -0,0 +1,104 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.common.service.otp;
+
+import com.iemr.common.exception.OtpRateLimitException;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Rate-limits OTP send/resend requests per mobile number using Redis counters.
+ *
+ * Limits (configurable via properties):
+ * otp.ratelimit.minute-limit – max OTPs per minute (default 3)
+ * otp.ratelimit.hour-limit – max OTPs per hour (default 10)
+ * otp.ratelimit.day-limit – max OTPs per day (default 20)
+ *
+ * Redis key pattern:
+ * rl:otp:min:{mobNo}:{minuteSlot} TTL 60 s
+ * rl:otp:hr:{mobNo}:{hourSlot} TTL 3600 s
+ * rl:otp:day:{mobNo}:{yyyyMMdd} TTL 86400 s
+ */
+@Component
+public class OtpRateLimiterService {
+
+ private final StringRedisTemplate redis;
+
+ @Value("${otp.ratelimit.enabled:true}")
+ private boolean enabled;
+
+ @Value("${otp.ratelimit.minute-limit:3}")
+ private int minuteLimit;
+
+ @Value("${otp.ratelimit.hour-limit:10}")
+ private int hourLimit;
+
+ @Value("${otp.ratelimit.day-limit:20}")
+ private int dayLimit;
+
+ public OtpRateLimiterService(StringRedisTemplate redis) {
+ this.redis = redis;
+ }
+
+ /**
+ * Checks all three rate-limit windows for the given mobile number.
+ * Throws {@link OtpRateLimitException} if any limit is exceeded.
+ * No-op when otp.ratelimit.enabled=false.
+ */
+ public void checkRateLimit(String mobNo) {
+ if (!enabled) return;
+ String today = LocalDate.now(ZoneId.of("Asia/Kolkata"))
+ .toString().replaceAll("-", ""); // yyyyMMdd
+ long minuteSlot = System.currentTimeMillis() / 60_000L;
+ long hourSlot = System.currentTimeMillis() / 3_600_000L;
+
+ String minKey = "rl:otp:min:" + mobNo + ":" + minuteSlot;
+ String hourKey = "rl:otp:hr:" + mobNo + ":" + hourSlot;
+ String dayKey = "rl:otp:day:" + mobNo + ":" + today;
+
+ if (incrementWithExpire(minKey, 60L) > minuteLimit) {
+ throw new OtpRateLimitException(
+ "OTP request limit exceeded. Maximum " + minuteLimit + " OTPs allowed per minute. Please try again later.");
+ }
+ if (incrementWithExpire(hourKey, 3600L) > hourLimit) {
+ throw new OtpRateLimitException(
+ "OTP request limit exceeded. Maximum " + hourLimit + " OTPs allowed per hour. Please try again later.");
+ }
+ if (incrementWithExpire(dayKey, 86400L) > dayLimit) {
+ throw new OtpRateLimitException(
+ "OTP request limit exceeded. Maximum " + dayLimit + " OTPs allowed per day. Please try again tomorrow.");
+ }
+ }
+
+ private long incrementWithExpire(String key, long ttlSeconds) {
+ Long value = redis.opsForValue().increment(key, 1L);
+ if (value != null && value == 1L) {
+ redis.expire(key, ttlSeconds, TimeUnit.SECONDS);
+ }
+ return value == null ? 0L : value;
+ }
+}