Skip to content

Commit

Permalink
Add SMS password recovery event compatibility with SMSNotificationHan…
Browse files Browse the repository at this point in the history
…dler
  • Loading branch information
RushanNanayakkara committed Jul 15, 2024
1 parent 5d0c5dc commit cf5bf67
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 35 deletions.
13 changes: 13 additions & 0 deletions components/org.wso2.carbon.identity.recovery/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,17 @@
<groupId>org.wso2.carbon.identity.governance</groupId>
<artifactId>org.wso2.carbon.identity.multi.attribute.login.service</artifactId>
</dependency>
<dependency>
<groupId>org.wso2.carbon.identity.auth.otp.commons</groupId>
<artifactId>org.wso2.carbon.identity.auth.otp.core</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>org.wso2.carbon.identity.framework</groupId>
<artifactId>org.wso2.carbon.identity.application.authentication.framework</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -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}",
</Import-Package>
<DynamicImport-Package>*</DynamicImport-Package>
</instructions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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'."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.*/
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -1687,4 +1688,39 @@ private static String convertFailureReasonsToString(List<ValidationFailureReason
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
return stringBuilder.toString();
}

/**
* Retrieve user claim of the user.
*
* @param user User from whom the claim needs to be retrieved.
* @param userClaim Claim URI of the user.
* @return Claim value of the user.
* @throws IdentityRecoveryServerException Error while retrieving the claim.
*/
public static String getUserClaim(User user, String userClaim) throws IdentityRecoveryServerException {

String userStoreDomain = user.getUserStoreDomain();
RealmService realmService = IdentityRecoveryServiceDataHolder.getInstance().getRealmService();
try {
org.wso2.carbon.user.api.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(), userClaim, null);

} catch (UserStoreException e) {
String error = String.format("Error occurred while retrieving claim for user: " +
"%s in tenant domain : %s", user.getUserName(), user.getTenantDomain());
throw new IdentityRecoveryServerException(error, e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.wso2.carbon.identity.auth.attribute.handler.exception.AuthAttributeHandlerException;
import org.wso2.carbon.identity.auth.attribute.handler.model.ValidationResult;
import org.wso2.carbon.identity.common.testng.WithCarbonHome;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.identity.event.IdentityEventException;
import org.wso2.carbon.identity.event.event.Event;
Expand All @@ -66,13 +67,18 @@
import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore;
import org.wso2.carbon.idp.mgt.IdentityProviderManager;
import org.wso2.carbon.user.api.Claim;
import org.wso2.carbon.user.api.UserRealm;
import org.wso2.carbon.user.api.UserStoreManager;
import org.wso2.carbon.user.core.service.RealmService;

import java.sql.Timestamp;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
Expand Down Expand Up @@ -102,10 +108,14 @@ public class UserSelfRegistrationManagerTest {
private IdentityProviderManager identityProviderManager;
private AuthAttributeHandlerManager authAttributeHandlerManager;
private IdentityGovernanceService identityGovernanceService;
private RealmService realmService;
private OTPGenerator otpGenerator;
private final String TEST_USER_NAME = "dummyUser";
private final String TEST_CLAIM_URI = "ttp://wso2.org/claims/emailaddress";
private final String TEST_CLAIM_VALUE = "[email protected]";
private final String TEST_MOBILE_CLAIM_VALUE = "0775553443";
private final UserRealm userRealm = Mockito.mock(UserRealm.class);
private final UserStoreManager userStoreManager = Mockito.mock(UserStoreManager.class);
private static final Log LOG = LogFactory.getLog(UserSelfRegistrationManagerTest.class);

@Mock
Expand All @@ -117,22 +127,26 @@ public class UserSelfRegistrationManagerTest {
private MockedStatic<IdentityUtil> mockedIdentityUtil;
private MockedStatic<JDBCRecoveryDataStore> mockedJDBCRecoveryDataStore;
private MockedStatic<IdentityProviderManager> mockedIdentityProviderManager;
private MockedStatic<IdentityTenantUtil> mockedIdentityTenantUtil;

@BeforeMethod
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);

}

Expand All @@ -142,6 +156,7 @@ public void tearDown() {
mockedIdentityUtil.close();
mockedJDBCRecoveryDataStore.close();
mockedIdentityProviderManager.close();
mockedIdentityTenantUtil.close();
}

@BeforeTest
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit cf5bf67

Please sign in to comment.