Skip to content

Commit

Permalink
Merge pull request #751 from Rashmini/recovery-api-v2
Browse files Browse the repository at this point in the history
Update recovery service for recovery V2 API
  • Loading branch information
Rashmini authored Sep 20, 2023
2 parents 5721150 + a516155 commit 9ec15c0
Show file tree
Hide file tree
Showing 18 changed files with 1,310 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -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'."),
Expand Down Expand Up @@ -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."),
Expand Down Expand Up @@ -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' "
Expand Down Expand Up @@ -620,13 +630,36 @@ 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 {

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 = ?";
Expand All @@ -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 =
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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();

Expand All @@ -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;
}

Expand Down Expand Up @@ -274,15 +291,16 @@ 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()),
notificationChannel)){
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;
}

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ public Map<String, String> 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;
}

Expand Down Expand Up @@ -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]);
}

Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, String> defaultProperties = new HashMap<>();
defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_BASED_PW_RECOVERY,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -411,6 +433,12 @@ public Map<String, Property> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public class PasswordRecoverDTO {
*/
private String message;

/**
* Recovery flow id.
*/
private String recoveryFlowId;

/**
* User notified channel.
*/
Expand Down Expand Up @@ -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.
*
Expand Down
Loading

0 comments on commit 9ec15c0

Please sign in to comment.