diff --git a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/pom.xml b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/pom.xml index d4d8093120eb..0376fb4b8170 100644 --- a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/pom.xml +++ b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/pom.xml @@ -229,6 +229,14 @@ org.wso2.carbon.identity.organization.management.core org.wso2.carbon.identity.organization.management.service + + org.wso2.carbon.identity.organization.management + org.wso2.carbon.identity.organization.discovery.service + + + org.wso2.carbon.identity.organization.management + org.wso2.carbon.identity.organization.config.service + org.wso2.carbon.identity.framework org.wso2.carbon.identity.role.v2.mgt.core @@ -303,6 +311,8 @@ version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.central.log.mgt.*; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.organization.management.service; version="${org.wso2.carbon.identity.organization.management.core.version.range}", + org.wso2.carbon.identity.organization.discovery.service; version="${org.wso2.carbon.identity.organization.management.version.range}", + org.wso2.carbon.identity.organization.config.service; version="${org.wso2.carbon.identity.organization.management.version.range}", org.wso2.carbon.identity.configuration.mgt.core; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.configuration.mgt.core.exception; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.configuration.mgt.core.model; version="${carbon.identity.package.import.version.range}", diff --git a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/EmailDomainValidationHandler.java b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/EmailDomainValidationHandler.java new file mode 100644 index 000000000000..d8a5eae85a4c --- /dev/null +++ b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/EmailDomainValidationHandler.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2025, 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.application.authentication.framework.handler.request.impl; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.CarbonContext; +import org.wso2.carbon.identity.application.authentication.framework.ApplicationAuthenticator; +import org.wso2.carbon.identity.application.authentication.framework.FederatedApplicationAuthenticator; +import org.wso2.carbon.identity.application.authentication.framework.config.model.AuthenticatorConfig; +import org.wso2.carbon.identity.application.authentication.framework.config.model.SequenceConfig; +import org.wso2.carbon.identity.application.authentication.framework.config.model.StepConfig; +import org.wso2.carbon.identity.application.authentication.framework.context.AuthenticationContext; +import org.wso2.carbon.identity.application.authentication.framework.exception.PostAuthenticationFailedException; +import org.wso2.carbon.identity.application.authentication.framework.handler.request.AbstractPostAuthnHandler; +import org.wso2.carbon.identity.application.authentication.framework.handler.request.PostAuthnHandlerFlowStatus; +import org.wso2.carbon.identity.application.authentication.framework.internal.FrameworkServiceDataHolder; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.organization.config.service.exception.OrganizationConfigClientException; +import org.wso2.carbon.identity.organization.config.service.exception.OrganizationConfigException; +import org.wso2.carbon.identity.organization.config.service.model.ConfigProperty; +import org.wso2.carbon.identity.organization.config.service.model.DiscoveryConfig; +import org.wso2.carbon.identity.organization.discovery.service.model.OrgDiscoveryAttribute; +import org.wso2.carbon.identity.organization.management.service.OrganizationManager; +import org.wso2.carbon.identity.organization.management.service.exception.OrganizationManagementException; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkErrorConstants.ErrorMessages.ERROR_WHILE_RETRIEVING_ORG_DISCOVERY_ATTRIBUTES; +import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkErrorConstants.ErrorMessages.INVALID_EMAIL_DOMAIN; +import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkErrorConstants.ErrorMessages.NO_EMAIL_ATTRIBUTE_FOUND; + +/** + * Responsible for validating the email domain of the user during the authentication flow. + */ +public class EmailDomainValidationHandler extends AbstractPostAuthnHandler { + + private static final Log LOG = LogFactory.getLog(EmailDomainValidationHandler.class); + private static final String EMAIL_DOMAIN_ENABLE = "emailDomain.enable"; + public static final String EMAIL_DOMAIN = "emailDomain"; + + private EmailDomainValidationHandler() { + + } + + private static class Holder { + + private static final EmailDomainValidationHandler INSTANCE = new EmailDomainValidationHandler(); + } + + public static EmailDomainValidationHandler getInstance() { + + return Holder.INSTANCE; + } + + @Override + public boolean isEnabled() { + + if (!super.isEnabled()) { + return false; + } + + try { + OrganizationManager organizationManager = FrameworkServiceDataHolder.getInstance().getOrganizationManager(); + String organizationId = organizationManager.resolveOrganizationId(CarbonContext + .getThreadLocalCarbonContext().getTenantDomain()); + + if (organizationManager.isPrimaryOrganization(organizationId)) { + // Skip email domain validation since email domains cannot be mapped to primary organizations. + return false; + } + organizationId = organizationManager.getPrimaryOrganizationId(organizationId); + + return isEmailDomainDiscoveryEnabled(organizationId); + } catch (OrganizationConfigClientException e) { + if (LOG.isDebugEnabled()) { + LOG.debug("No organization discovery configurations found for tenant domain: " + CarbonContext + .getThreadLocalCarbonContext().getTenantDomain()); + } + return false; + } catch (OrganizationManagementException | OrganizationConfigException e) { + LOG.error("Error while retrieving organization discovery configuration.", e); + return false; + } + } + + @Override + public int getPriority() { + + int priority = super.getPriority(); + if (priority == -1) { + priority = 15; + } + return priority; + } + + @Override + public PostAuthnHandlerFlowStatus handle(HttpServletRequest request, HttpServletResponse response, + AuthenticationContext context) throws PostAuthenticationFailedException { + + SequenceConfig sequenceConfig = context.getSequenceConfig(); + for (Map.Entry entry : sequenceConfig.getStepMap().entrySet()) { + StepConfig stepConfig = entry.getValue(); + AuthenticatorConfig authenticatorConfig = stepConfig.getAuthenticatedAutenticator(); + if (authenticatorConfig == null) { + continue; + } + + ApplicationAuthenticator authenticator = authenticatorConfig.getApplicationAuthenticator(); + if (authenticator instanceof FederatedApplicationAuthenticator) { + Map localClaimValues; + if (stepConfig.isSubjectAttributeStep()) { + localClaimValues = + (Map) context.getProperty(FrameworkConstants.UNFILTERED_LOCAL_CLAIM_VALUES); + } else { + /* + * Need to validate even if this is not the subject attribute step since + * jit provisioning will happen in both scenarios. + */ + localClaimValues = + FrameworkUtils.getLocalClaimValuesOfIDPInNonAttributeSelectionStep(context, stepConfig, + context.getExternalIdP()); + } + + Optional emailDomain = + extractEmailDomain(localClaimValues.get(FrameworkConstants.EMAIL_ADDRESS_CLAIM)); + + if (!emailDomain.isPresent()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Email address not found or is not in the correct format." + + " Email domain validation failed for tenant: " + context.getTenantDomain()); + } + throw new PostAuthenticationFailedException(NO_EMAIL_ATTRIBUTE_FOUND.getCode(), + NO_EMAIL_ATTRIBUTE_FOUND.getMessage()); + } + + if (!isValidEmailDomain(context, emailDomain.get())) { + throw new PostAuthenticationFailedException(INVALID_EMAIL_DOMAIN.getCode(), + String.format(INVALID_EMAIL_DOMAIN.getMessage(), context.getTenantDomain())); + } + } + } + return PostAuthnHandlerFlowStatus.SUCCESS_COMPLETED; + } + + private boolean isEmailDomainDiscoveryEnabled(String primaryOrganizationId) + throws OrganizationConfigException, OrganizationManagementException { + + String tenantDomain = FrameworkServiceDataHolder.getInstance().getOrganizationManager() + .resolveTenantDomain(primaryOrganizationId); + + DiscoveryConfig discoveryConfiguration = + FrameworkServiceDataHolder.getInstance().getOrganizationConfigManager() + .getDiscoveryConfigurationByTenantId(IdentityTenantUtil.getTenantId(tenantDomain)); + List configProperties = discoveryConfiguration.getConfigProperties(); + for (ConfigProperty configProperty : configProperties) { + if (EMAIL_DOMAIN_ENABLE.equals(configProperty.getKey())) { + return Boolean.parseBoolean(configProperty.getValue()); + } + } + return false; + } + + private boolean isValidEmailDomain(AuthenticationContext context, String emaildomain) + throws PostAuthenticationFailedException { + + try { + List organizationDiscoveryAttributes = + FrameworkServiceDataHolder.getInstance().getOrganizationDiscoveryManager() + .getOrganizationDiscoveryAttributes(context.getTenantDomain(), false); + + if (organizationDiscoveryAttributes.isEmpty()) { + LOG.debug("No email domains are mapped to the organization. Skipping email domain validation."); + return true; + } + + for (OrgDiscoveryAttribute orgDiscoveryAttribute : organizationDiscoveryAttributes) { + if (!EMAIL_DOMAIN.equals(orgDiscoveryAttribute.getType())) { + continue; + } + + List mappedEmailDomains = orgDiscoveryAttribute.getValues(); + if (mappedEmailDomains != null && !mappedEmailDomains.contains(emaildomain)) { + return false; + } + } + } catch (OrganizationManagementException e) { + LOG.error( + "Error while retrieving organization discovery attributes for tenant: " + context.getTenantDomain(), + e); + throw new PostAuthenticationFailedException(ERROR_WHILE_RETRIEVING_ORG_DISCOVERY_ATTRIBUTES.getCode(), + String.format(ERROR_WHILE_RETRIEVING_ORG_DISCOVERY_ATTRIBUTES.getMessage(), + context.getTenantDomain()), e); + } + return true; + } + + private Optional extractEmailDomain(String email) { + + if (StringUtils.isBlank(email)) { + return Optional.empty(); + } + + String[] emailSplit = email.split("@"); + return emailSplit.length == 2 ? Optional.of(emailSplit[1]) : Optional.empty(); + } +} diff --git a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/JITProvisioningPostAuthenticationHandler.java b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/JITProvisioningPostAuthenticationHandler.java index ca60572e759f..289e26da8e0e 100644 --- a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/JITProvisioningPostAuthenticationHandler.java +++ b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/JITProvisioningPostAuthenticationHandler.java @@ -304,8 +304,9 @@ private PostAuthnHandlerFlowStatus handleRequestFlow(HttpServletRequest request, localClaimValues = (Map) context .getProperty(FrameworkConstants.UNFILTERED_LOCAL_CLAIM_VALUES); } else { - localClaimValues = getLocalClaimValuesOfIDPInNonAttributeSelectionStep(context, stepConfig, - externalIdPConfig); + localClaimValues = + FrameworkUtils.getLocalClaimValuesOfIDPInNonAttributeSelectionStep(context, stepConfig, + externalIdPConfig); } if (localClaimValues == null || localClaimValues.size() == 0) { Map userAttributes = stepConfig.getAuthenticatedUser().getUserAttributes(); @@ -1083,59 +1084,6 @@ private String getUserIdClaimUriInLocalDialect(ExternalIdPConfig idPConfig) { return null; } - /** - * Uses to get local claim values of an authenticated user from an IDP in non attribute selection steps. - * - * @param context Authentication Context. - * @param stepConfig Current step configuration. - * @param externalIdPConfig Identity providers config. - * @return Mapped federated user values to local claims. - * @throws PostAuthenticationFailedException Post Authentication failed exception. - */ - private Map getLocalClaimValuesOfIDPInNonAttributeSelectionStep(AuthenticationContext context, - StepConfig stepConfig, - ExternalIdPConfig externalIdPConfig) - throws PostAuthenticationFailedException { - - boolean useDefaultIdpDialect = externalIdPConfig.useDefaultLocalIdpDialect(); - ApplicationAuthenticator authenticator = - stepConfig.getAuthenticatedAutenticator().getApplicationAuthenticator(); - String idPStandardDialect = authenticator.getClaimDialectURI(); - Map extAttrs = stepConfig.getAuthenticatedUser().getUserAttributes(); - Map originalExternalAttributeValueMap = FrameworkUtils.getClaimMappings(extAttrs, false); - Map claimMapping = new HashMap<>(); - Map localClaimValues = new HashMap<>(); - if (useDefaultIdpDialect && StringUtils.isNotBlank(idPStandardDialect)) { - try { - claimMapping = ClaimMetadataHandler.getInstance() - .getMappingsMapFromOtherDialectToCarbon(idPStandardDialect, - originalExternalAttributeValueMap.keySet(), context.getTenantDomain(), - true); - } catch (ClaimMetadataException e) { - throw new PostAuthenticationFailedException(ErrorMessages.ERROR_WHILE_HANDLING_CLAIM_MAPPINGS.getCode(), - ErrorMessages.ERROR_WHILE_HANDLING_CLAIM_MAPPINGS.getMessage(), e); - } - } else { - ClaimMapping[] customClaimMapping = context.getExternalIdP().getClaimMappings(); - for (ClaimMapping externalClaim : customClaimMapping) { - if (originalExternalAttributeValueMap.containsKey(externalClaim.getRemoteClaim().getClaimUri())) { - claimMapping.put(externalClaim.getLocalClaim().getClaimUri(), - externalClaim.getRemoteClaim().getClaimUri()); - } - } - } - - if (claimMapping != null && claimMapping.size() > 0) { - for (Map.Entry entry : claimMapping.entrySet()) { - if (originalExternalAttributeValueMap.containsKey(entry.getValue()) && - originalExternalAttributeValueMap.get(entry.getValue()) != null) { - localClaimValues.put(entry.getKey(), originalExternalAttributeValueMap.get(entry.getValue())); - } - } - } - return localClaimValues; - } - private UserRealm getUserRealm(String tenantDomain) throws UserStoreException { RealmService realmService = FrameworkServiceComponent.getRealmService(); diff --git a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/internal/FrameworkServiceComponent.java b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/internal/FrameworkServiceComponent.java index 42ecadb98176..e1eb42401c0c 100644 --- a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/internal/FrameworkServiceComponent.java +++ b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/internal/FrameworkServiceComponent.java @@ -60,6 +60,7 @@ import org.wso2.carbon.identity.application.authentication.framework.handler.claims.impl.DefaultClaimFilter; import org.wso2.carbon.identity.application.authentication.framework.handler.provisioning.listener.JITProvisioningIdentityProviderMgtListener; import org.wso2.carbon.identity.application.authentication.framework.handler.request.PostAuthenticationHandler; +import org.wso2.carbon.identity.application.authentication.framework.handler.request.impl.EmailDomainValidationHandler; import org.wso2.carbon.identity.application.authentication.framework.handler.request.impl.JITProvisioningPostAuthenticationHandler; import org.wso2.carbon.identity.application.authentication.framework.handler.request.impl.PostAuthAssociationHandler; import org.wso2.carbon.identity.application.authentication.framework.handler.request.impl.PostAuthenticatedSubjectIdentifierHandler; @@ -108,6 +109,8 @@ import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.functions.library.mgt.FunctionLibraryManagementService; import org.wso2.carbon.identity.multi.attribute.login.mgt.MultiAttributeLoginService; +import org.wso2.carbon.identity.organization.config.service.OrganizationConfigManager; +import org.wso2.carbon.identity.organization.discovery.service.OrganizationDiscoveryManager; import org.wso2.carbon.identity.organization.management.service.OrganizationManagementInitialize; import org.wso2.carbon.identity.organization.management.service.OrganizationManager; import org.wso2.carbon.identity.role.v2.mgt.core.RoleManagementService; @@ -343,6 +346,8 @@ protected void activate(ComponentContext ctxt) { bundleContext .registerService(PostAuthenticationHandler.class.getName(), postAuthenticatedUserDomainHandler, null); + PostAuthenticationHandler emailDomainValidationHandler = EmailDomainValidationHandler.getInstance(); + bundleContext.registerService(PostAuthenticationHandler.class.getName(), emailDomainValidationHandler, null); if (log.isDebugEnabled()) { log.debug("Application Authentication Framework bundle is activated"); } @@ -938,7 +943,7 @@ private void loadCodeForSecrets() { unbind = "unsetFederatedAssociationManagerService" ) protected void setFederatedAssociationManagerService(FederatedAssociationManager - federatedAssociationManagerService) { + federatedAssociationManagerService) { if (log.isDebugEnabled()) { log.debug("Federated Association Manager Service is set in the Application Authentication Framework " + @@ -1069,6 +1074,37 @@ protected void unsetOrganizationManager(OrganizationManager organizationManager) FrameworkServiceDataHolder.getInstance().setOrganizationManager(null); } + @Reference( + name = "identity.organization.discovery.management.component", + service = OrganizationDiscoveryManager.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetOrganizationDiscoveryManager") + protected void setOrganizationDiscoveryManager(OrganizationDiscoveryManager organizationDiscoveryManager) { + + FrameworkServiceDataHolder.getInstance().setOrganizationDiscoveryManager(organizationDiscoveryManager); + } + + protected void unsetOrganizationDiscoveryManager(OrganizationDiscoveryManager organizationDiscoveryManager) { + + FrameworkServiceDataHolder.getInstance().setOrganizationDiscoveryManager(null); + } + + @Reference(name = "identity.organization.config.management.component", + service = OrganizationConfigManager.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetOrganizationConfigManager") + protected void setOrganizationConfigManager(OrganizationConfigManager organizationConfigManager) { + + FrameworkServiceDataHolder.getInstance().setOrganizationConfigManager(organizationConfigManager); + } + + protected void unsetOrganizationConfigManager(OrganizationConfigManager organizationConfigManager) { + + FrameworkServiceDataHolder.getInstance().setOrganizationConfigManager(null); + } + @Reference( name = "resource.configuration.manager", service = ConfigurationManager.class, diff --git a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/internal/FrameworkServiceDataHolder.java b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/internal/FrameworkServiceDataHolder.java index b821d417f881..060f90026cb6 100644 --- a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/internal/FrameworkServiceDataHolder.java +++ b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/internal/FrameworkServiceDataHolder.java @@ -53,6 +53,8 @@ import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.functions.library.mgt.FunctionLibraryManagementService; import org.wso2.carbon.identity.multi.attribute.login.mgt.MultiAttributeLoginService; +import org.wso2.carbon.identity.organization.config.service.OrganizationConfigManager; +import org.wso2.carbon.identity.organization.discovery.service.OrganizationDiscoveryManager; import org.wso2.carbon.identity.organization.management.service.OrganizationManagementInitialize; import org.wso2.carbon.identity.organization.management.service.OrganizationManager; import org.wso2.carbon.identity.role.v2.mgt.core.RoleManagementService; @@ -123,6 +125,8 @@ public class FrameworkServiceDataHolder { private boolean isAdaptiveAuthenticationAvailable = false; private boolean isOrganizationManagementEnable = false; private OrganizationManager organizationManager; + private OrganizationDiscoveryManager organizationDiscoveryManager; + private OrganizationConfigManager organizationConfigManager; private RoleManagementService roleManagementServiceV2; private SecretResolveManager secretConfigManager; @@ -778,6 +782,46 @@ public void setOrganizationManager(OrganizationManager organizationManager) { this.organizationManager = organizationManager; } + /** + * Get {@link OrganizationDiscoveryManager}. + * + * @return organization discovery manager instance {@link OrganizationDiscoveryManager}. + */ + public OrganizationDiscoveryManager getOrganizationDiscoveryManager() { + + return organizationDiscoveryManager; + } + + /** + * Set {@link OrganizationDiscoveryManager}. + * + * @param organizationDiscoveryManager Instance of {@link OrganizationDiscoveryManager}. + */ + public void setOrganizationDiscoveryManager(OrganizationDiscoveryManager organizationDiscoveryManager) { + + this.organizationDiscoveryManager = organizationDiscoveryManager; + } + + /** + * Get {@link OrganizationConfigManager}. + * + * @return organization config manager instance {@link OrganizationConfigManager}. + */ + public OrganizationConfigManager getOrganizationConfigManager() { + + return organizationConfigManager; + } + + /** + * Set {@link OrganizationConfigManager}. + * + * @param organizationConfigManager Instance of {@link OrganizationConfigManager}. + */ + public void setOrganizationConfigManager(OrganizationConfigManager organizationConfigManager) { + + this.organizationConfigManager = organizationConfigManager; + } + public IdpManager getIdentityProviderManager() { return identityProviderManager; diff --git a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/util/FrameworkErrorConstants.java b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/util/FrameworkErrorConstants.java index 9d941122f0ee..c914419e5f81 100644 --- a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/util/FrameworkErrorConstants.java +++ b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/util/FrameworkErrorConstants.java @@ -76,6 +76,11 @@ public enum ErrorMessages { ERROR_WHILE_TRYING_TO_HANDLE_ROLE_CLAIM_FOR_PROVISIONED_USER("80030", "Error while trying to handle role " + "claim for provisioned user."), ERROR_WHILE_ENCRYPTING_TOTP_SECRET_KEY("80031", "Error while encrypting TOTP secret key for user. %s"), + ERROR_WHILE_RETRIEVING_ORG_DISCOVERY_ATTRIBUTES("80032", "Error while retrieving organization discovery " + + "attributes for tenantDomain: %s"), + INVALID_EMAIL_DOMAIN("80033", + "Email domain resolved from the authenticated federated IDP is not mapped to the organization: %s"), + NO_EMAIL_ATTRIBUTE_FOUND("80034", "No email attribute returned by the authenticated federated IDP"), MISMATCHING_TENANT_DOMAIN("AFW-60001", "Service Provider tenant domain must be equal to user tenant domain for non-SaaS applications"), SYSTEM_ERROR_WHILE_AUTHENTICATING("AFW-65001", "System error while authenticating"); @@ -90,6 +95,7 @@ public enum ErrorMessages { * @param message Relevant error message. */ ErrorMessages(String code, String message) { + this.code = code; this.message = message; } @@ -100,6 +106,7 @@ public enum ErrorMessages { * @return Error code. */ public String getCode() { + return code; } @@ -109,11 +116,13 @@ public String getCode() { * @return Error message. */ public String getMessage() { + return message; } @Override public String toString() { + return code + " - " + message; } } diff --git a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/util/FrameworkUtils.java b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/util/FrameworkUtils.java index e346a3c25f52..1fe95608f060 100644 --- a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/util/FrameworkUtils.java +++ b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/util/FrameworkUtils.java @@ -4297,4 +4297,59 @@ public static boolean isURLRelative(String uriString) throws URISyntaxException return !new URI(uriString).isAbsolute(); } + + /** + * Get local claim values of an authenticated user from an IDP in non attribute selection steps. + * + * @param context Authentication Context. + * @param stepConfig Current step configuration. + * @param externalIdPConfig Identity providers config. + * @return Mapped federated user values to local claims. + * @throws PostAuthenticationFailedException Post Authentication failed exception. + */ + public static Map getLocalClaimValuesOfIDPInNonAttributeSelectionStep(AuthenticationContext context, + StepConfig stepConfig, + ExternalIdPConfig externalIdPConfig) + throws PostAuthenticationFailedException { + + boolean useDefaultIdpDialect = externalIdPConfig.useDefaultLocalIdpDialect(); + ApplicationAuthenticator authenticator = + stepConfig.getAuthenticatedAutenticator().getApplicationAuthenticator(); + String idPStandardDialect = authenticator.getClaimDialectURI(); + Map extAttrs = stepConfig.getAuthenticatedUser().getUserAttributes(); + Map originalExternalAttributeValueMap = getClaimMappings(extAttrs, false); + Map claimMapping = new HashMap<>(); + Map localClaimValues = new HashMap<>(); + + if (useDefaultIdpDialect && StringUtils.isNotBlank(idPStandardDialect)) { + try { + claimMapping = ClaimMetadataHandler.getInstance() + .getMappingsMapFromOtherDialectToCarbon(idPStandardDialect, + originalExternalAttributeValueMap.keySet(), context.getTenantDomain(), + true); + } catch (ClaimMetadataException e) { + throw new PostAuthenticationFailedException( + FrameworkErrorConstants.ErrorMessages.ERROR_WHILE_HANDLING_CLAIM_MAPPINGS.getCode(), + FrameworkErrorConstants.ErrorMessages.ERROR_WHILE_HANDLING_CLAIM_MAPPINGS.getMessage(), e); + } + } else { + ClaimMapping[] customClaimMapping = context.getExternalIdP().getClaimMappings(); + for (ClaimMapping externalClaim : customClaimMapping) { + if (originalExternalAttributeValueMap.containsKey(externalClaim.getRemoteClaim().getClaimUri())) { + claimMapping.put(externalClaim.getLocalClaim().getClaimUri(), + externalClaim.getRemoteClaim().getClaimUri()); + } + } + } + + if (claimMapping != null && !claimMapping.isEmpty()) { + for (Map.Entry entry : claimMapping.entrySet()) { + if (originalExternalAttributeValueMap.containsKey(entry.getValue()) && + originalExternalAttributeValueMap.get(entry.getValue()) != null) { + localClaimValues.put(entry.getKey(), originalExternalAttributeValueMap.get(entry.getValue())); + } + } + } + return localClaimValues; + } } diff --git a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/test/java/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/EmailDomainValidationHandlerTest.java b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/test/java/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/EmailDomainValidationHandlerTest.java new file mode 100644 index 000000000000..fdf616fdd71f --- /dev/null +++ b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/test/java/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/EmailDomainValidationHandlerTest.java @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2025, 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.application.authentication.framework.handler.request.impl; + +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.Test; +import org.wso2.carbon.context.CarbonContext; +import org.wso2.carbon.identity.application.authentication.framework.AbstractFrameworkTest; +import org.wso2.carbon.identity.application.authentication.framework.FederatedApplicationAuthenticator; +import org.wso2.carbon.identity.application.authentication.framework.config.loader.UIBasedConfigurationLoader; +import org.wso2.carbon.identity.application.authentication.framework.config.model.AuthenticatorConfig; +import org.wso2.carbon.identity.application.authentication.framework.config.model.ExternalIdPConfig; +import org.wso2.carbon.identity.application.authentication.framework.config.model.SequenceConfig; +import org.wso2.carbon.identity.application.authentication.framework.config.model.StepConfig; +import org.wso2.carbon.identity.application.authentication.framework.context.AuthenticationContext; +import org.wso2.carbon.identity.application.authentication.framework.exception.PostAuthenticationFailedException; +import org.wso2.carbon.identity.application.authentication.framework.handler.request.PostAuthnHandlerFlowStatus; +import org.wso2.carbon.identity.application.authentication.framework.internal.FrameworkServiceDataHolder; +import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants; +import org.wso2.carbon.identity.application.common.model.ClaimMapping; +import org.wso2.carbon.identity.application.common.model.ServiceProvider; +import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataHandler; +import org.wso2.carbon.identity.common.testng.WithCarbonHome; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.organization.config.service.OrganizationConfigManager; +import org.wso2.carbon.identity.organization.config.service.exception.OrganizationConfigClientException; +import org.wso2.carbon.identity.organization.config.service.model.ConfigProperty; +import org.wso2.carbon.identity.organization.config.service.model.DiscoveryConfig; +import org.wso2.carbon.identity.organization.discovery.service.OrganizationDiscoveryManager; +import org.wso2.carbon.identity.organization.discovery.service.model.OrgDiscoveryAttribute; +import org.wso2.carbon.identity.organization.management.service.OrganizationManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link EmailDomainValidationHandler} + */ +@WithCarbonHome +public class EmailDomainValidationHandlerTest extends AbstractFrameworkTest { + + private static final String VALID_EMAIL = "user@test.com"; + private static final String INVALID_EMAIL = "user@testInvalid.com"; + private static final String SUPER_ORG_ID = "10084a8d-113f-4211-a0d5-efe36b082211"; + private static final String SUB_ORG_ID = "93d996f9-a5ba-4275-a52b-adaad9eba869"; + public static final String SUPER_ORG_TENANT_DOMAIN = "carbon.super"; + public static final String SUB_ORG_TENANT_DOMAIN = "test"; + public static final String EMAIL_ADDRESS_CLAIM_URI = "http://wso2.org/claims/emailaddress"; + public static final String EMAIL = "email"; + + private MockedStatic carbonContextMockedStatic; + private MockedStatic identityTenantUtil; + private MockedStatic claimMetadataHandler; + @Mock + private OrganizationDiscoveryManager organizationDiscoveryManager; + @Mock + private OrganizationConfigManager organizationConfigManager; + @Mock + private OrganizationManager organizationManager; + private CarbonContext carbonContext; + private EmailDomainValidationHandler emailDomainValidationHandler; + private HttpServletRequest request; + private HttpServletResponse response; + private UIBasedConfigurationLoader configurationLoader; + private ServiceProvider sp; + private AutoCloseable mocks; + + @BeforeClass + public void setUp() throws Exception { + + mocks = MockitoAnnotations.openMocks(this); + request = mock(HttpServletRequest.class); + response = mock(HttpServletResponse.class); + configurationLoader = new UIBasedConfigurationLoader(); + + identityTenantUtil = mockStatic(IdentityTenantUtil.class); + claimMetadataHandler = mockStatic(ClaimMetadataHandler.class); + + emailDomainValidationHandler = EmailDomainValidationHandler.getInstance(); + sp = getTestServiceProvider("email-domain-validation-sp.xml"); + + FrameworkServiceDataHolder.getInstance().setOrganizationDiscoveryManager(organizationDiscoveryManager); + FrameworkServiceDataHolder.getInstance().setOrganizationConfigManager(organizationConfigManager); + FrameworkServiceDataHolder.getInstance().setOrganizationManager(organizationManager); + + List orgDiscoveryAttributes = new ArrayList<>(); + OrgDiscoveryAttribute orgDiscoveryAttribute = new OrgDiscoveryAttribute(); + orgDiscoveryAttribute.setType("emailDomain"); + orgDiscoveryAttribute.setValues(Collections.singletonList("test.com")); + orgDiscoveryAttributes.add(orgDiscoveryAttribute); + when(organizationDiscoveryManager.getOrganizationDiscoveryAttributes(anyString(), anyBoolean())).thenReturn( + orgDiscoveryAttributes); + + carbonContextMockedStatic = mockStatic(CarbonContext.class); + carbonContext = mock(CarbonContext.class); + carbonContextMockedStatic.when(CarbonContext::getThreadLocalCarbonContext).thenReturn(carbonContext); + when(carbonContext.getTenantDomain()).thenReturn(SUPER_ORG_TENANT_DOMAIN); + } + + @AfterClass + public void tearDown() throws Exception { + + carbonContextMockedStatic.close(); + identityTenantUtil.close(); + claimMetadataHandler.close(); + mocks.close(); + } + + @Test(description = "Test whether the email domain validation handler is disabled for primary organizations.") + public void testIsDisabledForPrimaryOrganizations() throws Exception { + + when(carbonContext.getTenantDomain()).thenReturn(SUPER_ORG_TENANT_DOMAIN); + when(organizationManager.resolveOrganizationId(SUPER_ORG_TENANT_DOMAIN)).thenReturn(SUPER_ORG_ID); + when(organizationManager.isPrimaryOrganization(SUPER_ORG_ID)).thenReturn(true); + Assert.assertFalse(emailDomainValidationHandler.isEnabled(), + "Email domain validation handler should be disabled for primary organizations."); + } + + @Test(description = "Test whether the email domain validation handler is enabled for sub organizations when " + + "email domain discovery is enabled.") + public void testIsEnabledForSubOrganizationsWhenEmailDomainDiscoveryEnabled() throws Exception { + + reset(organizationConfigManager); + when(carbonContext.getTenantDomain()).thenReturn(SUB_ORG_TENANT_DOMAIN); + when(organizationManager.resolveOrganizationId(SUB_ORG_TENANT_DOMAIN)).thenReturn(SUB_ORG_ID); + when(organizationManager.isPrimaryOrganization(SUB_ORG_ID)).thenReturn(false); + when(organizationManager.getPrimaryOrganizationId(SUB_ORG_ID)).thenReturn(SUPER_ORG_ID); + when(organizationManager.resolveTenantDomain(SUPER_ORG_ID)).thenReturn(SUPER_ORG_TENANT_DOMAIN); + + identityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(SUPER_ORG_TENANT_DOMAIN)).thenReturn(-1234); + + List configProperties = new ArrayList<>(); + ConfigProperty configProperty = new ConfigProperty("emailDomain.enable", "true"); + configProperties.add(configProperty); + DiscoveryConfig discoveryConfig = new DiscoveryConfig(configProperties); + when(organizationConfigManager.getDiscoveryConfigurationByTenantId(-1234)).thenReturn(discoveryConfig); + + Assert.assertTrue(emailDomainValidationHandler.isEnabled(), + "Email domain validation handler should be enabled for" + + "sub organizations when email domain discovery is enabled."); + } + + @Test(description = "Test whether the email domain validation handler is disabled for sub organizations when " + + "email domain discovery is disabled.") + public void testIsDisabledWhenNoDiscoveryConfigsForOrganization() throws Exception { + + when(carbonContext.getTenantDomain()).thenReturn(SUB_ORG_TENANT_DOMAIN); + when(organizationManager.resolveOrganizationId(SUB_ORG_TENANT_DOMAIN)).thenReturn(SUB_ORG_ID); + when(organizationManager.isPrimaryOrganization(SUB_ORG_ID)).thenReturn(false); + when(organizationManager.getPrimaryOrganizationId(SUB_ORG_ID)).thenReturn(SUPER_ORG_ID); + when(organizationManager.resolveTenantDomain(SUPER_ORG_ID)).thenReturn(SUPER_ORG_TENANT_DOMAIN); + + identityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(SUPER_ORG_TENANT_DOMAIN)).thenReturn(-1234); + + when(organizationConfigManager.getDiscoveryConfigurationByTenantId(-1234)).thenThrow( + new OrganizationConfigClientException("No organization configs found.")); + + Assert.assertFalse(emailDomainValidationHandler.isEnabled(), + "Email domain validation handler should be disabled when there are no discovery" + + " configurations for the organization."); + } + + @Test(description = "Test if the validation pass with a valid email domain for the authenticated user.") + public void testAuthenticatedUserWithValidEmailDomain() throws Exception { + + AuthenticationContext context = buildAuthenticationContext(sp, VALID_EMAIL, false); + PostAuthnHandlerFlowStatus status = emailDomainValidationHandler.handle(request, response, context); + Assert.assertEquals(status, PostAuthnHandlerFlowStatus.SUCCESS_COMPLETED, + "Expected the email domain validation handler to succeed with a valid email domain."); + } + + @Test(description = "Test if the validation fails with an invalid email domain for the authenticated user.", + expectedExceptions = PostAuthenticationFailedException.class) + public void testAuthenticatedUserWithInvalidEmailDomain() throws Exception { + + AuthenticationContext context = buildAuthenticationContext(sp, INVALID_EMAIL, false); + emailDomainValidationHandler.handle(request, response, context); + } + + @Test(description = "Test if the validation pass with a valid email domain when the user authenticates in a" + + " non-subject attribute step.") + public void testAuthenticatedUserWithValidEmailDomainAndNotSubjectAttributeStep() throws Exception { + + AuthenticationContext context = buildAuthenticationContext(sp, VALID_EMAIL, true); + + Map mockedMappings = new HashMap<>(); + mockedMappings.put(EMAIL_ADDRESS_CLAIM_URI, EMAIL); + + ClaimMetadataHandler mockClaimMetadataHandler = mock(ClaimMetadataHandler.class); + when(mockClaimMetadataHandler.getMappingsMapFromOtherDialectToCarbon( + anyString(), anySet(), anyString(), anyBoolean())).thenReturn(mockedMappings); + claimMetadataHandler.when(ClaimMetadataHandler::getInstance).thenReturn(mockClaimMetadataHandler); + + PostAuthnHandlerFlowStatus status = emailDomainValidationHandler.handle(request, response, context); + Assert.assertEquals(status, PostAuthnHandlerFlowStatus.SUCCESS_COMPLETED, + "Expected the email domain validation handler to succeed with a valid email domain when the user " + + "authenticates in a non-subject attribute step."); + } + + private AuthenticationContext buildAuthenticationContext(ServiceProvider sp, String userEmail, + boolean notSubjectAttributeStep) throws Exception { + + AuthenticationContext authenticationContext = getAuthenticationContext(sp); + authenticationContext.setProperty(FrameworkConstants.STEP_BASED_SEQUENCE_HANDLER_TRIGGERED, true); + SequenceConfig sequenceConfig = + configurationLoader.getSequenceConfig(authenticationContext, Collections.emptyMap(), sp); + authenticationContext.setSequenceConfig(sequenceConfig); + + FederatedApplicationAuthenticator authenticator = mock(FederatedApplicationAuthenticator.class); + AuthenticatorConfig authenticatorConfig = new AuthenticatorConfig(); + when(authenticator.getClaimDialectURI()).thenReturn("http://wso2.org/oidc/claim"); + authenticatorConfig.setApplicationAuthenticator(authenticator); + + AuthenticatedUser user = new AuthenticatedUser(); + user.setUserName(userEmail); + user.setAuthenticatedSubjectIdentifier(userEmail); + + Map userAttributes = new HashMap<>(); + userAttributes.put(ClaimMapping.build( + EMAIL_ADDRESS_CLAIM_URI, + EMAIL, + null, false), userEmail); + + user.setUserAttributes(userAttributes); + + for (Map.Entry entry : sequenceConfig.getStepMap().entrySet()) { + StepConfig stepConfig = entry.getValue(); + stepConfig.setAuthenticatedUser(user); + stepConfig.setAuthenticatedAutenticator(authenticatorConfig); + if (notSubjectAttributeStep) { + stepConfig.setSubjectAttributeStep(false); + } + } + + if (notSubjectAttributeStep) { + ExternalIdPConfig externalIdPConfig = mock(ExternalIdPConfig.class); + when(externalIdPConfig.useDefaultLocalIdpDialect()).thenReturn(true); + authenticationContext.setExternalIdP(externalIdPConfig); + } + + authenticationContext.setSequenceConfig(sequenceConfig); + + Map unfilteredLocalClaimValues = new HashMap<>(); + unfilteredLocalClaimValues.put(EMAIL_ADDRESS_CLAIM_URI, userEmail); + authenticationContext.setProperty(FrameworkConstants.UNFILTERED_LOCAL_CLAIM_VALUES, unfilteredLocalClaimValues); + + return authenticationContext; + } +} diff --git a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/test/resources/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/email-domain-validation-sp.xml b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/test/resources/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/email-domain-validation-sp.xml new file mode 100644 index 000000000000..00f25e06a228 --- /dev/null +++ b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/test/resources/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/email-domain-validation-sp.xml @@ -0,0 +1,55 @@ + + + + 1 + default + Default Service Provider + + + + default + + + + + + + + + 1 + + + BasicMockAuthenticator + basicauth + true + + + true + true + + + + + + + + true + + + diff --git a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/test/resources/testng.xml b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/test/resources/testng.xml index 6a92baeb22da..4209f59bb02e 100644 --- a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/test/resources/testng.xml +++ b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/test/resources/testng.xml @@ -29,6 +29,7 @@ + diff --git a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml index 62e241854070..6a280df3d67d 100644 --- a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml +++ b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/identity.xml @@ -1322,6 +1322,12 @@ name="org.wso2.carbon.identity.mgt.listener.UserSessionTerminationListener" orderId="85" enable="true"/> + + + + + org.wso2.carbon.identity.organization.management.service ${org.wso2.carbon.identity.organization.management.core.version} + + org.wso2.carbon.identity.organization.management + org.wso2.carbon.identity.organization.discovery.service + ${org.wso2.carbon.identity.organization.management.version} + + + org.wso2.carbon.identity.organization.management + org.wso2.carbon.identity.organization.config.service + ${org.wso2.carbon.identity.organization.management.version} + @@ -1857,10 +1867,14 @@ [5.14.0, 8.0.0) [0.0.0,2.0.0) - 1.0.90 + 1.1.19 + 1.4.57 + [1.0.0, 2.0.0) + [1.0.0, 2.0.0) + 4.8.37