Skip to content

Commit

Permalink
Merge pull request #856 from PasinduYeshan/feature/password-expired-u…
Browse files Browse the repository at this point in the history
…sers

Add support for passwordExpiryTime in user claims on request
  • Loading branch information
PasinduYeshan authored Dec 14, 2024
2 parents 55b347e + 28c4788 commit 48c8969
Show file tree
Hide file tree
Showing 9 changed files with 758 additions and 38 deletions.
8 changes: 8 additions & 0 deletions components/org.wso2.carbon.identity.password.expiry/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wso2.carbon.identity.framework</groupId>
<artifactId>org.wso2.carbon.identity.testutil</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wso2.carbon.identity.organization.management.core</groupId>
<artifactId>org.wso2.carbon.identity.organization.management.service</artifactId>
Expand Down Expand Up @@ -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}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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=";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<Long> 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<PasswordExpiryRule> passwordExpiryRules = PasswordPolicyUtils.getPasswordExpiryRules(tenantDomain);

for (UserClaimSearchEntry userClaimSearchEntry : userClaimSearchEntries) {
String username = userClaimSearchEntry.getUserName();

if (userClaimSearchEntry.getClaims() == null) {
userClaimSearchEntry.setClaims(new HashMap<String, String>());
}
Optional<Long> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -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<PasswordExpiryRule> filteredRules = passwordExpiryRules.stream()
.filter(rule -> !skipIfNoApplicableRules ||
!PasswordExpiryRuleOperatorEnum.NE.equals(rule.getOperator()))
.collect(Collectors.toList());
List<PasswordExpiryRule> filteredRules =
filterApplicableExpiryRules(passwordExpiryRules, skipIfNoApplicableRules);

Map<PasswordExpiryRuleAttributeEnum, Set<String>> fetchedUserAttributes =
new EnumMap<>(PasswordExpiryRuleAttributeEnum.class);
Expand All @@ -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.
Expand Down Expand Up @@ -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<Long> 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<Long> getUserPasswordExpiryTime(String tenantDomain,
String tenantAwareUsername,
Boolean isPasswordExpiryEnabled,
Boolean isSkipIfNoApplicableRulesEnabled,
List<PasswordExpiryRule> 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<PasswordExpiryRuleAttributeEnum, Set<String>> userAttributes =
new EnumMap<>(PasswordExpiryRuleAttributeEnum.class);

List<PasswordExpiryRule> 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<PasswordExpiryRule> filterApplicableExpiryRules(List<PasswordExpiryRule> 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;
}

/**
Expand Down
Loading

0 comments on commit 48c8969

Please sign in to comment.