diff --git a/component/common/pom.xml b/component/common/pom.xml index 6942b3bf..644a2bab 100644 --- a/component/common/pom.xml +++ b/component/common/pom.xml @@ -102,6 +102,8 @@ org.wso2.carbon.context; version="${carbon.kernel.package.import.version.range}", org.wso2.carbon.identity.application.authentication.framework.store; version="${carbon.identity.framework.imp.pkg.version.range}", + org.wso2.carbon.identity.application.common.model; version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.core.util; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.event; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.event.event; version="${carbon.identity.package.import.version.range}", @@ -109,6 +111,7 @@ version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.event.services; version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.governance; version="${identity.governance.imp.pkg.version.range}", org.wso2.carbon.identity.governance.service.notification; version="${identity.governance.imp.pkg.version.range}", org.wso2.carbon.identity.recovery.internal; @@ -118,7 +121,11 @@ org.wso2.carbon.user.core.common; version="${carbon.kernel.package.import.version.range}", org.wso2.carbon.user.core.constants; version="${carbon.kernel.package.import.version.range}", - org.wso2.carbon.user.core.service; version="${carbon.kernel.package.import.version.range}" + org.wso2.carbon.user.core.service; version="${carbon.kernel.package.import.version.range}", + org.wso2.carbon.identity.handler.event.account.lock.exception; + version="${carbon.identity.account.lock.handler.imp.pkg.version.range}", + org.wso2.carbon.identity.handler.event.account.lock.service; + version="${carbon.identity.account.lock.handler.imp.pkg.version.range}" !org.wso2.carbon.identity.smsotp.common.internal, diff --git a/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/SMSOTPServiceImpl.java b/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/SMSOTPServiceImpl.java index 64fad1a6..ab3cf19f 100644 --- a/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/SMSOTPServiceImpl.java +++ b/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/SMSOTPServiceImpl.java @@ -20,12 +20,15 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang.math.NumberUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.slf4j.MDC; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.identity.application.authentication.framework.store.SessionDataStore; +import org.wso2.carbon.identity.application.common.model.Property; +import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.event.IdentityEventConstants; import org.wso2.carbon.identity.event.IdentityEventException; import org.wso2.carbon.identity.event.event.Event; @@ -172,11 +175,13 @@ public ValidationResponseDTO validateSMSOTP(String transactionId, String userId, } // Valid OTP. Clear OTP session data. SessionDataStore.getInstance().clearSessionData(sessionId, Constants.SESSION_TYPE_OTP); + resetOtpFailedAttempts(userId); return new ValidationResponseDTO(userId, true); } private ValidationResponseDTO isValid(SessionDTO sessionDTO, String smsOTP, String userId, - String transactionId, int validateAttempt, boolean showFailureReason) { + String transactionId, int validateAttempt, boolean showFailureReason) + throws SMSOTPException { FailureReasonDTO error; // Check if the provided OTP is correct. @@ -184,6 +189,10 @@ private ValidationResponseDTO isValid(SessionDTO sessionDTO, String smsOTP, Stri if (log.isDebugEnabled()) { log.debug(String.format("Invalid OTP provided for the user : %s.", userId)); } + ValidationResponseDTO responseDTO = handleAccountLock(userId, showFailureReason); + if (responseDTO != null) { + return responseDTO; + } int remainingFailedAttempts = SMSOTPServiceDataHolder.getConfigs().getMaxValidationAttemptsAllowed() - validateAttempt; error = showFailureReason @@ -388,4 +397,196 @@ public static String getCorrelationId() { return StringUtils.isBlank(MDC.get(Constants.CORRELATION_ID_MDC)) ? UUID.randomUUID().toString() : MDC.get(Constants.CORRELATION_ID_MDC); } + + /** + * Execute account lock flow for OTP verification failures. + * + */ + private ValidationResponseDTO handleAccountLock(String userId, boolean showFailureReason) + throws SMSOTPException { + + boolean lockAccountOnFailedAttempts = SMSOTPServiceDataHolder.getConfigs().isLockAccountOnFailedAttempts(); + if (!lockAccountOnFailedAttempts) { + return null; + } + + User user = getUserById(userId); + if (Utils.isAccountLocked(user)) { + FailureReasonDTO error = showFailureReason + ? new FailureReasonDTO(Constants.ErrorMessage.CLIENT_ACCOUNT_LOCKED, userId) + : null; + return new ValidationResponseDTO(userId, false, error); + } + + int maxAttempts = 0; + long unlockTimePropertyValue = 0; + double unlockTimeRatio = 1; + + Property[] connectorConfigs = Utils.getAccountLockConnectorConfigs(user.getTenantDomain()); + for (Property connectorConfig : connectorConfigs) { + switch (connectorConfig.getName()) { + case Constants.PROPERTY_ACCOUNT_LOCK_ON_FAILURE: + if (!Boolean.parseBoolean(connectorConfig.getValue())) { + return null; + } + case Constants.PROPERTY_ACCOUNT_LOCK_ON_FAILURE_MAX: + if (NumberUtils.isNumber(connectorConfig.getValue())) { + maxAttempts = Integer.parseInt(connectorConfig.getValue()); + } + break; + case Constants.PROPERTY_ACCOUNT_LOCK_TIME: + if (NumberUtils.isNumber(connectorConfig.getValue())) { + unlockTimePropertyValue = Integer.parseInt(connectorConfig.getValue()); + } + break; + case Constants.PROPERTY_LOGIN_FAIL_TIMEOUT_RATIO: + if (NumberUtils.isNumber(connectorConfig.getValue())) { + double value = Double.parseDouble(connectorConfig.getValue()); + if (value > 0) { + unlockTimeRatio = value; + } + } + break; + } + } + Map claimValues = getUserClaimValues(user, new String[]{ + Constants.SMS_OTP_FAILED_ATTEMPTS_CLAIM, Constants.FAILED_LOGIN_LOCKOUT_COUNT_CLAIM}); + if (claimValues == null) { + claimValues = new HashMap<>(); + } + int currentAttempts = 0; + if (NumberUtils.isNumber(claimValues.get(Constants.SMS_OTP_FAILED_ATTEMPTS_CLAIM))) { + currentAttempts = Integer.parseInt(claimValues.get(Constants.SMS_OTP_FAILED_ATTEMPTS_CLAIM)); + } + int failedLoginLockoutCountValue = 0; + if (NumberUtils.isNumber(claimValues.get(Constants.FAILED_LOGIN_LOCKOUT_COUNT_CLAIM))) { + failedLoginLockoutCountValue = + Integer.parseInt(claimValues.get(Constants.FAILED_LOGIN_LOCKOUT_COUNT_CLAIM)); + } + + Map updatedClaims = new HashMap<>(); + if ((currentAttempts + 1) >= maxAttempts) { + // Calculate the incremental unlock time interval in milli seconds. + unlockTimePropertyValue = (long) (unlockTimePropertyValue * 1000 * 60 * Math.pow(unlockTimeRatio, + failedLoginLockoutCountValue)); + // Calculate unlock time by adding current time and unlock time interval in milli seconds. + long unlockTime = System.currentTimeMillis() + unlockTimePropertyValue; + updatedClaims.put(Constants.ACCOUNT_LOCKED_CLAIM, Boolean.TRUE.toString()); + updatedClaims.put(Constants.SMS_OTP_FAILED_ATTEMPTS_CLAIM, "0"); + updatedClaims.put(Constants.ACCOUNT_UNLOCK_TIME_CLAIM, String.valueOf(unlockTime)); + updatedClaims.put(Constants.FAILED_LOGIN_LOCKOUT_COUNT_CLAIM, + String.valueOf(failedLoginLockoutCountValue + 1)); + updatedClaims.put(Constants.ACCOUNT_LOCKED_REASON_CLAIM_URI, + Constants.MAX_SMS_OTP_ATTEMPTS_EXCEEDED); + IdentityUtil.threadLocalProperties.get().put(Constants.ADMIN_INITIATED, false); + setUserClaimValues(user, updatedClaims); + FailureReasonDTO error = showFailureReason + ? new FailureReasonDTO(Constants.ErrorMessage.CLIENT_ACCOUNT_LOCKED, userId) + : null; + return new ValidationResponseDTO(userId, false, error); + } else { + updatedClaims.put(Constants.SMS_OTP_FAILED_ATTEMPTS_CLAIM, String.valueOf(currentAttempts + 1)); + setUserClaimValues(user, updatedClaims); + return null; + } + } + + private User getUserById(String userId) throws SMSOTPException { + + try { + AbstractUserStoreManager userStoreManager = (AbstractUserStoreManager) SMSOTPServiceDataHolder + .getInstance().getRealmService().getTenantUserRealm(getTenantId()).getUserStoreManager(); + return userStoreManager.getUser(userId, null); + } catch (UserStoreException e) { + // Handle user not found. + String errorCode = ((org.wso2.carbon.user.core.UserStoreException) e).getErrorCode(); + if (UserCoreErrorConstants.ErrorMessages.ERROR_CODE_NON_EXISTING_USER.getCode().equals(errorCode)) { + throw Utils.handleClientException(Constants.ErrorMessage.CLIENT_INVALID_USER_ID, userId); + } + throw Utils.handleServerException(Constants.ErrorMessage.SERVER_USER_STORE_MANAGER_ERROR, + String.format("Error while retrieving user for the ID : %s.", userId), e); + } + } + + private Map getUserClaimValues(User user, String[] claims) throws SMSOTPServerException { + + try { + AbstractUserStoreManager userStoreManager = (AbstractUserStoreManager) SMSOTPServiceDataHolder + .getInstance().getRealmService().getTenantUserRealm(getTenantId()).getUserStoreManager(); + return userStoreManager.getUserClaimValues(user.getDomainQualifiedUsername(), claims, + UserCoreConstants.DEFAULT_PROFILE); + } catch (UserStoreException e) { + log.error("Error while reading user claims.", e); + throw Utils.handleServerException(Constants.ErrorMessage.SERVER_USER_STORE_MANAGER_ERROR, + String.format("Failed to read user claims for user ID : %s.", user.getUserID()), e); + } + } + + private void setUserClaimValues(User user, Map updatedClaims) throws SMSOTPServerException { + + try { + AbstractUserStoreManager userStoreManager = (AbstractUserStoreManager) SMSOTPServiceDataHolder + .getInstance().getRealmService().getTenantUserRealm(getTenantId()).getUserStoreManager(); + userStoreManager.setUserClaimValues(user.getDomainQualifiedUsername(), updatedClaims, + UserCoreConstants.DEFAULT_PROFILE); + } catch (UserStoreException e) { + log.error("Error while updating user claims", e); + throw Utils.handleServerException(Constants.ErrorMessage.SERVER_USER_STORE_MANAGER_ERROR, + String.format("Failed to update user claims for user ID: %s.", user.getUserID()), e); + } + } + + /** + * Reset OTP Failed Attempts count upon successful completion of the OTP verification. + * + * @param userId The ID of the user. + * @throws SMSOTPException If an error occurred. + */ + private void resetOtpFailedAttempts(String userId) throws SMSOTPException { + + if (!SMSOTPServiceDataHolder.getConfigs().isLockAccountOnFailedAttempts()) { + return; + } + + User user = getUserById(userId); + Property[] connectorConfigs = Utils.getAccountLockConnectorConfigs(user.getTenantDomain()); + // Return if account lock handler is not enabled. + for (Property connectorConfig : connectorConfigs) { + if ((Constants.PROPERTY_ACCOUNT_LOCK_ON_FAILURE.equals(connectorConfig.getName())) && + !Boolean.parseBoolean(connectorConfig.getValue())) { + return; + } + } + + try { + AbstractUserStoreManager userStoreManager = (AbstractUserStoreManager) SMSOTPServiceDataHolder + .getInstance().getRealmService().getTenantUserRealm(getTenantId()).getUserStoreManager(); + + String[] claimsToCheck = {Constants.SMS_OTP_FAILED_ATTEMPTS_CLAIM, Constants.ACCOUNT_LOCKED_CLAIM}; + Map userClaims = userStoreManager.getUserClaimValues(user.getDomainQualifiedUsername(), + claimsToCheck, UserCoreConstants.DEFAULT_PROFILE); + String failedEmailOtpAttemptsClaimValue = userClaims.get(Constants.SMS_OTP_FAILED_ATTEMPTS_CLAIM); + String accountLockClaimValue = userClaims.get(Constants.ACCOUNT_LOCKED_CLAIM); + + Map updatedClaims = new HashMap<>(); + if (NumberUtils.isNumber(failedEmailOtpAttemptsClaimValue) && + Integer.parseInt(failedEmailOtpAttemptsClaimValue) > 0) { + updatedClaims.put(Constants.SMS_OTP_FAILED_ATTEMPTS_CLAIM, "0"); + } + if (Boolean.parseBoolean(accountLockClaimValue)) { + updatedClaims.put(Constants.ACCOUNT_LOCKED_CLAIM, Boolean.FALSE.toString()); + updatedClaims.put(Constants.ACCOUNT_UNLOCK_TIME_CLAIM, "0"); + } + if (!updatedClaims.isEmpty()) { + userStoreManager.setUserClaimValues(user.getDomainQualifiedUsername(), updatedClaims, + UserCoreConstants.DEFAULT_PROFILE); + } + } catch (UserStoreException e) { + String errorMessage = String.format("Failed to reset failed attempts count for user ID : %s.", + user.getUserID()); + log.error(errorMessage, e); + throw Utils.handleServerException(Constants.ErrorMessage.SERVER_USER_STORE_MANAGER_ERROR, + errorMessage, e); + } + } } diff --git a/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/constant/Constants.java b/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/constant/Constants.java index 3e0fa7fa..63e41303 100644 --- a/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/constant/Constants.java +++ b/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/constant/Constants.java @@ -49,6 +49,20 @@ public class Constants { public static final String SMS_OTP_RESEND_THROTTLE_INTERVAL = "smsOtp.resendThrottleInterval"; public static final String SMS_OTP_MAX_VALIDATION_ATTEMPTS_ALLOWED = "smsOtp.maxValidationAttemptsAllowed"; public static final String SMS_OTP_SHOW_FAILURE_REASON = "smsOtp.showValidationFailureReason"; + public static final String SMS_OTP_LOCK_ACCOUNT_ON_FAILED_ATTEMPTS = "smsOtp.lockAccountOnFailedAttempts"; + + public static final String PROPERTY_LOGIN_FAIL_TIMEOUT_RATIO = "account.lock.handler.login.fail.timeout.ratio"; + public static final String PROPERTY_ACCOUNT_LOCK_ON_FAILURE = "account.lock.handler.enable"; + public static final String PROPERTY_ACCOUNT_LOCK_ON_FAILURE_MAX = "account.lock.handler.On.Failure.Max.Attempts"; + public static final String PROPERTY_ACCOUNT_LOCK_TIME = "account.lock.handler.Time"; + public static final String SMS_OTP_FAILED_ATTEMPTS_CLAIM = "http://wso2.org/claims/identity/failedSmsOtpAttempts"; + public static final String FAILED_LOGIN_LOCKOUT_COUNT_CLAIM = + "http://wso2.org/claims/identity/failedLoginLockoutCount"; + public static final String ACCOUNT_LOCKED_CLAIM = "http://wso2.org/claims/identity/accountLocked"; + public static final String ACCOUNT_UNLOCK_TIME_CLAIM = "http://wso2.org/claims/identity/unlockTime"; + public static final String ACCOUNT_LOCKED_REASON_CLAIM_URI = "http://wso2.org/claims/identity/lockedReason"; + public static final String MAX_SMS_OTP_ATTEMPTS_EXCEEDED = "MAX_SMS_OTP_ATTEMPTS_EXCEEDED"; + public static final String ADMIN_INITIATED = "AdminInitiated"; public static final String CORRELATION_ID_MDC = "Correlation-ID"; public static final String CORRELATION_ID = "correlation-id"; @@ -72,12 +86,13 @@ public enum ErrorMessage { "Mandatory parameters not found : %s."), CLIENT_OTP_VALIDATION_FAILED("SMS-60008", "Provided OTP is invalid.", "Provided OTP is invalid. User id : %s."), - CLIENT_NO_OTP_FOR_USER("SMS-60009", "No OTP fround for the user.", + CLIENT_NO_OTP_FOR_USER("SMS-60009", "No OTP found for the user.", "No OTP found for the user Id : %s."), CLIENT_SLOW_DOWN_RESEND("SMS-60010", "Slow down.", "Please wait %s seconds before retrying."), CLIENT_OTP_VALIDATION_BLOCKED("SMS-60011", "Maximum allowed failed validation attempts exceeded.", "Maximum allowed failed validation attempts exceeded for user id : %s."), + CLIENT_ACCOUNT_LOCKED("SMS-60012", "Account locked.", "Account is locked for the user ID: %s."), // Server error codes. SERVER_USER_STORE_MANAGER_ERROR("SMS-65001", "User store manager error.", @@ -99,7 +114,11 @@ public enum ErrorMessage { SERVER_INVALID_RENEWAL_INTERVAL_ERROR("SMS-65009", "Invalid renewal interval value.", "Renewal interval should be smaller than the OTP validity period. Renewal interval: %s."), SERVER_UNEXPECTED_ERROR("SMS-65010", "An unexpected server error occurred.", - "An unexpected server error occurred."); + "An unexpected server error occurred."), + SERVER_ERROR_VALIDATING_ACCOUNT_LOCK_STATUS("SMS-65011", "Error validating account lock status.", + "Server encountered an error while validating account lock status for the user ID : %s."), + SERVER_ERROR_RETRIEVING_ACCOUNT_LOCK_CONFIGS("SMS-65012", "Can't retrieve account lock connector " + + "configurations.", "Server encountered an error while retrieving account lock connector configurations."); private final String code; private final String message; diff --git a/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/dto/ConfigsDTO.java b/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/dto/ConfigsDTO.java index ea6c8971..bf19c15b 100644 --- a/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/dto/ConfigsDTO.java +++ b/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/dto/ConfigsDTO.java @@ -34,6 +34,7 @@ public class ConfigsDTO { private int otpRenewalInterval; private int resendThrottleInterval; private int maxValidationAttemptsAllowed; + private boolean lockAccountOnFailedAttempts; public boolean isEnabled() { @@ -145,6 +146,16 @@ public void setMaxValidationAttemptsAllowed(int maxValidationAttemptsAllowed) { this.maxValidationAttemptsAllowed = maxValidationAttemptsAllowed; } + public boolean isLockAccountOnFailedAttempts() { + + return lockAccountOnFailedAttempts; + } + + public void setLockAccountOnFailedAttempts(boolean lockAccountOnFailedAttempts) { + + this.lockAccountOnFailedAttempts = lockAccountOnFailedAttempts; + } + @Override public String toString() { @@ -160,6 +171,7 @@ public String toString() { .append(",\n\totpRenewalInterval = ").append(otpRenewalInterval) .append(",\n\tresendThrottleInterval = ").append(resendThrottleInterval) .append(",\n\tmaxValidationAttemptsAllowed = ").append(maxValidationAttemptsAllowed) + .append(",\n\tlockAccountOnFailedAttempts = ").append(lockAccountOnFailedAttempts) .append("\n}"); return sb.toString(); } diff --git a/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/internal/SMSOTPServiceComponent.java b/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/internal/SMSOTPServiceComponent.java index c7c1fe92..1a88bb76 100644 --- a/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/internal/SMSOTPServiceComponent.java +++ b/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/internal/SMSOTPServiceComponent.java @@ -28,6 +28,8 @@ import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; import org.wso2.carbon.identity.event.services.IdentityEventService; +import org.wso2.carbon.identity.governance.IdentityGovernanceService; +import org.wso2.carbon.identity.handler.event.account.lock.service.AccountLockService; import org.wso2.carbon.identity.smsotp.common.SMSOTPService; import org.wso2.carbon.identity.smsotp.common.SMSOTPServiceImpl; import org.wso2.carbon.identity.smsotp.common.util.Utils; @@ -98,4 +100,38 @@ protected void unsetEventService(IdentityEventService eventService) { log.debug("Setting the Event Service."); } } + + @Reference( + name = "AccountLockService", + service = org.wso2.carbon.identity.handler.event.account.lock.service.AccountLockService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetAccountLockService" + ) + protected void setAccountLockService(AccountLockService accountLockService) { + + SMSOTPServiceDataHolder.getInstance().setAccountLockService(accountLockService); + } + + protected void unsetAccountLockService(AccountLockService accountLockService) { + + SMSOTPServiceDataHolder.getInstance().setAccountLockService(null); + } + + @Reference( + name = "IdentityGovernanceService", + service = org.wso2.carbon.identity.governance.IdentityGovernanceService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetIdentityGovernanceService" + ) + protected void setIdentityGovernanceService(IdentityGovernanceService identityGovernanceService) { + + SMSOTPServiceDataHolder.getInstance().setIdentityGovernanceService(identityGovernanceService); + } + + protected void unsetIdentityGovernanceService(IdentityGovernanceService identityGovernanceService) { + + SMSOTPServiceDataHolder.getInstance().setIdentityGovernanceService(null); + } } diff --git a/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/internal/SMSOTPServiceDataHolder.java b/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/internal/SMSOTPServiceDataHolder.java index 7f029607..61c59a81 100644 --- a/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/internal/SMSOTPServiceDataHolder.java +++ b/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/internal/SMSOTPServiceDataHolder.java @@ -18,6 +18,8 @@ package org.wso2.carbon.identity.smsotp.common.internal; +import org.wso2.carbon.identity.governance.IdentityGovernanceService; +import org.wso2.carbon.identity.handler.event.account.lock.service.AccountLockService; import org.wso2.carbon.identity.smsotp.common.dto.ConfigsDTO; import org.wso2.carbon.user.core.service.RealmService; @@ -28,6 +30,8 @@ public class SMSOTPServiceDataHolder { private static final SMSOTPServiceDataHolder dataHolder = new SMSOTPServiceDataHolder(); private RealmService realmService; + private AccountLockService accountLockService; + private IdentityGovernanceService identityGovernanceService; private static final ConfigsDTO configs = new ConfigsDTO(); public static SMSOTPServiceDataHolder getInstance() { @@ -49,4 +53,44 @@ public static ConfigsDTO getConfigs() { return configs; } + + /** + * Get Account Lock service. + * + * @return Account Lock service. + */ + public AccountLockService getAccountLockService() { + + return accountLockService; + } + + /** + * Set Account Lock service. + * + * @param accountLockService Account Lock service. + */ + public void setAccountLockService(AccountLockService accountLockService) { + + this.accountLockService = accountLockService; + } + + /** + * Get Identity Governance service. + * + * @return Identity Governance service. + */ + public IdentityGovernanceService getIdentityGovernanceService() { + + return identityGovernanceService; + } + + /** + * Set Identity Governance service. + * + * @param identityGovernanceService Identity Governance service. + */ + public void setIdentityGovernanceService(IdentityGovernanceService identityGovernanceService) { + + this.identityGovernanceService = identityGovernanceService; + } } diff --git a/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/util/Utils.java b/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/util/Utils.java index ace4342d..879ec3b0 100644 --- a/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/util/Utils.java +++ b/component/common/src/main/java/org/wso2/carbon/identity/smsotp/common/util/Utils.java @@ -22,14 +22,18 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.application.common.model.Property; import org.wso2.carbon.identity.event.IdentityEventConfigBuilder; import org.wso2.carbon.identity.event.IdentityEventException; import org.wso2.carbon.identity.event.bean.ModuleConfiguration; +import org.wso2.carbon.identity.governance.IdentityGovernanceException; +import org.wso2.carbon.identity.handler.event.account.lock.exception.AccountLockServiceException; import org.wso2.carbon.identity.smsotp.common.constant.Constants; import org.wso2.carbon.identity.smsotp.common.dto.ConfigsDTO; import org.wso2.carbon.identity.smsotp.common.exception.SMSOTPClientException; import org.wso2.carbon.identity.smsotp.common.exception.SMSOTPServerException; import org.wso2.carbon.identity.smsotp.common.internal.SMSOTPServiceDataHolder; +import org.wso2.carbon.user.core.common.User; import java.util.Properties; import java.util.UUID; @@ -136,6 +140,10 @@ private static void sanitizeAndPopulateConfigs(Properties properties) throws SMS Constants.DEFAULT_MAX_VALIDATION_ATTEMPTS_ALLOWED; configs.setMaxValidationAttemptsAllowed(maxValidationAttemptsAllowed); + boolean lockAccountOnFailedAttempts = Boolean.parseBoolean(org.apache.commons.lang.StringUtils.trim( + properties.getProperty(Constants.SMS_OTP_LOCK_ACCOUNT_ON_FAILED_ATTEMPTS))); + configs.setLockAccountOnFailedAttempts(lockAccountOnFailedAttempts); + // Should we send the same OTP upon the next generation request. Defaults to 'false'. boolean resendSameOtp = (otpRenewalInterval > 0) && (otpRenewalInterval < otpValidityPeriod); configs.setResendSameOtp(resendSameOtp); @@ -199,4 +207,41 @@ public static SMSOTPServerException handleServerException(Constants.ErrorMessage } return new SMSOTPServerException(error.getCode(), error.getMessage(), description); } + + /** + * Check whether a given user is locked. + * + * @param user The user. + * @return True if user account is locked. + */ + public static boolean isAccountLocked(User user) throws SMSOTPServerException { + + try { + return SMSOTPServiceDataHolder.getInstance().getAccountLockService().isAccountLocked(user.getUsername(), + user.getTenantDomain(), user.getUserStoreDomain()); + } catch (AccountLockServiceException e) { + throw Utils.handleServerException(Constants.ErrorMessage.SERVER_ERROR_VALIDATING_ACCOUNT_LOCK_STATUS, + user.getUserID(), e); + } + } + + /** + * Get the account lock connector configurations. + * + * @param tenantDomain Tenant domain. + * @return Account lock connector configurations. + * @throws SMSOTPServerException Server exception while retrieving account lock configurations. + */ + public static Property[] getAccountLockConnectorConfigs(String tenantDomain) throws SMSOTPServerException { + + try { + return SMSOTPServiceDataHolder.getInstance().getIdentityGovernanceService().getConfiguration + (new String[]{Constants.PROPERTY_ACCOUNT_LOCK_ON_FAILURE, + Constants.PROPERTY_ACCOUNT_LOCK_ON_FAILURE_MAX, Constants.PROPERTY_ACCOUNT_LOCK_TIME, + Constants.PROPERTY_LOGIN_FAIL_TIMEOUT_RATIO}, tenantDomain); + } catch (IdentityGovernanceException e) { + throw Utils.handleServerException(Constants.ErrorMessage.SERVER_ERROR_RETRIEVING_ACCOUNT_LOCK_CONFIGS, null, + e); + } + } } diff --git a/docs/sms_otp_service.md b/docs/sms_otp_service.md index ca57e903..7ee33b90 100644 --- a/docs/sms_otp_service.md +++ b/docs/sms_otp_service.md @@ -28,15 +28,22 @@ properties.tokenRenewalInterval=60 properties.resendThrottleInterval=30 # Set the maximum validation attempts allowed until the generated sms-otp expires. properties.maxValidationAttemptsAllowed=5 +# Lock the account after reaching the maximum number of failed login attempts. +properties.lockAccountOnFailedAttempts = true ``` -4. If notifications are managed by the Identity Server, configure the **SMS template** by appending below at the end of + + **NOTE:** If `properties.lockAccountOnFailedAttempts` is set to `true`, at tenant level it is required to enable + the account lock capability and configure other properties such as unlock time duration. + For more details, refer to the documentation: https://is.docs.wso2.com/en/5.11.0/learn/account-locking-by-failed-login-attempts/#configuring-wso2-is-for-account-locking + +5. If notifications are managed by the Identity Server, configure the **SMS template** by appending below at the end of the `/repository/conf/sms/sms-templates-admin-config.xml` file. ```xml Your One Time Password is : {{confirmation-code}} ``` -5. If notifications are managed by the Identity Server, configure the **event publisher** by creating +6. If notifications are managed by the Identity Server, configure the **event publisher** by creating `SMSPublisher.xml` file in the `/deployment/server/eventpublishers/` directory. (Make sure to add a valid webhook endpoint in `http.url` section.) ```xml @@ -53,7 +60,7 @@ properties.maxValidationAttemptsAllowed=5 ``` -6. Restart the server. +7. Restart the server. **NOTE::** To include a **unique identification** in the **SMS template**, use the `correlation-id` variable in the following syntax,