Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions components/org.wso2.carbon.identity.recovery/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
<groupId>org.wso2.carbon.identity.framework</groupId>
<artifactId>org.wso2.carbon.identity.configuration.mgt.core</artifactId>
</dependency>
<dependency>
<groupId>org.wso2.carbon.identity.framework</groupId>
<artifactId>org.wso2.carbon.identity.compatibility.settings.core</artifactId>
</dependency>
<dependency>
<groupId>org.json.wso2</groupId>
<artifactId>json</artifactId>
Expand Down Expand Up @@ -197,6 +201,8 @@
version="${carbon.identity.framework.imp.pkg.version.range}",
org.wso2.carbon.identity.configuration.mgt.core.*;
version="${carbon.identity.framework.imp.pkg.version.range}",
org.wso2.carbon.identity.compatibility.settings.core.*;
version="${carbon.identity.framework.imp.pkg.version.range}",
org.wso2.carbon.identity.base; version="${carbon.identity.framework.imp.pkg.version.range}",
org.wso2.carbon.identity.central.log.mgt.utils;
version="${carbon.identity.framework.imp.pkg.version.range}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@ public class IdentityRecoveryConstants {
public static final int RECOVERY_CONFIRMATION_CODE_DEFAULT_TOLERANCE = 0;
public static final int ASK_PASSWORD_CODE_DEFAULT_TOLERANCE = 0;
public static final int SELF_SIGN_UP_CODE_DEFAULT_TOLERANCE = 0;

// Compatibility setting that keeps admin-initiated email verification codes on the legacy
// SELF_SIGN_UP recovery scenario. See UserEmailVerificationHandler.
public static final String USER_ONBOARDING_COMPATIBILITY_SETTING_GROUP = "userOnboarding";
public static final String ENABLE_LEGACY_EMAIL_VERIFICATION_SCENARIO =
"enableLegacyEmailVerificationScenario";
public static final String EMAIL_TEMPLATE_PATH = "/identity/email";

