diff --git a/components/org.wso2.carbon.identity.password.expiry/pom.xml b/components/org.wso2.carbon.identity.password.expiry/pom.xml index e405526b5..d9db8d23c 100644 --- a/components/org.wso2.carbon.identity.password.expiry/pom.xml +++ b/components/org.wso2.carbon.identity.password.expiry/pom.xml @@ -156,6 +156,8 @@ 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.user.core.jdbc; version="${carbon.kernel.package.import.version.range}", + org.wso2.carbon.user.core.ldap; 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.*; diff --git a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/EnforcePasswordResetAuthenticationHandler.java b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/EnforcePasswordResetAuthenticationHandler.java index aaa106041..835391447 100644 --- a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/EnforcePasswordResetAuthenticationHandler.java +++ b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/EnforcePasswordResetAuthenticationHandler.java @@ -85,6 +85,17 @@ public PostAuthnHandlerFlowStatus handle(HttpServletRequest httpServletRequest, authenticationContext.getCurrentAuthenticatedIdPs().get(PasswordPolicyConstants.AUTHENTICATOR_TYPE) .getAuthenticators(); + /* + Skip the Password Reset Authenticator Handler when the Password Reset Enforcer Authenticator is in the + authenticators list, as password expiration is already checked by the Password Reset Enforcer Authenticator. + */ + boolean isPostAuthenticationHandlerSkipped = authenticators.stream() + .anyMatch(authenticator -> PasswordPolicyConstants.PASSWORD_RESET_ENFORCER_AUTHENTICATOR + .equals(authenticator.getName())); + if (isPostAuthenticationHandlerSkipped) { + return PostAuthnHandlerFlowStatus.SUCCESS_COMPLETED; + } + for (AuthenticatorConfig authenticator : authenticators) { if (PasswordPolicyConstants.BASIC_AUTHENTICATOR.equals(authenticator.getName())) { if (!authenticatedUser.isFederatedUser()) { 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 14985c0ea..6b626e459 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 @@ -57,6 +57,7 @@ public class PasswordPolicyConstants { public static final String PASSWORD_GRANT_POST_AUTHENTICATION_EVENT = "PASSWORD_GRANT_POST_AUTHENTICATION"; public static final String AUTHENTICATION_STATUS = "authenticationStatus"; public static final String BASIC_AUTHENTICATOR = "BasicAuthenticator"; + public static final String PASSWORD_RESET_ENFORCER_AUTHENTICATOR = "password-reset-enforcer"; public static final String FALSE = "false"; public static final String TRUE = "true"; public static final String CONFIRMATION_QUERY_PARAM = "&confirmation="; @@ -65,6 +66,10 @@ public class PasswordPolicyConstants { public static final String PASSWORD_EXPIRY_RULES_PREFIX = "passwordExpiry.rule"; public static final Integer MAX_PASSWORD_EXPIRY_RULE_VALUES = 5; + // Time conversion constants. + public static final long WINDOWS_EPOCH_DIFF = 11644473600000L; + public static final long HUNDREDS_OF_NANOSECONDS = 10000L; + public enum ErrorMessages { ERROR_WHILE_GETTING_USER_STORE_DOMAIN("80001", "Error occurred while getting the user store domain."), 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 ca12272a6..87784cb20 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 @@ -44,6 +44,8 @@ import org.wso2.carbon.user.api.UserStoreException; import org.wso2.carbon.user.core.UserStoreManager; import org.wso2.carbon.user.core.common.AbstractUserStoreManager; +import org.wso2.carbon.user.core.jdbc.UniqueIDJDBCUserStoreManager; +import org.wso2.carbon.user.core.ldap.UniqueIDActiveDirectoryUserStoreManager; import org.wso2.carbon.user.core.service.RealmService; import org.wso2.carbon.user.core.util.UserCoreUtil; import org.wso2.carbon.utils.multitenancy.MultitenantConstants; @@ -60,7 +62,9 @@ import java.util.stream.Collectors; import static org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants.CONNECTOR_CONFIG_NAME; +import static org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants.HUNDREDS_OF_NANOSECONDS; import static org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants.PASSWORD_RESET_PAGE; +import static org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants.WINDOWS_EPOCH_DIFF; /** * Utilities for password change enforcing. @@ -609,6 +613,11 @@ private static String getLastPasswordUpdatedTime(String tenantAwareUsername, Use getLastPasswordUpdateTime(userStoreManager, claimURI, tenantAwareUsername); } } + // Check if the Identity datastore is set to Active Directory and do the conversion accordingly. + if (!lastPasswordUpdatedTime.isEmpty() && + isUserStoreBasedIdentityDataStore() && isActiveDirectoryUserStore(userStoreManager)) { + lastPasswordUpdatedTime = convertWindowsFileTimeToUnixTime(lastPasswordUpdatedTime); + } } catch (UserStoreException e) { throw new PostAuthenticationFailedException( PasswordPolicyConstants.ErrorMessages.ERROR_WHILE_GETTING_CLAIM_MAPPINGS.getCode(), @@ -711,4 +720,57 @@ public static String getPasswordResetPageUrl(String tenantDomain) throws PostAut PasswordPolicyConstants.ErrorMessages.ERROR_WHILE_BUILDING_PASSWORD_RESET_PAGE_URL.getMessage()); } } -} \ No newline at end of file + + /** + * Check whether the user store is based on identity data store. + * + * @return true if the user store is based on identity data store. + */ + public static boolean isUserStoreBasedIdentityDataStore() { + + return EnforcePasswordResetComponentDataHolder.getInstance().getIdentityDataStoreService() + .isUserStoreBasedIdentityDataStore(); + } + + /** + * Check whether the user store is based on Active Directory. + * + * @param userStoreManager The user store manager. + * @return true if the user store is based on Active Directory. + */ + public static boolean isActiveDirectoryUserStore(UserStoreManager userStoreManager) { + + return userStoreManager instanceof UniqueIDJDBCUserStoreManager + && userStoreManager.getSecondaryUserStoreManager() instanceof UniqueIDActiveDirectoryUserStoreManager; + } + + /** + * Converts a Windows FileTime string to Unix time in milliseconds. + * + * Windows FileTime is a 64-bit value representing the number of 100-nanosecond + * intervals since January 1, 1601 (UTC). + * + * The conversion to Unix time (milliseconds since January 1, 1970, UTC) involves two steps: + * + * 1. Convert the Windows FileTime value from 100-nanosecond intervals to milliseconds: + * - This is done by dividing the FileTime value by 10,000 (HUNDREDS_OF_NANOSECONDS). + * - This converts the FileTime value from 100-nanosecond intervals to milliseconds. + * + * 2. Adjust for the difference in epoch start dates between Windows and Unix: + * - Windows epoch starts on January 1, 1601, while Unix epoch starts on January 1, 1970. + * - The difference between these two epochs is 11644473600000 milliseconds (WINDOWS_EPOCH_DIFF). + * - Subtracting this value aligns the converted milliseconds with the Unix epoch. + * + * The resulting value represents the number of milliseconds since the Unix epoch, + * which is returned as a string. + * + * @param windowsFileTime A string representing the Windows FileTime to be converted. + * @return A string representing the Unix time in milliseconds. + */ + public static String convertWindowsFileTimeToUnixTime(String windowsFileTime) { + + long fileTime = Long.parseLong(windowsFileTime); + long millisSinceEpoch = (fileTime / HUNDREDS_OF_NANOSECONDS) - WINDOWS_EPOCH_DIFF; + return String.valueOf(millisSinceEpoch); + } +} 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 1600a8024..ceade78ae 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 @@ -23,6 +23,7 @@ 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.governance.service.IdentityDataStoreService; 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; @@ -94,6 +95,9 @@ public class PasswordPolicyUtilsTest { @Mock private ClaimManager claimManager; + @Mock + private IdentityDataStoreService identityDataStoreService; + @Mock private org.wso2.carbon.user.core.UserRealm userRealm; private MockedStatic mockedStaticIdentityTenantUtil; @@ -150,6 +154,7 @@ public void setUp() { EnforcePasswordResetComponentDataHolder.getInstance().setIdentityGovernanceService(identityGovernanceService); EnforcePasswordResetComponentDataHolder.getInstance().setRealmService(realmService); EnforcePasswordResetComponentDataHolder.getInstance().setRoleManagementService(roleManagementService); + EnforcePasswordResetComponentDataHolder.getInstance().setIdentityDataStoreService(identityDataStoreService); } @Test @@ -263,6 +268,7 @@ public void testIsPasswordExpiredWithoutRules(Integer daysAgo, boolean expectedE when(userRealm.getClaimManager()).thenReturn(claimManager); when(UserCoreUtil.addDomainToName(any(), any())).thenReturn(tenantAwareUsername); when(abstractUserStoreManager.getUserIDFromUserName(tenantAwareUsername)).thenReturn(userId); + when(identityDataStoreService.isUserStoreBasedIdentityDataStore()).thenReturn(false); mockPasswordExpiryEnabled(identityGovernanceService, PasswordPolicyConstants.TRUE); @@ -317,6 +323,7 @@ public void testIsPasswordExpiredWithRules(int daysAgo, String[] roles, String[] when(abstractUserStoreManager.getUserIDFromUserName(tenantAwareUsername)).thenReturn(userId); when(UserCoreUtil.addDomainToName(any(), any())).thenReturn(tenantAwareUsername); when(roleManagementService.getRoleListOfUser(userId, tenantDomain)).thenReturn(getRoles(roles)); + when(identityDataStoreService.isUserStoreBasedIdentityDataStore()).thenReturn(false); mockPasswordExpiryEnabled(identityGovernanceService, PasswordPolicyConstants.TRUE); @@ -367,6 +374,7 @@ public void testGetUserPasswordExpiryTime(Integer daysAgo, String[] roles, Strin when(userRealm.getClaimManager()).thenReturn(claimManager); when(abstractUserStoreManager.getUserIDFromUserName(tenantAwareUsername)).thenReturn(userId); when(UserCoreUtil.addDomainToName(any(), any())).thenReturn(tenantAwareUsername); + when(identityDataStoreService.isUserStoreBasedIdentityDataStore()).thenReturn(false); // Mock last password update time. Long updateTime = daysAgo != null ? System.currentTimeMillis() - getDaysTimeInMillis(daysAgo) : null;