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 3330601967..867d8d0e69 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 @@ -45,6 +45,7 @@ public class IdentityRecoveryConstants { public static final String NOTIFICATION_TYPE_RESEND_ADMIN_FORCED_PASSWORD_RESET_WITH_OTP = "resendAdminForcedPasswordResetWithOTP"; public static final String NOTIFICATION_TYPE_ACCOUNT_CONFIRM = "accountconfirmation"; + public static final String NOTIFICATION_TYPE_ACCOUNT_CONFIRM_EMAIL_OTP = "accountConfirmationEmailOTP"; public static final String NOTIFICATION_TYPE_RESEND_ACCOUNT_CONFIRM = "resendaccountconfirmation"; public static final String NOTIFICATION_TYPE_EMAIL_CONFIRM = "emailconfirm"; public static final String NOTIFICATION_TYPE_LITE_USER_EMAIL_CONFIRM = "liteUserEmailConfirmation"; @@ -71,6 +72,7 @@ public class IdentityRecoveryConstants { public static final String TEMPLATE_TYPE = "TEMPLATE_TYPE"; public static final String EMAIL_TEMPLATE_NAME = "templateName"; public static final String CONFIRMATION_CODE = "confirmation-code"; + public static final String EMAIL_OTP_CODE = "OTPCode"; public static final String VERIFICATION_PENDING_EMAIL = "verification-pending-email"; public static final String NEW_EMAIL_ADDRESS = "new-email-address"; public static final String NOTIFY = "notify"; @@ -163,6 +165,7 @@ public class IdentityRecoveryConstants { public static final String USER_ACCOUNT_RECOVERY = "UAR"; public static final int SMS_OTP_CODE_LENGTH = 6; + public static final int EMAIL_OTP_CODE_LENGTH = 6; public static final String ENABLE_DETAILED_ERROR_RESPONSE = "Recovery.ErrorMessage.EnableDetailedErrorMessages"; // Recovery code given at the username and password recovery initiation. public static final int RECOVERY_CODE_DEFAULT_EXPIRY_TIME = 1; @@ -537,6 +540,7 @@ public static class ConnectorConfig { ".Password.ReCaptcha.MaxFailedAttempts"; public static final String RECOVERY_CALLBACK_REGEX = "Recovery.CallbackRegex"; public static final String ENABLE_SELF_SIGNUP = "SelfRegistration.Enable"; + public static final String ENABLE_EMAIL_OTP_VERIFICATION = "SelfRegistration.EmailOTPVerification.Enable"; public static final String ACCOUNT_LOCK_ON_CREATION = "SelfRegistration.LockOnCreation"; public static final String SEND_CONFIRMATION_NOTIFICATION = "SelfRegistration.SendConfirmationOnCreation"; public static final String SIGN_UP_NOTIFICATION_INTERNALLY_MANAGE = "SelfRegistration.Notification" + @@ -606,6 +610,9 @@ public static class ConnectorConfig { public static final String SELF_REGISTRATION_AUTO_LOGIN_ALIAS_NAME = "SelfRegistration.AutoLogin.AliasName"; } + /** + * This class contains the database queries. + */ public static class SQLQueries { public static final String STORE_RECOVERY_DATA = "INSERT INTO IDN_RECOVERY_DATA " diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/SelfRegistrationConfigImpl.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/SelfRegistrationConfigImpl.java index 05ba6be45f..64ec5d091a 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/SelfRegistrationConfigImpl.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/SelfRegistrationConfigImpl.java @@ -85,6 +85,8 @@ public Map getPropertyNameMapping() { Map nameMapping = new HashMap<>(); nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP, "User self registration"); + nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_OTP_VERIFICATION, + "Email OTP based verification"); nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.ACCOUNT_LOCK_ON_CREATION, "Lock user account on creation"); nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.SEND_CONFIRMATION_NOTIFICATION, @@ -94,7 +96,8 @@ public Map getPropertyNameMapping() { nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.SELF_REGISTRATION_RE_CAPTCHA, "Prompt reCaptcha"); nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.SELF_REGISTRATION_VERIFICATION_CODE_EXPIRY_TIME, "User self registration verification link expiry time"); - nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.SELF_REGISTRATION_SMSOTP_VERIFICATION_CODE_EXPIRY_TIME, + nameMapping.put( + IdentityRecoveryConstants.ConnectorConfig.SELF_REGISTRATION_SMSOTP_VERIFICATION_CODE_EXPIRY_TIME, "User self registration SMS OTP expiry time"); nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.SELF_REGISTRATION_SMS_OTP_REGEX, "User self registration SMS OTP regex"); @@ -118,6 +121,8 @@ public Map getPropertyDescriptionMapping() { Map descriptionMapping = new HashMap<>(); descriptionMapping.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP, "Allow user's to self register to the system."); + descriptionMapping.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_OTP_VERIFICATION, + "Enable email OTP-based verification during user registration."); descriptionMapping.put(IdentityRecoveryConstants.ConnectorConfig.ACCOUNT_LOCK_ON_CREATION, "Lock self registered user account until e-mail verification."); descriptionMapping.put(IdentityRecoveryConstants.ConnectorConfig.SEND_CONFIRMATION_NOTIFICATION, @@ -155,6 +160,7 @@ public String[] getPropertyNames() { List properties = new ArrayList<>(); properties.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP); + properties.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_OTP_VERIFICATION); properties.add(IdentityRecoveryConstants.ConnectorConfig.ACCOUNT_LOCK_ON_CREATION); properties.add(IdentityRecoveryConstants.ConnectorConfig.SEND_CONFIRMATION_NOTIFICATION); properties.add(IdentityRecoveryConstants.ConnectorConfig.SIGN_UP_NOTIFICATION_INTERNALLY_MANAGE); @@ -176,6 +182,7 @@ public String[] getPropertyNames() { public Properties getDefaultPropertyValues(String tenantDomain) throws IdentityGovernanceException { String enableSelfSignUp = "false"; + String enableEmailOTPverification = "false"; String enableAccountLockOnCreation = "true"; String enableSendNotificationOnCreation = "false"; String enableNotificationInternallyManage = "true"; @@ -191,6 +198,8 @@ public Properties getDefaultPropertyValues(String tenantDomain) throws IdentityG String selfSignUpProperty = IdentityUtil.getProperty( IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP); + String emailOTPverificationProprty = IdentityUtil.getProperty( + IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_OTP_VERIFICATION); String accountLockProperty = IdentityUtil.getProperty( IdentityRecoveryConstants.ConnectorConfig.ACCOUNT_LOCK_ON_CREATION); String sendNotificationOnCreationProperty = IdentityUtil.getProperty( @@ -219,6 +228,9 @@ public Properties getDefaultPropertyValues(String tenantDomain) throws IdentityG if (StringUtils.isNotEmpty(selfSignUpProperty)) { enableSelfSignUp = selfSignUpProperty; } + if (StringUtils.isNotEmpty(emailOTPverificationProprty)) { + enableEmailOTPverification = emailOTPverificationProprty; + } if (StringUtils.isNotEmpty(accountLockProperty)) { enableAccountLockOnCreation = accountLockProperty; } @@ -258,6 +270,8 @@ public Properties getDefaultPropertyValues(String tenantDomain) throws IdentityG Map defaultProperties = new HashMap<>(); defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP, enableSelfSignUp); + defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_OTP_VERIFICATION, + enableEmailOTPverification); defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.ACCOUNT_LOCK_ON_CREATION, enableAccountLockOnCreation); defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.SEND_CONFIRMATION_NOTIFICATION, @@ -311,6 +325,9 @@ public Map getMetaData() { meta.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP, getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue())); + meta.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_OTP_VERIFICATION, + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue())); + meta.put(IdentityRecoveryConstants.ConnectorConfig.ACCOUNT_LOCK_ON_CREATION, getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue())); diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserSelfRegistrationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserSelfRegistrationHandler.java index 7bb1082ca6..288a2ce433 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserSelfRegistrationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserSelfRegistrationHandler.java @@ -47,12 +47,10 @@ import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; import org.wso2.carbon.identity.recovery.util.Utils; -import org.wso2.carbon.registry.core.utils.UUIDGenerator; import org.wso2.carbon.user.core.UserCoreConstants; import org.wso2.carbon.user.core.UserStoreException; import org.wso2.carbon.user.core.UserStoreManager; -import java.security.SecureRandom; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; @@ -60,6 +58,9 @@ import java.util.List; import java.util.Map; +/** + * This handler class is used to self register a user. + */ public class UserSelfRegistrationHandler extends AbstractEventHandler { private static final Log log = LogFactory.getLog(UserSelfRegistrationHandler.class); @@ -77,10 +78,13 @@ public void handleEvent(Event event) throws IdentityEventException { Map eventProperties = event.getEventProperties(); String userName = (String) eventProperties.get(IdentityEventConstants.EventProperty.USER_NAME); - UserStoreManager userStoreManager = (UserStoreManager) eventProperties.get(IdentityEventConstants.EventProperty.USER_STORE_MANAGER); + UserStoreManager userStoreManager = (UserStoreManager) eventProperties.get( + IdentityEventConstants.EventProperty.USER_STORE_MANAGER); - String tenantDomain = (String) eventProperties.get(IdentityEventConstants.EventProperty.TENANT_DOMAIN); - String domainName = userStoreManager.getRealmConfiguration().getUserStoreProperty(UserCoreConstants.RealmConfig.PROPERTY_DOMAIN_NAME); + String tenantDomain = (String) eventProperties.get( + IdentityEventConstants.EventProperty.TENANT_DOMAIN); + String domainName = userStoreManager.getRealmConfiguration().getUserStoreProperty( + UserCoreConstants.RealmConfig.PROPERTY_DOMAIN_NAME); String[] roleList = (String[]) eventProperties.get(IdentityEventConstants.EventProperty.ROLE_LIST); @@ -118,7 +122,8 @@ public void handleEvent(Event event) throws IdentityEventException { (IdentityRecoveryConstants.ConnectorConfig.SEND_CONFIRMATION_NOTIFICATION, user.getTenantDomain())); boolean isNotificationInternallyManage = Boolean.parseBoolean(Utils.getConnectorConfig - (IdentityRecoveryConstants.ConnectorConfig.SIGN_UP_NOTIFICATION_INTERNALLY_MANAGE, user.getTenantDomain())); + (IdentityRecoveryConstants.ConnectorConfig.SIGN_UP_NOTIFICATION_INTERNALLY_MANAGE, + user.getTenantDomain())); if (IdentityEventConstants.Event.POST_ADD_USER.equals(event.getEventName())) { UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); @@ -137,7 +142,8 @@ public void handleEvent(Event event) throws IdentityEventException { } boolean isSelfRegistrationConfirmationNotify = Boolean.parseBoolean(Utils.getSignUpConfigs - (IdentityRecoveryConstants.ConnectorConfig.SELF_REGISTRATION_NOTIFY_ACCOUNT_CONFIRMATION, user.getTenantDomain())); + (IdentityRecoveryConstants.ConnectorConfig.SELF_REGISTRATION_NOTIFY_ACCOUNT_CONFIRMATION, + user.getTenantDomain())); // If notify confirmation is enabled and both iAccountLockOnCreation && // EnableConfirmationOnCreation are disabled then send account creation notification. @@ -407,8 +413,8 @@ protected void triggerNotification(User user, String type, String code, Property try { IdentityRecoveryServiceDataHolder.getInstance().getIdentityEventService().handleEvent(identityMgtEvent); } catch (IdentityEventException e) { - throw Utils.handleServerException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_TRIGGER_NOTIFICATION, user - .getUserName(), e); + throw Utils.handleServerException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_TRIGGER_NOTIFICATION, + user.getUserName(), e); } } @@ -425,8 +431,18 @@ protected void triggerNotification(User user, String type, String code, Property private void triggerNotification(User user, String notificationChannel, String code, Property[] props, String eventName) throws IdentityRecoveryException { + boolean emailOTPenabled = false; + try { + emailOTPenabled = Boolean.parseBoolean(Utils.getConnectorConfig( + IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_OTP_VERIFICATION, user.getTenantDomain())); + } catch (IdentityEventException e) { + throw Utils.handleServerException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_ERROR_GETTING_CONNECTOR_CONFIG, + user.getTenantDomain(), e); + } + if (log.isDebugEnabled()) { - log.debug("Sending self user registration notification user: " + user.getUserName()); + log.debug("Sending account confirmation notification to user: " + user.getUserName()); } HashMap properties = new HashMap<>(); properties.put(IdentityEventConstants.EventProperty.USER_NAME, user.getUserName()); @@ -440,10 +456,19 @@ private void triggerNotification(User user, String notificationChannel, String c } } if (StringUtils.isNotBlank(code)) { - properties.put(IdentityRecoveryConstants.CONFIRMATION_CODE, code); + if (emailOTPenabled) { + properties.put(IdentityRecoveryConstants.EMAIL_OTP_CODE, code); + } else { + properties.put(IdentityRecoveryConstants.CONFIRMATION_CODE, code); + } } - properties.put(IdentityRecoveryConstants.TEMPLATE_TYPE, + if (emailOTPenabled) { + properties.put(IdentityRecoveryConstants.TEMPLATE_TYPE, + IdentityRecoveryConstants.NOTIFICATION_TYPE_ACCOUNT_CONFIRM_EMAIL_OTP); + } else { + properties.put(IdentityRecoveryConstants.TEMPLATE_TYPE, IdentityRecoveryConstants.NOTIFICATION_TYPE_ACCOUNT_CONFIRM); + } Event identityMgtEvent = new Event(eventName, properties); try { IdentityRecoveryServiceDataHolder.getInstance().getIdentityEventService().handleEvent(identityMgtEvent); 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 160e238799..cc62ecbc62 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 @@ -428,12 +428,12 @@ public static String doHash(String value) throws UserStoreException { } /** - * Set claim to user store manager + * Set claim to user store manager. * * @param user user * @param claim claim uri * @param value claim value - * @throws IdentityException if fails + * @throws UserStoreException if fails */ public static void setClaimInUserStoreManager(User user, String claim, String value) throws UserStoreException { @@ -570,7 +570,7 @@ public static String getSignUpConfigs(String key, String tenantDomain) throws Id Property[] connectorConfigs; IdentityGovernanceService identityGovernanceService = IdentityRecoveryServiceDataHolder.getInstance() .getIdentityGovernanceService(); - connectorConfigs = identityGovernanceService.getConfiguration(new String[]{key,}, tenantDomain); + connectorConfigs = identityGovernanceService.getConfiguration(new String[]{key, }, tenantDomain); return connectorConfigs[0].getValue(); } catch (IdentityGovernanceException e) { throw Utils.handleServerException( @@ -584,7 +584,7 @@ public static String getConnectorConfig(String key, String tenantDomain) throws Property[] connectorConfigs; IdentityGovernanceService identityGovernanceService = IdentityRecoveryServiceDataHolder.getInstance() .getIdentityGovernanceService(); - connectorConfigs = identityGovernanceService.getConfiguration(new String[]{key,}, tenantDomain); + connectorConfigs = identityGovernanceService.getConfiguration(new String[]{key, }, tenantDomain); return connectorConfigs[0].getValue(); } catch (IdentityGovernanceException e) { throw new IdentityEventException("Error while getting connector configurations", e); @@ -733,7 +733,7 @@ public static String getCallbackURL(org.wso2.carbon.identity.recovery.model.Prop } /** - * Get whether this is tenant flow + * Get whether this is tenant flow. * * @param properties * @return @@ -775,7 +775,7 @@ public static boolean isUserPortalURL(org.wso2.carbon.identity.recovery.model.Pr } /** - * Check if the exception contains a password pattern violation message and act accordingly + * Check if the exception contains a password pattern violation message and act accordingly. * * @param exception An UserStoreException * @throws IdentityRecoveryClientException If exception's message contains a password pattern violation message @@ -809,7 +809,7 @@ public static void checkPasswordPatternViolation(UserStoreException exception, U } /** - * Get RealmConfiguration by tenantId + * Get RealmConfiguration by tenantId. * * @param user User * @return realmConfiguration RealmConfiguration of the given tenant @@ -1132,44 +1132,81 @@ public static String generateSecretKey(String channel, String tenantDomain, Stri throws IdentityRecoveryServerException { if (NotificationChannels.SMS_CHANNEL.getChannelType().equals(channel)) { - int otpLength = IdentityRecoveryConstants.SMS_OTP_CODE_LENGTH; - String otpRegex = null; - boolean useNumeric = true; - boolean useUppercaseLetters = true; - boolean useLowercaseLetters = true; - if (StringUtils.equals(RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY.name(), recoveryScenario)) { - otpRegex = Utils.getRecoveryConfigs(IdentityRecoveryConstants.ConnectorConfig. - PASSWORD_RECOVERY_SMS_OTP_REGEX, tenantDomain); - } else if (StringUtils.equals(RecoveryScenarios.SELF_SIGN_UP.name(), recoveryScenario)) { - otpRegex = Utils.getRecoveryConfigs(IdentityRecoveryConstants.ConnectorConfig. - SELF_REGISTRATION_SMS_OTP_REGEX, tenantDomain); - } else if (StringUtils.equals(RecoveryScenarios.LITE_SIGN_UP.name(), recoveryScenario)) { - otpRegex = Utils.getRecoveryConfigs(IdentityRecoveryConstants.ConnectorConfig. - LITE_REGISTRATION_SMS_OTP_REGEX, tenantDomain); - } - // If the OTP regex is not specified we need to ensure that the default behavior will be executed. - if (StringUtils.isNotBlank(otpRegex)) { - if (StringUtils.equals(RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY.name(), recoveryScenario) || - StringUtils.equals(RecoveryScenarios.SELF_SIGN_UP.name(), recoveryScenario) || - StringUtils.equals(RecoveryScenarios.LITE_SIGN_UP.name(), recoveryScenario)) { - if (!Pattern.matches(IdentityRecoveryConstants.VALID_SMS_OTP_REGEX_PATTERN, otpRegex)) { - throw new IdentityRecoveryServerException( - IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UNSUPPORTED_SMS_OTP_REGEX.getCode(), - IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UNSUPPORTED_SMS_OTP_REGEX.getMessage()); - } - String charsRegex = otpRegex.replaceAll("[{].*", ""); - otpLength = Integer.parseInt(otpRegex.replaceAll(".*[{]", "").replaceAll("}", "")); - if (!charsRegex.contains("A-Z")) { - useUppercaseLetters = false; - } - if (!charsRegex.contains("a-z")) { - useLowercaseLetters = false; - } - if (!charsRegex.contains("0-9")) { - useNumeric = false; - } + return generateSMSSecretKey(tenantDomain, recoveryScenario); + } else if (NotificationChannels.EMAIL_CHANNEL.getChannelType().equals(channel)) { + return generateEmailSecretKey(tenantDomain, recoveryScenario); + } + return UUIDGenerator.generateUUID(); + } + + private static String generateSMSSecretKey(String tenantDomain, String recoveryScenario) + throws IdentityRecoveryServerException { + + int otpLength = IdentityRecoveryConstants.SMS_OTP_CODE_LENGTH; + String otpRegex = null; + boolean useNumeric = true; + boolean useUppercaseLetters = true; + boolean useLowercaseLetters = true; + if (StringUtils.equals(RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY.name(), recoveryScenario)) { + otpRegex = Utils.getRecoveryConfigs(IdentityRecoveryConstants.ConnectorConfig. + PASSWORD_RECOVERY_SMS_OTP_REGEX, tenantDomain); + } else if (StringUtils.equals(RecoveryScenarios.SELF_SIGN_UP.name(), recoveryScenario)) { + otpRegex = Utils.getRecoveryConfigs(IdentityRecoveryConstants.ConnectorConfig. + SELF_REGISTRATION_SMS_OTP_REGEX, tenantDomain); + } else if (StringUtils.equals(RecoveryScenarios.LITE_SIGN_UP.name(), recoveryScenario)) { + otpRegex = Utils.getRecoveryConfigs(IdentityRecoveryConstants.ConnectorConfig. + LITE_REGISTRATION_SMS_OTP_REGEX, tenantDomain); + } + // If the OTP regex is not specified we need to ensure that the default behavior will be executed. + if (StringUtils.isNotBlank(otpRegex)) { + if (StringUtils.equals(RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY.name(), recoveryScenario) || + StringUtils.equals(RecoveryScenarios.SELF_SIGN_UP.name(), recoveryScenario) || + StringUtils.equals(RecoveryScenarios.LITE_SIGN_UP.name(), recoveryScenario)) { + if (!Pattern.matches(IdentityRecoveryConstants.VALID_SMS_OTP_REGEX_PATTERN, otpRegex)) { + throw new IdentityRecoveryServerException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UNSUPPORTED_SMS_OTP_REGEX.getCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UNSUPPORTED_SMS_OTP_REGEX.getMessage()); + } + String charsRegex = otpRegex.replaceAll("[{].*", ""); + otpLength = Integer.parseInt(otpRegex.replaceAll(".*[{]", "").replaceAll("}", "")); + if (!charsRegex.contains("A-Z")) { + useUppercaseLetters = false; + } + if (!charsRegex.contains("a-z")) { + useLowercaseLetters = false; + } + if (!charsRegex.contains("0-9")) { + useNumeric = false; } } + } + try { + OTPGenerator otpGenerator = IdentityRecoveryServiceDataHolder.getInstance().getOtpGenerator(); + return otpGenerator.generateOTP(useNumeric, useUppercaseLetters, useLowercaseLetters, otpLength, + recoveryScenario); + } catch (OTPGeneratorException otpGeneratorException) { + throw new IdentityRecoveryServerException(otpGeneratorException.getErrorCode(), + otpGeneratorException.getMessage()); + } + } + + private static String generateEmailSecretKey(String tenantDomain, String recoveryScenario) + throws IdentityRecoveryServerException { + + boolean emailOTPenabled; + try { + emailOTPenabled = Boolean.parseBoolean(Utils.getConnectorConfig( + IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_OTP_VERIFICATION, tenantDomain)); + } catch (IdentityEventException e) { + throw Utils.handleServerException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_ERROR_GETTING_CONNECTOR_CONFIG, + tenantDomain, e); + } + if (emailOTPenabled) { + int otpLength = IdentityRecoveryConstants.EMAIL_OTP_CODE_LENGTH; + boolean useNumeric = true; + boolean useUppercaseLetters = false; + boolean useLowercaseLetters = false; try { OTPGenerator otpGenerator = IdentityRecoveryServiceDataHolder.getInstance().getOtpGenerator(); return otpGenerator.generateOTP(useNumeric, useUppercaseLetters, useLowercaseLetters, otpLength, diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/SelfRegistrationConfigImplTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/SelfRegistrationConfigImplTest.java index 8a154964d3..0b9baca5db 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/SelfRegistrationConfigImplTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/SelfRegistrationConfigImplTest.java @@ -93,6 +93,7 @@ public void testGetPropertyNameMapping() { Map nameMappingExpected = new HashMap(); nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP, "User self registration"); + nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_OTP_VERIFICATION, "Email OTP based verification"); nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.ACCOUNT_LOCK_ON_CREATION, "Lock user account on creation"); nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.SEND_CONFIRMATION_NOTIFICATION, @@ -128,6 +129,8 @@ public void testGetPropertyDescriptionMapping() { Map descriptionMappingExpected = new HashMap<>(); descriptionMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP, "Allow user's to self register to the system."); + descriptionMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_OTP_VERIFICATION, + "Enable if email verification is done by sending an OTP to user's email."); descriptionMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.ACCOUNT_LOCK_ON_CREATION, "Lock self registered user account until e-mail verification."); descriptionMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.SEND_CONFIRMATION_NOTIFICATION, @@ -168,6 +171,7 @@ public void testGetPropertyNames() { List propertiesExpected = new ArrayList<>(); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP); + propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_OTP_VERIFICATION); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.ACCOUNT_LOCK_ON_CREATION); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.SEND_CONFIRMATION_NOTIFICATION); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.SIGN_UP_NOTIFICATION_INTERNALLY_MANAGE); @@ -196,6 +200,7 @@ public void testGetPropertyNames() { public void testGetDefaultPropertyValues() throws IdentityGovernanceException { String testEnableSelfSignUp = "false"; + String testEnableEmailOTPverification = "false"; String testEnableAccountLockOnCreation = "true"; String testEnableSendNotificationOnCreation = "false"; String testEnableNotificationInternallyManage = "true"; @@ -211,6 +216,7 @@ public void testGetDefaultPropertyValues() throws IdentityGovernanceException { Map propertiesExpected = new HashMap<>(); propertiesExpected.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP, testEnableSelfSignUp); + propertiesExpected.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_OTP_VERIFICATION, testEnableEmailOTPverification); propertiesExpected.put(IdentityRecoveryConstants.ConnectorConfig.ACCOUNT_LOCK_ON_CREATION, testEnableAccountLockOnCreation); propertiesExpected.put(IdentityRecoveryConstants.ConnectorConfig.SEND_CONFIRMATION_NOTIFICATION, diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java index 9c33c0e2f9..433bcfcde8 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java @@ -59,11 +59,13 @@ import org.wso2.carbon.identity.recovery.RecoverySteps; import org.wso2.carbon.identity.recovery.bean.NotificationResponseBean; import org.wso2.carbon.identity.recovery.exception.SelfRegistrationException; +import org.wso2.carbon.identity.recovery.handler.UserSelfRegistrationHandler; import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; import org.wso2.carbon.identity.recovery.model.Property; import org.wso2.carbon.identity.recovery.model.UserRecoveryData; import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; +import org.wso2.carbon.identity.recovery.util.Utils; import org.wso2.carbon.idp.mgt.IdentityProviderManager; import org.wso2.carbon.user.api.Claim; @@ -76,6 +78,7 @@ import static org.testng.Assert.assertEquals; import static org.wso2.carbon.identity.auth.attribute.handler.AuthAttributeHandlerConstants.ErrorMessages.ERROR_CODE_AUTH_ATTRIBUTE_HANDLER_NOT_FOUND; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP; +import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_OTP_VERIFICATION; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.SELF_REGISTRATION_SMS_OTP_REGEX; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.SIGN_UP_NOTIFICATION_INTERNALLY_MANAGE; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_REGISTRATION_OPTION; @@ -181,7 +184,7 @@ public void testResendConfirmationCode(String username, String userstore, String // Storing preferred notification channel in remaining set ids. userRecoveryData.setRemainingSetIds(preferredChannel); - mockConfigurations("true", enableInternalNotificationManagement); + mockConfigurations("true", "true", enableInternalNotificationManagement); mockJDBCRecoveryDataStore(userRecoveryData); mockEmailTrigger(); @@ -225,13 +228,18 @@ private Object[][] userDetailsForResendingAccountConfirmation() { * @param enableInternalNotifications Enable notifications internal management. * @throws Exception If an error occurred while mocking configurations. */ - private void mockConfigurations(String enableSelfSignUp, String enableInternalNotifications) throws Exception { + private void mockConfigurations(String enableSelfSignUp, String enableEmailOTP, String enableInternalNotifications) throws Exception { org.wso2.carbon.identity.application.common.model.Property signupConfig = new org.wso2.carbon.identity.application.common.model.Property(); signupConfig.setName(ENABLE_SELF_SIGNUP); signupConfig.setValue(enableSelfSignUp); + org.wso2.carbon.identity.application.common.model.Property emailOTPConfig = + new org.wso2.carbon.identity.application.common.model.Property(); + emailOTPConfig.setName(ENABLE_EMAIL_OTP_VERIFICATION); + emailOTPConfig.setValue(enableEmailOTP); + org.wso2.carbon.identity.application.common.model.Property notificationConfig = new org.wso2.carbon.identity.application.common.model.Property(); notificationConfig.setName(SIGN_UP_NOTIFICATION_INTERNALLY_MANAGE); @@ -245,6 +253,9 @@ private void mockConfigurations(String enableSelfSignUp, String enableInternalNo when(identityGovernanceService .getConfiguration(new String[]{ENABLE_SELF_SIGNUP}, TEST_TENANT_DOMAIN_NAME)) .thenReturn(new org.wso2.carbon.identity.application.common.model.Property[]{signupConfig}); + when(identityGovernanceService + .getConfiguration(new String[]{ENABLE_EMAIL_OTP_VERIFICATION}, TEST_TENANT_DOMAIN_NAME)) + .thenReturn(new org.wso2.carbon.identity.application.common.model.Property[]{signupConfig}); when(identityGovernanceService .getConfiguration(new String[]{SIGN_UP_NOTIFICATION_INTERNALLY_MANAGE}, TEST_TENANT_DOMAIN_NAME)) .thenReturn(new org.wso2.carbon.identity.application.common.model.Property[]{notificationConfig}); @@ -403,4 +414,62 @@ public AddReceiptResponse addConsent(ReceiptInput receiptInput) throws ConsentMa return null; } } + + @Test(dataProvider = "userDetailsForTestEmailOTPVerification") + public void testEmailOTPVerification(String username, String userstore, String tenantDomain, + String preferredChannel, String errorMsg, + String enableInternalNotificationManagement, String enableEmailOTP, String expectedChannel) + throws Exception { + + // Build recovery user. + User user = new User(); + user.setUserName(username); + user.setUserStoreDomain(userstore); + user.setTenantDomain(tenantDomain); + + mockConfigurations("true", enableEmailOTP, enableInternalNotificationManagement); + + String secretKey = Utils.generateSecretKey(preferredChannel,tenantDomain, String.valueOf(RecoveryScenarios.SELF_SIGN_UP)); + + UserSelfRegistrationHandler handler = new UserSelfRegistrationHandler(); + + UserRecoveryData userRecoveryData = new UserRecoveryData(user, secretKey, RecoveryScenarios + .SELF_SIGN_UP, RecoverySteps.CONFIRM_SIGN_UP); + // Storing preferred notification channel in remaining set ids. + userRecoveryData.setRemainingSetIds(preferredChannel); + + mockJDBCRecoveryDataStore(userRecoveryData); + mockEmailTrigger(); + } + + @DataProvider(name = "userDetailsForTestEmailOTPVerification") + private Object[][] userDetailsForTestEmailOTPVerification() { + + String username = "test-user"; + // Notification channel types. + String EMAIL = NotificationChannels.EMAIL_CHANNEL.getChannelType(); + String SMS = NotificationChannels.SMS_CHANNEL.getChannelType(); + String EXTERNAL = NotificationChannels.EXTERNAL_CHANNEL.getChannelType(); + + /* ArrayOrder: Username, Userstore, Tenant domain, Preferred channel, Error message, Manage notifications + internally, excepted channel */ + return new Object[][]{ + {username, TEST_USERSTORE_DOMAIN, TEST_TENANT_DOMAIN_NAME, EMAIL, "User with EMAIL as Preferred " + + "Notification Channel and EmailOTP is enabled: ", "TRUE", "TRUE", EMAIL}, + {username, TEST_USERSTORE_DOMAIN, TEST_TENANT_DOMAIN_NAME, EMAIL, "User with EMAIL as Preferred " + + "Notification Channel and EmailOTP is disabled: ", "TRUE", "FALSE", EMAIL}, + {username, TEST_USERSTORE_DOMAIN, TEST_TENANT_DOMAIN_NAME, EMAIL, "User with EMAIL as Preferred " + + "Notification Channel but notifications are externally managed and EmailOTP is enabled: ", "FALSE", "TRUE", EXTERNAL}, + {username, TEST_USERSTORE_DOMAIN, TEST_TENANT_DOMAIN_NAME, EMAIL, "User with EMAIL as Preferred " + + "Notification Channel but notifications are externally managed and EmailOTP is disabled: ", "FALSE", "FALSE", EXTERNAL}, + {username, TEST_USERSTORE_DOMAIN, TEST_TENANT_DOMAIN_NAME, SMS, "User with SMS as Preferred " + + "Notification Channel and EmailOTP is enabled: ", "TRUE", "TRUE", SMS}, + {username, TEST_USERSTORE_DOMAIN, TEST_TENANT_DOMAIN_NAME, SMS, "User with SMS as Preferred " + + "Notification Channel and EmailOTP is disabled: ", "TRUE", "FALSE", SMS}, + {username, TEST_USERSTORE_DOMAIN, TEST_TENANT_DOMAIN_NAME, StringUtils.EMPTY, + "User no preferred channel specified and EmailOTP is enabled: ", "TRUE", "TRUE", null}, + {username, TEST_USERSTORE_DOMAIN, TEST_TENANT_DOMAIN_NAME, StringUtils.EMPTY, + "User no preferred channel specified and EmailOTP is disabled: ", "TRUE", "FALSE", null} + }; + } } \ No newline at end of file