// Workflow constants.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,17 @@ public void handleEvent(Event event) throws IdentityEventException {
IdentityRecoveryConstants.PENDING_EMAIL_VERIFICATION, userStoreManager, user);
}
String notificationType = IdentityRecoveryConstants.NOTIFICATION_TYPE_EMAIL_CONFIRM;
RecoveryScenarios recoveryScenario = RecoveryScenarios.SELF_SIGN_UP;
RecoverySteps recoveryStep = RecoverySteps.CONFIRM_SIGN_UP;
/*
An administratively created user pending email verification is not a self sign-up, but the code
used to be stored as one, which made it expire on the self registration dial. Organizations that
have not moved off that behaviour keep it via the compatibility setting.
*/
boolean isLegacyScenario =
Utils.isLegacyEmailVerificationScenarioEnabled(user.getTenantDomain());
RecoveryScenarios recoveryScenario = isLegacyScenario ? RecoveryScenarios.SELF_SIGN_UP
: RecoveryScenarios.EMAIL_VERIFICATION;
RecoverySteps recoveryStep = isLegacyScenario ? RecoverySteps.CONFIRM_SIGN_UP
: RecoverySteps.CONFIRM_PENDING_EMAIL_VERIFICATION;
try {
boolean isSendEmailOTPEnabled =
Boolean.parseBoolean(getRecoveryConfigs(EMAIL_VERIFICATION_SEND_OTP,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.wso2.carbon.identity.application.mgt.ApplicationManagementService;
import org.wso2.carbon.identity.auth.attribute.handler.AuthAttributeHandlerManager;
import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService;
import org.wso2.carbon.identity.compatibility.settings.core.service.CompatibilitySettingsService;
import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager;
import org.wso2.carbon.identity.consent.mgt.services.ConsentUtilityService;
import org.wso2.carbon.identity.core.persistence.registry.RegistryResourceMgtService;
Expand Down Expand Up @@ -539,4 +540,20 @@ protected void unsetWorkflowService(WorkflowManagementService workflowManagement

IdentityRecoveryServiceDataHolder.getInstance().setWorkflowManagementService(null);
}

@Reference(
name = "compatibility.settings.service",
service = CompatibilitySettingsService.class,
cardinality = ReferenceCardinality.OPTIONAL,
policy = ReferencePolicy.DYNAMIC,
unbind = "unsetCompatibilitySettingsService")
protected void setCompatibilitySettingsService(CompatibilitySettingsService compatibilitySettingsService) {

IdentityRecoveryServiceDataHolder.getInstance().setCompatibilitySettingsService(compatibilitySettingsService);
}

protected void unsetCompatibilitySettingsService(CompatibilitySettingsService compatibilitySettingsService) {

IdentityRecoveryServiceDataHolder.getInstance().setCompatibilitySettingsService(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.wso2.carbon.identity.application.mgt.ApplicationManagementService;
import org.wso2.carbon.identity.auth.attribute.handler.AuthAttributeHandlerManager;
import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService;
import org.wso2.carbon.identity.compatibility.settings.core.service.CompatibilitySettingsService;
import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager;
import org.wso2.carbon.identity.consent.mgt.services.ConsentUtilityService;
import org.wso2.carbon.identity.core.persistence.registry.RegistryResourceMgtService;
Expand Down Expand Up @@ -65,6 +66,7 @@ public class IdentityRecoveryServiceDataHolder {
private ApplicationManagementService applicationManagementService;
private static Map<Integer, UserOperationEventListener> userOperationEventListeners = new TreeMap<>();
private WorkflowManagementService workflowService;
private CompatibilitySettingsService compatibilitySettingsService;

public static IdentityRecoveryServiceDataHolder getInstance() {

Expand Down Expand Up @@ -367,4 +369,24 @@ public WorkflowManagementService getWorkflowManagementService() {

return this.workflowService;
}

/**
* Set CompatibilitySettingsService OSGi service.
*
* @param compatibilitySettingsService Compatibility Settings Service.
*/
public void setCompatibilitySettingsService(CompatibilitySettingsService compatibilitySettingsService) {

this.compatibilitySettingsService = compatibilitySettingsService;
}

/**
* Get CompatibilitySettingsService OSGi service.
*
* @return Compatibility Settings Service.
*/
public CompatibilitySettingsService getCompatibilitySettingsService() {

return this.compatibilitySettingsService;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,10 @@ private boolean isCodeExpired(String tenantDomain, Enum recoveryScenario, Enum r
notificationExpiryTimeInMinutes = Integer.parseInt(
Utils.getRecoveryConfigs(IdentityRecoveryConstants.ConnectorConfig.EXPIRY_TIME, tenantDomain));
}
} else if (RecoveryScenarios.EMAIL_VERIFICATION.equals(recoveryScenario) ||
RecoveryScenarios.EMAIL_VERIFICATION_OTP.equals(recoveryScenario)) {
notificationExpiryTimeInMinutes = Integer.parseInt(Utils.getRecoveryConfigs(IdentityRecoveryConstants
.ConnectorConfig.EMAIL_VERIFICATION_EXPIRY_TIME, tenantDomain));
} else if (RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE.equals(recoveryScenario) ||
RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE.equals(recoveryScenario)) {
notificationExpiryTimeInMinutes = Integer.parseInt(Utils.getRecoveryConfigs(IdentityRecoveryConstants
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException;
import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim;
import org.wso2.carbon.identity.claim.metadata.mgt.util.ClaimConstants;
import org.wso2.carbon.identity.compatibility.settings.core.exception.CompatibilitySettingException;
import org.wso2.carbon.identity.compatibility.settings.core.model.CompatibilitySetting;
import org.wso2.carbon.identity.compatibility.settings.core.model.CompatibilitySettingGroup;
import org.wso2.carbon.identity.compatibility.settings.core.service.CompatibilitySettingsService;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.identity.event.IdentityEventConstants;
Expand Down Expand Up @@ -1759,6 +1763,48 @@ private static int getSelfRegistrationCodeToleranceInMinutes(String tenantDomain
}
}

/**
* Check whether admin-initiated email verification codes should keep using the legacy
* {@link org.wso2.carbon.identity.recovery.RecoveryScenarios#SELF_SIGN_UP} recovery scenario.
* <p>
* Historically a user created with the {@code verifyEmail} claim was recorded as a self sign-up, so its
* confirmation code expired on the self registration dial. The behaviour is retained for organizations that
* have not moved off it, and is governed by the {@code userOnboarding.enableLegacyEmailVerificationScenario}
* compatibility setting. Any failure to resolve the setting keeps the legacy behaviour, so a missing or
* unreachable service never changes how existing codes are issued or validated.
*
* @param tenantDomain Tenant domain.
* @return {@code true} if the legacy scenario should be used.
*/
public static boolean isLegacyEmailVerificationScenarioEnabled(String tenantDomain) {
Comment thread
sadilchamishka marked this conversation as resolved.

CompatibilitySettingsService compatibilitySettingsService =
IdentityRecoveryServiceDataHolder.getInstance().getCompatibilitySettingsService();
if (compatibilitySettingsService == null) {
log.debug("Compatibility settings service is not available. Using the legacy email verification " +
"recovery scenario.");
return true;
}
try {
CompatibilitySetting setting = compatibilitySettingsService.getCompatibilitySettingsByGroupAndSetting(
tenantDomain, IdentityRecoveryConstants.USER_ONBOARDING_COMPATIBILITY_SETTING_GROUP,
IdentityRecoveryConstants.ENABLE_LEGACY_EMAIL_VERIFICATION_SCENARIO);
CompatibilitySettingGroup group = setting == null ? null : setting.getCompatibilitySetting(
IdentityRecoveryConstants.USER_ONBOARDING_COMPATIBILITY_SETTING_GROUP);
String value = group == null ? null : group.getSettingValue(
IdentityRecoveryConstants.ENABLE_LEGACY_EMAIL_VERIFICATION_SCENARIO);
if (StringUtils.isBlank(value)) {
return true;
}
return Boolean.parseBoolean(value);
} catch (CompatibilitySettingException e) {
log.error("Error while reading the compatibility setting: " +
IdentityRecoveryConstants.ENABLE_LEGACY_EMAIL_VERIFICATION_SCENARIO + " for tenant: " +
tenantDomain + ". Using the legacy email verification recovery scenario.", e);
return true;
}
}

/**
* Retrieves the ask password confirmation code tolerance period in minutes.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,61 @@ public void testHandleEventPostAddUserVerifyEmailClaim() throws IdentityEventExc
any()));
}

@DataProvider(name = "emailVerificationScenarioData")
public Object[][] emailVerificationScenarioData() {

// Legacy compatibility setting, send-OTP enabled, expected scenario, expected step.
return new Object[][]{
{true, false, RecoveryScenarios.SELF_SIGN_UP, RecoverySteps.CONFIRM_SIGN_UP},
{false, false, RecoveryScenarios.EMAIL_VERIFICATION,
RecoverySteps.CONFIRM_PENDING_EMAIL_VERIFICATION},
{true, true, RecoveryScenarios.EMAIL_VERIFICATION_OTP,
RecoverySteps.CONFIRM_PENDING_EMAIL_VERIFICATION},
{false, true, RecoveryScenarios.EMAIL_VERIFICATION_OTP,
RecoverySteps.CONFIRM_PENDING_EMAIL_VERIFICATION}
};
}

/**
* An administratively created user pending email verification is not a self sign-up. The compatibility
* setting decides whether the code is still recorded as one. The OTP mode overrides both regardless,
* which is why the setting is varied across both of its values here.
*/
@Test(dataProvider = "emailVerificationScenarioData")
public void testHandleEventPostAddUserVerifyEmailClaimRecoveryScenario(boolean isLegacyScenario,
boolean isSendOTPEnabled,
RecoveryScenarios expectedScenario,
RecoverySteps expectedStep)
throws IdentityEventException, IdentityRecoveryException {

mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_VERIFICATION, true);
mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig.EMAIL_ACCOUNT_LOCK_ON_CREATION, true);
mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig
.EMAIL_VERIFICATION_NOTIFICATION_INTERNALLY_MANAGE, true);
mockedUtils.when(() -> Utils.getRecoveryConfigs(
IdentityRecoveryConstants.ConnectorConfig.EMAIL_VERIFICATION_SEND_OTP, TEST_TENANT_DOMAIN))
.thenReturn(String.valueOf(isSendOTPEnabled));
mockedUtils.when(() -> Utils.isLegacyEmailVerificationScenarioEnabled(TEST_TENANT_DOMAIN))
.thenReturn(isLegacyScenario);
mockedUtils.when(() -> Utils.isAccountStateClaimExisting(anyString())).thenReturn(true);

Claim claim = new Claim();
claim.setClaimUri(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM);
claim.setValue(Boolean.TRUE.toString());
mockedUtils.when(Utils::getEmailVerifyTemporaryClaim).thenReturn(claim);

Event event = createEvent(IdentityEventConstants.Event.POST_ADD_USER, IdentityRecoveryConstants.TRUE,
null, null, null);
userEmailVerificationHandler.handleEvent(event);

ArgumentCaptor<UserRecoveryData> recoveryDataCaptor = ArgumentCaptor.forClass(UserRecoveryData.class);
verify(userRecoveryDataStore).store(recoveryDataCaptor.capture());
UserRecoveryData capturedRecoveryData = recoveryDataCaptor.getValue();

Assert.assertEquals(capturedRecoveryData.getRecoveryScenario(), expectedScenario);
Assert.assertEquals(capturedRecoveryData.getRecoveryStep(), expectedStep);
}

@Test
public void testGetPriority() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,47 @@ public void testLoadExpiredCode() throws Exception {
}
}

@DataProvider(name = "emailVerificationCodeExpiry")
private Object[][] emailVerificationCodeExpiry() {

// Scenario, legacy compatibility setting. The expiry is a property of the scenario alone, so the
// compatibility setting -- which only decides which scenario gets issued -- must not affect it.
return new Object[][] {
{ RecoveryScenarios.EMAIL_VERIFICATION, false },
{ RecoveryScenarios.EMAIL_VERIFICATION, true },
{ RecoveryScenarios.EMAIL_VERIFICATION_OTP, false },
{ RecoveryScenarios.EMAIL_VERIFICATION_OTP, true }
};
}

/**
* Codes issued for an administratively created user pending email verification must expire on
* EmailVerification.ExpiryTime (stubbed at 20 minutes), not on the generic Recovery.ExpiryTime
* (stubbed at 10 minutes). The code under test is 11 minutes old, so the two dials disagree.
*/
@Test(dataProvider = "emailVerificationCodeExpiry")
public void testEmailVerificationCodeExpiry(RecoveryScenarios recoveryScenario, boolean isLegacyScenario)
throws Exception {

User user = createSampleUser();

when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet);
when(mockResultSet.next()).thenReturn(true);
when(mockResultSet.getString("REMAINING_SETS")).thenReturn(null);
when(mockResultSet.getTimestamp(eq("TIME_CREATED"), any(Calendar.class)))
.thenReturn(new Timestamp(System.currentTimeMillis() - 660000));

mockExpiryTimes();
mockUtilsErrors();
mockedUtils.when(() -> Utils.isLegacyEmailVerificationScenarioEnabled(TEST_TENANT_DOMAIN))
.thenReturn(isLegacyScenario);

UserRecoveryData result = userRecoveryDataStore.load(user, recoveryScenario,
RecoverySteps.CONFIRM_PENDING_EMAIL_VERIFICATION, TEST_SECRET_CODE);
assertNotNull(result);
assertEquals(result.getRecoveryScenario(), recoveryScenario);
}

@DataProvider(name = "askPasswordUserOnboardScenarios")
private Object[][] askPasswordUserOnboardScenarios() {

Expand Down Expand Up @@ -300,6 +341,9 @@ private void mockExpiryTimes() {
mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants
.ConnectorConfig.EXPIRY_TIME, TEST_TENANT_DOMAIN))
.thenReturn("10");
mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants
.ConnectorConfig.EMAIL_VERIFICATION_EXPIRY_TIME, TEST_TENANT_DOMAIN))
.thenReturn("20");
mockedIdentityUtil.when(() -> IdentityUtil.getProperty(IdentityRecoveryConstants
.ConnectorConfig.TENANT_ADMIN_ASK_PASSWORD_EXPIRY_TIME))
.thenReturn("10");
Expand Down
Loading
Loading