/*
*
* Copyright (c) 2014, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.wso2.carbon.identity.mgt;
import org.apache.axis2.context.MessageContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.identity.base.IdentityException;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.identity.mgt.beans.VerificationBean;
import org.wso2.carbon.identity.mgt.config.Config;
import org.wso2.carbon.identity.mgt.config.ConfigBuilder;
import org.wso2.carbon.identity.mgt.config.ConfigType;
import org.wso2.carbon.identity.mgt.config.StorageType;
import org.wso2.carbon.identity.mgt.constants.IdentityMgtConstants;
import org.wso2.carbon.identity.mgt.dto.NotificationDataDTO;
import org.wso2.carbon.identity.mgt.dto.UserDTO;
import org.wso2.carbon.identity.mgt.dto.UserRecoveryDTO;
import org.wso2.carbon.identity.mgt.dto.UserRecoveryDataDO;
import org.wso2.carbon.identity.mgt.internal.IdentityMgtServiceComponent;
import org.wso2.carbon.identity.mgt.mail.Notification;
import org.wso2.carbon.identity.mgt.mail.NotificationBuilder;
import org.wso2.carbon.identity.mgt.mail.NotificationData;
import org.wso2.carbon.identity.mgt.store.UserIdentityDataStore;
import org.wso2.carbon.identity.mgt.store.UserRecoveryDataStore;
import org.wso2.carbon.identity.mgt.util.Utils;
import org.wso2.carbon.registry.core.utils.UUIDGenerator;
import org.wso2.carbon.user.api.Tenant;
import org.wso2.carbon.user.api.UserStoreException;
import org.wso2.carbon.user.api.UserStoreManager;
import org.wso2.carbon.user.core.tenant.TenantManager;
import org.wso2.carbon.user.core.util.UserCoreUtil;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
*
*
*/
public class RecoveryProcessor {
private static final Log log = LogFactory.getLog(RecoveryProcessor.class);
private final static String USER_STORE_DOMAIN = "userstore-domain";
private final static String USER_NAME = "user-name";
private final static String TENANT_DOMAIN = "tenant-domain";
private final static String FIRST_NAME = "first-name";
private final static String CONFIRMATION_CODE = "confirmation-code";
private final static String TEMPORARY_PASSWORD = "temporary-password";
/*
* Delimiter that will be used to store the registry resource entries. Must be valid characters.
* If this changed the split regex also need to changed in getUserExternalCodeStr method.
*/
private final String REG_DELIMITER = "___";
private Map<String, NotificationSendingModule> modules =
new HashMap<String, NotificationSendingModule>();
private NotificationSendingModule defaultModule;
private UserRecoveryDataStore dataStore;
private NotificationSender notificationSender;
private ChallengeQuestionProcessor questionProcessor;
public RecoveryProcessor() {
List<NotificationSendingModule> notificationSendingModules =
IdentityMgtConfig.getInstance().getNotificationSendingModules();
this.defaultModule = notificationSendingModules.get(0);
for (NotificationSendingModule module : notificationSendingModules) {
this.modules.put(module.getNotificationType(), module);
}
this.dataStore = IdentityMgtConfig.getInstance().getRecoveryDataStore();
this.notificationSender = new NotificationSender();
questionProcessor = new ChallengeQuestionProcessor();
}
/**
* Processing recovery
*
* @param recoveryDTO class that contains user and tenant Information
* @return true if the reset request is processed successfully.
* @throws IdentityException if fails
*/
public NotificationDataDTO recoverWithNotification(UserRecoveryDTO recoveryDTO) throws IdentityException {
String notificationAddress;
String secretKey = null;
String confirmationKey = null;
NotificationSendingModule module = null;
boolean persistData = true;
String userId = recoveryDTO.getUserId();
String domainName = recoveryDTO.getTenantDomain();
int tenantId = recoveryDTO.getTenantId();
String userStore = IdentityUtil.extractDomainFromName(userId);
String userName = UserCoreUtil.removeDomainFromName(userId);
TenantManager tenantManager = IdentityMgtServiceComponent.getRealmService().getTenantManager();
try {
Tenant tenant = tenantManager.getTenant(tenantId);
if (tenant != null) {
domainName = tenant.getDomain();
}
} catch (UserStoreException e) {
if(log.isDebugEnabled()){
log.debug("No Tenant domain for tenant id " + tenantId, e);
}
}
NotificationDataDTO notificationData = new NotificationDataDTO();
if(MessageContext.getCurrentMessageContext() != null &&
MessageContext.getCurrentMessageContext().getProperty(
MessageContext.TRANSPORT_HEADERS) != null) {
notificationData.setTransportHeaders(new HashMap(
(Map)MessageContext.getCurrentMessageContext().getProperty(
MessageContext.TRANSPORT_HEADERS)));
}
String internalCode = null;
String type = recoveryDTO.getNotificationType();
if (type != null) {
module = modules.get(type);
}
if (module == null) {
module = defaultModule;
}
NotificationData emailNotificationData = new NotificationData();
String emailTemplate = null;
notificationAddress = Utils.getEmailAddressForUser(userId, tenantId);
String firstName = Utils.getClaimFromUserStoreManager(userId, tenantId, "http://wso2.org/claims/givenname");
emailNotificationData.setTagData(FIRST_NAME, firstName);
emailNotificationData.setTagData(USER_STORE_DOMAIN, userStore);
emailNotificationData.setTagData(USER_NAME, userName);
emailNotificationData.setTagData(TENANT_DOMAIN, domainName);
if ((notificationAddress == null) || (notificationAddress.trim().length() < 0)) {
throw IdentityException.error("Notification sending failure. Notification address is not defined for user : "
+ userId);
}
emailNotificationData.setSendTo(notificationAddress);
if (log.isDebugEnabled()) {
log.debug("Building notification with data - First name: " + firstName +
" User name: " + userId + " Send To: " + notificationAddress);
}
Config config = null;
ConfigBuilder configBuilder = ConfigBuilder.getInstance();
try {
config = configBuilder.loadConfiguration(ConfigType.EMAIL, StorageType.REGISTRY, tenantId);
} catch (Exception e1) {
throw IdentityException.error("Error while loading email templates for user : " + userId, e1);
}
if (recoveryDTO.getNotification() != null) {
emailTemplate = config.getProperty(recoveryDTO.getNotification().trim());
String notification = recoveryDTO.getNotification().trim();
notificationData.setNotification(notification);
if (IdentityMgtConstants.Notification.PASSWORD_RESET_RECOVERY.equals(notification)) {
internalCode = generateUserCode(2, userId);
try {
confirmationKey = getUserExternalCodeStr(internalCode);
} catch (Exception e) {
throw IdentityException.error("Error while getting user's external code string.",e);
}
secretKey = UUIDGenerator.generateUUID();
emailNotificationData.setTagData(CONFIRMATION_CODE, confirmationKey);
emailTemplate = config.getProperty(IdentityMgtConstants.Notification.PASSWORD_RESET_RECOVERY);
} else if (IdentityMgtConstants.Notification.ACCOUNT_CONFORM.equals(notification)) {
confirmationKey = UUIDGenerator.generateUUID();
secretKey = UUIDGenerator.generateUUID();
emailNotificationData.setTagData(CONFIRMATION_CODE, confirmationKey);
emailTemplate = config.getProperty(IdentityMgtConstants.Notification.ACCOUNT_CONFORM);
} else if (IdentityMgtConstants.Notification.TEMPORARY_PASSWORD.equals(notification)) {
String temporaryPassword = recoveryDTO.getTemporaryPassword(); // TODO
if (temporaryPassword == null || temporaryPassword.trim().length() < 1) {
char[] chars = IdentityMgtConfig.getInstance().getPasswordGenerator().generatePassword();
temporaryPassword = new String(chars);
}
Utils.updatePassword(userId, tenantId, temporaryPassword);
emailNotificationData.setTagData(TEMPORARY_PASSWORD, temporaryPassword);
emailTemplate = config.getProperty(IdentityMgtConstants.Notification.TEMPORARY_PASSWORD);
persistData = false;
} else if (IdentityMgtConstants.Notification.ACCOUNT_UNLOCK.equals(notification)) {
emailTemplate = config.getProperty(IdentityMgtConstants.Notification.ACCOUNT_UNLOCK);
persistData = false;
} else if (IdentityMgtConstants.Notification.ACCOUNT_ID_RECOVERY.equals(notification)) {
emailTemplate = config.getProperty(IdentityMgtConstants.Notification.ACCOUNT_ID_RECOVERY);
persistData = false;
} else if (IdentityMgtConstants.Notification.ASK_PASSWORD.equals(notification)) {
if (firstName == null || firstName.isEmpty()) {
emailNotificationData.setTagData(FIRST_NAME, userId);
}
internalCode = generateUserCode(2, userId);
try {
confirmationKey = getUserExternalCodeStr(internalCode);
} catch (Exception e) {
throw IdentityException.error("Error while with recovering with password.", e);
}
secretKey = UUIDGenerator.generateUUID();
emailNotificationData.setTagData(CONFIRMATION_CODE, confirmationKey);
emailTemplate = config.getProperty(IdentityMgtConstants.Notification.ASK_PASSWORD);
}
if (log.isDebugEnabled()) {
log.debug("Notification type: " + notification);
}
}
Notification emailNotification = null;
try {
emailNotification = NotificationBuilder.createNotification("EMAIL", emailTemplate, emailNotificationData);
} catch (Exception e) {
throw IdentityException.error("Error when creating notification for user : "+ userId, e);
}
notificationData.setNotificationAddress(notificationAddress);
notificationData.setUserId(userId);
notificationData.setDomainName(domainName);
notificationData.setNotificationType(recoveryDTO.getNotificationType());
if (persistData) {
UserRecoveryDataDO recoveryDataDO =
new UserRecoveryDataDO(userId, tenantId, internalCode, secretKey);
dataStore.invalidate(userId, tenantId);
dataStore.store(recoveryDataDO);
}
if (IdentityMgtConfig.getInstance().isNotificationInternallyManaged()) {
module.setNotificationData(notificationData);
module.setNotification(emailNotification);
notificationSender.sendNotification(module);
notificationData.setNotificationSent(true);
} else {
notificationData.setNotificationSent(false);
notificationData.setNotificationCode(confirmationKey);
}
return notificationData;
}
/**
* Confirm that confirmation key has been sent to the same user.
*
* @param sequence TODO
* @param username TODO
* @param confirmationKey confirmation key from the user
* @return verification result as a bean
*/
public VerificationBean verifyConfirmationKey(String confirmationKey) {
UserRecoveryDataDO dataDO = null;
try {
dataDO = dataStore.load(confirmationKey);
dataStore.invalidate(dataDO);
} catch (IdentityException e) {
log.error("Invalid User for confirmation code", e);
return new VerificationBean(VerificationBean.ERROR_CODE_INVALID_USER);
}
if (dataDO == null) {
return new VerificationBean(VerificationBean.ERROR_CODE_INVALID_CODE);
}
if (!dataDO.isValid()) {
return new VerificationBean(VerificationBean.ERROR_CODE_EXPIRED_CODE);
} else {
// Verification successful.
return new VerificationBean(true);
}
}
/**
* This method is used to verify the confirmation code supplied by user. This invalidates
* the current code and generates a new code and send to user.
*
* @param sequence TODO
* @param username TODO
* @param code
* @param userDto
* @return
* @throws IdentityException
*/
public VerificationBean verifyConfirmationCode(int sequence, String username, String code) throws IdentityException {
UserRecoveryDataDO dataDO = null;
String internalCode = getUserInternalCodeStr(sequence, username, code);
try {
dataDO = dataStore.load(internalCode);
if (dataDO != null && sequence != 2 && sequence != 40) {
dataStore.invalidate(dataDO);
}
} catch (IdentityException e) {
throw IdentityException.error("Error loading recovery data for user : " + username, e);
}
if (dataDO == null && (sequence == 30 || sequence == 20)) {
return new VerificationBean(false);
}
if (dataDO == null) {
throw IdentityException.error("Invalid confirmation code");
}
if (!dataDO.isValid()) {
throw IdentityException.error("Expired code");
} else {
return new VerificationBean(true);
}
}
public VerificationBean updateConfirmationCode(int sequence, String username, int tenantId) throws IdentityException {
String confirmationKey = generateUserCode(sequence, username);
String secretKey = UUIDGenerator.generateUUID();
UserRecoveryDataDO recoveryDataDO = new UserRecoveryDataDO(username,
tenantId, confirmationKey, secretKey);
if (sequence != 3 && sequence != 30) {
dataStore.invalidate(username, tenantId);
}
dataStore.store(recoveryDataDO);
String externalCode = null;
try {
externalCode = getUserExternalCodeStr(confirmationKey);
} catch (Exception e) {
throw IdentityException.error("Error occurred while getting external code for user : "
+ username, e);
}
return new VerificationBean(username, externalCode);
}
/**
* Verifies user id with underline user store
*
* @param sequence TODO
* @param userDTO bean class that contains user and tenant Information
* @return true/false whether user is verified or not. If user is a tenant
* user then always return false
*/
public VerificationBean verifyUserForRecovery(int sequence, UserDTO userDTO) {
String userId = userDTO.getUserId();
int tenantId = userDTO.getTenantId();
boolean success = false;
VerificationBean bean = null;
try {
UserStoreManager userStoreManager = IdentityMgtServiceComponent.getRealmService().
getTenantUserRealm(tenantId).getUserStoreManager();
if (userStoreManager.isExistingUser(userId)) {
if (IdentityMgtConfig.getInstance().isAuthPolicyAccountLockCheck()) {
String accountLock = userStoreManager.
getUserClaimValue(userId, UserIdentityDataStore.ACCOUNT_LOCK, null);
if (!Boolean.parseBoolean(accountLock)) {
success = true;
}
} else {
success = true;
}
} else {
log.error("User with user name : " + userId
+ " does not exists in tenant domain : " + userDTO.getTenantDomain());
bean = new VerificationBean(VerificationBean.ERROR_CODE_INVALID_USER + " "
+ "User does not exists");
}
if (success) {
String internalCode = generateUserCode(sequence, userId);
String key = UUID.randomUUID().toString();
UserRecoveryDataDO dataDO =
new UserRecoveryDataDO(userId, tenantId, internalCode, key);
if (sequence != 3) {
dataStore.invalidate(userId, tenantId);
}
dataStore.store(dataDO);
log.info("User verification successful for user : " + userId +
" from tenant domain :" + userDTO.getTenantDomain());
bean = new VerificationBean(userId, getUserExternalCodeStr(internalCode));
}
} catch (Exception e) {
String errorMessage = "Error verifying user : " + userId;
log.error(errorMessage, e);
bean = new VerificationBean(VerificationBean.ERROR_CODE_UNEXPECTED + " "
+ errorMessage);
}
if (bean == null) {
bean = new VerificationBean(VerificationBean.ERROR_CODE_UNEXPECTED);
}
return bean;
}
public void createConfirmationCode(UserDTO userDTO, String code) throws IdentityException {
String key = UUID.randomUUID().toString();
UserRecoveryDataDO dataDO =
new UserRecoveryDataDO(userDTO.getUserId(), userDTO.getTenantId(), key, code);
dataStore.invalidate(userDTO.getUserId(), userDTO.getTenantId());
dataStore.store(dataDO);
}
public ChallengeQuestionProcessor getQuestionProcessor() {
return questionProcessor;
}
/*
* TODO - Important. Refactor this method and use recoveryWithNotification instead.
*/
public NotificationDataDTO notifyWithEmail(UserRecoveryDTO notificationBean) throws IdentityException {
String notificationAddress;
String confirmationKey = null;
NotificationSendingModule module = null;
String userId = notificationBean.getUserId();
String domainName = notificationBean.getTenantDomain();
int tenantId = notificationBean.getTenantId();
confirmationKey = notificationBean.getConfirmationCode();
String userStore = IdentityUtil.extractDomainFromName(userId);
String userName = UserCoreUtil.removeDomainFromName(userId);
NotificationDataDTO notificationData = new NotificationDataDTO();
if(MessageContext.getCurrentMessageContext() != null &&
MessageContext.getCurrentMessageContext().getProperty(
MessageContext.TRANSPORT_HEADERS) != null) {
notificationData.setTransportHeaders(new HashMap(
(Map)MessageContext.getCurrentMessageContext().getProperty(
MessageContext.TRANSPORT_HEADERS)));
}
String type = notificationBean.getNotificationType();
if (type != null) {
module = modules.get(type);
}
if (module == null) {
module = defaultModule;
}
NotificationData emailNotificationData = new NotificationData();
String emailTemplate = null;
notificationAddress = module.getNotificationAddress(userId, tenantId);
if ((notificationAddress == null) || (notificationAddress.trim().length() < 0)) {
log.warn("Notification address is not defined for user " + userId);
}
String firstName = Utils.getClaimFromUserStoreManager(userId, tenantId, "http://wso2.org/claims/givenname");
emailNotificationData.setTagData(FIRST_NAME, firstName);
emailNotificationData.setTagData(USER_STORE_DOMAIN, userStore);
emailNotificationData.setTagData(USER_NAME, userName);
emailNotificationData.setTagData(TENANT_DOMAIN, domainName);
emailNotificationData.setSendTo(notificationAddress);
Config config = null;
ConfigBuilder configBuilder = ConfigBuilder.getInstance();
try {
config = configBuilder.loadConfiguration(ConfigType.EMAIL,
StorageType.REGISTRY, tenantId);
} catch (Exception e1) {
throw IdentityException.error("Error occurred while loading email templates for user : "
+ userId, e1);
}
if (notificationBean.getNotification() != null) {
emailTemplate = config.getProperty(notificationBean.getNotification().trim());
String notification = notificationBean.getNotification().trim();
notificationData.setNotification(notification);
if (IdentityMgtConstants.Notification.PASSWORD_RESET_RECOVERY.equals(notification)) {
notificationData.setNotificationCode(confirmationKey);
emailNotificationData.setTagData(CONFIRMATION_CODE, confirmationKey);
emailTemplate = config.getProperty(IdentityMgtConstants.Notification.PASSWORD_RESET_RECOVERY);
} else if (IdentityMgtConstants.Notification.ACCOUNT_CONFORM.equals(notification)) {
notificationData.setNotificationCode(confirmationKey);
emailNotificationData.setTagData(CONFIRMATION_CODE, confirmationKey);
emailTemplate = config.getProperty(IdentityMgtConstants.Notification.ACCOUNT_CONFORM);
} else if (IdentityMgtConstants.Notification.TEMPORARY_PASSWORD.equals(notification)) {
String temporaryPassword = notificationBean.getTemporaryPassword(); // TODO
notificationData.setNotificationCode(temporaryPassword);
emailNotificationData.setTagData(TEMPORARY_PASSWORD, temporaryPassword);
emailTemplate = config.getProperty(IdentityMgtConstants.Notification.TEMPORARY_PASSWORD);
} else if (IdentityMgtConstants.Notification.ACCOUNT_UNLOCK.equals(notification)) {
emailTemplate = config.getProperty(IdentityMgtConstants.Notification.ACCOUNT_UNLOCK);
notificationData.setNotificationCode(userId);
} else if (IdentityMgtConstants.Notification.ACCOUNT_ID_RECOVERY.equals(notification)) {
emailTemplate = config.getProperty(IdentityMgtConstants.Notification.ACCOUNT_ID_RECOVERY);
notificationData.setNotificationCode(userId);
} else if (IdentityMgtConstants.Notification.ASK_PASSWORD.equals(notification)) {
notificationData.setNotificationCode(confirmationKey);
emailTemplate = config.getProperty(IdentityMgtConstants.Notification.ASK_PASSWORD);
emailNotificationData.setTagData(CONFIRMATION_CODE, confirmationKey);
}
}
Notification emailNotification = null;
try {
emailNotification = NotificationBuilder.createNotification("EMAIL", emailTemplate, emailNotificationData);
} catch (Exception e) {
throw IdentityException.error(
"Error occurred while creating notification from email template : "
+ emailTemplate, e);
}
notificationData.setNotificationAddress(notificationAddress);
notificationData.setUserId(userId);
notificationData.setDomainName(domainName);
notificationData.setNotificationType(notificationBean.getNotificationType());
if (IdentityMgtConfig.getInstance().isNotificationInternallyManaged()) {
module.setNotificationData(notificationData);
module.setNotification(emailNotification);
notificationSender.sendNotification(module);
notificationData.setNotificationSent(true);
} else {
notificationData.setNotificationSent(false);
notificationData.setNotificationCode(confirmationKey);
}
return notificationData;
}
/**
* Generates the code specific to user and operations sequence value.
*
* @param sequence
* @param username
* @return
*/
private String generateUserCode(int sequence, String username) {
String genCode = null;
if (username != null) {
StringBuilder userCode = new StringBuilder();
userCode.append(sequence);
userCode.append(REG_DELIMITER);
userCode.append(stripSpecialChars(username));
userCode.append(REG_DELIMITER);
userCode.append(UUID.randomUUID().toString());
genCode = userCode.toString();
}
return genCode;
}
/**
* Creates the user specific code by the given sequence, username and code to be search in
* the datastore.
*
* @param sequence
* @param username
* @param code - user provided code
* @return
*/
private String getUserInternalCodeStr(int sequence, String username, String code) {
String searchCode = null;
if (username != null && code != null) {
StringBuilder userCode = new StringBuilder();
userCode.append(sequence);
userCode.append(REG_DELIMITER);
userCode.append(stripSpecialChars(username));
userCode.append(REG_DELIMITER);
userCode.append(code);
searchCode = userCode.toString();
}
return searchCode;
}
/**
* @param internalCode - code with the format "sequence_username_usercode".
* @return
*/
private String getUserExternalCodeStr(String internalCode) throws IdentityMgtServiceException {
String userCode = null;
if (internalCode != null) {
String[] codeParts = internalCode.split("_{3}", 3);
// Must have 3 elements and 3rd one must have code.
if (codeParts.length == 3) {
userCode = codeParts[2];
} else {
throw new IdentityMgtServiceException("Invalid code");
}
} else {
throw new IdentityMgtServiceException("Code not found");
}
return userCode;
}
/**
* This removes the special chars which the registry resource does not accepts.
*
* @param input
* @return
*/
private String stripSpecialChars(String input) {
StringBuilder output = new StringBuilder();
if (input != null) {
char[] inputArr = input.toCharArray();
for (char c : inputArr) {
switch (c) {
case '~':
case '!':
case '@':
case '#':
case ';':
case '%':
case '^':
case '*':
case '(':
case ')':
case '+':
case '=':
case '{':
case '}':
case '|':
case '\\':
case '<':
case '>':
case '"':
case '\'':
case ',':
case '$':
output.append('z');
break;
default:
output.append(c);
}
}
}
return output.toString();
}
}