From cf5bf6783b263af401b4a01a12f85428bcc516c3 Mon Sep 17 00:00:00 2001 From: rushannanayakkara Date: Mon, 15 Jul 2024 17:30:20 +0530 Subject: [PATCH] Add SMS password recovery event compatibility with SMSNotificationHandler --- .../org.wso2.carbon.identity.recovery/pom.xml | 13 ++++ .../recovery/IdentityRecoveryConstants.java | 3 + .../ResendConfirmationManager.java | 15 ++++- .../NotificationPasswordRecoveryManager.java | 62 +++++++++---------- .../carbon/identity/recovery/util/Utils.java | 40 +++++++++++- .../UserSelfRegistrationManagerTest.java | 22 +++++++ pom.xml | 8 +++ 7 files changed, 128 insertions(+), 35 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/pom.xml b/components/org.wso2.carbon.identity.recovery/pom.xml index ad208cddc5..ba22577755 100644 --- a/components/org.wso2.carbon.identity.recovery/pom.xml +++ b/components/org.wso2.carbon.identity.recovery/pom.xml @@ -132,6 +132,17 @@ org.wso2.carbon.identity.governance org.wso2.carbon.identity.multi.attribute.login.service + + org.wso2.carbon.identity.auth.otp.commons + org.wso2.carbon.identity.auth.otp.core + provided + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.application.authentication.framework + + + @@ -188,6 +199,8 @@ version="${carbon.identity.framework.imp.pkg.version.range}", org.wso2.carbon.identity.multi.attribute.login.service; version="${identity.governance.imp.pkg.version.range}", + org.wso2.carbon.identity.auth.otp.core.model; + version="${identity.auth.otp.commons.version.range}", * 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 595438fdc6..ec263f31c2 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 @@ -73,6 +73,7 @@ public class IdentityRecoveryConstants { public static final String RESEND_EMAIL_TEMPLATE_NAME = "resendTemplateName"; public static final String INITIATED_PLATFORM = "initiated-platform"; public static final String CONFIRMATION_CODE = "confirmation-code"; + public static final String OTP_TOKEN = "otpToken"; 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"; @@ -134,6 +135,7 @@ public class IdentityRecoveryConstants { public static final String NOTIFICATION_EVENTNAME_PREFIX = "TRIGGER_"; public static final String NOTIFICATION_EVENTNAME_SUFFIX = "_NOTIFICATION"; + public static final String NOTIFICATION_EVENTNAME_SUFFIX_LOCAL = "_LOCAL"; public static final String SEND_TO = "send-to"; public static final String LOCALE_EN_US = "en_US"; public static final String LOCALE_LK_LK = "lk_lk"; @@ -229,6 +231,7 @@ public enum ErrorMessages { 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_MOBILE_NOT_FOUND("18021", "Mobile number is not found for the user %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'."), 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 00df8c3705..ee68937bce 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 @@ -25,6 +25,7 @@ import org.wso2.carbon.base.MultitenantConstants; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.identity.application.common.model.User; +import org.wso2.carbon.identity.auth.otp.core.model.OTP; import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.event.IdentityEventConstants; import org.wso2.carbon.identity.event.IdentityEventException; @@ -267,8 +268,20 @@ private void triggerNotification(User user, String notificationChannel, String t notificationChannel = NotificationChannels.EMAIL_CHANNEL.getChannelType(); } properties.put(IdentityEventConstants.EventProperty.NOTIFICATION_CHANNEL, notificationChannel); + if (NotificationChannels.SMS_CHANNEL.getChannelType().equals(notificationChannel)) { + String sendTo = Utils.getUserClaim(user, IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); + if (StringUtils.isEmpty(sendTo)) { + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MOBILE_NOT_FOUND, user.getUserName()); + } + properties.put(IdentityRecoveryConstants.SEND_TO, sendTo); + } if (StringUtils.isNotBlank(code)) { - properties.put(IdentityRecoveryConstants.CONFIRMATION_CODE, code); + if (NotificationChannels.SMS_CHANNEL.getChannelType().equals(notificationChannel)) { + properties.put(IdentityRecoveryConstants.OTP_TOKEN, new OTP(code, 0, 0)); + } else { + properties.put(IdentityRecoveryConstants.CONFIRMATION_CODE, code); + } } if (metaProperties != null) { for (Property metaProperty : metaProperties) { 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 195414b47d..7ad7d572b8 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 @@ -24,6 +24,7 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.auth.otp.core.model.OTP; import org.json.JSONObject; import org.slf4j.MDC; import org.wso2.carbon.base.MultitenantConstants; @@ -58,16 +59,15 @@ import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; import org.wso2.carbon.identity.recovery.util.Utils; import org.wso2.carbon.registry.core.Resource; -import org.wso2.carbon.user.api.UserRealm; import org.wso2.carbon.user.api.UserStoreException; import org.wso2.carbon.user.api.UserStoreManager; -import org.wso2.carbon.user.core.common.AbstractUserStoreManager; import org.wso2.carbon.user.core.service.RealmService; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import java.util.Base64; import java.util.HashMap; import java.util.Map; @@ -141,7 +141,8 @@ IdentityEventConstants.Event.PRE_SEND_RECOVERY_NOTIFICATION, new UserRecoveryDat user.getUserName()); } return new NotificationResponseBean(user); - } else if (isExistingUser(user) && StringUtils.isEmpty(getEmail(user))) { + } else if (isExistingUser(user) && StringUtils.isEmpty(Utils.getUserClaim(user, + IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM))) { /* If the email is not found for the user, Check for NOTIFY_RECOVERY_EMAIL_EXISTENCE property. If the property is not enabled, notify with an empty NotificationResponseBean.*/ @@ -193,6 +194,16 @@ IdentityEventConstants.Event.PRE_SEND_RECOVERY_NOTIFICATION, new UserRecoveryDat recoveryDataDO = generateNewConfirmationCode(user, notificationChannel); } secretKey = recoveryDataDO.getSecret(); + if (NotificationChannels.SMS_CHANNEL.getChannelType().equals(notificationChannel)) { + String sendTo = Utils.getUserClaim(user, IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); + if (StringUtils.isEmpty(sendTo)) { + /* If the mobile number is not found for the user, notify with an empty + NotificationResponseBean.*/ + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MOBILE_NOT_FOUND, user.getUserName()); + } + properties = addMobileNumberToProperties(properties, sendTo); + } NotificationResponseBean notificationResponseBean = new NotificationResponseBean(user); if (isNotificationInternallyManage) { // Manage notifications by the identity server. @@ -1010,7 +1021,13 @@ private void triggerNotification(User user, String notificationChannel, String t userRecoveryData.getRecoveryScenario().name()); } if (StringUtils.isNotBlank(code)) { - properties.put(IdentityRecoveryConstants.CONFIRMATION_CODE, code); + if (NotificationChannels.SMS_CHANNEL.getChannelType().equals(notificationChannel)) { + /* Generate time and validity period not added since only used to pass the otp code to notification + event handler. */ + properties.put(IdentityRecoveryConstants.OTP_TOKEN, new OTP(code, 0,0)); + } else { + properties.put(IdentityRecoveryConstants.CONFIRMATION_CODE, code); + } } if (metaProperties != null) { for (Property metaProperty : metaProperties) { @@ -1099,6 +1116,9 @@ private void publishEvent(User user, String notify, String code, String password } if (StringUtils.isNotBlank(code)) { properties.put(IdentityRecoveryConstants.CONFIRMATION_CODE, code); + /* Generate time and validity period not added since only used to pass the otp code to notification + event handler. */ + properties.put(IdentityRecoveryConstants.OTP_TOKEN, new OTP(code, 0,0)); } if (StringUtils.isNotBlank(notify)) { @@ -1289,35 +1309,13 @@ private boolean isAskPasswordEmailTemplateTypeExists(String tenantDomain) { return templateType != null; } - /** - * Retrieve email address of the user. - * - * @param user User the email need to be retrieved. - * @return email address of the user. - * @throws IdentityRecoveryServerException - */ - private String getEmail(User user) throws IdentityRecoveryServerException { + private Property[] addMobileNumberToProperties(Property[] properties, String mobile) { - String userStoreDomain = user.getUserStoreDomain(); - RealmService realmService = IdentityRecoveryServiceDataHolder.getInstance().getRealmService(); - try { - UserRealm userRealm = realmService.getTenantUserRealm(IdentityTenantUtil.getTenantId(user.getTenantDomain())); - UserStoreManager userStoreManager = userRealm.getUserStoreManager(); - - if (userStoreManager == null) { - throw new IdentityRecoveryServerException(String.format("userStoreManager is null for user: " + - "%s in tenant domain : %s", user.getUserName(), user.getTenantDomain())); - } - if (StringUtils.isNotBlank(userStoreDomain) && !PRIMARY_DEFAULT_DOMAIN_NAME.equals(userStoreDomain)) { - userStoreManager = ((AbstractUserStoreManager) userStoreManager).getSecondaryUserStoreManager(userStoreDomain); - } - - return userStoreManager.getUserClaimValue(user.getUserName(), IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM, null); - - } catch (UserStoreException e) { - String error = String.format("Error occurred while retrieving existing email address for user: " + - "%s in tenant domain : %s", user.getUserName(), user.getTenantDomain()); - throw new IdentityRecoveryServerException(error, e); + if (ArrayUtils.isEmpty(properties)) { + properties = new Property[0]; } + Property[] newProperties = Arrays.copyOf(properties, properties.length + 1); + newProperties[properties.length] = new Property(IdentityRecoveryConstants.SEND_TO, mobile); + return newProperties; } } 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 50ca856066..4ee2f66146 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 @@ -82,7 +82,6 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -94,6 +93,7 @@ import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_REGISTRATION_OPTION; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_USER_ATTRIBUTES_FOR_REGISTRATION; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UNEXPECTED_ERROR_VALIDATING_ATTRIBUTES; +import static org.wso2.carbon.user.core.UserCoreConstants.PRIMARY_DEFAULT_DOMAIN_NAME; import static org.wso2.carbon.utils.CarbonUtils.isLegacyAuditLogsDisabled; /** @@ -949,7 +949,8 @@ public static String resolveEventName(String notificationChannel) { if (NotificationChannels.SMS_CHANNEL.getChannelType().equals(notificationChannel)) { return IdentityRecoveryConstants.NOTIFICATION_EVENTNAME_PREFIX + notificationChannel - + IdentityRecoveryConstants.NOTIFICATION_EVENTNAME_SUFFIX; + + IdentityRecoveryConstants.NOTIFICATION_EVENTNAME_SUFFIX + + IdentityRecoveryConstants.NOTIFICATION_EVENTNAME_SUFFIX_LOCAL; } else { return IdentityEventConstants.Event.TRIGGER_NOTIFICATION; } @@ -1687,4 +1688,39 @@ private static String convertFailureReasonsToString(List mockedIdentityUtil; private MockedStatic mockedJDBCRecoveryDataStore; private MockedStatic mockedIdentityProviderManager; + private MockedStatic mockedIdentityTenantUtil; @BeforeMethod public void setUp() { @@ -124,15 +135,18 @@ public void setUp() { mockedIdentityUtil = Mockito.mockStatic(IdentityUtil.class); mockedJDBCRecoveryDataStore = Mockito.mockStatic(JDBCRecoveryDataStore.class); mockedIdentityProviderManager = Mockito.mockStatic(IdentityProviderManager.class); + mockedIdentityTenantUtil = Mockito.mockStatic(IdentityTenantUtil.class); identityProviderManager = Mockito.mock(IdentityProviderManager.class); authAttributeHandlerManager = Mockito.mock(AuthAttributeHandlerManager.class); identityGovernanceService = Mockito.mock(IdentityGovernanceService.class); otpGenerator = Mockito.mock(OTPGenerator.class); + realmService = Mockito.mock(RealmService.class); IdentityRecoveryServiceDataHolder.getInstance().setIdentityEventService(identityEventService); IdentityRecoveryServiceDataHolder.getInstance().setIdentityGovernanceService(identityGovernanceService); IdentityRecoveryServiceDataHolder.getInstance().setOtpGenerator(otpGenerator); IdentityRecoveryServiceDataHolder.getInstance().setAuthAttributeHandlerManager(authAttributeHandlerManager); + IdentityRecoveryServiceDataHolder.getInstance().setRealmService(realmService); } @@ -142,6 +156,7 @@ public void tearDown() { mockedIdentityUtil.close(); mockedJDBCRecoveryDataStore.close(); mockedIdentityProviderManager.close(); + mockedIdentityTenantUtil.close(); } @BeforeTest @@ -194,6 +209,13 @@ public void testResendConfirmationCode(String username, String userstore, String mockConfigurations("true", enableInternalNotificationManagement); mockJDBCRecoveryDataStore(userRecoveryData); mockEmailTrigger(); + when(realmService.getTenantUserRealm(anyInt())).thenReturn(userRealm); + when(userRealm.getUserStoreManager()).thenReturn(userStoreManager); + when(userStoreManager.getUserClaimValue(any(), eq(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM), any())) + .thenReturn(TEST_MOBILE_CLAIM_VALUE); + when(userStoreManager.getUserClaimValue(any(), eq(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM), any())) + .thenReturn(TEST_CLAIM_VALUE); + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(anyString())).thenReturn(-1234); NotificationResponseBean responseBean = userSelfRegistrationManager.resendConfirmationCode(user, null); diff --git a/pom.xml b/pom.xml index 55fe0320e3..7dd8e68e14 100644 --- a/pom.xml +++ b/pom.xml @@ -505,6 +505,11 @@ org.wso2.carbon.identity.multi.attribute.login.service ${project.version} + + org.wso2.carbon.identity.auth.otp.commons + org.wso2.carbon.identity.auth.otp.core + ${identity.auth.otp.commons.version} + org.wso2.carbon.identity.governance org.wso2.carbon.identity.multi.attribute.login.server.feature @@ -714,6 +719,9 @@ 4.7.50 [4.7.2, 5.0.0) + 1.0.3 + [1.0.0, 2.0.0) + 1.0.0