diff --git a/components/org.wso2.carbon.identity.password.expiry/pom.xml b/components/org.wso2.carbon.identity.password.expiry/pom.xml
index 9d89cfc225..5b09f408fe 100644
--- a/components/org.wso2.carbon.identity.password.expiry/pom.xml
+++ b/components/org.wso2.carbon.identity.password.expiry/pom.xml
@@ -89,6 +89,11 @@
mockito-inline
test
+
+ org.wso2.carbon.identity.framework
+ org.wso2.carbon.identity.testutil
+ test
+
org.wso2.carbon.identity.organization.management.core
org.wso2.carbon.identity.organization.management.service
@@ -149,6 +154,9 @@
org.wso2.carbon.user.core; version="${carbon.kernel.package.import.version.range}",
org.wso2.carbon.user.core.util; version="${carbon.kernel.package.import.version.range}",
org.wso2.carbon.user.core.common; version="${carbon.kernel.package.import.version.range}",
+ org.wso2.carbon.user.core.listener; version="${carbon.kernel.package.import.version.range}",
+ org.wso2.carbon.user.core.model; version="${carbon.kernel.package.import.version.range}",
+ org.wso2.carbon.context; version="${carbon.kernel.package.import.version.range}",
org.wso2.carbon.user.api.*; version="${carbon.user.api.imp.pkg.version.range}",
org.wso2.carbon.identity.application.common.model.*;
version="${carbon.identity.framework.imp.pkg.version.range}",
diff --git a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/constants/PasswordPolicyConstants.java b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/constants/PasswordPolicyConstants.java
index f48eb32e1d..14985c0eaa 100644
--- a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/constants/PasswordPolicyConstants.java
+++ b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/constants/PasswordPolicyConstants.java
@@ -28,6 +28,7 @@ public class PasswordPolicyConstants {
"http://wso2.org/claims/identity/lastPasswordUpdateTime";
public static final String LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM_NON_IDENTITY =
"http://wso2.org/claims/lastPasswordChangedTimestamp";
+ public static final String PASSWORD_EXPIRY_TIME_CLAIM = "http://wso2.org/claims/identity/passwordExpiryTime";
public static final String PASSWORD_RESET_PAGE = "/accountrecoveryendpoint/password-recovery-confirm.jsp";
public static final String PASSWORD_CHANGE_EVENT_HANDLER_NAME = "enforcePasswordResetEventHandler";
public static final String ENFORCE_PASSWORD_RESET_HANDLER = "EnforcePasswordResetHandler";
@@ -57,6 +58,7 @@ public class PasswordPolicyConstants {
public static final String AUTHENTICATION_STATUS = "authenticationStatus";
public static final String BASIC_AUTHENTICATOR = "BasicAuthenticator";
public static final String FALSE = "false";
+ public static final String TRUE = "true";
public static final String CONFIRMATION_QUERY_PARAM = "&confirmation=";
public static final String PASSWORD_EXPIRED_QUERY_PARAMS = "&passwordExpired=true";
public static final String PASSWORD_EXPIRED_MSG_QUERY_PARAM = "&passwordExpiredMsg=";
diff --git a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/internal/EnforcePasswordResetComponent.java b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/internal/EnforcePasswordResetComponent.java
index 536128988b..a3bdce3194 100644
--- a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/internal/EnforcePasswordResetComponent.java
+++ b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/internal/EnforcePasswordResetComponent.java
@@ -35,8 +35,10 @@
import org.wso2.carbon.identity.event.handler.AbstractEventHandler;
import org.wso2.carbon.identity.governance.IdentityGovernanceService;
import org.wso2.carbon.identity.governance.common.IdentityConnectorConfig;
+import org.wso2.carbon.identity.password.expiry.listener.PasswordExpiryEventListener;
import org.wso2.carbon.identity.password.expiry.services.ExpiredPasswordIdentificationService;
import org.wso2.carbon.identity.password.expiry.services.impl.ExpiredPasswordIdentificationServiceImpl;
+import org.wso2.carbon.user.core.listener.UserOperationEventListener;
import org.wso2.carbon.user.core.service.RealmService;
import org.wso2.carbon.identity.role.v2.mgt.core.RoleManagementService;
@@ -56,6 +58,10 @@ public class EnforcePasswordResetComponent {
protected void activate(ComponentContext context) {
try {
+ // Register the listener to capture user operations.
+ PasswordExpiryEventListener listener = new PasswordExpiryEventListener();
+ context.getBundleContext().registerService(UserOperationEventListener.class, listener, null);
+
EnforcePasswordResetAuthenticationHandler enforcePasswordResetAuthenticationHandler =
new EnforcePasswordResetAuthenticationHandler();
BundleContext bundleContext = context.getBundleContext();
diff --git a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/listener/PasswordExpiryEventListener.java b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/listener/PasswordExpiryEventListener.java
new file mode 100644
index 0000000000..6f1babafdd
--- /dev/null
+++ b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/listener/PasswordExpiryEventListener.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2024, 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.password.expiry.listener;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.wso2.carbon.context.PrivilegedCarbonContext;
+import org.wso2.carbon.identity.application.authentication.framework.exception.PostAuthenticationFailedException;
+import org.wso2.carbon.identity.core.AbstractIdentityUserOperationEventListener;
+import org.wso2.carbon.identity.core.util.IdentityCoreConstants;
+import org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants;
+import org.wso2.carbon.identity.password.expiry.exceptions.ExpiredPasswordIdentificationException;
+import org.wso2.carbon.identity.password.expiry.models.PasswordExpiryRule;
+import org.wso2.carbon.identity.password.expiry.util.PasswordPolicyUtils;
+import org.wso2.carbon.user.core.UserStoreException;
+import org.wso2.carbon.user.core.UserStoreManager;
+import org.wso2.carbon.user.core.model.UserClaimSearchEntry;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * This is an implementation of UserOperationEventListener. This defines additional operations for some of
+ * the core user management operations.
+ */
+public class PasswordExpiryEventListener extends AbstractIdentityUserOperationEventListener {
+
+ private static final Log log = LogFactory.getLog(PasswordExpiryEventListener.class);
+
+ public int getExecutionOrderId() {
+
+ int orderId = getOrderId();
+ if (orderId != IdentityCoreConstants.EVENT_LISTENER_ORDER_ID) {
+ return orderId;
+ }
+ return 102;
+ }
+
+ @Override
+ public boolean doPostGetUserClaimValues(String username, String[] claims, String profileName,
+ Map claimMap, UserStoreManager userStoreManager)
+ throws UserStoreException {
+
+ if (!isEnable() || !Arrays.asList(claims).contains(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM)) {
+ return true;
+ }
+ log.debug("post get user claim values with id is called in PasswordExpiryEventListener");
+
+ try {
+ String userTenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain();
+ Optional passwordExpiryTime =
+ PasswordPolicyUtils.getUserPasswordExpiryTime(userTenantDomain, username);
+ passwordExpiryTime.ifPresent(expiryTime -> claimMap.put(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM,
+ String.valueOf(expiryTime)));
+ } catch (ExpiredPasswordIdentificationException e) {
+ throw new UserStoreException("Error while retrieving password expiry time.", e);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean doPostGetUsersClaimValues(String[] userNames, String[] claims, String profileName,
+ UserClaimSearchEntry[] userClaimSearchEntries) throws UserStoreException {
+
+ if (!isEnable() || !Arrays.asList(claims).contains(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM)) {
+ return true;
+ }
+ log.debug("Method doPostGetUsersClaimValues getting executed in the PasswordExpiryEventListener.");
+
+ try {
+ String tenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain();
+ if (!PasswordPolicyUtils.isPasswordExpiryEnabled(tenantDomain)) return true;
+
+ boolean isSkipIfNoApplicableRulesEnabled =
+ PasswordPolicyUtils.isSkipIfNoApplicableRulesEnabled(tenantDomain);
+ int defaultPasswordExpiryInDays = PasswordPolicyUtils.getPasswordExpiryInDays(tenantDomain);
+ List passwordExpiryRules = PasswordPolicyUtils.getPasswordExpiryRules(tenantDomain);
+
+ for (UserClaimSearchEntry userClaimSearchEntry : userClaimSearchEntries) {
+ String username = userClaimSearchEntry.getUserName();
+
+ if (userClaimSearchEntry.getClaims() == null) {
+ userClaimSearchEntry.setClaims(new HashMap());
+ }
+ Optional passwordExpiryTime = PasswordPolicyUtils.getUserPasswordExpiryTime(
+ tenantDomain, username, true, isSkipIfNoApplicableRulesEnabled,
+ passwordExpiryRules, defaultPasswordExpiryInDays);
+ passwordExpiryTime.ifPresent(expiryTime -> userClaimSearchEntry.getClaims()
+ .put(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM, String.valueOf(expiryTime)));
+ }
+ } catch (PostAuthenticationFailedException | ExpiredPasswordIdentificationException e) {
+ throw new UserStoreException("Error while retrieving password expiry time.", e);
+ }
+ return true;
+ }
+}
diff --git a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/util/PasswordPolicyUtils.java b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/util/PasswordPolicyUtils.java
index 8469fde2ec..6d1a998667 100644
--- a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/util/PasswordPolicyUtils.java
+++ b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/util/PasswordPolicyUtils.java
@@ -48,11 +48,13 @@
import org.wso2.carbon.user.core.util.UserCoreUtil;
import org.wso2.carbon.utils.multitenancy.MultitenantConstants;
import org.wso2.carbon.user.core.common.Group;
+import org.wso2.carbon.identity.password.expiry.exceptions.ExpiredPasswordIdentificationException;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@@ -159,6 +161,8 @@ public static boolean isPasswordExpired(String tenantDomain, String tenantAwareU
throws PostAuthenticationFailedException {
try {
+ if (!isPasswordExpiryEnabled(tenantDomain)) return false;
+
UserRealm userRealm = getUserRealm(tenantDomain);
UserStoreManager userStoreManager = getUserStoreManager(userRealm);
String userId = ((AbstractUserStoreManager) userStoreManager).getUserIDFromUserName(tenantAwareUsername);
@@ -176,11 +180,8 @@ public static boolean isPasswordExpired(String tenantDomain, String tenantAwareU
skipIfNoApplicableRules);
}
- // If the default behavior is to skip the password expiry, rules with skip logic are not necessary.
- List filteredRules = passwordExpiryRules.stream()
- .filter(rule -> !skipIfNoApplicableRules ||
- !PasswordExpiryRuleOperatorEnum.NE.equals(rule.getOperator()))
- .collect(Collectors.toList());
+ List filteredRules =
+ filterApplicableExpiryRules(passwordExpiryRules, skipIfNoApplicableRules);
Map> fetchedUserAttributes =
new EnumMap<>(PasswordExpiryRuleAttributeEnum.class);
@@ -193,7 +194,7 @@ public static boolean isPasswordExpired(String tenantDomain, String tenantAwareU
}
int expiryDays =
rule.getExpiryDays() > 0 ? rule.getExpiryDays() : getPasswordExpiryInDays(tenantDomain);
- return daysDifference >= expiryDays || lastPasswordUpdatedTime == null;
+ return daysDifference >= expiryDays || StringUtils.isBlank(lastPasswordUpdatedTime);
}
}
// Apply default password expiry policy if no specific rule applies.
@@ -292,7 +293,137 @@ private static boolean isPasswordExpiredUnderDefaultPolicy(String tenantDomain,
throws PostAuthenticationFailedException {
if (skipIfNoApplicableRules) return false;
- return lastPasswordUpdatedTime == null || daysDifference >= getPasswordExpiryInDays(tenantDomain);
+ return StringUtils.isBlank(lastPasswordUpdatedTime) || daysDifference >= getPasswordExpiryInDays(tenantDomain);
+ }
+
+ /**
+ * This method returns password expiry time for the given user.
+ *
+ * @param tenantDomain The tenant domain.
+ * @param tenantAwareUsername The tenant aware username.
+ * @return Optional containing the password expiry time in milliseconds, or empty if not applicable.
+ * @throws ExpiredPasswordIdentificationException If an error occurred while getting the password expiry time.
+ */
+ public static Optional getUserPasswordExpiryTime(String tenantDomain, String tenantAwareUsername)
+ throws ExpiredPasswordIdentificationException {
+
+ return getUserPasswordExpiryTime(tenantDomain, tenantAwareUsername, null,
+ null, null, null);
+ }
+
+ /**
+ * This method returns password expiry time for the given user.
+ *
+ * @param tenantDomain The tenant domain.
+ * @param tenantAwareUsername The tenant aware username.
+ * @param isPasswordExpiryEnabled Whether password expiry is enabled.
+ * @param isSkipIfNoApplicableRulesEnabled Whether skip if no applicable rules config is enabled.
+ * @param passwordExpiryRules Password expiry rules.
+ * @param defaultPasswordExpiryInDays Default password expiry in days.
+ * @return Optional containing the password expiry time in milliseconds, or empty if not applicable.
+ * @throws ExpiredPasswordIdentificationException If an error occurred while getting the password expiry time.
+ */
+ public static Optional getUserPasswordExpiryTime(String tenantDomain,
+ String tenantAwareUsername,
+ Boolean isPasswordExpiryEnabled,
+ Boolean isSkipIfNoApplicableRulesEnabled,
+ List passwordExpiryRules,
+ Integer defaultPasswordExpiryInDays)
+ throws ExpiredPasswordIdentificationException {
+
+ try {
+ if (isPasswordExpiryEnabled == null) {
+ isPasswordExpiryEnabled = isPasswordExpiryEnabled(tenantDomain);
+ }
+ // If the password expiry is not enabled, password expiry time is not applicable.
+ if (!isPasswordExpiryEnabled) return Optional.empty();
+
+ if (isSkipIfNoApplicableRulesEnabled == null) {
+ isSkipIfNoApplicableRulesEnabled = isSkipIfNoApplicableRulesEnabled(tenantDomain);
+ }
+ if (defaultPasswordExpiryInDays == null) {
+ defaultPasswordExpiryInDays = getPasswordExpiryInDays(tenantDomain);
+ }
+ if (passwordExpiryRules == null) {
+ passwordExpiryRules = getPasswordExpiryRules(tenantDomain);
+ }
+
+ UserRealm userRealm = getUserRealm(tenantDomain);
+ UserStoreManager userStoreManager = getUserStoreManager(userRealm);
+ String userId = ((AbstractUserStoreManager) userStoreManager).getUserIDFromUserName(tenantAwareUsername);
+ String lastPasswordUpdatedTime =
+ getLastPasswordUpdatedTime(tenantAwareUsername, userStoreManager, userRealm);
+
+ long lastPasswordUpdatedTimeInMillis = 0L;
+ boolean isLastPasswordUpdatedTimeBlank = StringUtils.isBlank(lastPasswordUpdatedTime);
+ if (!isLastPasswordUpdatedTimeBlank) {
+ lastPasswordUpdatedTimeInMillis = getLastPasswordUpdatedTimeInMillis(lastPasswordUpdatedTime);
+ }
+
+ // If no rules are defined, use the default expiry time if "skipIfNoApplicableRules" is disabled.
+ if (CollectionUtils.isEmpty(passwordExpiryRules)) {
+ if (isSkipIfNoApplicableRulesEnabled) return Optional.empty();
+ // If lastPasswordUpdatedTime is blank, set expiry time to now.
+ if (isLastPasswordUpdatedTimeBlank) {
+ return Optional.of(System.currentTimeMillis());
+ }
+ return Optional.of(
+ lastPasswordUpdatedTimeInMillis + getDaysTimeInMillis(defaultPasswordExpiryInDays));
+ }
+
+ Map> userAttributes =
+ new EnumMap<>(PasswordExpiryRuleAttributeEnum.class);
+
+ List filteredRules =
+ filterApplicableExpiryRules(passwordExpiryRules, isSkipIfNoApplicableRulesEnabled);
+ for (PasswordExpiryRule rule : filteredRules) {
+ if (isRuleApplicable(rule, userAttributes, tenantDomain, userId, userStoreManager)) {
+ // Skip the rule if the operator is not equals.
+ if (PasswordExpiryRuleOperatorEnum.NE.equals(rule.getOperator())) {
+ return Optional.empty();
+ }
+ if (isLastPasswordUpdatedTimeBlank) {
+ return Optional.of(System.currentTimeMillis());
+ }
+ int expiryDays =
+ rule.getExpiryDays() > 0 ? rule.getExpiryDays() : getPasswordExpiryInDays(tenantDomain);
+ return Optional.of(lastPasswordUpdatedTimeInMillis + getDaysTimeInMillis(expiryDays));
+ }
+ }
+
+ if (isSkipIfNoApplicableRulesEnabled) return Optional.empty();
+ if (isLastPasswordUpdatedTimeBlank) {
+ return Optional.of(System.currentTimeMillis());
+ }
+ return Optional.of(
+ lastPasswordUpdatedTimeInMillis + getDaysTimeInMillis(defaultPasswordExpiryInDays));
+ } catch (UserStoreException | PostAuthenticationFailedException e) {
+ throw new ExpiredPasswordIdentificationException(PasswordPolicyConstants.ErrorMessages.
+ ERROR_WHILE_GETTING_USER_STORE_DOMAIN.getCode(),
+ PasswordPolicyConstants.ErrorMessages.ERROR_WHILE_GETTING_USER_STORE_DOMAIN.getMessage());
+ }
+ }
+
+ private static List filterApplicableExpiryRules(List passwordExpiryRules,
+ boolean skipIfNoApplicableRules) {
+
+ if (!skipIfNoApplicableRules) {
+ return passwordExpiryRules;
+ }
+ // If the default behavior is to skip the password expiry, rules with skip logic are not required.
+ return passwordExpiryRules.stream().filter(
+ rule -> !PasswordExpiryRuleOperatorEnum.NE.equals(rule.getOperator())).collect(Collectors.toList());
+ }
+
+ /**
+ * This method returns the time in milliseconds for the given number of days.
+ *
+ * @param days The number of days.
+ * @return The time in milliseconds.
+ */
+ private static long getDaysTimeInMillis(int days) {
+
+ return (long) days * 24 * 60 * 60 * 1000;
}
/**
diff --git a/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordPolicyUtilsTest.java b/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordPolicyUtilsTest.java
index 09f15c6226..1600a8024d 100644
--- a/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordPolicyUtilsTest.java
+++ b/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordPolicyUtilsTest.java
@@ -19,7 +19,12 @@
package org.wso2.carbon.identity.password.expiry;
import org.testng.annotations.DataProvider;
+import org.wso2.carbon.base.MultitenantConstants;
+import org.wso2.carbon.identity.core.ServiceURL;
+import org.wso2.carbon.identity.core.ServiceURLBuilder;
+import org.wso2.carbon.identity.core.URLBuilderException;
import org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants;
+import org.wso2.carbon.identity.password.expiry.exceptions.ExpiredPasswordIdentificationException;
import org.wso2.carbon.identity.password.expiry.internal.EnforcePasswordResetComponentDataHolder;
import org.wso2.carbon.identity.password.expiry.models.PasswordExpiryRuleAttributeEnum;
import org.wso2.carbon.identity.governance.bean.ConnectorConfig;
@@ -56,6 +61,8 @@
import java.util.List;
import java.util.Map;
import java.util.HashMap;
+import java.util.Optional;
+import java.util.stream.Collectors;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -65,6 +72,7 @@
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
+import static org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants.PASSWORD_RESET_PAGE;
/**
* Tests for password change utils.
@@ -93,11 +101,20 @@ public class PasswordPolicyUtilsTest {
@Mock
private RoleManagementService roleManagementService;
+ @Mock
+ private ServiceURLBuilder serviceURLBuilder;
+
+ @Mock
+ private ServiceURL serviceURL;
+
private MockedStatic mockedStaticUserCoreUtil;
+ private MockedStatic mockedStaticServiceURLBuilder;
private final String tenantDomain = "test.com";
private final String tenantAwareUsername = "tom@gmail.com";
private final String userId = "testUserId";
+ private static final long TIME_TOLERANCE_MS = 2000;
+ private static final int DEFAULT_EXPIRY_DAYS = 30;
private static final Map ROLE_MAP = new HashMap<>();
static {
@@ -116,6 +133,7 @@ public void beforeTest() {
mockedStaticIdentityTenantUtil = mockStatic(IdentityTenantUtil.class);
mockedStaticUserCoreUtil = mockStatic(UserCoreUtil.class);
+ mockedStaticServiceURLBuilder = mockStatic(ServiceURLBuilder.class);
}
@AfterClass
@@ -144,13 +162,7 @@ public void testGetPasswordExpiryPropertyNames() {
@Test
public void testPasswordExpiryEnabled() throws PostAuthenticationFailedException, IdentityGovernanceException {
- Property property = new Property();
- property.setName(PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY);
- property.setValue(PasswordPolicyConstants.FALSE);
- Property[] properties = new Property[1];
- properties[0] = property;
- when(identityGovernanceService.getConfiguration(new String[]{
- PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY}, tenantDomain)).thenReturn(properties);
+ mockPasswordExpiryEnabled(identityGovernanceService, PasswordPolicyConstants.FALSE);
Assert.assertFalse(PasswordPolicyUtils.isPasswordExpiryEnabled(tenantDomain));
}
@@ -189,6 +201,40 @@ public void testGetPasswordExpiryRules() throws PostAuthenticationFailedExceptio
Assert.assertEquals(Arrays.asList(ROLE_MAP.get("employee"), ROLE_MAP.get("manager")), rule3.getValues());
}
+ @Test
+ public void testGetPasswordExpiryRulesWithInvalidRules() throws PostAuthenticationFailedException, IdentityGovernanceException {
+
+ Property expiryRule1 = new Property();
+ Property expiryRule2 = new Property();
+ Property expiryRule3 = new Property();
+ Property expiryRule4 = new Property();
+ expiryRule1.setName(PasswordPolicyConstants.PASSWORD_EXPIRY_RULES_PREFIX+"1");
+ expiryRule1.setValue(String.format("1,0,groups,ne,%s", GROUP_MAP.get("admin")));
+ expiryRule2.setName(PasswordPolicyConstants.PASSWORD_EXPIRY_RULES_PREFIX+"2");
+ expiryRule2.setValue(
+ String.format("2,40,invalid_rule,%s,%s", ROLE_MAP.get("employee"), ROLE_MAP.get("contractor")));
+ expiryRule3.setName(PasswordPolicyConstants.PASSWORD_EXPIRY_RULES_PREFIX+"3");
+ expiryRule3.setValue(
+ String.format("bbb,40,groups,ne,%s,%s", ROLE_MAP.get("employee"), ROLE_MAP.get("contractor")));
+ expiryRule4.setName(PasswordPolicyConstants.PASSWORD_EXPIRY_RULES_PREFIX+"4");
+ expiryRule4.setValue(
+ String.format("-1,40,groups,ne,%s,%s", ROLE_MAP.get("employee"), ROLE_MAP.get("contractor")));
+
+ Property[] properties = new Property[4];
+ properties[0] = expiryRule1;
+ properties[1] = expiryRule2;
+ properties[2] = expiryRule3;
+ properties[3] = expiryRule4;
+ ConnectorConfig connectorConfig = new ConnectorConfig();
+ connectorConfig.setProperties(properties);
+
+ when(identityGovernanceService.getConnectorWithConfigs(tenantDomain,
+ PasswordPolicyConstants.CONNECTOR_CONFIG_NAME)).thenReturn(connectorConfig);
+
+ List rules = PasswordPolicyUtils.getPasswordExpiryRules(tenantDomain);
+ Assert.assertEquals(rules.size(), 1);
+ }
+
@Test
public void testGetUserRoles() throws PostAuthenticationFailedException, IdentityRoleManagementException {
@@ -216,9 +262,10 @@ public void testIsPasswordExpiredWithoutRules(Integer daysAgo, boolean expectedE
when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager);
when(userRealm.getClaimManager()).thenReturn(claimManager);
when(UserCoreUtil.addDomainToName(any(), any())).thenReturn(tenantAwareUsername);
-
when(abstractUserStoreManager.getUserIDFromUserName(tenantAwareUsername)).thenReturn(userId);
+ mockPasswordExpiryEnabled(identityGovernanceService, PasswordPolicyConstants.TRUE);
+
// Mock last password updated time.
Long updateTime = getUpdateTime(daysAgo);
mockLastPasswordUpdateTime(updateTime, abstractUserStoreManager);
@@ -242,17 +289,17 @@ public void testIsPasswordExpiredWithoutRules(Integer daysAgo, boolean expectedE
public Object[][] passwordExpiryTestCases() {
return new Object[][] {
// {daysAgo, roles, groups, skipIfNoApplicableRules, expectedExpired, description}.
- {55, new String[]{ROLE_MAP.get("employee"), ROLE_MAP.get("manager")}, new String[]{}, false, false,
+ {55, new String[]{"employee", "manager"}, new String[]{}, false, false,
"Not expired: 3rd rule (60) applies"},
- {55, new String[]{ROLE_MAP.get("employee"), ROLE_MAP.get("manager"), ROLE_MAP.get("contractor")},
+ {55, new String[]{"employee", "manager", "contractor"},
new String[]{}, false, true, "Expired: 2nd rule (40) applies"},
- {35, new String[]{ROLE_MAP.get("employee"), ROLE_MAP.get("contractor")}, new String[]{}, false, false,
+ {35, new String[]{"employee", "contractor"}, new String[]{}, false, false,
"Not expired: 2nd rule (40) applies"},
- {35, new String[]{ROLE_MAP.get("employee"), ROLE_MAP.get("contractor")}, new String[]{"admin"}, false,
+ {35, new String[]{"employee", "contractor"}, new String[]{"admin"}, false,
false, "Not expired: 1st rule (skip) applies."},
- {35, new String[]{ROLE_MAP.get("employee")}, new String[]{}, false, true,
+ {35, new String[]{"employee"}, new String[]{}, false, true,
"Expired: Default expiry policy applies."},
- {35, new String[]{ROLE_MAP.get("employee")}, new String[]{}, true, false,
+ {35, new String[]{"employee"}, new String[]{}, true, false,
"Not expired: Default expiry policy applies - skip if no rules applicable."},
};
}
@@ -271,13 +318,9 @@ public void testIsPasswordExpiredWithRules(int daysAgo, String[] roles, String[]
when(UserCoreUtil.addDomainToName(any(), any())).thenReturn(tenantAwareUsername);
when(roleManagementService.getRoleListOfUser(userId, tenantDomain)).thenReturn(getRoles(roles));
- List userGroups = new ArrayList<>();
- Arrays.stream(groups).forEach(groupName -> {
- Group groupObj = new Group();
- groupObj.setGroupID(GROUP_MAP.get(groupName));
- userGroups.add(groupObj);
- });
- when(abstractUserStoreManager.getGroupListOfUser(userId, null, null)).thenReturn(userGroups);
+ mockPasswordExpiryEnabled(identityGovernanceService, PasswordPolicyConstants.TRUE);
+
+ when(abstractUserStoreManager.getGroupListOfUser(userId, null, null)).thenReturn(getGroups(groups));
// Mock last password update time.
Long updateTime = getUpdateTime(daysAgo);
@@ -300,22 +343,216 @@ public void testIsPasswordExpiredWithRules(int daysAgo, String[] roles, String[]
Assert.assertEquals(isExpired, expectedExpired, description);
}
+ @DataProvider(name = "passwordExpiryTimeTestCases")
+ public Object[][] passwordExpiryTimeTestCases() {
+ return new Object[][] {
+ // {daysAgo, roles, groups, expiryDays, description}
+ {null, new String[]{"employee", "manager"}, new String[]{}, 0, "Expiry time: Now"},
+ {30, new String[]{"employee", "manager"}, new String[]{}, 60, "60 days expiry: 3rd rule applies"},
+ {100, new String[]{"employee"}, new String[]{"admin"}, null, "1st rule (skip) applies."},
+ {10, new String[]{"employee"}, new String[]{}, 30, "30 days expiry: Default expiry policy applies"},
+ {50, new String[]{"employee", "contractor"}, new String[]{}, 40, "40 days expiry: 2nd rule applies"}
+ };
+ }
+
+ @Test(dataProvider = "passwordExpiryTimeTestCases")
+ public void testGetUserPasswordExpiryTime(Integer daysAgo, String[] roles, String[] groups, Integer expiryDays,
+ String description)
+ throws IdentityGovernanceException, UserStoreException, ExpiredPasswordIdentificationException,
+ IdentityRoleManagementException {
+
+ when(IdentityTenantUtil.getTenantId(anyString())).thenReturn(3);
+ when(realmService.getTenantUserRealm(anyInt())).thenReturn(userRealm);
+ when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager);
+ when(userRealm.getClaimManager()).thenReturn(claimManager);
+ when(abstractUserStoreManager.getUserIDFromUserName(tenantAwareUsername)).thenReturn(userId);
+ when(UserCoreUtil.addDomainToName(any(), any())).thenReturn(tenantAwareUsername);
+
+ // Mock last password update time.
+ Long updateTime = daysAgo != null ? System.currentTimeMillis() - getDaysTimeInMillis(daysAgo) : null;
+ mockLastPasswordUpdateTime(updateTime, abstractUserStoreManager);
+
+ mockPasswordExpiryEnabled(identityGovernanceService, PasswordPolicyConstants.TRUE);
+
+ // Mock password expiry rules.
+ ConnectorConfig connectorConfig = new ConnectorConfig();
+ connectorConfig.setProperties(getPasswordExpiryRulesProperties());
+ when(identityGovernanceService.getConnectorWithConfigs(tenantDomain,
+ PasswordPolicyConstants.CONNECTOR_CONFIG_NAME)).thenReturn(connectorConfig);
+
+ when(identityGovernanceService.getConfiguration(
+ new String[]{PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS},
+ tenantDomain)).thenReturn(getPasswordExpiryInDaysProperty());
+ when(identityGovernanceService.getConfiguration(
+ new String[]{PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES},
+ tenantDomain)).thenReturn(getSkipIfNoRulesApplicableProperty(PasswordPolicyConstants.FALSE));
+
+ // Mock user roles.
+ when(roleManagementService.getRoleListOfUser(userId, tenantDomain)).thenReturn(getRoles(roles));
+
+ // Mock user groups.
+ when(abstractUserStoreManager.getGroupListOfUser(userId, null, null))
+ .thenReturn(getGroups(groups));
+
+ long testStartTime = System.currentTimeMillis();
+ Optional expiryTime =
+ PasswordPolicyUtils.getUserPasswordExpiryTime(tenantDomain, tenantAwareUsername);
+ long testEndTime = System.currentTimeMillis();
+
+ if (expiryDays == null) {
+ Assert.assertFalse(expiryTime.isPresent(), description);
+ } else if (expiryDays == 0) {
+ Assert.assertTrue(expiryTime.isPresent());
+ Assert.assertTrue(expiryTime.get() >= testStartTime && expiryTime.get() <= testEndTime);
+ } else {
+ Assert.assertTrue(expiryTime.isPresent());
+ Assert.assertNotNull(updateTime);
+ long expectedExpiryTime = updateTime + getDaysTimeInMillis(expiryDays);
+ Assert.assertTrue(Math.abs(expiryTime.get() - expectedExpiryTime) <= TIME_TOLERANCE_MS);
+ }
+ }
+
+ @Test
+ public void testGetUserPasswordExpiryTime()
+ throws IdentityGovernanceException, UserStoreException, ExpiredPasswordIdentificationException {
+
+ // Case 1: Password expiry disabled.
+ Optional expiryTime = PasswordPolicyUtils.getUserPasswordExpiryTime(
+ tenantDomain, tenantAwareUsername, false, null,
+ null, null);
+ Assert.assertFalse(expiryTime.isPresent());
+
+ // Case 2: Password expiry enabled, but no rules.
+ mockPasswordExpiryEnabled(identityGovernanceService, PasswordPolicyConstants.TRUE);
+ when(IdentityTenantUtil.getTenantId(anyString())).thenReturn(3);
+ when(realmService.getTenantUserRealm(anyInt())).thenReturn(userRealm);
+ when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager);
+ when(userRealm.getClaimManager()).thenReturn(claimManager);
+ when(abstractUserStoreManager.getUserIDFromUserName(tenantAwareUsername)).thenReturn(userId);
+ when(UserCoreUtil.addDomainToName(any(), any())).thenReturn(tenantAwareUsername);
+
+ // Mock last password update time to 20 days.
+ Long updateTime = System.currentTimeMillis() - getDaysTimeInMillis(20);
+ mockLastPasswordUpdateTime(updateTime, abstractUserStoreManager);
+
+ expiryTime = PasswordPolicyUtils.getUserPasswordExpiryTime(
+ tenantDomain, tenantAwareUsername, true, false,
+ Collections.emptyList(), DEFAULT_EXPIRY_DAYS);
+
+ long expectedExpiryTime = updateTime + getDaysTimeInMillis(DEFAULT_EXPIRY_DAYS);
+ Assert.assertTrue(Math.abs(expiryTime.get() - expectedExpiryTime) <= TIME_TOLERANCE_MS);
+
+ // Case 3: Password expiry enabled, no applicable rules, skipIfNoApplicableRules enabled.
+ when(identityGovernanceService.getConfiguration(
+ new String[]{PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES},
+ tenantDomain)).thenReturn(getSkipIfNoRulesApplicableProperty(PasswordPolicyConstants.TRUE));
+
+ expiryTime = PasswordPolicyUtils.getUserPasswordExpiryTime(tenantDomain, tenantAwareUsername,
+ true, true, Collections.emptyList(),
+ DEFAULT_EXPIRY_DAYS);
+ Assert.assertFalse(expiryTime.isPresent());
+
+ // Case 4: UserStoreException.
+ when(abstractUserStoreManager.getUserIDFromUserName(tenantAwareUsername)).thenThrow(
+ new org.wso2.carbon.user.core.UserStoreException());
+ try {
+ PasswordPolicyUtils.getUserPasswordExpiryTime(tenantDomain, tenantAwareUsername,
+ true, true, Collections.emptyList(),
+ DEFAULT_EXPIRY_DAYS);
+ Assert.fail("Expected PostAuthenticationFailedException was not thrown");
+ } catch (Exception e) {
+ Assert.assertTrue(e instanceof ExpiredPasswordIdentificationException);
+ }
+ }
+
+ @Test
+ public void testGetPasswordResetPageUrl() throws Exception {
+
+ // Mocking ServiceURLBuilder
+ mockedStaticServiceURLBuilder.when(
+ (MockedStatic.Verification) ServiceURLBuilder.create()).thenReturn(serviceURLBuilder);
+ when(serviceURLBuilder.addPath(PASSWORD_RESET_PAGE)).thenReturn(serviceURLBuilder);
+ when(serviceURLBuilder.setTenant(anyString())).thenReturn(serviceURLBuilder);
+ when(serviceURLBuilder.build()).thenReturn(serviceURL);
+
+ // Case 1: Tenant qualified URLs enabled.
+ mockedStaticIdentityTenantUtil.when(IdentityTenantUtil::isTenantQualifiedUrlsEnabled).thenReturn(true);
+ String tenantQualifiedURL =
+ String.format("https://example.com/t/%s/accountrecoveryendpoint/password-reset", tenantDomain);
+ when(serviceURL.getAbsolutePublicURL()).thenReturn(tenantQualifiedURL);
+
+ String result = PasswordPolicyUtils.getPasswordResetPageUrl(tenantDomain);
+ Assert.assertEquals(tenantQualifiedURL, result);
+
+ // Case 2: Tenant qualified URLs disabled, non-super tenant.
+ mockedStaticIdentityTenantUtil.when(IdentityTenantUtil::isTenantQualifiedUrlsEnabled).thenReturn(false);
+ String serverURL = "https://example.com";
+ when(serviceURL.getAbsolutePublicURL()).thenReturn(serverURL);
+
+ result = PasswordPolicyUtils.getPasswordResetPageUrl(tenantDomain);
+ Assert.assertEquals(
+ String.format("%s/t/%s%s?tenantDomain=%s", serverURL, tenantDomain, PASSWORD_RESET_PAGE, tenantDomain),
+ result);
+
+ // Case 3: Tenant qualified URLs disabled, super tenant.
+ result = PasswordPolicyUtils.getPasswordResetPageUrl(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME);
+ Assert.assertEquals(String.format("%s%s", serverURL, PASSWORD_RESET_PAGE), result);
+
+ // Case 4: URLBuilderException.
+ when(serviceURLBuilder.build()).thenThrow(new URLBuilderException("Test exception"));
+ try {
+ PasswordPolicyUtils.getPasswordResetPageUrl(tenantDomain);
+ Assert.fail("Expected PostAuthenticationFailedException was not thrown");
+ } catch (PostAuthenticationFailedException e) {
+ Assert.assertEquals(
+ PasswordPolicyConstants.ErrorMessages.ERROR_WHILE_BUILDING_PASSWORD_RESET_PAGE_URL.getCode(),
+ e.getErrorCode());
+ }
+ }
+
+ private void mockPasswordExpiryEnabled(IdentityGovernanceService identityGovernanceService, String enabled) throws IdentityGovernanceException {
+
+ Property property = new Property();
+ property.setName(PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY);
+ property.setValue(enabled);
+ Property[] properties = new Property[1];
+ properties[0] = property;
+ when(identityGovernanceService.getConfiguration(new String[]{
+ PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY}, tenantDomain)).thenReturn(properties);
+ }
+
+ private static Long getDaysTimeInMillis(Integer days) {
+
+ return days != null ? (long) days * 24 * 60 * 60 * 1000 : null;
+ }
+
private static Long getUpdateTime(Integer daysAgo) {
- return daysAgo != null ? System.currentTimeMillis() - daysAgo * 24 * 60 * 60 * 1000L : null;
+ return daysAgo != null ? System.currentTimeMillis() - getDaysTimeInMillis(daysAgo) : null;
}
- private List getRoles(String[] roleIds) {
+ private List getRoles(String[] roleNames) {
List userRoles = new ArrayList<>();
- for (String roleId : roleIds) {
+ for (String roleId : roleNames) {
RoleBasicInfo roleInfo = new RoleBasicInfo();
- roleInfo.setId(roleId);
+ roleInfo.setId(ROLE_MAP.get(roleId));
userRoles.add(roleInfo);
}
return userRoles;
}
+ private static List getGroups(String[] groupNames) {
+
+ List userGroups = new ArrayList<>();
+ Arrays.stream(groupNames).forEach(groupName -> {
+ Group groupObj = new Group();
+ groupObj.setGroupID(GROUP_MAP.get(groupName));
+ userGroups.add(groupObj);
+ });
+ return userGroups;
+ }
+
private Property[] getPasswordExpiryRulesProperties() {
Property expiryRule1 = new Property();
@@ -341,7 +578,7 @@ private Property[] getPasswordExpiryInDaysProperty() {
Property property1 = new Property();
property1.setName(PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS);
- property1.setValue(String.valueOf(30));
+ property1.setValue(String.valueOf(DEFAULT_EXPIRY_DAYS));
Property[] properties = new Property[1];
properties[0] = property1;
return properties;
@@ -359,10 +596,18 @@ private Property[] getSkipIfNoRulesApplicableProperty(String value) {
private void mockLastPasswordUpdateTime(Long updateTime, UserStoreManager userStoreManager) throws UserStoreException {
- Map claims = new HashMap<>();
- claims.put(PasswordPolicyConstants.LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM,
- updateTime != null ? String.valueOf(updateTime) : null);
- String[] claimURIs = new String[]{PasswordPolicyConstants.LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM};
- when(userStoreManager.getUserClaimValues(anyString(), eq(claimURIs), isNull())).thenReturn(claims);
+ String updateTimeString = updateTime != null ? String.valueOf(updateTime) : null;
+
+ // Mock for LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM.
+ Map claims1 = new HashMap<>();
+ claims1.put(PasswordPolicyConstants.LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM, updateTimeString);
+ String[] claimURIs1 = new String[]{PasswordPolicyConstants.LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM};
+ when(userStoreManager.getUserClaimValues(anyString(), eq(claimURIs1), isNull())).thenReturn(claims1);
+
+ // Mock for LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM_NON_IDENTITY.
+ Map claims2 = new HashMap<>();
+ claims2.put(PasswordPolicyConstants.LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM_NON_IDENTITY, updateTimeString);
+ String[] claimURIs2 = new String[]{PasswordPolicyConstants.LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM_NON_IDENTITY};
+ when(userStoreManager.getUserClaimValues(anyString(), eq(claimURIs2), isNull())).thenReturn(claims2);
}
}
diff --git a/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordResetEnforcerHandlerTest.java b/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordResetEnforcerHandlerTest.java
index 1e15901c78..2ee2a2e4d4 100644
--- a/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordResetEnforcerHandlerTest.java
+++ b/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordResetEnforcerHandlerTest.java
@@ -155,6 +155,13 @@ public void testHandle() throws Exception {
when(authenticationContext.getCurrentAuthenticatedIdPs()).thenReturn(idPs);
idPs.put(AUTHENTICATOR_TYPE, authenticatedIdPData);
+ // Case 1 : Password expiry is not enabled.
+ when(PasswordPolicyUtils.isPasswordExpiryEnabled(anyString())).thenReturn(false);
+ PostAuthnHandlerFlowStatus flowStatus1 = enforcePasswordResetAuthenticationHandler.handle(httpServletRequest,
+ httpServletResponse, authenticationContext);
+ Assert.assertEquals(flowStatus1, PostAuthnHandlerFlowStatus.SUCCESS_COMPLETED);
+
+ // Case 2 : Password expiry is enabled.
List authenticators = getAuthenticatorConfigs();
when(PasswordPolicyUtils.isPasswordExpiryEnabled(anyString())).thenReturn(true);
when(PasswordPolicyUtils.isPasswordExpired(anyString(), anyString())).thenReturn(true);
@@ -179,6 +186,12 @@ public void testHandle() throws Exception {
httpServletResponse, authenticationContext);
Assert.assertEquals(flowStatus, PostAuthnHandlerFlowStatus.INCOMPLETE);
verify(httpServletResponse).sendRedirect(captor.capture());
+
+ // Case 3 : Password expiry is enabled and password is not expired.
+ when(PasswordPolicyUtils.isPasswordExpired(anyString(), anyString())).thenReturn(false);
+ PostAuthnHandlerFlowStatus flowStatus2 = enforcePasswordResetAuthenticationHandler.handle(httpServletRequest,
+ httpServletResponse, authenticationContext);
+ Assert.assertEquals(flowStatus2, PostAuthnHandlerFlowStatus.SUCCESS_COMPLETED);
}
private static List getAuthenticatorConfigs() {
diff --git a/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/listener/PasswordExpiryEventListenerTest.java b/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/listener/PasswordExpiryEventListenerTest.java
new file mode 100644
index 0000000000..93941822b0
--- /dev/null
+++ b/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/listener/PasswordExpiryEventListenerTest.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (c) 2024, 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.password.expiry.listener;
+
+import org.wso2.carbon.context.PrivilegedCarbonContext;
+import org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants;
+import org.wso2.carbon.identity.password.expiry.exceptions.ExpiredPasswordIdentificationException;
+import org.wso2.carbon.identity.password.expiry.util.PasswordPolicyUtils;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.MockitoAnnotations;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.wso2.carbon.identity.common.testng.WithCarbonHome;
+import org.wso2.carbon.user.core.UserStoreException;
+import org.wso2.carbon.user.core.UserStoreManager;
+import org.wso2.carbon.user.core.model.UserClaimSearchEntry;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit test cases for PasswordExpiryEventListener.
+ */
+@WithCarbonHome
+public class PasswordExpiryEventListenerTest {
+
+ private static final String TENANT_DOMAIN = "test.com";
+ private PasswordExpiryEventListener passwordExpiryEventListener;
+
+ @Mock
+ PrivilegedCarbonContext privilegedCarbonContext;
+ @Mock
+ UserStoreManager userStoreManager;
+
+ private MockedStatic mockedPrivilegedCarbonContext;
+ private MockedStatic mockedPasswordPolicyUtils;
+
+ @BeforeMethod
+ public void setUp() {
+
+ MockitoAnnotations.openMocks(this);
+ passwordExpiryEventListener = new PasswordExpiryEventListener();
+
+ mockedPrivilegedCarbonContext.when(PrivilegedCarbonContext::getThreadLocalCarbonContext)
+ .thenReturn(privilegedCarbonContext);
+
+ when(privilegedCarbonContext.getTenantDomain()).thenReturn(TENANT_DOMAIN);
+ }
+
+ @BeforeClass
+ public void init() {
+
+ mockedPrivilegedCarbonContext = mockStatic(PrivilegedCarbonContext.class);
+ mockedPasswordPolicyUtils = mockStatic(PasswordPolicyUtils.class);
+ }
+
+ @AfterClass
+ public void close() {
+
+ mockedPrivilegedCarbonContext.close();
+ mockedPasswordPolicyUtils.close();
+ }
+
+ @Test
+ public void testGetExecutionOrderId() {
+
+ Assert.assertEquals(passwordExpiryEventListener.getExecutionOrderId(), 102);
+ }
+
+ @Test
+ public void testDoPostGetUserClaimValuesWithPasswordExpiryClaim() throws UserStoreException {
+
+ String username = "testUser";
+ String[] claims;
+ Map claimMap = new HashMap<>();
+ String profileName = "default";
+
+ // Case 1: When claims contains PASSWORD_EXPIRY_TIME_CLAIM.
+ claims = new String[]{PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM};
+
+ mockedPasswordPolicyUtils.when(() -> PasswordPolicyUtils.getUserPasswordExpiryTime(
+ eq(TENANT_DOMAIN), eq(username))).thenReturn(Optional.of(1000L));
+
+ passwordExpiryEventListener.doPostGetUserClaimValues(username, claims, profileName, claimMap, userStoreManager);
+ Assert.assertNotNull(claimMap.get(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM));
+
+ // Case 2: PostAuthenticationFailedException is thrown.
+ mockedPasswordPolicyUtils.when(() ->
+ PasswordPolicyUtils.getUserPasswordExpiryTime(eq(TENANT_DOMAIN), eq(username)))
+ .thenThrow(new ExpiredPasswordIdentificationException("test-error", "test-error"));
+ try {
+ passwordExpiryEventListener.doPostGetUserClaimValues(username, claims, profileName, claimMap, userStoreManager);
+ } catch (Exception e) {
+ Assert.assertTrue(e instanceof UserStoreException);
+ }
+ }
+
+ @Test
+ public void testDoPostGetUserClaimValuesWithoutPasswordExpiryClaim() throws UserStoreException {
+
+ String username = "testUser";
+ String[] claims;
+ Map claimMap = new HashMap<>();
+ String profileName = "default";
+ claims = new String[]{"claim1", "claim2"};
+
+ passwordExpiryEventListener.doPostGetUserClaimValues(username, claims, profileName, claimMap, userStoreManager);
+ Assert.assertFalse(claimMap.containsKey(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM));
+ }
+
+ @Test
+ public void testDoPostGetUsersClaimValuesWithPasswordExpiryClaim() throws UserStoreException {
+
+ String[] userNames = new String[]{"testUser1", "testUser2"};
+ String[] claims = new String[]{PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM};
+ String profileName = "default";
+
+ UserClaimSearchEntry[] userClaimSearchEntries = new UserClaimSearchEntry[2];
+ userClaimSearchEntries[0] = new UserClaimSearchEntry();
+ userClaimSearchEntries[0].setUserName("testUser1");
+ userClaimSearchEntries[1] = new UserClaimSearchEntry();
+ userClaimSearchEntries[1].setUserName("testUser1");
+
+ mockedPasswordPolicyUtils.when(() ->
+ PasswordPolicyUtils.isPasswordExpiryEnabled(TENANT_DOMAIN)).thenReturn(true);
+ mockedPasswordPolicyUtils.when(() ->
+ PasswordPolicyUtils.isSkipIfNoApplicableRulesEnabled(TENANT_DOMAIN)).thenReturn(false);
+ mockedPasswordPolicyUtils.when(() ->
+ PasswordPolicyUtils.getPasswordExpiryInDays(TENANT_DOMAIN)).thenReturn(30);
+ mockedPasswordPolicyUtils.when(() ->
+ PasswordPolicyUtils.getPasswordExpiryRules(TENANT_DOMAIN)).thenReturn(Collections.emptyList());
+ mockedPasswordPolicyUtils.when(() -> PasswordPolicyUtils.getUserPasswordExpiryTime(
+ eq(TENANT_DOMAIN), anyString(), eq(true), eq(false), any(), eq(30)))
+ .thenReturn(Optional.of(1000L));
+
+ passwordExpiryEventListener.doPostGetUsersClaimValues(userNames, claims, profileName, userClaimSearchEntries);
+ Assert.assertNotNull(
+ userClaimSearchEntries[0].getClaims().get(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM));
+ Assert.assertNotNull(
+ userClaimSearchEntries[1].getClaims().get(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM));
+
+ // Case 2: PostAuthenticationFailedException is thrown.
+ mockedPasswordPolicyUtils.when(() -> PasswordPolicyUtils.getUserPasswordExpiryTime(
+ eq(TENANT_DOMAIN), anyString(), eq(true), eq(false), any(), eq(30)))
+ .thenThrow(new ExpiredPasswordIdentificationException("test-error", "test-error"));
+ try {
+ passwordExpiryEventListener.doPostGetUsersClaimValues(userNames, claims,
+ profileName, userClaimSearchEntries);
+ } catch (Exception e) {
+ Assert.assertTrue(e instanceof UserStoreException);
+ }
+ }
+
+ @Test
+ public void testDoPostGetUsersClaimValuesWithoutPasswordExpiryClaims() throws UserStoreException {
+
+ String[] userNames = new String[]{"testUser1", "testUser2"};
+ String[] claims = new String[]{"claim1", "claim2"};
+ String profileName = "default";
+
+ UserClaimSearchEntry[] userClaimSearchEntries = new UserClaimSearchEntry[2];
+ userClaimSearchEntries[0] = new UserClaimSearchEntry();
+ userClaimSearchEntries[0].setUserName("testUser1");
+ userClaimSearchEntries[1] = new UserClaimSearchEntry();
+ userClaimSearchEntries[1].setUserName("testUser1");
+
+ passwordExpiryEventListener.doPostGetUsersClaimValues(userNames, claims, profileName, userClaimSearchEntries);
+ Assert.assertNull(userClaimSearchEntries[0].getClaims());
+ Assert.assertNull(userClaimSearchEntries[1].getClaims());
+ }
+}
diff --git a/components/org.wso2.carbon.identity.password.expiry/src/test/resources/testng.xml b/components/org.wso2.carbon.identity.password.expiry/src/test/resources/testng.xml
index 2aac16e379..ed19db6cef 100644
--- a/components/org.wso2.carbon.identity.password.expiry/src/test/resources/testng.xml
+++ b/components/org.wso2.carbon.identity.password.expiry/src/test/resources/testng.xml
@@ -14,6 +14,7 @@
+