Skip to content

Commit

Permalink
Add feature to send OTPs in e-mail verifications.
Browse files Browse the repository at this point in the history
  • Loading branch information
DilshaniSH authored and shanggeeth committed Oct 18, 2023
1 parent 8e2b3a4 commit 40c6db4
Show file tree
Hide file tree
Showing 22 changed files with 885 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,18 @@ private AccountStates() {

}
}

/**
* OTP generator constants.
*/
public static class OTPGeneratorConstants {

public static final int OTP_CODE_DEFAULT_LENGTH = 6;
public static final int OTP_CODE_MIN_LENGTH = 4;
public static final int OTP_CODE_MAX_LENGTH = 10;
public static final String OTP_GENERATE_ALPHABET_CHAR_SET_UPPERCASE = "ABCDEFGHJKLMNPRSTUVWXYZ";
public static final String OTP_GENERATE_ALPHABET_CHAR_SET_LOWERCASE = "abcdefghjkmnpqrstuvwxyz";
public static final String OTP_GENERATE_NUMERIC_CHAR_SET_WITH_ZERO = "0123456789";
public static final String OTP_GENERATE_NUMERIC_CHAR_SET_WITHOUT_ZERO = "123456789";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.wso2.carbon.identity.governance.common.IdentityConnectorConfig;
import org.wso2.carbon.identity.governance.internal.service.impl.notification.DefaultNotificationChannelManager;
import org.wso2.carbon.identity.governance.internal.service.impl.otp.DefaultOTPGenerator;
import org.wso2.carbon.identity.governance.internal.service.impl.otp.OTPGeneratorImpl;
import org.wso2.carbon.identity.governance.service.IdentityDataStoreService;
import org.wso2.carbon.identity.governance.service.IdentityDataStoreServiceImpl;
import org.wso2.carbon.identity.governance.service.notification.NotificationChannelManager;
Expand Down Expand Up @@ -71,9 +72,9 @@ protected void activate(ComponentContext context) {
new DefaultNotificationChannelManager();
context.getBundleContext()
.registerService(NotificationChannelManager.class.getName(), defaultNotificationChannelManager, null);
DefaultOTPGenerator defaultOtpGenerator = new DefaultOTPGenerator();
OTPGeneratorImpl otpGeneratorImpl = new OTPGeneratorImpl();
context.getBundleContext()
.registerService(OTPGenerator.class.getName(), defaultOtpGenerator, null);
.registerService(OTPGenerator.class.getName(), otpGeneratorImpl, null);

if (log.isDebugEnabled()) {
log.debug("Identity Management Listener is enabled");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@
import java.security.SecureRandom;

/**
* Default class to generate OTP.
* @deprecated
* {@link org.wso2.carbon.identity.governance.internal.service.impl.otp.OTPGeneratorImpl
* This class is deprecated and OTPGeneratorImpl has been introduced as alternative.
*/
@Deprecated
public class DefaultOTPGenerator implements OTPGenerator {

private static final String SMS_OTP_GENERATE_ALPHABET_CHAR_SET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com).
*
* WSO2 LLC. 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.governance.internal.service.impl.otp;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.identity.governance.IdentityMgtConstants;
import org.wso2.carbon.identity.governance.service.otp.OTPGenerator;

import java.security.SecureRandom;

/**
* Default class to generate OTP.
*/
public class OTPGeneratorImpl implements OTPGenerator {

private static final Log log = LogFactory.getLog(OTPGeneratorImpl.class);
private static final String OTP_GENERATOR_IMPL = "OTPGeneratorImpl";

/**
* Generate the OTP according to given length and pattern. If pattern is not defined default pattern will be used.
* Default pattern is to use all three type characters (uppercase characters, lowercase characters and numerics).
*
* @param useUppercaseLetters Whether uppercase characters should be used for OTP.
* @param useLowercaseLetters Whether lowercase characters should be used for OTP.
* @param useNumeric Whether numeric characters should be used for OTP.
* @param otpLength OTP length.
* @param recoveryScenario Recovery scenario.
* @return Secret key.
*/
@Override
public String generateOTP(boolean useNumeric, boolean useUppercaseLetters, boolean useLowercaseLetters,
int otpLength, String recoveryScenario) {

if (otpLength < IdentityMgtConstants.OTPGeneratorConstants.OTP_CODE_MIN_LENGTH ||
otpLength > IdentityMgtConstants.OTPGeneratorConstants.OTP_CODE_MAX_LENGTH) {
otpLength = IdentityMgtConstants.OTPGeneratorConstants.OTP_CODE_DEFAULT_LENGTH;
if (log.isDebugEnabled()) {
log.debug("Configured OTP length is not in the range of " +
IdentityMgtConstants.OTPGeneratorConstants.OTP_CODE_MIN_LENGTH + "-" +
IdentityMgtConstants.OTPGeneratorConstants.OTP_CODE_MAX_LENGTH + ". Hence using default length for OTP");
}
}
StringBuilder charSet = new StringBuilder();
if (!useNumeric && !useUppercaseLetters && !useLowercaseLetters) {
charSet.append(IdentityMgtConstants.OTPGeneratorConstants.OTP_GENERATE_ALPHABET_CHAR_SET_UPPERCASE);
charSet.append(IdentityMgtConstants.OTPGeneratorConstants.OTP_GENERATE_ALPHABET_CHAR_SET_LOWERCASE);
charSet.append(IdentityMgtConstants.OTPGeneratorConstants.OTP_GENERATE_NUMERIC_CHAR_SET_WITHOUT_ZERO);
return generateOTP(charSet.toString(), otpLength, recoveryScenario);
}
if (useUppercaseLetters) {
charSet.append(IdentityMgtConstants.OTPGeneratorConstants.OTP_GENERATE_ALPHABET_CHAR_SET_UPPERCASE);
}
if (useLowercaseLetters) {
charSet.append(IdentityMgtConstants.OTPGeneratorConstants.OTP_GENERATE_ALPHABET_CHAR_SET_LOWERCASE);
}
if (useNumeric) {
if (useUppercaseLetters || useLowercaseLetters) {
charSet.append(IdentityMgtConstants.OTPGeneratorConstants.OTP_GENERATE_NUMERIC_CHAR_SET_WITHOUT_ZERO);
} else {
charSet.append(IdentityMgtConstants.OTPGeneratorConstants.OTP_GENERATE_NUMERIC_CHAR_SET_WITH_ZERO);
}

}
return generateOTP(charSet.toString(), otpLength, recoveryScenario);
}

/**
* Generates the OTP based on the provided charSet and length.
*
* @param charSet Character set allowed for OTP.
* @param otpLength Length of OTP.
* @param recoveryScenario Recovery Scenario.
* @return Value of OTP string.
*/
@Override
public String generateOTP(String charSet, int otpLength, String recoveryScenario) {

SecureRandom secureRandom = new SecureRandom();
StringBuilder stringBuilder = new StringBuilder();
char[] otpCharacters = charSet.toCharArray();
for (int otpCharacterIndex = 0; otpCharacterIndex < otpLength; otpCharacterIndex++) {
stringBuilder.append(otpCharacters[secureRandom.nextInt(otpCharacters.length)]);
}
return stringBuilder.toString();
}

/**
* Retrieve the OTP Generator name.
*/
@Override
public String getOTPGeneratorName() {

return OTP_GENERATOR_IMPL;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,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 OTP_CODE_DEFAULT_LENGTH = 6;
public static final String ENABLE_DETAILED_ERROR_RESPONSE = "Recovery.ErrorMessage.EnableDetailedErrorMessages";
public static final int RECOVERY_FLOW_ID_DEFAULT_EXPIRY_TIME = 15;
// Recovery code given at the username and password recovery initiation.
Expand Down Expand Up @@ -543,6 +544,14 @@ public static class ConnectorConfig {
public static final String RECOVERY_CODE_EXPIRY_TIME = "Recovery.Notification.ExpiryTime.RecoveryCode";
public static final String ENABLE_ACCOUNT_LOCK_FOR_VERIFIED_PREFERRED_CHANNEL =
"SelfRegistration.EnableAccountLockForVerifiedPreferredChannel";
public static final String PASSWORD_RECOVERY_SEND_OTP_IN_EMAIL = "Recovery.Notification.Password.OTP.SendOTPInEmail";
public static final String PASSWORD_RECOVERY_USE_UPPERCASE_CHARACTERS_IN_OTP = "Recovery.Notification.Password." +
"OTP.UseUppercaseCharactersInOTP";
public static final String PASSWORD_RECOVERY_USE_LOWERCASE_CHARACTERS_IN_OTP = "Recovery.Notification.Password." +
"OTP.UseLowercaseCharactersInOTP";
public static final String PASSWORD_RECOVERY_USE_NUMBERS_IN_OTP = "Recovery.Notification.Password.OTP." +
"UseNumbersInOTP";
public static final String PASSWORD_RECOVERY_OTP_LENGTH = "Recovery.Notification.Password.OTP.OTPLength";
public static final String NOTIFICATION_INTERNALLY_MANAGE = "Recovery.Notification.InternallyManage";
public static final String NOTIFY_USER_EXISTENCE = "Recovery.NotifyUserExistence";
public static final String NOTIFY_RECOVERY_EMAIL_EXISTENCE = "Recovery.NotifyRecoveryEmailExistence";
Expand All @@ -564,6 +573,14 @@ public static class ConnectorConfig {
public static final String RECOVERY_CALLBACK_REGEX = "Recovery.CallbackRegex";
public static final String ENABLE_SELF_SIGNUP = "SelfRegistration.Enable";
public static final String ACCOUNT_LOCK_ON_CREATION = "SelfRegistration.LockOnCreation";
public static final String SELF_REGISTRATION_SEND_OTP_IN_EMAIL = "SelfRegistration.OTP.SendOTPInEmail";
public static final String SELF_REGISTRATION_USE_UPPERCASE_CHARACTERS_IN_OTP = "SelfRegistration.OTP." +
"UseUppercaseCharactersInOTP";
public static final String SELF_REGISTRATION_USE_LOWERCASE_CHARACTERS_IN_OTP = "SelfRegistration.OTP." +
"UseLowercaseCharactersInOTP";
public static final String SELF_REGISTRATION_USE_NUMBERS_IN_OTP = "SelfRegistration.OTP." +
"UseNumbersInOTP";
public static final String SELF_REGISTRATION_OTP_LENGTH = "SelfRegistration.OTP.OTPLength";
public static final String SEND_CONFIRMATION_NOTIFICATION = "SelfRegistration.SendConfirmationOnCreation";
public static final String SIGN_UP_NOTIFICATION_INTERNALLY_MANAGE = "SelfRegistration.Notification" +
".InternallyManage";
Expand All @@ -581,6 +598,14 @@ public static class ConnectorConfig {

public static final String ENABLE_LITE_SIGN_UP = "LiteRegistration.Enable";
public static final String LITE_ACCOUNT_LOCK_ON_CREATION = "LiteRegistration.LockOnCreation"; //if passwordless
public static final String LITE_REGISTRATION_SEND_OTP_IN_EMAIL = "LiteRegistration.OTP.SendOTPInEmail";
public static final String LITE_REGISTRATION_USE_UPPERCASE_CHARACTERS_IN_OTP = "LiteRegistration.OTP." +
"UseUppercaseCharactersInOTP";
public static final String LITE_REGISTRATION_USE_LOWERCASE_CHARACTERS_IN_OTP = "LiteRegistration.OTP." +
"UseLowercaseCharactersInOTP";
public static final String LITE_REGISTRATION_USE_NUMBERS_IN_OTP = "LiteRegistration.OTP." +
"UseNumbersInOTP";
public static final String LITE_REGISTRATION_OTP_LENGTH = "LiteRegistration.OTP.OTPLength";
public static final String LITE_SIGN_UP_NOTIFICATION_INTERNALLY_MANAGE = "LiteRegistration.Notification" +
".InternallyManage";
public static final String LITE_REGISTRATION_RE_CAPTCHA = "LiteRegistration.ReCaptcha";
Expand All @@ -594,9 +619,25 @@ public static class ConnectorConfig {
public static final String LITE_REGISTRATION_RESEND_VERIFICATION_ON_USER_EXISTENCE =
"LiteRegistration.ResendVerificationOnUserExistence";
public static final String ENABLE_EMAIL_VERIFICATION = "EmailVerification.Enable";
public static final String EMAIL_VERIFICATION_SEND_OTP_IN_EMAIL = "EmailVerification.OTP.SendOTPInEmail";
public static final String EMAIL_VERIFICATION_USE_UPPERCASE_CHARACTERS_IN_OTP = "EmailVerification.OTP." +
"UseUppercaseCharactersInOTP";
public static final String EMAIL_VERIFICATION_USE_LOWERCASE_CHARACTERS_IN_OTP = "EmailVerification.OTP." +
"UseLowercaseCharactersInOTP";
public static final String EMAIL_VERIFICATION_USE_NUMBERS_IN_OTP = "EmailVerification.OTP." +
"UseNumbersInOTP";
public static final String EMAIL_VERIFICATION_OTP_LENGTH = "EmailVerification.OTP.OTPLength";
public static final String EMAIL_VERIFICATION_EXPIRY_TIME = "EmailVerification.ExpiryTime";
public static final String ENABLE_EMAIL_VERIFICATION_ON_UPDATE = "UserClaimUpdate.Email." +
"EnableVerification";
public static final String EMAIL_VERIFICATION_ON_UPDATE_SEND_OTP_IN_EMAIL = "UserClaimUpdate.OTP.SendOTPInEmail";
public static final String EMAIL_VERIFICATION_ON_UPDATE_USE_UPPERCASE_CHARACTERS_IN_OTP = "UserClaimUpdate." +
"OTP.UseUppercaseCharactersInOTP";
public static final String EMAIL_VERIFICATION_ON_UPDATE_USE_LOWERCASE_CHARACTERS_IN_OTP = "UserClaimUpdate." +
"OTP.UseLowercaseCharactersInOTP";
public static final String EMAIL_VERIFICATION_ON_UPDATE_USE_NUMBERS_IN_OTP = "UserClaimUpdate." +
"OTP.UseNumbersInOTP";
public static final String EMAIL_VERIFICATION_ON_UPDATE_OTP_LENGTH = "UserClaimUpdate.OTP.OTPLength";
public static final String EMAIL_VERIFICATION_ON_UPDATE_EXPIRY_TIME = "UserClaimUpdate.Email.VerificationCode" +
".ExpiryTime";
public static final String ENABLE_NOTIFICATION_ON_EMAIL_UPDATE = "UserClaimUpdate.Email.EnableNotification";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ public ResendConfirmationDTO resendConfirmation(String tenantDomain, String rese
confirmationCode = confirmationCodeRecoveryData.getSecret();
} else {
userRecoveryDataStore.invalidate(user);
confirmationCode = Utils.generateSecretKey(notificationChannel, user.getTenantDomain(), recoveryScenario);
confirmationCode = getSecretKey(notificationChannel, recoveryScenario, user.getTenantDomain());
confirmationCode = Utils.concatRecoveryFlowIdWithSecretKey(recoveryFlowId, notificationChannel,
confirmationCode);
try {
Expand Down Expand Up @@ -477,6 +477,9 @@ private NotificationResponseBean resendAccountRecoveryNotification(User user, St
preferredChannel = NotificationChannels.EXTERNAL_CHANNEL.getChannelType();
}
}
if (RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario)) {
preferredChannel = NotificationChannels.EMAIL_CHANNEL.getChannelType();
}
if (RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario)) {
preferredChannel = NotificationChannels.SMS_CHANNEL.getChannelType();
}
Expand All @@ -487,7 +490,7 @@ private NotificationResponseBean resendAccountRecoveryNotification(User user, St
} else {
// Invalid previous confirmation code.
userRecoveryDataStore.invalidate(userRecoveryData.getSecret());
secretKey = Utils.generateSecretKey(preferredChannel, user.getTenantDomain(), recoveryScenario);
secretKey = getSecretKey(preferredChannel, recoveryScenario, user.getTenantDomain());
UserRecoveryData recoveryDataDO = new UserRecoveryData(user, secretKey, RecoveryScenarios
.getRecoveryScenario(recoveryScenario), RecoverySteps.getRecoveryStep(recoveryStep));
/* Notified channel is stored in remaining setIds for recovery purposes. Having a EMPTY preferred channel
Expand Down Expand Up @@ -692,4 +695,36 @@ private void validateWithOldConfirmationCode(String code, String recoveryScenari
IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_PROVIDED_CONFIRMATION_CODE_NOT_VALID, code);
}
}

private String getSecretKey(String preferredChannel, String recoveryScenario, String tenantDomain)
throws IdentityRecoveryException {

try {
switch (RecoveryScenarios.getRecoveryScenario(recoveryScenario)) {
case SELF_SIGN_UP:
return Utils.generateSecretKey(preferredChannel, recoveryScenario, tenantDomain,
"SelfRegistration");
case NOTIFICATION_BASED_PW_RECOVERY:
return Utils.generateSecretKey(preferredChannel, recoveryScenario, tenantDomain,
"Recovery.Notification.Password");
case EMAIL_VERIFICATION_ON_UPDATE:
case MOBILE_VERIFICATION_ON_UPDATE:
return Utils.generateSecretKey(preferredChannel, recoveryScenario, tenantDomain,
"UserClaimUpdate");
case ASK_PASSWORD:
return Utils.generateSecretKey(preferredChannel, recoveryScenario, tenantDomain,
"EmailVerification");
case LITE_SIGN_UP:
return Utils.generateSecretKey(preferredChannel, recoveryScenario, tenantDomain,
"LiteRegistration");
default:
return Utils.generateSecretKey(preferredChannel, recoveryScenario, tenantDomain,
null);
}
} catch (IdentityRecoveryClientException identityRecoveryClientException) {
throw new IdentityRecoveryException(identityRecoveryClientException.getErrorCode(),
identityRecoveryClientException.getMessage());
}

}
}
Loading

0 comments on commit 40c6db4

Please sign in to comment.