Skip to content

Commit

Permalink
Merge pull request #859 from KD23243/userNameRecovery2ndApproach
Browse files Browse the repository at this point in the history
Support usernames recovery when claims are not unique
  • Loading branch information
ashensw authored Oct 2, 2024
2 parents f70253e + e7b697e commit b914f08
Show file tree
Hide file tree
Showing 6 changed files with 1,262 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ public static class ConnectorConfig {
public static final String FORCE_ADD_PW_RECOVERY_QUESTION = "Recovery.Question.Password.Forced.Enable";
public static final String FORCE_MIN_NO_QUESTION_ANSWERED = "Recovery.Question.MinQuestionsToAnswer";
public static final String USERNAME_RECOVERY_ENABLE = "Recovery.Notification.Username.Enable";
public static final String USERNAME_RECOVERY_NON_UNIQUE_USERNAME = "Recovery.Notification.Username.NonUniqueUsername";
public static final String QUESTION_CHALLENGE_SEPARATOR = "Recovery.Question.Password.Separator";
public static final String QUESTION_MIN_NO_ANSWER = "Recovery.Question.Password.MinAnswers";
public static final String EXPIRY_TIME = "Recovery.ExpiryTime";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
* Copyright (c) 2020, WSO2 LLC. (https://www.wso2.org)
*
* WSO2 Inc. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
Expand All @@ -18,7 +18,6 @@
package org.wso2.carbon.identity.recovery.internal.service.impl;

import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Expand Down Expand Up @@ -69,12 +68,12 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import static java.lang.Integer.MAX_VALUE;
import static org.wso2.carbon.identity.recovery.RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY;
import static org.wso2.carbon.identity.recovery.RecoveryScenarios.QUESTION_BASED_PWD_RECOVERY;
import static org.wso2.carbon.identity.recovery.RecoveryScenarios.USERNAME_RECOVERY;
Expand Down Expand Up @@ -360,6 +359,83 @@ public String getUsernameByClaims(Map<String, String> claims, String tenantDomai
}
}

/**
* Get the userlist for the given claims.
*
* @param claims List of UserClaims
* @param tenantDomain Tenant domain
* @return resultedUserList (Returns an empty list if there are no users).
* @throws IdentityRecoveryException Error while retrieving the users list.
*/
public ArrayList<org.wso2.carbon.user.core.common.User> getUserListByClaims(Map<String, String> claims, String tenantDomain)
throws IdentityRecoveryException {

ArrayList<org.wso2.carbon.user.core.common.User> resultedUserList = new ArrayList<>();

if (MapUtils.isEmpty(claims)) {
// Get error code with scenario.
String errorCode = Utils.prependOperationScenarioToErrorCode(
IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_FIELD_FOUND_FOR_USER_RECOVERY.getCode(),
IdentityRecoveryConstants.USER_ACCOUNT_RECOVERY);
throw Utils.handleClientException(errorCode,
IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_FIELD_FOUND_FOR_USER_RECOVERY.getMessage(),
null);
}

MultiAttributeLoginService multiAttributeLoginService = IdentityRecoveryServiceDataHolder.getInstance()
.getMultiAttributeLoginService();

if (multiAttributeLoginService.isEnabled(tenantDomain) && claims.containsKey(MultiAttributeLoginConstants
.MULTI_ATTRIBUTE_USER_IDENTIFIER_CLAIM_URI)) {
/* Multiple claims are not allowed when user identifier claim is enabled since identifier claim cannot be
used in combination with other claims. */
if (claims.keySet().size() > 1) {
String errorCode = Utils.prependOperationScenarioToErrorCode(
IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_CLAIMS_WITH_MULTI_ATTRIBUTE_URI
.getCode(), IdentityRecoveryConstants.USER_ACCOUNT_RECOVERY);
throw Utils.handleClientException(errorCode,
IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_CLAIMS_WITH_MULTI_ATTRIBUTE_URI
.getMessage(), null);
}
// Resolve the user with the multi attribute login service.
ResolvedUserResult resolvedUserResult = multiAttributeLoginService.resolveUser(
claims.get(MultiAttributeLoginConstants.MULTI_ATTRIBUTE_USER_IDENTIFIER_CLAIM_URI), tenantDomain);
if (resolvedUserResult != null && ResolvedUserResult.UserResolvedStatus.SUCCESS
.equals(resolvedUserResult.getResolvedStatus())) {
resultedUserList.add(resolvedUserResult.getUser());
return resultedUserList;
}
return resultedUserList;
}

int tenantId = IdentityTenantUtil.getTenantId(tenantDomain);
try {
AbstractUserStoreManager abstractUserStoreManager = (AbstractUserStoreManager)
getUserStoreManager(tenantId);
String userstoreDomain = extractDomainFromClaims(claims, abstractUserStoreManager);
if (userstoreDomain != null) {
populateUserListFromClaimsForDomain(tenantId, claims, userstoreDomain, resultedUserList,
abstractUserStoreManager);
} else {
// If a userstore domain is not specified in the request, consider all userstores.
List<String> userStoreDomainNames = getDomainNames(tenantId);
for (String domain : userStoreDomainNames) {
populateUserListFromClaimsForDomain(tenantId, claims, domain, resultedUserList,
abstractUserStoreManager);
}
}
return resultedUserList;
} catch (org.wso2.carbon.user.core.UserStoreException e) {
if (log.isDebugEnabled()) {
log.debug("Error while retrieving users from user store for the given claim set: " +
Arrays.toString(claims.keySet().toArray()));
}
throw new IdentityRecoveryException(e.getErrorCode(), "Error occurred while retrieving users.", e);
} catch (UserStoreException | IdentityRecoveryServerException e) {
throw new IdentityRecoveryException(e.getMessage(), e);
}
}

/**
* Extract and remove the userstore domain from the claim set.
*
Expand Down Expand Up @@ -421,11 +497,15 @@ private void populateUserListFromClaimsForDomain(int tenantId, Map<String, Strin

if (!expressionConditionList.isEmpty()) {
Condition operationalCondition = getOperationalCondition(expressionConditionList);
// Get the user list that matches the condition limit : 2, offset : 1, sortBy : null, sortOrder : null
boolean nonUniqueUsernameEnabled = Boolean.parseBoolean(IdentityUtil.getProperty(
IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_NON_UNIQUE_USERNAME));
int limit = nonUniqueUsernameEnabled ? MAX_VALUE : 2;
// Get the user list that matches the condition limit : MAX_VALUE or 2, offset : 1, sortBy : null, sortOrder : null
userList.addAll(abstractUserStoreManager.getUserListWithID(operationalCondition, userstoreDomain,
UserCoreConstants.DEFAULT_PROFILE, 2, 1, null, null));
UserCoreConstants.DEFAULT_PROFILE, limit, 1, null, null));

if (userList.size() > 1) {
//If multiple users are found for the given claim set and the config is not enabled, throw an exception.
if (userList.size() > 1 && !nonUniqueUsernameEnabled) {
log.warn("Multiple users matched for given claims set: " + claims.keySet());
throw Utils.handleClientException(
IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_MATCHING_USERS, null);
Expand Down Expand Up @@ -665,49 +745,6 @@ private String getLocalClaimMaskingRegex(String claimURI, String tenantDomain)
return localClaimMaskingRegex;
}

/**
* Get the users list for a matching claim.
*
* @param tenantId Tenant ID
* @param claimUri Claim to be searched
* @param claimValue Claim value to be matched
* @return Matched users list
* @throws IdentityRecoveryServerException If an error occurred while retrieving claims from the userstore manager.
*/
private String[] getUserList(int tenantId, String claimUri, String claimValue)
throws IdentityRecoveryServerException {

String[] userList = new String[0];
UserStoreManager userStoreManager = getUserStoreManager(tenantId);
try {
if (userStoreManager != null) {
if (StringUtils.isNotBlank(claimValue) && claimValue.contains(FORWARD_SLASH)) {
String extractedDomain = IdentityUtil.extractDomainFromName(claimValue);
UserStoreManager secondaryUserStoreManager = userStoreManager.
getSecondaryUserStoreManager(extractedDomain);
/*
Some claims (Eg:- Birth date) can have "/" in claim values. But in user store level we are trying
to extract the claim value and find the user store domain. Hence we are adding an extra "/" to
the claim value to avoid such issues.
*/
if (secondaryUserStoreManager == null) {
claimValue = FORWARD_SLASH + claimValue;
}
}
userList = userStoreManager.getUserList(claimUri, claimValue, null);
}
return userList;
} catch (UserStoreException e) {
if (log.isDebugEnabled()) {
String error = String
.format("Unable to retrieve the claim : %1$s for the given tenant : %2$s", claimUri, tenantId);
log.debug(error, e);
}
throw Utils.handleServerException(
IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_ERROR_RETRIEVING_USER_CLAIM, claimUri, e);
}
}

/**
* Get all the domain names related to user stores.
* @param tenantId Tenant ID
Expand Down Expand Up @@ -788,39 +825,6 @@ private UserStoreManager getUserStoreManager(User user) throws IdentityRecoveryE
}
}

/**
* Keep the common users list from the previously matched list and the new list.
*
* @param resultedUserList Already matched users for previous claims
* @param matchedUserList Retrieved users list for the given claim
* @param claim Claim used for filtering
* @param value Value given for the claim
* @return Users list with no duplicates.
*/
private String[] getCommonUserEntries(String[] resultedUserList, String[] matchedUserList, String claim,
String value) {

ArrayList<String> matchedUsers = new ArrayList<>(Arrays.asList(matchedUserList));
ArrayList<String> resultedUsers = new ArrayList<>(Arrays.asList(resultedUserList));
// Remove not matching users.
resultedUsers.retainAll(matchedUsers);
if (resultedUsers.size() > 0) {
resultedUserList = resultedUsers.toArray(new String[0]);
if (log.isDebugEnabled()) {
log.debug("Current matching temporary user list :" + Arrays.toString(resultedUserList));
}
return resultedUserList;
} else {
if (log.isDebugEnabled()) {
String message = String
.format("There are no common users for claim : %1$s with the value : %2$s with the "
+ "previously filtered user list", claim, value);
log.debug(message);
}
return new String[0];
}
}

/**
* Get claim values of a user for a given list of claims.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
* Copyright (c) 2020, WSO2 LLC. (http://www.wso2.org)
*
* WSO2 Inc. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
Expand Down Expand Up @@ -53,6 +53,7 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

Expand Down Expand Up @@ -90,40 +91,42 @@ public RecoveryInformationDTO initiate(Map<String, String> claims, String tenant
boolean manageNotificationsInternally = Utils.isNotificationsInternallyManaged(tenantDomain, properties);
if (useLegacyAPIApproach) {
// Use legacy API approach to support legacy username recovery.
String username = userAccountRecoveryManager.getUsernameByClaims(claims, tenantDomain);
if (StringUtils.isNotEmpty(username)) {
if (manageNotificationsInternally) {
User user = createUser(username, tenantDomain);
triggerNotification(user, NotificationChannels.EMAIL_CHANNEL.getChannelType(),
IdentityEventConstants.Event.TRIGGER_NOTIFICATION, null);
ArrayList<org.wso2.carbon.user.core.common.User> resultedUserList = userAccountRecoveryManager
.getUserListByClaims(claims, tenantDomain);
for (org.wso2.carbon.user.core.common.User recoveredUser : resultedUserList) {
String username = recoveredUser.getDomainQualifiedUsername();
if (StringUtils.isNotEmpty(username)) {
if (manageNotificationsInternally) {
User user = createUser(username, tenantDomain);
triggerNotification(user, NotificationChannels.EMAIL_CHANNEL.getChannelType(),
IdentityEventConstants.Event.TRIGGER_NOTIFICATION, null);
if (log.isDebugEnabled()) {
log.debug("Successful username recovery for user: " + username + ". " +
"User notified Internally");
}
auditUserNameRecovery(AuditConstants.ACTION_USERNAME_RECOVERY, claims, NOTIFICATION_TYPE_INTERNAL,
username, null, FrameworkConstants.AUDIT_SUCCESS);
}
if (log.isDebugEnabled()) {
log.debug("Successful username recovery for user: " + username + ". " +
"User notified Internally");
log.debug("Successful username recovery for user: " + username + ". User notified Externally");
}
auditUserNameRecovery(AuditConstants.ACTION_USERNAME_RECOVERY, claims, NOTIFICATION_TYPE_INTERNAL,
auditUserNameRecovery(AuditConstants.ACTION_USERNAME_RECOVERY, claims, NOTIFICATION_TYPE_EXTERNAL,
username, null, FrameworkConstants.AUDIT_SUCCESS);
return null;
}
if (log.isDebugEnabled()) {
log.debug("Successful username recovery for user: " + username + ". User notified Externally");
}
auditUserNameRecovery(AuditConstants.ACTION_USERNAME_RECOVERY, claims, NOTIFICATION_TYPE_EXTERNAL,
username, null, FrameworkConstants.AUDIT_SUCCESS);
recoveryInformationDTO.setUsername(username);
} else {
String errorMsg =
String.format("No user found for the given claims in tenant domain : %s", tenantDomain);
if (log.isDebugEnabled()) {
log.debug(errorMsg);
}
auditUserNameRecovery(AuditConstants.ACTION_USERNAME_RECOVERY, claims, "N/A", username, errorMsg,
FrameworkConstants.AUDIT_FAILED);
if (Boolean.parseBoolean(
IdentityUtil.getProperty(IdentityRecoveryConstants.ConnectorConfig.NOTIFY_USER_EXISTENCE))) {
throw Utils.handleClientException(
IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_USER_FOUND, null);
recoveryInformationDTO.setUsername(username);
} else {
String errorMsg =
String.format("No user found for the given claims in tenant domain : %s", tenantDomain);
if (log.isDebugEnabled()) {
log.debug(errorMsg);
}
auditUserNameRecovery(AuditConstants.ACTION_USERNAME_RECOVERY, claims, "N/A", username, errorMsg,
FrameworkConstants.AUDIT_FAILED);
if (Boolean.parseBoolean(
IdentityUtil.getProperty(IdentityRecoveryConstants.ConnectorConfig.NOTIFY_USER_EXISTENCE))) {
throw Utils.handleClientException(
IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_USER_FOUND, null);
}
}
return null;
}
return recoveryInformationDTO;
}
Expand Down Expand Up @@ -187,18 +190,7 @@ public UsernameRecoverDTO notify(String recoveryCode, String channelId, String t
*/
private boolean useLegacyAPIApproach(Map<String, String> properties) {

if (MapUtils.isNotEmpty(properties)) {
try {
return Boolean.parseBoolean(properties.get(IdentityRecoveryConstants.USE_LEGACY_API_PROPERTY_KEY));
} catch (NumberFormatException e) {
if (log.isDebugEnabled()) {
String message = String.format("Invalid boolean value : %s to enable legacyAPIs", properties
.get(IdentityRecoveryConstants.USE_LEGACY_API_PROPERTY_KEY));
log.debug(message);
}
}
}
return false;
return Boolean.parseBoolean(properties.get(IdentityRecoveryConstants.USE_LEGACY_API_PROPERTY_KEY));
}

/**
Expand Down
Loading

0 comments on commit b914f08

Please sign in to comment.