diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java index 8b6cad99fa..3e7c7700fb 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java @@ -157,6 +157,7 @@ public class IdentityRecoveryConstants { public static final String NOTIFICATION_CHANNEL_PROPERTY_KEY = "notificationChannel"; public static final String VERIFIED_USER_PROPERTY_KEY = "verifiedUser"; public static final String MANAGE_NOTIFICATIONS_INTERNALLY_PROPERTY_KEY = "manageNotificationsInternally"; + public static final String CONFIRMATION_CODE_SEPARATOR = "."; // Recovery Scenarios. public static final String USER_NAME_RECOVERY = "UNR"; @@ -166,6 +167,7 @@ public class IdentityRecoveryConstants { public static final int SMS_OTP_CODE_LENGTH = 6; public static final String ENABLE_DETAILED_ERROR_RESPONSE = "Recovery.ErrorMessage.EnableDetailedErrorMessages"; + public static final int RECOVERY_FLOW_ID_DEFAULT_EXPIRY_TIME = 15; // Recovery code given at the username and password recovery initiation. public static final int RECOVERY_CODE_DEFAULT_EXPIRY_TIME = 1; public static final int RESEND_CODE_DEFAULT_EXPIRY_TIME = 1; @@ -217,6 +219,8 @@ public enum ErrorMessages { ERROR_CODE_INVALID_TENANT("18016", "Invalid tenant '%s'."), ERROR_CODE_CHALLENGE_QUESTION_NOT_FOUND("18017", "No challenge question found. %s"), ERROR_CODE_EMAIL_NOT_FOUND("18018", "Sending email address is not found for the user %s."), + ERROR_CODE_INVALID_FLOW_ID("18019", "Invalid flow confirmation code '%s'."), + ERROR_CODE_EXPIRED_FLOW_ID("18020", "Expired flow confirmation code '%s'."), ERROR_CODE_INVALID_CREDENTIALS("17002", "Invalid Credentials"), ERROR_CODE_LOCKED_ACCOUNT("17003", "User account is locked - '%s'."), ERROR_CODE_DISABLED_ACCOUNT("17004", "user account is disabled '%s'."), @@ -298,6 +302,8 @@ public enum ErrorMessages { ERROR_CODE_DISABLE_LITE_SIGN_UP("20060", "Lite sign up feature is disabled"), ERROR_CODE_ERROR_DELETING_RECOVERY_DATA("20061", "Error deleting user recovery data of the tenant: %s"), ERROR_CODE_ERROR_GETTING_CONNECTOR_CONFIG("20062", "Error while getting connector configurations"), + ERROR_CODE_STORING_RECOVERY_FLOW_DATA("20063", "Error while storing recovery data."), + ERROR_CODE_UPDATING_RECOVERY_FLOW_DATA("20064", "Error while updating recovery data."), ERROR_CODE_ERROR_RETRIVING_CLAIM("18004", "Error when retrieving the locale claim of user '%s' of '%s' domain."), ERROR_CODE_RECOVERY_DATA_NOT_FOUND_FOR_USER("18005", "Recovery data not found."), @@ -366,6 +372,10 @@ public enum ErrorMessages { ERROR_CODE_EXPIRED_RECOVERY_CODE("UAR-10013", "Invalid recovery code: '%s'"), ERROR_CODE_USER_ACCOUNT_RECOVERY_VALIDATION_FAILED("UAR-10014", "User account recovery validation failed for user account: '%s'"), + ERROR_CODE_INVALID_RECOVERY_FLOW_ID("UAR-10015", "Invalid confirmation code : '%s'."), + ERROR_CODE_EXPIRED_RECOVERY_FLOW_ID("UAR-10016", "Expired confirmation code : '%s'."), + ERROR_CODE_NO_RECOVERY_FLOW_DATA("UAR-10018", "No recovery flow data found for " + + "recovery flow id : '%s'."), ERROR_CODE_ERROR_STORING_RECOVERY_DATA("UAR-15001", "Error storing user recovery data"), ERROR_CODE_ERROR_GETTING_USERSTORE_MANAGER("UAR-15002", "Error getting userstore manager"), ERROR_CODE_ERROR_RETRIEVING_USER_CLAIM("UAR-15003", "Error getting the claims: '%s' " @@ -620,6 +630,24 @@ public static class ConnectorConfig { public static final String ENABLE_AUTO_LGOIN_AFTER_PASSWORD_RESET = "Recovery.AutoLogin.Enable"; public static final String SELF_REGISTRATION_AUTO_LOGIN = "SelfRegistration.AutoLogin.Enable"; public static final String SELF_REGISTRATION_AUTO_LOGIN_ALIAS_NAME = "SelfRegistration.AutoLogin.AliasName"; + public static final String RECOVERY_OTP_PASSWORD_MAX_FAILED_ATTEMPTS = "Recovery.OTP" + + ".Password.MaxFailedAttempts"; + public static final String RECOVERY_OTP_PASSWORD_MAX_RESEND_ATTEMPTS = "Recovery.OTP" + + ".Password.MaxResendAttempts"; + } + + public static class DBConstants { + + public static final String USER_NAME = "USER_NAME"; + public static final String TENANT_ID = "TENANT_ID"; + public static final String USER_DOMAIN = "USER_DOMAIN"; + public static final String CODE = "CODE"; + public static final String SCENARIO = "SCENARIO"; + public static final String REMAINING_SETS = "REMAINING_SETS"; + public static final String RECOVERY_FLOW_ID = "RECOVERY_FLOW_ID"; + public static final String FAILED_ATTEMPTS = "FAILED_ATTEMPTS"; + public static final String RESEND_COUNT = "RESEND_COUNT"; + public static final String TIME_CREATED = "TIME_CREATED"; } public static class SQLQueries { @@ -627,6 +655,11 @@ public static class SQLQueries { public static final String STORE_RECOVERY_DATA = "INSERT INTO IDN_RECOVERY_DATA " + "(USER_NAME, USER_DOMAIN, TENANT_ID, CODE, SCENARIO,STEP, TIME_CREATED, REMAINING_SETS)" + "VALUES (?,?,?,?,?,?,?,?)"; + + public static final String STORE_RECOVERY_DATA_WITH_FLOW_ID = "INSERT INTO IDN_RECOVERY_DATA " + + "(USER_NAME, USER_DOMAIN, TENANT_ID, CODE, SCENARIO,STEP, TIME_CREATED, REMAINING_SETS, " + + "RECOVERY_FLOW_ID) VALUES (?,?,?,?,?,?,?,?,?)"; + public static final String LOAD_RECOVERY_DATA = "SELECT " + "* FROM IDN_RECOVERY_DATA WHERE USER_NAME = ? AND USER_DOMAIN = ? AND TENANT_ID = ? AND CODE = ? AND " + "SCENARIO = ? AND STEP = ?"; @@ -637,6 +670,9 @@ public static class SQLQueries { public static final String LOAD_RECOVERY_DATA_FROM_CODE = "SELECT * FROM IDN_RECOVERY_DATA WHERE CODE = ?"; + public static final String LOAD_RECOVERY_DATA_FROM_RECOVERY_FLOW_ID = "SELECT * FROM IDN_RECOVERY_DATA WHERE" + + " RECOVERY_FLOW_ID = ? AND STEP = ?"; + public static final String INVALIDATE_CODE = "DELETE FROM IDN_RECOVERY_DATA WHERE CODE = ?"; public static final String INVALIDATE_USER_CODES = @@ -682,6 +718,24 @@ public static class SQLQueries { public static final String LOAD_RECOVERY_DATA_OF_USER_BY_STEP_CASE_INSENSITIVE = "SELECT " + "* FROM IDN_RECOVERY_DATA WHERE LOWER(USER_NAME)=LOWER(?) AND SCENARIO = ? AND USER_DOMAIN = ? " + "AND TENANT_ID = ? AND STEP = ?"; + + public static final String STORE_RECOVERY_FLOW_DATA = "INSERT INTO IDN_RECOVERY_FLOW_DATA " + + "(RECOVERY_FLOW_ID, CODE, FAILED_ATTEMPTS, RESEND_COUNT, TIME_CREATED) VALUES (?,?,?,?,?)"; + + public static final String UPDATE_RECOVERY_FLOW_DATA = "UPDATE IDN_RECOVERY_FLOW_DATA SET CODE = ? " + + "WHERE RECOVERY_FLOW_ID = ?"; + + public static final String UPDATE_FAILED_ATTEMPTS = "UPDATE IDN_RECOVERY_FLOW_DATA SET FAILED_ATTEMPTS = ? " + + "WHERE RECOVERY_FLOW_ID = ?"; + + public static final String UPDATE_CODE_RESEND_COUNT = "UPDATE IDN_RECOVERY_FLOW_DATA SET RESEND_COUNT = ? " + + "WHERE RECOVERY_FLOW_ID = ?"; + + public static final String LOAD_RECOVERY_FLOW_DATA_FROM_RECOVERY_FLOW_ID = "SELECT * " + + "FROM IDN_RECOVERY_FLOW_DATA WHERE RECOVERY_FLOW_ID = ?"; + + public static final String INVALIDATE_BY_RECOVERY_FLOW_ID = "DELETE FROM IDN_RECOVERY_FLOW_DATA WHERE " + + "RECOVERY_FLOW_ID = ?"; } public static class Questions { diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManager.java index b4d4276317..65b185ab2c 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManager.java @@ -43,6 +43,7 @@ import org.wso2.carbon.identity.recovery.internal.service.impl.UserAccountRecoveryManager; import org.wso2.carbon.identity.recovery.model.Property; import org.wso2.carbon.identity.recovery.model.UserRecoveryData; +import org.wso2.carbon.identity.recovery.model.UserRecoveryFlowData; import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; import org.wso2.carbon.identity.recovery.util.Utils; @@ -147,6 +148,19 @@ public ResendConfirmationDTO resendConfirmation(String tenantDomain, String rese UserRecoveryData userRecoveryData = userAccountRecoveryManager .getUserRecoveryData(resendCode, RecoverySteps.RESEND_CONFIRMATION_CODE); User user = userRecoveryData.getUser(); + String recoveryFlowId = userRecoveryData.getRecoveryFlowId(); + UserRecoveryFlowData userRecoveryFlowData = userAccountRecoveryManager.loadUserRecoveryFlowData( + userRecoveryData); + int resendCount = userRecoveryFlowData.getResendCount(); + if (resendCount >= Integer.parseInt(Utils.getRecoveryConfigs(IdentityRecoveryConstants.ConnectorConfig. + RECOVERY_OTP_PASSWORD_MAX_RESEND_ATTEMPTS, tenantDomain))) { + userAccountRecoveryManager.invalidateRecoveryData(recoveryFlowId); + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_FLOW_ID.getCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_FLOW_ID.getMessage(), + recoveryFlowId); + } + userAccountRecoveryManager.updateRecoveryDataResendCount(recoveryFlowId, resendCount + 1); // Validate the tenant domain and the recovery scenario in the request. validateRequestAttributes(user, scenario, userRecoveryData.getRecoveryScenario(), tenantDomain, resendCode); @@ -164,8 +178,10 @@ public ResendConfirmationDTO resendConfirmation(String tenantDomain, String rese } else { userRecoveryDataStore.invalidate(user); confirmationCode = Utils.generateSecretKey(notificationChannel, user.getTenantDomain(), recoveryScenario); + confirmationCode = Utils.concatRecoveryFlowIdWithSecretKey(recoveryFlowId, notificationChannel, + confirmationCode); // Store new confirmation code. - addRecoveryDataObject(confirmationCode, notificationChannel, scenario, step, user); + addRecoveryDataObject(confirmationCode, recoveryFlowId, notificationChannel, scenario, step, user); } ResendConfirmationDTO resendConfirmationDTO = new ResendConfirmationDTO(); @@ -185,6 +201,7 @@ public ResendConfirmationDTO resendConfirmation(String tenantDomain, String rese IdentityRecoveryConstants.SuccessEvents.SUCCESS_STATUS_CODE_RESEND_CONFIRMATION_CODE.getCode()); resendConfirmationDTO.setSuccessMessage( IdentityRecoveryConstants.SuccessEvents.SUCCESS_STATUS_CODE_RESEND_CONFIRMATION_CODE.getMessage()); + resendConfirmationDTO.setRecoveryFlowId(recoveryFlowId); return resendConfirmationDTO; } @@ -274,6 +291,7 @@ private String generateResendCode(String notificationChannel, RecoveryScenarios UserRecoveryData userRecoveryData) throws IdentityRecoveryServerException { String resendCode = UUID.randomUUID().toString(); + String recoveryFlowId = userRecoveryData.getRecoveryFlowId(); /* Checking whether the existing confirmation code issued time is in the tolerance period. If so this code updates the existing RESEND_CONFIRMATION_CODE with the new one by not changing the TIME_CREATED. */ if (Utils.reIssueExistingConfirmationCode(getResendConfirmationCodeData(userRecoveryData.getUser()), @@ -281,8 +299,8 @@ private String generateResendCode(String notificationChannel, RecoveryScenarios invalidateResendConfirmationCode(resendCode, notificationChannel, userRecoveryData); return resendCode; } - addRecoveryDataObject(resendCode, notificationChannel, scenario, RecoverySteps.RESEND_CONFIRMATION_CODE, - userRecoveryData.getUser()); + addRecoveryDataObject(resendCode, recoveryFlowId, notificationChannel, scenario, + RecoverySteps.RESEND_CONFIRMATION_CODE, userRecoveryData.getUser()); return resendCode; } @@ -336,23 +354,28 @@ private void invalidateResendConfirmationCode(String resendCode, String notifica * Add the notification channel recovery data to the store. * * @param secretKey RecoveryId + * @param recoveryFlowId Recovery flow ID. * @param recoveryData Data to be stored as mata which are needed to evaluate the recovery data object * @param recoveryScenario Recovery scenario * @param recoveryStep Recovery step * @param user User object * @throws IdentityRecoveryServerException Error storing recovery data */ - private void addRecoveryDataObject(String secretKey, String recoveryData, RecoveryScenarios recoveryScenario, - RecoverySteps recoveryStep, User user) + private void addRecoveryDataObject(String secretKey, String recoveryFlowId, String recoveryData, + RecoveryScenarios recoveryScenario, RecoverySteps recoveryStep, User user) throws IdentityRecoveryServerException { - UserRecoveryData recoveryDataDO = new UserRecoveryData(user, secretKey, recoveryScenario, recoveryStep); + UserRecoveryData recoveryDataDO = new UserRecoveryData(user, recoveryFlowId, secretKey, recoveryScenario, recoveryStep); // Store available channels in remaining setIDs. recoveryDataDO.setRemainingSetIds(recoveryData); try { UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); - userRecoveryDataStore.store(recoveryDataDO); + if (StringUtils.equals(RecoverySteps.UPDATE_PASSWORD.name(), String.valueOf(recoveryStep))) { + userRecoveryDataStore.storeConfirmationCode(recoveryDataDO); + } else { + userRecoveryDataStore.store(recoveryDataDO); + } } catch (IdentityRecoveryException e) { throw Utils.handleServerException( IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_ERROR_STORING_RECOVERY_DATA, diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImpl.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImpl.java index 3286603bf9..0405768fad 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImpl.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImpl.java @@ -112,6 +112,10 @@ public Map getPropertyNameMapping() { "Recovery callback URL regex"); nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_AUTO_LGOIN_AFTER_PASSWORD_RESET, "Enable Auto Login After Password Reset"); + nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_OTP_PASSWORD_MAX_FAILED_ATTEMPTS, + "Max failed attempts for OTP based recovery"); + nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_OTP_PASSWORD_MAX_RESEND_ATTEMPTS, + "Max resend attempts for OTP based recovery"); return nameMapping; } @@ -168,6 +172,8 @@ public String[] getPropertyNames() { properties.add(IdentityRecoveryConstants.ConnectorConfig.FORCE_MIN_NO_QUESTION_ANSWERED); properties.add(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_CALLBACK_REGEX); properties.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_AUTO_LGOIN_AFTER_PASSWORD_RESET); + properties.add(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_OTP_PASSWORD_MAX_FAILED_ATTEMPTS); + properties.add(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_OTP_PASSWORD_MAX_RESEND_ATTEMPTS); return properties.toArray(new String[0]); } @@ -194,6 +200,8 @@ public Properties getDefaultPropertyValues(String tenantDomain) throws IdentityG String minimumForcedChallengeQuestionsAnswered = "1"; String recoveryCallbackRegex = IdentityRecoveryConstants.DEFAULT_CALLBACK_REGEX; String enableAdminPasswordResetAutoLoginProperty = "false"; + String recoveryOTPMaxFailedAttempts = "3"; + String recoveryOTPMaxResendAttempts = "5"; String notificationBasedPasswordRecovery = IdentityUtil.getProperty( IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_BASED_PW_RECOVERY); @@ -234,6 +242,10 @@ public Properties getDefaultPropertyValues(String tenantDomain) throws IdentityG IdentityRecoveryConstants.ConnectorConfig.RECOVERY_CALLBACK_REGEX); String adminPasswordResetAutoLoginProperty = IdentityUtil.getProperty( IdentityRecoveryConstants.ConnectorConfig.ENABLE_AUTO_LGOIN_AFTER_PASSWORD_RESET); + String otpMaxFailedAttempts = IdentityUtil.getProperty(IdentityRecoveryConstants. + ConnectorConfig.RECOVERY_OTP_PASSWORD_MAX_FAILED_ATTEMPTS); + String otpMaxResendAttempts = IdentityUtil.getProperty(IdentityRecoveryConstants. + ConnectorConfig.RECOVERY_OTP_PASSWORD_MAX_RESEND_ATTEMPTS); if (StringUtils.isNotEmpty(expiryTimeSMSOTPProperty)) { expiryTimeSMSOTP = expiryTimeSMSOTPProperty; @@ -295,6 +307,12 @@ public Properties getDefaultPropertyValues(String tenantDomain) throws IdentityG if (StringUtils.isNotEmpty(adminPasswordResetAutoLoginProperty)) { enableAdminPasswordResetAutoLoginProperty = adminPasswordResetAutoLoginProperty; } + if (StringUtils.isNotEmpty(otpMaxFailedAttempts)) { + recoveryOTPMaxFailedAttempts = otpMaxFailedAttempts; + } + if (StringUtils.isNotEmpty(otpMaxResendAttempts)) { + recoveryOTPMaxResendAttempts = otpMaxResendAttempts; + } Map defaultProperties = new HashMap<>(); defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_BASED_PW_RECOVERY, @@ -336,6 +354,10 @@ public Properties getDefaultPropertyValues(String tenantDomain) throws IdentityG defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_CALLBACK_REGEX, recoveryCallbackRegex); defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_AUTO_LGOIN_AFTER_PASSWORD_RESET, enableAdminPasswordResetAutoLoginProperty); + defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_OTP_PASSWORD_MAX_FAILED_ATTEMPTS, + recoveryOTPMaxFailedAttempts); + defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_OTP_PASSWORD_MAX_RESEND_ATTEMPTS, + recoveryOTPMaxResendAttempts); Properties properties = new Properties(); properties.putAll(defaultProperties); @@ -411,6 +433,12 @@ public Map getMetaData() { meta.put(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_CALLBACK_REGEX, getPropertyObject(IdentityMgtConstants.DataTypes.STRING.getValue())); + meta.put(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_OTP_PASSWORD_MAX_FAILED_ATTEMPTS, + getPropertyObject(IdentityMgtConstants.DataTypes.INTEGER.getValue())); + + meta.put(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_OTP_PASSWORD_MAX_RESEND_ATTEMPTS, + getPropertyObject(IdentityMgtConstants.DataTypes.INTEGER.getValue())); + return meta; } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/PasswordRecoverDTO.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/PasswordRecoverDTO.java index c623e80a60..5c713f8b09 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/PasswordRecoverDTO.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/PasswordRecoverDTO.java @@ -32,6 +32,11 @@ public class PasswordRecoverDTO { */ private String message; + /** + * Recovery flow id. + */ + private String recoveryFlowId; + /** * User notified channel. */ @@ -108,6 +113,26 @@ public void setMessage(String message) { this.message = message; } + /** + * Get the recovery flow id. + * + * @return Recovery flow id. + */ + public String getRecoveryFlowId() { + + return recoveryFlowId; + } + + /** + * Set the recovery flow id. + * + * @param recoveryFlowId Recovery flow id. + */ + public void setRecoveryFlowId(String recoveryFlowId) { + + this.recoveryFlowId = recoveryFlowId; + } + /** * Get the channel which the notification was sent. * diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/RecoveryChannelInfoDTO.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/RecoveryChannelInfoDTO.java index 731f1a3143..b04c0f19ac 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/RecoveryChannelInfoDTO.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/RecoveryChannelInfoDTO.java @@ -27,6 +27,11 @@ public class RecoveryChannelInfoDTO { */ private String username; + /** + * Recovery flow id of the recovery flow. + */ + private String recoveryFlowId; + /** * Recovery Code given to the user. */ @@ -57,6 +62,26 @@ public void setUsername(String username) { this.username = username; } + /** + * Get the recovery Flow id of the recovery flow. + * + * @return Recovery Flow id of the recovery flow. + */ + public String getRecoveryFlowId() { + + return recoveryFlowId; + } + + /** + * Set the recovery Flow id of the recovery flow. + * + * @param recoveryFlowId Recovery Flow Id. + */ + public void setRecoveryFlowId(String recoveryFlowId) { + + this.recoveryFlowId = recoveryFlowId; + } + /** * Get the recovery Code of the user. * diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/RecoveryInformationDTO.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/RecoveryInformationDTO.java index 2d35c23bc4..a237420e5b 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/RecoveryInformationDTO.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/RecoveryInformationDTO.java @@ -27,6 +27,11 @@ public class RecoveryInformationDTO { */ private String username; + /** + * Recovery flow id of the initiated recovery flow. + */ + private String recoveryFlowId; + /** * Available Recovery channel Information. */ @@ -132,6 +137,26 @@ public void setUsername(String userName) { this.username = userName; } + /** + * Get recovery flow id. + * + * @return recoveryFlowId. + */ + public String getRecoveryFlowId() { + + return recoveryFlowId; + } + + /** + * Set recovery flow id. + * + * @param recoveryFlowId RecoveryFlowId. + */ + public void setRecoveryFlowId(String recoveryFlowId) { + + this.recoveryFlowId = recoveryFlowId; + } + /** * Get RecoveryChannelInfoDTO. * diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/ResendConfirmationDTO.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/ResendConfirmationDTO.java index 76f7b51b0f..91ed8f1b29 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/ResendConfirmationDTO.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/dto/ResendConfirmationDTO.java @@ -27,6 +27,11 @@ public class ResendConfirmationDTO { */ private String successMessage; + /** + * Recovery flow id. + */ + private String recoveryFlowId; + /** * Recovery Info sent channel. */ @@ -47,6 +52,26 @@ public class ResendConfirmationDTO { */ private String externalConfirmationCode; + /** + * Get recovery flow id. + * + * @return Recovery flow id. + */ + public String getRecoveryFlowId() { + + return recoveryFlowId; + } + + /** + * Set recovery flow id. + * + * @param recoveryFlowId Recovery Flow Id. + */ + public void setRecoveryFlowId(String recoveryFlowId) { + + this.recoveryFlowId = recoveryFlowId; + } + /** * Get external confirmation code. * diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java index fbbc468009..844bc80b40 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java @@ -42,6 +42,7 @@ import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; import org.wso2.carbon.identity.recovery.model.NotificationChannel; import org.wso2.carbon.identity.recovery.model.UserRecoveryData; +import org.wso2.carbon.identity.recovery.model.UserRecoveryFlowData; import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; import org.wso2.carbon.identity.recovery.util.Utils; @@ -146,6 +147,7 @@ public RecoveryChannelInfoDTO retrieveUserRecoveryInformation(Map getInternalNotificationChannelList(String user * Prepare the response to be sent to the recovery APIs. * * @param username Username of the user + * @param recoveryFlowId Recovery flow ID. * @param recoveryCode Recovery code given to the user * @param notificationChannelDTOs List of NotificationChannelsResponseDTOs available for the user. * @return RecoveryChannelInfoDTO object. */ - private RecoveryChannelInfoDTO buildUserRecoveryInformationResponseDTO(String username, String recoveryCode, - NotificationChannelDTO[] notificationChannelDTOs) { + private RecoveryChannelInfoDTO buildUserRecoveryInformationResponseDTO(String username, String recoveryFlowId, + String recoveryCode, NotificationChannelDTO[] notificationChannelDTOs) { RecoveryChannelInfoDTO recoveryChannelInfoDTO = new RecoveryChannelInfoDTO(); recoveryChannelInfoDTO.setUsername(username); + recoveryChannelInfoDTO.setRecoveryFlowId(recoveryFlowId); recoveryChannelInfoDTO.setRecoveryCode(recoveryCode); recoveryChannelInfoDTO.setNotificationChannelDTOs(notificationChannelDTOs); return recoveryChannelInfoDTO; @@ -925,30 +931,147 @@ public UserRecoveryData getUserRecoveryData(String code, RecoverySteps step) thr return recoveryData; } + /** + * Get user recovery data using the recovery flow id. + * + * @param recoveryFlowId Recovery flow id of the user. + * @param step Recovery step. + * @return UserRecoveryData Data associated with the provided recoveryFlowId. + * @throws IdentityRecoveryException If an error occurred while validating the recoveryId. + */ + public UserRecoveryData getUserRecoveryDataFromFlowId(String recoveryFlowId, RecoverySteps step) + throws IdentityRecoveryException { + + UserRecoveryData recoveryData; + UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); + try { + // Retrieve recovery data bound to the recoveryFlowId. + recoveryData = userRecoveryDataStore.loadFromRecoveryFlowId(recoveryFlowId, step); + } catch (IdentityRecoveryException e) { + // Map code expired error to new error codes for user account recovery. + if (IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_FLOW_ID.getCode().equals(e.getErrorCode())) { + e.setErrorCode(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_FLOW_ID.getCode()); + } else if (IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_FLOW_ID.getCode().equals(e.getErrorCode())) { + e.setErrorCode(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_RECOVERY_FLOW_ID.getCode()); + } else if (IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_CODE.getCode() + .equals(e.getErrorCode())) { + e.setErrorCode(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_RECOVERY_CODE.getCode()); + } else { + e.setErrorCode(Utils.prependOperationScenarioToErrorCode(e.getErrorCode(), + IdentityRecoveryConstants.USER_ACCOUNT_RECOVERY)); + } + throw e; + } + if (recoveryData == null) { + throw Utils + .handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_ACCOUNT_RECOVERY_DATA, + recoveryFlowId); + } + return recoveryData; + } + + /** + * Get user recovery flow data using the recovery flow id. + * + * @param recoveryDataDO User Recovery Data object. + * @return UserRecoveryFlowData Data associated with the provided UserRecoveryData. + * @throws IdentityRecoveryException If an error occurred while validating the recoveryId. + */ + public UserRecoveryFlowData loadUserRecoveryFlowData(UserRecoveryData recoveryDataDO) + throws IdentityRecoveryException { + + UserRecoveryFlowData userRecoveryFlowData; + UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); + try { + userRecoveryFlowData = userRecoveryDataStore.loadRecoveryFlowData(recoveryDataDO); + } catch (IdentityRecoveryException e) { + // Map code expired error to new error codes for user account recovery. + if (IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_FLOW_ID.getCode().equals(e.getErrorCode())) { + e.setErrorCode(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_FLOW_ID.getCode()); + } else if (IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_FLOW_ID.getCode().equals(e.getErrorCode())) { + e.setErrorCode(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_RECOVERY_FLOW_ID.getCode()); + } else { + e.setErrorCode(Utils.prependOperationScenarioToErrorCode(e.getErrorCode(), + IdentityRecoveryConstants.USER_ACCOUNT_RECOVERY)); + } + throw e; + } + if (userRecoveryFlowData == null) { + throw Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_RECOVERY_FLOW_DATA, + recoveryDataDO.getRecoveryFlowId()); + } + return userRecoveryFlowData; + } + + /** + * Update recovery OTP attempt. + * + * @param recoveryFlowId Recovery Flow Id. + * @param failedAttempts Failed Attempts. + * @throws IdentityRecoveryException If an error occurred while updating the recovery flow data. + */ + public void updateRecoveryDataFailedAttempts(String recoveryFlowId, int failedAttempts) + throws IdentityRecoveryException { + + UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); + userRecoveryDataStore.updateFailedAttempts(recoveryFlowId, failedAttempts); + } + + /** + * Update recovery OTP resend count. + * + * @param recoveryFlowId Recovery Flow Id. + * @param resendCount Current Resend Count. + * @throws IdentityRecoveryException If an error occurred while updating the recovery flow data. + */ + public void updateRecoveryDataResendCount(String recoveryFlowId, int resendCount) throws IdentityRecoveryException { + + UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); + userRecoveryDataStore.updateCodeResendCount(recoveryFlowId, resendCount); + } + + /** + * Invalidate the recovery Data. + * + * @param recoveryFlowId Recovery Flow Id. + * @throws IdentityRecoveryException If an error occurred while invalidating recovery data. + */ + public void invalidateRecoveryData(String recoveryFlowId) throws IdentityRecoveryException { + + UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); + userRecoveryDataStore.invalidateWithRecoveryFlowId(recoveryFlowId); + } + /** * Add the notification channel recovery data to the store. * * @param username Username * @param tenantDomain Tenant domain + * @param recoveryFlowId Recovery flow ID. * @param secretKey RecoveryId * @param scenario RecoveryScenario * @param recoveryData Data to be stored as mata which are needed to evaluate the recovery data object * @throws IdentityRecoveryServerException If an error occurred while storing recovery data. */ - private void addRecoveryDataObject(String username, String tenantDomain, String secretKey, + private void addRecoveryDataObject(String username, String tenantDomain, String recoveryFlowId, String secretKey, RecoveryScenarios scenario, String recoveryData) throws IdentityRecoveryServerException { // Create a user object. User user = Utils.buildUser(username, tenantDomain); - UserRecoveryData recoveryDataDO = new UserRecoveryData(user, secretKey, scenario, + UserRecoveryData recoveryDataDO = new UserRecoveryData(user, recoveryFlowId, secretKey, scenario, RecoverySteps.SEND_RECOVERY_INFORMATION); // Store available channels in remaining setIDs. recoveryDataDO.setRemainingSetIds(recoveryData); try { UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); - userRecoveryDataStore.invalidate(user); - userRecoveryDataStore.store(recoveryDataDO); + UserRecoveryData userRecoveryDataDO = userRecoveryDataStore.loadWithoutCodeExpiryValidation(user); + if (userRecoveryDataDO != null && userRecoveryDataDO.getRecoveryFlowId() != null) { + userRecoveryDataStore.invalidateWithRecoveryFlowId(userRecoveryDataDO.getRecoveryFlowId()); + } else { + userRecoveryDataStore.invalidate(user); + } + userRecoveryDataStore.storeInit(recoveryDataDO); } catch (IdentityRecoveryException e) { throw Utils.handleServerException( IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_ERROR_STORING_RECOVERY_DATA, diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/password/PasswordRecoveryManagerImpl.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/password/PasswordRecoveryManagerImpl.java index 769adc833a..da23fb6fc8 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/password/PasswordRecoveryManagerImpl.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/password/PasswordRecoveryManagerImpl.java @@ -58,6 +58,7 @@ import java.util.ArrayList; import java.util.Map; import java.util.UUID; +import java.util.regex.Pattern; /** * Class that implements the PasswordRecoveryManager. @@ -106,7 +107,9 @@ public RecoveryInformationDTO initiate(Map claims, String tenant properties); RecoveryInformationDTO recoveryInformationDTO = new RecoveryInformationDTO(); String username = recoveryChannelInfoDTO.getUsername(); + String recoveryFlowId = recoveryChannelInfoDTO.getRecoveryFlowId(); recoveryInformationDTO.setUsername(username); + recoveryInformationDTO.setRecoveryFlowId(recoveryFlowId); // Do not add recovery channel information if Notification based recovery is not enabled. recoveryInformationDTO.setNotificationBasedRecoveryEnabled(isNotificationBasedRecoveryEnabled); if (isNotificationBasedRecoveryEnabled) { @@ -168,7 +171,9 @@ public PasswordRecoverDTO notify(String recoveryCode, String channelId, String t manageNotificationsInternally, properties); String secretKey = notificationResponseBean.getKey(); String resendCode = generateResendCode(notificationChannel, userRecoveryData); - return buildPasswordRecoveryResponseDTO(notificationChannel, secretKey, resendCode); + String recoveryFlowId = userRecoveryData.getRecoveryFlowId(); + userAccountRecoveryManager.loadUserRecoveryFlowData(userRecoveryData); + return buildPasswordRecoveryResponseDTO(notificationChannel, secretKey, resendCode, recoveryFlowId); } /** @@ -202,6 +207,75 @@ public PasswordResetCodeDTO confirm(String confirmationCode, String tenantDomain return buildPasswordResetCodeDTO(confirmationCode); } + /** + * Validate the code given for password recovery and return the password reset code. + * + * @param otp One Time Password. + * @param confirmationCode Confirmation code. + * @param tenantDomain Tenant domain. + * @param properties Meta properties in the confirmation request. + * @return PasswordResetCodeDTO {@link PasswordResetCodeDTO} object which contains password reset code. + * @throws IdentityRecoveryException Error while confirming password recovery. + */ + @Override + public PasswordResetCodeDTO confirm(String otp, String confirmationCode, String tenantDomain, + Map properties) throws IdentityRecoveryException { + + validateTenantDomain(tenantDomain); + UserAccountRecoveryManager userAccountRecoveryManager = UserAccountRecoveryManager.getInstance(); + /* In the recovery scenarios which are not OTP based, the confirmation code is a combination of the + recovery flow id and another UUID generated. Hence, we need to get the recovery flow id from the + confirmation code. In the OTP based recovery scenario, the confirmation code is the recovery flow id. */ + String[] ids = confirmationCode.split(Pattern.quote(IdentityRecoveryConstants.CONFIRMATION_CODE_SEPARATOR)); + String recoveryFlowId; + String code; + if (ids.length == 2) { + recoveryFlowId = ids[0]; + code = confirmationCode; + } else { + recoveryFlowId = confirmationCode; + code = otp; + } + try { + UserRecoveryData userRecoveryData = userAccountRecoveryManager + .getUserRecoveryDataFromFlowId(recoveryFlowId, RecoverySteps.UPDATE_PASSWORD); + int failedAttempts = userRecoveryData.getFailedAttempts(); + if (!tenantDomain.equals(userRecoveryData.getUser().getTenantDomain())) { + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_USER_TENANT_DOMAIN_MISS_MATCH_WITH_CONTEXT, + tenantDomain); + } + String domainQualifiedName = IdentityUtil.addDomainToName(userRecoveryData.getUser().getUserName(), + userRecoveryData.getUser().getUserStoreDomain()); + if (StringUtils.equals(code, userRecoveryData.getSecret())) { + if (log.isDebugEnabled()) { + log.debug("Valid confirmation code for user: " + domainQualifiedName); + } + return buildPasswordResetCodeDTO(code); + } + failedAttempts = failedAttempts + 1; + if (failedAttempts >= Integer.parseInt(Utils.getRecoveryConfigs(IdentityRecoveryConstants.ConnectorConfig. + RECOVERY_OTP_PASSWORD_MAX_FAILED_ATTEMPTS, tenantDomain))) { + userAccountRecoveryManager.invalidateRecoveryData(recoveryFlowId); + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_FLOW_ID.getCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_FLOW_ID.getMessage(), + recoveryFlowId); + } + userAccountRecoveryManager.updateRecoveryDataFailedAttempts(recoveryFlowId, failedAttempts); + if (log.isDebugEnabled()) { + log.debug("Invalid confirmation code for user: " + domainQualifiedName); + } + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_CODE.getCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_CODE.getMessage(), code); + } catch (IdentityRecoveryException e) { + /* This is a fallback logic to support already initiated email link based recovery flows using the + recovery V1 API, which do not have recovery flow ids. */ + return validateConfirmationCode(userAccountRecoveryManager, recoveryFlowId, tenantDomain); + } + } + /** * Reset the password for password recovery, if the password reset code is valid. * @@ -246,6 +320,50 @@ public SuccessfulPasswordResetDTO reset(String resetCode, char[] password, Map properties) + throws IdentityRecoveryException { + + // Validate the password. + if (ArrayUtils.isEmpty(password)) { + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_PASSWORD_IN_REQUEST.getCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_PASSWORD_IN_REQUEST.getMessage(), null); + } + String newPassword = String.valueOf(password); + NotificationPasswordRecoveryManager notificationPasswordRecoveryManager = NotificationPasswordRecoveryManager + .getInstance(); + Property[] metaProperties = buildPropertyList(null, properties); + try { + notificationPasswordRecoveryManager.updatePassword(resetCode, confirmationCode, newPassword, metaProperties); + } catch (IdentityRecoveryServerException e) { + String errorCode = Utils.prependOperationScenarioToErrorCode(e.getErrorCode(), + IdentityRecoveryConstants.PASSWORD_RECOVERY_SCENARIO); + throw Utils.handleServerException(errorCode, e.getMessage(), null); + } catch (IdentityRecoveryClientException e) { + throw mapClientExceptionWithImprovedErrorCodes(e); + } catch (IdentityEventException e) { + if (log.isDebugEnabled()) { + log.debug("PasswordRecoveryManagerImpl: Error while resetting password ", e); + } + throw Utils.handleServerException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UNEXPECTED_ERROR_PASSWORD_RESET.getCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UNEXPECTED_ERROR_PASSWORD_RESET.getMessage(), + null); + } + return buildSuccessfulPasswordUpdateDTO(); + } + /** * Resend the password recovery information to the user via user specified channel. * @@ -458,10 +576,11 @@ private PasswordResetCodeDTO buildPasswordResetCodeDTO(String resetCode) { * @param notificationChannel Notified channel * @param confirmationCode Confirmation code for confirm recovery * @param resendCode Code to resend recovery confirmation code + * @param recoveryFlowId Recovery flow ID. * @return PasswordRecoverDTO object */ private PasswordRecoverDTO buildPasswordRecoveryResponseDTO(String notificationChannel, String confirmationCode, - String resendCode) { + String resendCode, String recoveryFlowId) { PasswordRecoverDTO passwordRecoverDTO = new PasswordRecoverDTO(); passwordRecoverDTO.setNotificationChannel(notificationChannel); @@ -469,6 +588,7 @@ private PasswordRecoverDTO buildPasswordRecoveryResponseDTO(String notificationC passwordRecoverDTO.setConfirmationCode(confirmationCode); } passwordRecoverDTO.setResendCode(resendCode); + passwordRecoverDTO.setRecoveryFlowId(recoveryFlowId); passwordRecoverDTO.setCode( IdentityRecoveryConstants.SuccessEvents.SUCCESS_STATUS_CODE_PASSWORD_RECOVERY_INTERNALLY_NOTIFIED .getCode()); @@ -581,7 +701,8 @@ private String generateResendCode(String notificationChannel, UserRecoveryData u invalidateRecoveryInfoSendCode(resendCode, notificationChannel, userRecoveryData); return resendCode; } - addRecoveryDataObject(resendCode, notificationChannel, userRecoveryData.getUser()); + addRecoveryDataObject(resendCode, userRecoveryData.getRecoveryFlowId(), notificationChannel, + userRecoveryData.getUser()); return resendCode; } @@ -635,13 +756,14 @@ private void invalidateRecoveryInfoSendCode(String resendCode, String notificati * Add the notification channel recovery data to the store. * * @param secretKey RecoveryId + * @param recoveryFlowId Recovery flow ID. * @param recoveryData Data to be stored as mata which are needed to evaluate the recovery data object * @throws IdentityRecoveryServerException Error storing recovery data */ - private void addRecoveryDataObject(String secretKey, String recoveryData, User user) + private void addRecoveryDataObject(String secretKey, String recoveryFlowId, String recoveryData, User user) throws IdentityRecoveryServerException { - UserRecoveryData recoveryDataDO = new UserRecoveryData(user, secretKey, + UserRecoveryData recoveryDataDO = new UserRecoveryData(user, recoveryFlowId, secretKey, RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY, RecoverySteps.RESEND_CONFIRMATION_CODE); // Store available channels in remaining setIDs. recoveryDataDO.setRemainingSetIds(recoveryData); @@ -739,4 +861,49 @@ private boolean isMinNoOfRecoveryQuestionsAnswered(String username, String tenan return isMinNoOfRecoveryQuestionsAnswered; } + /** + * This method is to validate the confirmation code when there's no recovery flow id. This is added as a fallback + * logic to handle the already initiated email link based recovery flows which do not have recovery flow ids, + * which were initiated before moving to the Recovery V2 API. This shouldn't be used for any other purpose and + * should be kept for sometime. + * + * @param userAccountRecoveryManager UserAccountRecoveryManager. + * @param confirmationCode Confirmation code. + * @param tenantDomain Tenant domain. + * @return PasswordResetCodeDTO {@link PasswordResetCodeDTO} object which contains password reset code. + * @throws IdentityRecoveryException Error while confirming password recovery. + */ + @Deprecated + private PasswordResetCodeDTO validateConfirmationCode(UserAccountRecoveryManager userAccountRecoveryManager, + String confirmationCode, String tenantDomain) + throws IdentityRecoveryException { + + UserRecoveryData userRecoveryData; + try { + userRecoveryData = userAccountRecoveryManager.getUserRecoveryData(confirmationCode, + RecoverySteps.UPDATE_PASSWORD); + } catch (IdentityRecoveryException e) { + if (IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_CODE.getCode().equals( + e.getErrorCode())) { + e.setErrorCode(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_FLOW_ID.getCode()); + } + throw e; + } + if (!StringUtils.equals(userRecoveryData.getRemainingSetIds(), + NotificationChannels.EMAIL_CHANNEL.getChannelType())) { + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_FLOW_ID, confirmationCode); + } + if (!tenantDomain.equals(userRecoveryData.getUser().getTenantDomain())) { + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_USER_TENANT_DOMAIN_MISS_MATCH_WITH_CONTEXT, + tenantDomain); + } + String domainQualifiedName = IdentityUtil.addDomainToName(userRecoveryData.getUser().getUserName(), + userRecoveryData.getUser().getUserStoreDomain()); + if (log.isDebugEnabled()) { + log.debug("Valid confirmation code for user: " + domainQualifiedName); + } + return buildPasswordResetCodeDTO(confirmationCode); + } } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java index 5cc530d0c4..5fd5ebcf9a 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java @@ -160,7 +160,12 @@ public UsernameRecoverDTO notify(String recoveryCode, String channelId, String t // Validate Recovery data. UserRecoveryData userRecoveryData = recoveryAccountManager .getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION); - invalidateRecoveryCode(recoveryCode); + String recoveryFlowId = userRecoveryData.getRecoveryFlowId(); + if (recoveryFlowId != null) { + invalidateRecoveryFlowId(recoveryFlowId); + } else { + invalidateRecoveryCode(recoveryCode); + } String notificationChannel = extractNotificationChannelDetails(userRecoveryData.getRemainingSetIds(), channelIdCode); @@ -285,6 +290,18 @@ private void invalidateRecoveryCode(String recoveryCode) throws IdentityRecovery userRecoveryDataStore.invalidate(recoveryCode); } + /** + * Invalidate the recovery flow id. + * + * @param recoveryFlowId Recovery flow id. + * @throws IdentityRecoveryException If an error occurred while invalidating recovery data. + */ + private void invalidateRecoveryFlowId(String recoveryFlowId) throws IdentityRecoveryException { + + UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); + userRecoveryDataStore.invalidateWithRecoveryFlowId(recoveryFlowId); + } + /** * Trigger notification to send userName recovery information. * diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/model/UserRecoveryData.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/model/UserRecoveryData.java index 0a856dff67..fc30fded96 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/model/UserRecoveryData.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/model/UserRecoveryData.java @@ -28,7 +28,10 @@ */ public class UserRecoveryData { private User user; + private String recoveryFlowId; private String secret; + private int failedAttempts; + private int resendCount; private String remainingSetIds; private boolean codeExpired; @@ -46,6 +49,15 @@ public UserRecoveryData(User user, String secret, Enum recoveryScenario, Enum re this.recoveryStep = recoveryStep; } + public UserRecoveryData(User user, String recoveryFlowId, String secret, Enum recoveryScenario, Enum recoveryStep) { + + this.user = user; + this.recoveryFlowId= recoveryFlowId; + this.secret = secret; + this.recoveryScenario = recoveryScenario; + this.recoveryStep = recoveryStep; + } + public UserRecoveryData(User user, String secret, Enum recoveryScenario) { this.user = user; @@ -71,6 +83,31 @@ public UserRecoveryData(User user, String secret, Enum recoveryScenario, Enum re this.timeCreated = timeCreated; } + public UserRecoveryData(User user, String recoveryFlowId, String secret, Enum recoveryScenario, Enum recoveryStep, + Timestamp timeCreated) { + + this.user = user; + this.recoveryFlowId=recoveryFlowId; + this.secret = secret; + this.recoveryScenario = recoveryScenario; + this.recoveryStep = recoveryStep; + this.timeCreated = timeCreated; + } + + public UserRecoveryData(User user, String recoveryFlowId, String secret, int failedAttempts, int resendCount, + Enum recoveryScenario, Enum recoveryStep, String remainingSetIds, Timestamp timeCreated) { + + this.user = user; + this.recoveryFlowId=recoveryFlowId; + this.secret = secret; + this.failedAttempts = failedAttempts; + this.resendCount = resendCount; + this.recoveryScenario = recoveryScenario; + this.recoveryStep = recoveryStep; + this.remainingSetIds = remainingSetIds; + this.timeCreated = timeCreated; + } + public Timestamp getTimeCreated() { return timeCreated; @@ -81,6 +118,16 @@ public void setTimeCreated(Timestamp timeCreated) { this.timeCreated = timeCreated; } + public String getRecoveryFlowId() { + + return recoveryFlowId; + } + + public void setRecoveryFlowId(String recoveryFlowId) { + + this.recoveryFlowId = recoveryFlowId; + } + public String getRemainingSetIds() { return remainingSetIds; } @@ -98,6 +145,26 @@ public User getUser() { return user; } + public int getFailedAttempts() { + + return failedAttempts; + } + + public void setFailedAttempts(int failedAttempts) { + + this.failedAttempts = failedAttempts; + } + + public int getResendCount() { + + return resendCount; + } + + public void setResendCount(int resendCount) { + + this.resendCount = resendCount; + } + public Enum getRecoveryScenario() { return recoveryScenario; } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/model/UserRecoveryFlowData.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/model/UserRecoveryFlowData.java new file mode 100644 index 0000000000..856e91c2eb --- /dev/null +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/model/UserRecoveryFlowData.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.recovery.model; + +import java.sql.Timestamp; + +/** + * This object represents an entry of the identity metadata database. + */ +public class UserRecoveryFlowData { + + private String recoveryFlowId; + + private Timestamp timeCreated; + + private int failedAttempts; + + private int resendCount; + + public UserRecoveryFlowData(String recoveryFlowId) { + + this.recoveryFlowId = recoveryFlowId; + } + + public UserRecoveryFlowData(String recoveryFlowId, Timestamp timeCreated) { + + this.recoveryFlowId = recoveryFlowId; + this.timeCreated = timeCreated; + } + + public UserRecoveryFlowData(String recoveryFlowId, Timestamp timeCreated, int failedAttempts, int resendCount) { + + this.recoveryFlowId = recoveryFlowId; + this.timeCreated = timeCreated; + this.failedAttempts = failedAttempts; + this.resendCount = resendCount; + } + + /** + * Get the time created. + * + * @return Created time. + */ + public Timestamp getTimeCreated() { + + return timeCreated; + } + + /** + * Set the time created. + * + * @param timeCreated Created time. + */ + public void setTimeCreated(Timestamp timeCreated) { + + this.timeCreated = timeCreated; + } + + /** + * Get the recovery flow id. + * + * @return Recovery Flow Id. + */ + public String getRecoveryFlowId() { + + return recoveryFlowId; + } + + /** + * Set the recovery flow id. + * + * @param recoveryFlowId Recovery Flow Id. + */ + public void setRecoveryFlowId(String recoveryFlowId) { + + this.recoveryFlowId = recoveryFlowId; + } + + /** + * Get the failed attempts. + * + * @return failed attempts. + */ + public int getFailedAttempts() { + + return failedAttempts; + } + + /** + * Set the failed attempts. + * + * @param failedAttempts OTP failed attempts. + */ + public void setFailedAttempts(int failedAttempts) { + + this.failedAttempts = failedAttempts; + } + + /** + * Get the resendCount. + * + * @return resendCount. + */ + public int getResendCount() { + + return resendCount; + } + + /** + * Set the resendCount. + * + * @param resendCount resendCount. + */ + public void setResendCount(int resendCount) { + + this.resendCount = resendCount; + } +} diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/password/NotificationPasswordRecoveryManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/password/NotificationPasswordRecoveryManager.java index 1f69cc8d1a..8117a42f80 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/password/NotificationPasswordRecoveryManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/password/NotificationPasswordRecoveryManager.java @@ -50,6 +50,7 @@ import org.wso2.carbon.identity.recovery.RecoverySteps; import org.wso2.carbon.identity.recovery.bean.NotificationResponseBean; import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; +import org.wso2.carbon.identity.recovery.internal.service.impl.UserAccountRecoveryManager; import org.wso2.carbon.identity.recovery.model.Property; import org.wso2.carbon.identity.recovery.model.UserRecoveryData; import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; @@ -249,16 +250,22 @@ private void checkAccountPendingStatus(User user) throws IdentityRecoveryExcepti private UserRecoveryData generateNewConfirmationCode(User user, String notificationChannel) throws IdentityRecoveryException { + String recoveryFlowId = null; UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); + UserRecoveryData userRecoveryData = userRecoveryDataStore.loadWithoutCodeExpiryValidation(user); + if (userRecoveryData != null) { + recoveryFlowId = userRecoveryData.getRecoveryFlowId(); + } userRecoveryDataStore.invalidate(user); String secretKey = Utils.generateSecretKey(notificationChannel, user.getTenantDomain(), RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY.name()); - UserRecoveryData recoveryDataDO = new UserRecoveryData(user, secretKey, + secretKey = Utils.concatRecoveryFlowIdWithSecretKey(recoveryFlowId, notificationChannel, secretKey); + UserRecoveryData recoveryDataDO = new UserRecoveryData(user, recoveryFlowId, secretKey, RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY, RecoverySteps.UPDATE_PASSWORD); // Store the notified channel in the recovery object for future reference. recoveryDataDO.setRemainingSetIds(notificationChannel); - userRecoveryDataStore.store(recoveryDataDO); + userRecoveryDataStore.storeConfirmationCode(recoveryDataDO); return recoveryDataDO; } @@ -533,6 +540,22 @@ public void updatePassword(String code, String password, Property[] properties) updateUserPassword(code, password, properties); } + /** + * Update the password of the user. + * + * @param code Password Reset code. + * @param confirmationCode Confirmation code. + * @param password New password. + * @param properties Properties. + * @throws IdentityRecoveryException Error while updating the password. + * @throws IdentityEventException Error while updating the password. + */ + public void updatePassword(String code, String confirmationCode, String password, Property[] properties) + throws IdentityRecoveryException, IdentityEventException { + + updateUserPassword(code, confirmationCode, password, properties); + } + /** * Update the password of the user. * @@ -552,6 +575,8 @@ public User updateUserPassword(String code, String password, Property[] properti publishEvent(userRecoveryData.getUser(), null, code, password, properties, IdentityEventConstants.Event.PRE_ADD_NEW_PASSWORD, userRecoveryData); validateTenantDomain(userRecoveryData.getUser()); + String recoveryFlowId = userRecoveryDataStore.loadWithoutCodeExpiryValidation(userRecoveryData.getUser()) + .getRecoveryFlowId(); // Validate recovery step. if (!RecoverySteps.UPDATE_PASSWORD.equals(userRecoveryData.getRecoveryStep())) { @@ -575,7 +600,11 @@ public User updateUserPassword(String code, String password, Property[] properti // Update the password. updateNewPassword(userRecoveryData.getUser(), password, domainQualifiedName, userRecoveryData, notificationsInternallyManaged); - userRecoveryDataStore.invalidate(userRecoveryData.getUser()); + if (recoveryFlowId != null) { + userRecoveryDataStore.invalidateWithRecoveryFlowId(recoveryFlowId); + } else { + userRecoveryDataStore.invalidate(userRecoveryData.getUser()); + } if (notificationsInternallyManaged && !NotificationChannels.EXTERNAL_CHANNEL.getChannelType().equals(notificationChannel)) { String emailTemplate = null; @@ -617,6 +646,160 @@ public User updateUserPassword(String code, String password, Property[] properti return userRecoveryData.getUser(); } + /** + * Update the password of the user. + * + * @param code Password Reset code. + * @param confirmationCode Confirmation code. + * @param password New password. + * @param properties Properties. + * @throws IdentityRecoveryException Error while updating the password. + * @throws IdentityEventException Error while updating the password. + */ + public User updateUserPassword(String code, String confirmationCode, String password, Property[] properties) + throws IdentityRecoveryException, IdentityEventException { + + UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); + UserRecoveryData userRecoveryData; + try { + userRecoveryData = userRecoveryDataStore.loadFromRecoveryFlowId(confirmationCode, + RecoverySteps.UPDATE_PASSWORD); + validateCallback(properties, userRecoveryData.getUser().getTenantDomain()); + publishEvent(userRecoveryData.getUser(), null, null, password, properties, + IdentityEventConstants.Event.PRE_ADD_NEW_PASSWORD, userRecoveryData); + validateTenantDomain(userRecoveryData.getUser()); + int failedAttempts = userRecoveryData.getFailedAttempts(); + + if (!StringUtils.equals(code, userRecoveryData.getSecret())) { + if ((failedAttempts + 1) >= Integer.parseInt(Utils.getRecoveryConfigs(IdentityRecoveryConstants. + ConnectorConfig.RECOVERY_OTP_PASSWORD_MAX_FAILED_ATTEMPTS, userRecoveryData.getUser(). + getTenantDomain()))) { + userRecoveryDataStore.invalidateWithRecoveryFlowId(confirmationCode); + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_FLOW_ID.getCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_FLOW_ID.getMessage(), + confirmationCode); + } + userRecoveryDataStore.updateFailedAttempts(confirmationCode, failedAttempts + 1); + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_CODE.getCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_CODE.getMessage(), code); + } + } catch (IdentityRecoveryException e) { + /* This is a fallback logic to support already initiated email link based recovery flows using the + recovery V1 API, which do not have recovery flow ids. */ + userRecoveryData = validateUserRecoveryDataFromCode(code, confirmationCode, password, properties); + } + + // Get the notification channel. + String notificationChannel = getServerSupportedNotificationChannel(userRecoveryData.getRemainingSetIds()); + boolean notificationsInternallyManaged = Boolean.parseBoolean( + Utils.getRecoveryConfigs(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_INTERNALLY_MANAGE, + userRecoveryData.getUser().getTenantDomain())); + boolean isNotificationSendWhenSuccess = Boolean.parseBoolean(Utils.getRecoveryConfigs( + IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_SEND_RECOVERY_NOTIFICATION_SUCCESS, + userRecoveryData.getUser().getTenantDomain())); + boolean isNotificationSendOnAccountActivation = Boolean.parseBoolean(Utils.getRecoveryConfigs( + IdentityRecoveryConstants.ConnectorConfig.EMAIL_VERIFICATION_NOTIFICATION_ACCOUNT_ACTIVATION, + userRecoveryData.getUser().getTenantDomain())); + String domainQualifiedName = IdentityUtil.addDomainToName(userRecoveryData.getUser().getUserName(), + userRecoveryData.getUser().getUserStoreDomain()); + + // Update the password. + updateNewPassword(userRecoveryData.getUser(), password, domainQualifiedName, userRecoveryData, + notificationsInternallyManaged); + if (userRecoveryData.getRecoveryFlowId() != null) { + userRecoveryDataStore.invalidateWithRecoveryFlowId(userRecoveryData.getRecoveryFlowId()); + } else { + userRecoveryDataStore.invalidate(userRecoveryData.getUser()); + } + if (notificationsInternallyManaged && + !NotificationChannels.EXTERNAL_CHANNEL.getChannelType().equals(notificationChannel)) { + String emailTemplate = null; + if (isAskPasswordFlow(userRecoveryData) && + isAskPasswordEmailTemplateTypeExists(userRecoveryData.getUser().getTenantDomain())) { + if (isNotificationSendOnAccountActivation) { + emailTemplate = IdentityRecoveryConstants.ACCOUNT_ACTIVATION_SUCCESS; + } + } else if (isNotificationSendWhenSuccess) { + emailTemplate = IdentityRecoveryConstants.NOTIFICATION_TYPE_PASSWORD_RESET_SUCCESS; + } + try { + String eventName = Utils.resolveEventName(notificationChannel); + if (StringUtils.isNotBlank(emailTemplate)) { + triggerNotification(userRecoveryData.getUser(), notificationChannel, emailTemplate, + StringUtils.EMPTY, eventName, properties, userRecoveryData); + } + } catch (IdentityRecoveryException e) { + String errorMsg = String.format("Error while sending password reset success notification to user : %s", + userRecoveryData.getUser().getUserName()); + log.error(errorMsg); + String recoveryScenario = userRecoveryData.getRecoveryScenario().name(); + String recoveryStep = userRecoveryData.getRecoveryStep().name(); + auditPasswordReset(userRecoveryData.getUser(), AuditConstants.ACTION_PASSWORD_RESET, errorMsg, + FrameworkConstants.AUDIT_SUCCESS, recoveryScenario, recoveryStep); + } + } + publishEvent(userRecoveryData.getUser(), null, code, password, properties, + IdentityEventConstants.Event.POST_ADD_NEW_PASSWORD, userRecoveryData); + if (log.isDebugEnabled()) { + String msg = "Password is updated for user: " + domainQualifiedName; + log.debug(msg); + } + String recoveryScenario = userRecoveryData.getRecoveryScenario().name(); + String recoveryStep = userRecoveryData.getRecoveryStep().name(); + auditPasswordReset(userRecoveryData.getUser(), AuditConstants.ACTION_PASSWORD_RESET, null, + FrameworkConstants.AUDIT_SUCCESS, recoveryScenario, recoveryStep); + + return userRecoveryData.getUser(); + } + + /** + * This method is to validate user recovery data using the reset code when there's no recovery flow id. + * This is added as a fallback logic to handle the already initiated email link based recovery flows which do not + * have recovery flow ids, which were initiated before moving to the Recovery V2 API. + * This shouldn't be used for any other purpose and should be kept for sometime. + * + * @param code Password Reset code. + * @param confirmationCode Confirmation code. + * @param password New password. + * @param properties Properties. + * @return UserRecoveryData. + * @throws IdentityRecoveryException Error while updating the password. + */ + @Deprecated + private UserRecoveryData validateUserRecoveryDataFromCode(String code, String confirmationCode, String password, + Property[] properties) throws IdentityRecoveryException { + + UserRecoveryData userRecoveryData; + UserAccountRecoveryManager userAccountRecoveryManager = UserAccountRecoveryManager.getInstance(); + try { + userRecoveryData = userAccountRecoveryManager.getUserRecoveryData(code, RecoverySteps.UPDATE_PASSWORD); + } catch (IdentityRecoveryException e) { + if (IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_CODE.getCode().equals( + e.getErrorCode())) { + e.setErrorCode(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_FLOW_ID.getCode()); + } + throw e; + } + if (!StringUtils.equals(userRecoveryData.getRemainingSetIds(), + NotificationChannels.EMAIL_CHANNEL.getChannelType())) { + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_FLOW_ID, confirmationCode); + } + validateCallback(properties, userRecoveryData.getUser().getTenantDomain()); + publishEvent(userRecoveryData.getUser(), null, code, password, properties, + IdentityEventConstants.Event.PRE_ADD_NEW_PASSWORD, userRecoveryData); + validateTenantDomain(userRecoveryData.getUser()); + + // Validate recovery step. + if (!RecoverySteps.UPDATE_PASSWORD.equals(userRecoveryData.getRecoveryStep())) { + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_CODE, code); + } + return userRecoveryData; + } + /** * Update the new password of the user. * diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/services/password/PasswordRecoveryManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/services/password/PasswordRecoveryManager.java index 6571b3d9d5..0a2ee598f3 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/services/password/PasswordRecoveryManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/services/password/PasswordRecoveryManager.java @@ -70,6 +70,19 @@ PasswordRecoverDTO notify(String recoveryId, String channelId, String tenantDoma PasswordResetCodeDTO confirm(String confirmationCode, String tenantDomain, Map properties) throws IdentityRecoveryException; + /** + * Validate the code given for password recovery and return the password reset token. + * + * @param otp One Time Password. + * @param confirmationCode Confirmation code. + * @param tenantDomain Tenant domain. + * @param properties Meta properties in the confirmation request. + * @return PasswordResetCodeDTO {@link PasswordResetCodeDTO} object which contains password reset code. + * @throws IdentityRecoveryException Error while confirming password recovery. + */ + PasswordResetCodeDTO confirm(String otp, String confirmationCode, String tenantDomain, Map properties) throws IdentityRecoveryException; + /** * Update the password for password recovery, if the password reset code is valid. * @@ -83,6 +96,20 @@ PasswordResetCodeDTO confirm(String confirmationCode, String tenantDomain, Map properties) throws IdentityRecoveryException; + /** + * Update the password for password recovery, if the password reset code is valid. + * + * @param resetCode Password reset code. + * @param confirmationCode Confirmation code. + * @param password New password. + * @param properties Properties. + * @return SuccessfulPasswordResetDTO {@link SuccessfulPasswordResetDTO} object which contain the information + * for a successful password update. + * @throws IdentityRecoveryException Error while resetting the password. + */ + SuccessfulPasswordResetDTO reset(String resetCode, String confirmationCode, char[] password, Map properties) + throws IdentityRecoveryException; + /** * Resend the password recovery information to the user via user specified channel. * diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStore.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStore.java index 4212167d30..060445a32b 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStore.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStore.java @@ -36,6 +36,7 @@ import org.wso2.carbon.identity.recovery.RecoverySteps; import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; import org.wso2.carbon.identity.recovery.model.UserRecoveryData; +import org.wso2.carbon.identity.recovery.model.UserRecoveryFlowData; import org.wso2.carbon.identity.recovery.util.Utils; import java.sql.Connection; @@ -58,6 +59,8 @@ import static org.wso2.carbon.identity.event.IdentityEventConstants.EventProperty.OPERATION_DESCRIPTION; import static org.wso2.carbon.identity.event.IdentityEventConstants.EventProperty.OPERATION_STATUS; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_CODE; +import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_FLOW_ID; +import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_FLOW_ID; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_CODE; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_RECOVERY_DATA_NOT_FOUND_FOR_USER; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UNEXPECTED; @@ -83,7 +86,8 @@ public void store(UserRecoveryData recoveryDataDO) throws IdentityRecoveryExcept Connection connection = IdentityDatabaseUtil.getDBConnection(true); PreparedStatement prepStmt = null; try { - prepStmt = connection.prepareStatement(IdentityRecoveryConstants.SQLQueries.STORE_RECOVERY_DATA); + prepStmt = connection.prepareStatement(IdentityRecoveryConstants.SQLQueries. + STORE_RECOVERY_DATA_WITH_FLOW_ID); prepStmt.setString(1, recoveryDataDO.getUser().getUserName()); prepStmt.setString(2, recoveryDataDO.getUser().getUserStoreDomain().toUpperCase()); prepStmt.setInt(3, IdentityTenantUtil.getTenantId(recoveryDataDO.getUser().getTenantDomain())); @@ -93,6 +97,7 @@ public void store(UserRecoveryData recoveryDataDO) throws IdentityRecoveryExcept prepStmt.setTimestamp(7, new Timestamp(new Date().getTime()), Calendar.getInstance(TimeZone.getTimeZone(UTC))); prepStmt.setString(8, recoveryDataDO.getRemainingSetIds()); + prepStmt.setString(9, recoveryDataDO.getRecoveryFlowId()); prepStmt.execute(); IdentityDatabaseUtil.commitTransaction(connection); } catch (SQLException e) { @@ -105,6 +110,127 @@ public void store(UserRecoveryData recoveryDataDO) throws IdentityRecoveryExcept } } + @Override + public void storeInit(UserRecoveryData recoveryDataDO) throws IdentityRecoveryException { + + Connection connection = IdentityDatabaseUtil.getDBConnection(true); + PreparedStatement prepStmt1 = null; + PreparedStatement prepStmt2 = null; + try { + prepStmt1 = connection.prepareStatement(IdentityRecoveryConstants.SQLQueries.STORE_RECOVERY_DATA_WITH_FLOW_ID); + prepStmt1.setString(1, recoveryDataDO.getUser().getUserName()); + prepStmt1.setString(2, recoveryDataDO.getUser().getUserStoreDomain().toUpperCase()); + prepStmt1.setInt(3, IdentityTenantUtil.getTenantId(recoveryDataDO.getUser().getTenantDomain())); + prepStmt1.setString(4, recoveryDataDO.getSecret()); + prepStmt1.setString(5, String.valueOf(recoveryDataDO.getRecoveryScenario())); + prepStmt1.setString(6, String.valueOf(recoveryDataDO.getRecoveryStep())); + prepStmt1.setTimestamp(7, new Timestamp(new Date().getTime()), + Calendar.getInstance(TimeZone.getTimeZone(UTC))); + prepStmt1.setString(8, recoveryDataDO.getRemainingSetIds()); + prepStmt1.setString(9, recoveryDataDO.getRecoveryFlowId()); + + prepStmt2 = connection.prepareStatement(IdentityRecoveryConstants.SQLQueries.STORE_RECOVERY_FLOW_DATA); + prepStmt2.setString(1, recoveryDataDO.getRecoveryFlowId()); + prepStmt2.setString(2, null); + prepStmt2.setInt(3, 0); + prepStmt2.setInt(4, 0); + prepStmt2.setTimestamp(5, new Timestamp(new Date().getTime()), + Calendar.getInstance(TimeZone.getTimeZone(UTC))); + + prepStmt2.execute(); + prepStmt1.execute(); + + IdentityDatabaseUtil.commitTransaction(connection); + } catch (SQLException e) { + IdentityDatabaseUtil.rollbackTransaction(connection); + throw Utils.handleServerException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_STORING_RECOVERY_FLOW_DATA, null, e); + } finally { + IdentityDatabaseUtil.closeStatement(prepStmt1); + IdentityDatabaseUtil.closeStatement(prepStmt2); + IdentityDatabaseUtil.closeConnection(connection); + } + } + + @Override + public void storeConfirmationCode(UserRecoveryData recoveryDataDO) throws IdentityRecoveryException { + + Connection connection = IdentityDatabaseUtil.getDBConnection(true); + PreparedStatement prepStmt1 = null; + PreparedStatement prepStmt2 = null; + try { + prepStmt1 = connection.prepareStatement(IdentityRecoveryConstants.SQLQueries.STORE_RECOVERY_DATA_WITH_FLOW_ID); + prepStmt1.setString(1, recoveryDataDO.getUser().getUserName()); + prepStmt1.setString(2, recoveryDataDO.getUser().getUserStoreDomain().toUpperCase()); + prepStmt1.setInt(3, IdentityTenantUtil.getTenantId(recoveryDataDO.getUser().getTenantDomain())); + prepStmt1.setString(4, recoveryDataDO.getSecret()); + prepStmt1.setString(5, String.valueOf(recoveryDataDO.getRecoveryScenario())); + prepStmt1.setString(6, String.valueOf(recoveryDataDO.getRecoveryStep())); + prepStmt1.setTimestamp(7, new Timestamp(new Date().getTime()), + Calendar.getInstance(TimeZone.getTimeZone(UTC))); + prepStmt1.setString(8, recoveryDataDO.getRemainingSetIds()); + prepStmt1.setString(9, recoveryDataDO.getRecoveryFlowId()); + + prepStmt2 = connection.prepareStatement(IdentityRecoveryConstants.SQLQueries.UPDATE_RECOVERY_FLOW_DATA); + prepStmt2.setString(1, recoveryDataDO.getSecret()); + prepStmt2.setString(2, recoveryDataDO.getRecoveryFlowId()); + + prepStmt1.execute(); + prepStmt2.execute(); + IdentityDatabaseUtil.commitTransaction(connection); + } catch (SQLException e) { + IdentityDatabaseUtil.rollbackTransaction(connection); + throw Utils.handleServerException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_STORING_RECOVERY_DATA, null, e); + } finally { + IdentityDatabaseUtil.closeStatement(prepStmt1); + IdentityDatabaseUtil.closeStatement(prepStmt2); + IdentityDatabaseUtil.closeConnection(connection); + } + } + + @Override + public void updateFailedAttempts(String recoveryFlowId, int failedAttempts) throws IdentityRecoveryException { + + Connection connection = IdentityDatabaseUtil.getDBConnection(true); + PreparedStatement prepStmt = null; + try { + prepStmt = connection.prepareStatement(IdentityRecoveryConstants.SQLQueries.UPDATE_FAILED_ATTEMPTS); + prepStmt.setInt(1, failedAttempts); + prepStmt.setString(2, recoveryFlowId); + prepStmt.execute(); + IdentityDatabaseUtil.commitTransaction(connection); + } catch (SQLException e) { + IdentityDatabaseUtil.rollbackTransaction(connection); + throw Utils.handleServerException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UPDATING_RECOVERY_FLOW_DATA, null, e); + } finally { + IdentityDatabaseUtil.closeStatement(prepStmt); + IdentityDatabaseUtil.closeConnection(connection); + } + } + + @Override + public void updateCodeResendCount(String recoveryFlowId, int resendCount) throws IdentityRecoveryException { + + Connection connection = IdentityDatabaseUtil.getDBConnection(true); + PreparedStatement prepStmt = null; + try { + prepStmt = connection.prepareStatement(IdentityRecoveryConstants.SQLQueries.UPDATE_CODE_RESEND_COUNT); + prepStmt.setInt(1, resendCount); + prepStmt.setString(2, recoveryFlowId); + prepStmt.execute(); + IdentityDatabaseUtil.commitTransaction(connection); + } catch (SQLException e) { + IdentityDatabaseUtil.rollbackTransaction(connection); + throw Utils.handleServerException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UPDATING_RECOVERY_FLOW_DATA, null, e); + } finally { + IdentityDatabaseUtil.closeStatement(prepStmt); + IdentityDatabaseUtil.closeConnection(connection); + } + } + @Override public UserRecoveryData load(User user, Enum recoveryScenario, Enum recoveryStep, String code) throws IdentityRecoveryException { @@ -209,8 +335,9 @@ public UserRecoveryData load(String code, boolean skipExpiryValidation) throws I Enum recoveryStep = RecoverySteps.valueOf(resultSet.getString("STEP")); Timestamp timeCreated = resultSet.getTimestamp("TIME_CREATED", Calendar.getInstance(TimeZone.getTimeZone(UTC))); + String recoveryFlowId = resultSet.getString(IdentityRecoveryConstants.DBConstants.RECOVERY_FLOW_ID); - userRecoveryData = new UserRecoveryData(user, code, recoveryScenario, recoveryStep, + userRecoveryData = new UserRecoveryData(user, recoveryFlowId, code, recoveryScenario, recoveryStep, timeCreated); if (StringUtils.isNotBlank(resultSet.getString("REMAINING_SETS"))) { userRecoveryData.setRemainingSetIds(resultSet.getString("REMAINING_SETS")); @@ -244,6 +371,139 @@ public UserRecoveryData load(String code, boolean skipExpiryValidation) throws I throw Utils.handleClientException(ERROR_CODE_INVALID_CODE, code); } + @Override + public UserRecoveryData loadFromRecoveryFlowId(String recoveryFlowId, Enum recoveryStep) + throws IdentityRecoveryException { + + handleRecoveryDataEventPublishing(PRE_GET_USER_RECOVERY_DATA, + GET_USER_RECOVERY_DATA_SCENARIO_WITH_CODE_EXPIRY_VALIDATION, null, null, null, null, + new UserRecoveryData(null, recoveryFlowId, null, null, recoveryStep)); + + PreparedStatement prepStmt1 = null; + PreparedStatement prepStmt2 = null; + ResultSet resultSet1 = null; + ResultSet resultSet2 = null; + Connection connection = IdentityDatabaseUtil.getDBConnection(false); + + User user = null; + String code = null; + int failedAttempts = 0; + int resendCount = 0; + long createdTimeStamp = 0; + UserRecoveryData userRecoveryData = null; + Boolean isOperationSuccess = false; + Enum description = ERROR_CODE_INVALID_FLOW_ID; + try { + String sql1 = IdentityRecoveryConstants.SQLQueries.LOAD_RECOVERY_FLOW_DATA_FROM_RECOVERY_FLOW_ID; + prepStmt1 = connection.prepareStatement(sql1); + prepStmt1.setString(1, recoveryFlowId); + + resultSet1 = prepStmt1.executeQuery(); + + if (resultSet1.next()) { + failedAttempts = resultSet1.getInt(IdentityRecoveryConstants.DBConstants.FAILED_ATTEMPTS); + resendCount = resultSet1.getInt(IdentityRecoveryConstants.DBConstants.RESEND_COUNT); + Timestamp timeCreated = resultSet1.getTimestamp(IdentityRecoveryConstants.DBConstants.TIME_CREATED, + Calendar.getInstance(TimeZone.getTimeZone(UTC))); + createdTimeStamp = timeCreated.getTime(); + } + + String sql2 = IdentityRecoveryConstants.SQLQueries.LOAD_RECOVERY_DATA_FROM_RECOVERY_FLOW_ID; + prepStmt2 = connection.prepareStatement(sql2); + prepStmt2.setString(1, recoveryFlowId); + prepStmt2.setString(2, String.valueOf(recoveryStep)); + + resultSet2 = prepStmt2.executeQuery(); + + if (resultSet2.next()) { + user = new User(); + user.setUserName(resultSet2.getString(IdentityRecoveryConstants.DBConstants.USER_NAME)); + user.setTenantDomain(IdentityTenantUtil.getTenantDomain(resultSet2.getInt( + IdentityRecoveryConstants.DBConstants.TENANT_ID))); + user.setUserStoreDomain(resultSet2.getString(IdentityRecoveryConstants.DBConstants.USER_DOMAIN)); + + code = resultSet2.getString(IdentityRecoveryConstants.DBConstants.CODE); + Enum recoveryScenario = RecoveryScenarios.valueOf(resultSet2.getString(IdentityRecoveryConstants. + DBConstants.SCENARIO)); + String remainingSets = resultSet2.getString(IdentityRecoveryConstants.DBConstants.REMAINING_SETS); + Timestamp secretCreatedTime = resultSet2.getTimestamp(IdentityRecoveryConstants.DBConstants.TIME_CREATED, + Calendar.getInstance(TimeZone.getTimeZone(UTC))); + + userRecoveryData = new UserRecoveryData(user, recoveryFlowId, code, failedAttempts, resendCount, + recoveryScenario, recoveryStep, remainingSets, secretCreatedTime); + long secretCreatedTimeStamp = secretCreatedTime.getTime(); + boolean isCodeExpired = isCodeExpired(user.getTenantDomain(), userRecoveryData.getRecoveryScenario(), + userRecoveryData.getRecoveryStep(), secretCreatedTimeStamp, userRecoveryData.getRemainingSetIds()); + if (isCodeExpired) { + isOperationSuccess = false; + description = ERROR_CODE_EXPIRED_CODE; + throw Utils.handleClientException(ERROR_CODE_EXPIRED_CODE, null); + } + boolean isRecoveryFlowIdExpired = isRecoveryFlowIdExpired(user.getTenantDomain(), createdTimeStamp, + userRecoveryData.getRemainingSetIds()); + if (isRecoveryFlowIdExpired) { + isOperationSuccess = false; + description = ERROR_CODE_EXPIRED_FLOW_ID; + throw Utils.handleClientException(ERROR_CODE_EXPIRED_FLOW_ID, recoveryFlowId); + } + isOperationSuccess = true; + description = null; + return userRecoveryData; + } + } catch (SQLException e) { + isOperationSuccess = false; + description = ERROR_CODE_UNEXPECTED; + throw Utils.handleServerException(ERROR_CODE_UNEXPECTED, null, e); + } finally { + handleRecoveryDataEventPublishing(POST_GET_USER_RECOVERY_DATA, + GET_USER_RECOVERY_DATA_SCENARIO_WITH_CODE_EXPIRY_VALIDATION, isOperationSuccess, description, code, user, + userRecoveryData); + IdentityDatabaseUtil.closeAllConnections(connection, resultSet1, prepStmt1); + IdentityDatabaseUtil.closeAllConnections(connection, resultSet2, prepStmt2); + } + throw Utils.handleClientException(ERROR_CODE_INVALID_FLOW_ID, recoveryFlowId); + } + + @Override + public UserRecoveryFlowData loadRecoveryFlowData(UserRecoveryData recoveryDataDO) + throws IdentityRecoveryException { + + PreparedStatement prepStmt = null; + ResultSet resultSet = null; + Connection connection = IdentityDatabaseUtil.getDBConnection(false); + + try { + String sql = IdentityRecoveryConstants.SQLQueries.LOAD_RECOVERY_FLOW_DATA_FROM_RECOVERY_FLOW_ID; + prepStmt = connection.prepareStatement(sql); + prepStmt.setString(1, recoveryDataDO.getRecoveryFlowId()); + + resultSet = prepStmt.executeQuery(); + + if (resultSet.next()) { + int failedAttempts = resultSet.getInt(IdentityRecoveryConstants.DBConstants.FAILED_ATTEMPTS); + int resendCount = resultSet.getInt(IdentityRecoveryConstants.DBConstants.RESEND_COUNT); + Timestamp timeCreated = resultSet.getTimestamp(IdentityRecoveryConstants.DBConstants.TIME_CREATED, + Calendar.getInstance(TimeZone.getTimeZone(UTC))); + long createdTimeStamp = timeCreated.getTime(); + + UserRecoveryFlowData userRecoveryFlowData = new UserRecoveryFlowData(recoveryDataDO.getRecoveryFlowId(), + timeCreated, failedAttempts, resendCount); + + boolean isRecoveryFlowIdExpired = isRecoveryFlowIdExpired(recoveryDataDO.getUser().getTenantDomain(), + createdTimeStamp, recoveryDataDO.getRemainingSetIds()); + if (isRecoveryFlowIdExpired) { + throw Utils.handleClientException(ERROR_CODE_EXPIRED_FLOW_ID, recoveryDataDO.getRecoveryFlowId()); + } + return userRecoveryFlowData; + } + } catch (SQLException e) { + throw Utils.handleServerException(ERROR_CODE_UNEXPECTED, null, e); + } finally { + IdentityDatabaseUtil.closeAllConnections(connection, resultSet, prepStmt); + } + throw Utils.handleClientException(ERROR_CODE_INVALID_FLOW_ID, recoveryDataDO.getRecoveryFlowId()); + } + @Override public void invalidate(String code) throws IdentityRecoveryException { @@ -370,9 +630,10 @@ public UserRecoveryData loadWithoutCodeExpiryValidation(User user) throws Identi code = resultSet.getString("CODE"); Timestamp timeCreated = resultSet.getTimestamp("TIME_CREATED", Calendar.getInstance(TimeZone.getTimeZone(UTC))); + String recoveryFlowId = resultSet.getString(IdentityRecoveryConstants.DBConstants.RECOVERY_FLOW_ID); userRecoveryData = - new UserRecoveryData(user, code, scenario, step, timeCreated); + new UserRecoveryData(user, recoveryFlowId, code, scenario, step, timeCreated); if (StringUtils.isNotBlank(resultSet.getString("REMAINING_SETS"))) { userRecoveryData.setRemainingSetIds(resultSet.getString("REMAINING_SETS")); } @@ -567,6 +828,29 @@ public void invalidate(User user, Enum recoveryScenario, Enum recoveryStep) thro } } + @Override + public void invalidateWithRecoveryFlowId(String recoveryFlowId) throws IdentityRecoveryException { + + PreparedStatement prepStmt = null; + Connection connection = IdentityDatabaseUtil.getDBConnection(true); + try { + + String sql = IdentityRecoveryConstants.SQLQueries.INVALIDATE_BY_RECOVERY_FLOW_ID; + + prepStmt = connection.prepareStatement(sql); + prepStmt.setString(1, recoveryFlowId); + prepStmt.execute(); + + IdentityDatabaseUtil.commitTransaction(connection); + } catch (SQLException e) { + IdentityDatabaseUtil.rollbackTransaction(connection); + throw Utils.handleServerException(ERROR_CODE_UNEXPECTED, null, e); + } finally { + IdentityDatabaseUtil.closeStatement(prepStmt); + IdentityDatabaseUtil.closeConnection(connection); + } + } + @Override public void invalidateWithoutChangeTimeCreated(String oldCode, String code, Enum recoveryStep, String channelList) throws IdentityRecoveryException { @@ -759,6 +1043,39 @@ private boolean isCodeExpired(String tenantDomain, Enum recoveryScenario, Enum r return System.currentTimeMillis() > expiryTime; } + /** + * Checks whether the recovery flow id has expired or not. + * + * @param tenantDomain Tenant domain. + * @param createdTimestamp Time stamp. + * @param recoveryData Additional data for validate the code. + * @return Whether the recovery flow id has expired or not. + * @throws IdentityRecoveryServerException Error while reading the configs. + */ + private boolean isRecoveryFlowIdExpired(String tenantDomain, long createdTimestamp, String recoveryData) + throws IdentityRecoveryServerException { + + int codeExpiryTime; + int allowedResendAttempts; + int recoveryFlowIdExpiryTime; + if (NotificationChannels.SMS_CHANNEL.getChannelType().equals(recoveryData)) { + codeExpiryTime = Integer.parseInt(Utils.getRecoveryConfigs( + IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_SMS_OTP_EXPIRY_TIME, tenantDomain)); + allowedResendAttempts = Integer.parseInt(Utils.getRecoveryConfigs( + IdentityRecoveryConstants.ConnectorConfig.RECOVERY_OTP_PASSWORD_MAX_RESEND_ATTEMPTS, tenantDomain)); + recoveryFlowIdExpiryTime = codeExpiryTime * allowedResendAttempts; + } else { + recoveryFlowIdExpiryTime = Integer.parseInt( + Utils.getRecoveryConfigs(IdentityRecoveryConstants.ConnectorConfig.EXPIRY_TIME, tenantDomain)); + } + if (recoveryFlowIdExpiryTime < 1) { + recoveryFlowIdExpiryTime = IdentityRecoveryConstants.RECOVERY_FLOW_ID_DEFAULT_EXPIRY_TIME; + } + + long expiryTime = createdTimestamp + TimeUnit.MINUTES.toMillis(recoveryFlowIdExpiryTime); + return System.currentTimeMillis() > expiryTime; + } + /** * Get the expiry time of the recovery code given at username recovery and password recovery init. * diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/store/UserRecoveryDataStore.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/store/UserRecoveryDataStore.java index bb22243be7..0f1b0361f7 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/store/UserRecoveryDataStore.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/store/UserRecoveryDataStore.java @@ -22,10 +22,19 @@ import org.wso2.carbon.identity.application.common.model.User; import org.wso2.carbon.identity.recovery.IdentityRecoveryException; import org.wso2.carbon.identity.recovery.model.UserRecoveryData; +import org.wso2.carbon.identity.recovery.model.UserRecoveryFlowData; public interface UserRecoveryDataStore { void store(UserRecoveryData recoveryDataDO) throws IdentityRecoveryException; + void storeInit(UserRecoveryData recoveryDataDO) throws IdentityRecoveryException; + + void storeConfirmationCode(UserRecoveryData recoveryDataDO) throws IdentityRecoveryException; + + void updateFailedAttempts(String recoveryFlowId, int failedAttempts) throws IdentityRecoveryException; + + void updateCodeResendCount(String recoveryFlowId, int resendCount) throws IdentityRecoveryException; + /* * returns UserRecoveryData if the code is validated. Otherwise returns an exception. */ @@ -53,6 +62,10 @@ default UserRecoveryData load(String code, boolean skipExpiryValidation) throws throw new NotImplementedException("This functionality is not implemented"); } + UserRecoveryData loadFromRecoveryFlowId(String recoveryFlowId, Enum recoveryStep) throws IdentityRecoveryException; + + UserRecoveryFlowData loadRecoveryFlowData(UserRecoveryData recoveryDataDO) throws IdentityRecoveryException; + UserRecoveryData loadWithoutCodeExpiryValidation(User user) throws IdentityRecoveryException; @@ -71,6 +84,9 @@ void invalidate(User user) throws void invalidate(User user, Enum recoveryScenario, Enum recoveryStep) throws IdentityRecoveryException; + void invalidateWithRecoveryFlowId(String recoveryFlowId) throws + IdentityRecoveryException; + /** * Delete all recovery data by tenant id * diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java index bff5db30b8..ac4d242ab2 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java @@ -1183,6 +1183,23 @@ public static String generateSecretKey(String channel, String tenantDomain, Stri } } + /** + * Concatenate recovery flow id with the generated secret key if the notification channel is email. + * + * @param recoveryFlowId Recovery flow id. + * @param notificationChannel Recovery notification channel. + * @param secretKey Secret Key. + * @return Secret key. + */ + public static String concatRecoveryFlowIdWithSecretKey(String recoveryFlowId, String notificationChannel, + String secretKey) { + if (recoveryFlowId != null && StringUtils.equals(notificationChannel, + NotificationChannels.EMAIL_CHANNEL.getChannelType())) { + secretKey = recoveryFlowId + IdentityRecoveryConstants.CONFIRMATION_CODE_SEPARATOR + secretKey; + } + return secretKey; + } + /** * Return user account state. * diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImplTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImplTest.java index 6782f5b845..007fa278cd 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImplTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImplTest.java @@ -122,6 +122,10 @@ public void testGetPropertyNameMapping() { "Recovery callback URL regex"); nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_AUTO_LGOIN_AFTER_PASSWORD_RESET, "Enable Auto Login After Password Reset"); + nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_OTP_PASSWORD_MAX_FAILED_ATTEMPTS, + "Max failed attempts for OTP based recovery"); + nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_OTP_PASSWORD_MAX_RESEND_ATTEMPTS, + "Max resend attempts for OTP based recovery"); Map nameMapping = recoveryConfigImpl.getPropertyNameMapping(); @@ -216,6 +220,8 @@ public void testGetDefaultPropertyValues() throws IdentityGovernanceException { String challengeQuestionAnswerRegex = IdentityRecoveryConstants.DEFAULT_REGEX; String enforceChallengeQuestionAnswerUniqueness = "false"; String enableAutoLoginAfterPasswordReset = "false"; + String recoveryOTPMaxFailedAttempts = "3"; + String recoveryOTPMaxResendAttempts = "5"; Map defaultPropertiesExpected = new HashMap<>(); defaultPropertiesExpected.put(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_BASED_PW_RECOVERY, @@ -258,6 +264,10 @@ public void testGetDefaultPropertyValues() throws IdentityGovernanceException { ENFORCE_CHALLENGE_QUESTION_ANSWER_UNIQUENESS, enforceChallengeQuestionAnswerUniqueness); defaultPropertiesExpected.put(IdentityRecoveryConstants.ConnectorConfig. ENABLE_AUTO_LGOIN_AFTER_PASSWORD_RESET, enableAutoLoginAfterPasswordReset); + defaultPropertiesExpected.put(IdentityRecoveryConstants.ConnectorConfig. + RECOVERY_OTP_PASSWORD_MAX_FAILED_ATTEMPTS, recoveryOTPMaxFailedAttempts); + defaultPropertiesExpected.put(IdentityRecoveryConstants.ConnectorConfig. + RECOVERY_OTP_PASSWORD_MAX_RESEND_ATTEMPTS, recoveryOTPMaxResendAttempts); String tenantDomain = "admin"; // Here tenantDomain parameter is not used by method itself