From 801638335fb3696bf67a09584a49ed1eee499400 Mon Sep 17 00:00:00 2001 From: dhaura Date: Wed, 21 Aug 2024 19:31:22 +0530 Subject: [PATCH] Add support for login_hint for param in organization discovery. --- .../login/OrganizationAuthenticator.java | 84 +++++++++++++++++++ .../login/OrganizationAuthenticatorTest.java | 74 ++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/components/org.wso2.carbon.identity.application.authenticator.organization.login/src/main/java/org/wso2/carbon/identity/application/authenticator/organization/login/OrganizationAuthenticator.java b/components/org.wso2.carbon.identity.application.authenticator.organization.login/src/main/java/org/wso2/carbon/identity/application/authenticator/organization/login/OrganizationAuthenticator.java index 095cf38..7429254 100644 --- a/components/org.wso2.carbon.identity.application.authenticator.organization.login/src/main/java/org/wso2/carbon/identity/application/authenticator/organization/login/OrganizationAuthenticator.java +++ b/components/org.wso2.carbon.identity.application.authenticator.organization.login/src/main/java/org/wso2/carbon/identity/application/authenticator/organization/login/OrganizationAuthenticator.java @@ -143,6 +143,7 @@ import static org.wso2.carbon.identity.organization.management.service.constant.OrganizationManagementConstants.ErrorMessages.ERROR_CODE_ERROR_RETRIEVING_ORGANIZATIONS_BY_NAME; import static org.wso2.carbon.identity.organization.management.service.constant.OrganizationManagementConstants.ErrorMessages.ERROR_CODE_ERROR_RETRIEVING_ORGANIZATION_NAME_BY_ID; import static org.wso2.carbon.identity.organization.management.service.constant.OrganizationManagementConstants.ErrorMessages.ERROR_CODE_ERROR_VALIDATING_ORGANIZATION_DISCOVERY_ATTRIBUTE; +import static org.wso2.carbon.identity.organization.management.service.constant.OrganizationManagementConstants.ErrorMessages.ERROR_CODE_ERROR_VALIDATING_ORGANIZATION_LOGIN_HINT_ATTRIBUTE; import static org.wso2.carbon.identity.organization.management.service.constant.OrganizationManagementConstants.ErrorMessages.ERROR_CODE_INVALID_APPLICATION; import static org.wso2.carbon.identity.organization.management.service.constant.OrganizationManagementConstants.ErrorMessages.ERROR_CODE_INVALID_ORGANIZATION_ID; import static org.wso2.carbon.identity.organization.management.service.constant.OrganizationManagementConstants.ErrorMessages.ERROR_CODE_ORGANIZATION_NOT_FOUND_FOR_TENANT; @@ -168,6 +169,7 @@ public class OrganizationAuthenticator extends OpenIDConnectAuthenticator { "\n" + "\n" + ""; + private static final String EMAIL_DOMAIN_DISCOVERY_TYPE = "emailDomain"; @Override public String getFriendlyName() { @@ -407,6 +409,13 @@ public AuthenticatorFlowStatus process(HttpServletRequest request, HttpServletRe context.setProperty(ORG_ID_PARAMETER, organizationId); String organizationName = getOrganizationNameById(organizationId); context.setProperty(ORG_PARAMETER, organizationName); + } else if (request.getParameterMap().containsKey(LOGIN_HINT_PARAMETER)) { + String loginHint = request.getParameter(LOGIN_HINT_PARAMETER); + context.setProperty(ORG_DISCOVERY_PARAMETER, loginHint); + if (!validateLoginHintAttributeValue(loginHint, context, request, response)) { + context.removeProperty(ORG_DISCOVERY_PARAMETER); + return AuthenticatorFlowStatus.INCOMPLETE; + } } else if (request.getParameterMap().containsKey(ORG_DISCOVERY_PARAMETER)) { String discoveryInput = request.getParameter(ORG_DISCOVERY_PARAMETER); context.setProperty(ORG_DISCOVERY_PARAMETER, discoveryInput); @@ -519,6 +528,49 @@ private boolean validateOrganizationName(String organizationName, Authentication return false; } + /** + * Validates given login_hint parameter. + * + * @param loginHintInput Given login_hint parameter value. + * @param request Servlet request. + * @param response Servlet response. + * @param context Authentication context. + * @return True if the login_hint parameter is valid. + * @throws AuthenticationFailedException If an error occurs while validating login_hint parameter. + */ + private boolean validateLoginHintAttributeValue(String loginHintInput, AuthenticationContext context, + HttpServletRequest request, HttpServletResponse response) + throws AuthenticationFailedException { + + // Default discovery type is set to `emailDomain`. + String discoveryType = EMAIL_DOMAIN_DISCOVERY_TYPE; + if (request.getParameterMap().containsKey(ORGANIZATION_DISCOVERY_TYPE)) { + discoveryType = request.getParameter(ORGANIZATION_DISCOVERY_TYPE); + } + + if (!isOrganizationDiscoveryTypeEnabled(discoveryType)) { + context.setProperty(ORGANIZATION_LOGIN_FAILURE, "Organization discovery type is invalid or not enabled"); + redirectToOrgDiscoveryInputCapture(response, context); + return false; + } + context.setProperty(ORGANIZATION_DISCOVERY_TYPE, discoveryType); + + try { + String appResideOrgId = getOrgIdByTenantDomain(context.getLoginTenantDomain()); + String organizationId = getOrganizationDiscoveryManager().getOrganizationIdByDiscoveryAttribute + (discoveryType, loginHintInput, appResideOrgId); + if (StringUtils.isNotBlank(organizationId)) { + context.setProperty(ORG_ID_PARAMETER, organizationId); + return true; + } + context.setProperty(ORGANIZATION_LOGIN_FAILURE, "Can't identify organization"); + redirectToOrgDiscoveryInputCapture(response, context); + return false; + } catch (OrganizationManagementException e) { + throw handleAuthFailures(ERROR_CODE_ERROR_VALIDATING_ORGANIZATION_LOGIN_HINT_ATTRIBUTE, e); + } + } + private boolean validateDiscoveryAttributeValue(String discoveryInput, AuthenticationContext context, HttpServletResponse response) throws AuthenticationFailedException { @@ -650,6 +702,38 @@ private String getQueryParams(AuthenticationContext context, ClaimMapping[] clai return paramBuilder.toString(); } + /** + * Check whether the given organization discovery type is enabled. + * + * @param discoveryType Organization discovery type. + * @return True if the organization discovery type is enabled. + * @throws AuthenticationFailedException If an error occurs while checking the organization discovery type. + */ + private boolean isOrganizationDiscoveryTypeEnabled(String discoveryType) + throws AuthenticationFailedException { + + try { + DiscoveryConfig discoveryConfig = getOrganizationConfigManager().getDiscoveryConfiguration(); + Map discoveryHandlers = + getOrganizationDiscoveryManager().getAttributeBasedOrganizationDiscoveryHandlers(); + + List configProperties = discoveryConfig.getConfigProperties(); + for (ConfigProperty configProperty : configProperties) { + String type = configProperty.getKey().split(ENABLE_CONFIG)[0]; + if (discoveryType.equals(type) && discoveryHandlers.get(type) != null && + Boolean.parseBoolean(configProperty.getValue())) { + return true; + } + } + } catch (OrganizationConfigException e) { + if (ERROR_CODE_DISCOVERY_CONFIG_NOT_EXIST.getCode().equals(e.getErrorCode())) { + return false; + } + throw handleAuthFailures(ERROR_CODE_ERROR_GETTING_ORGANIZATION_DISCOVERY_CONFIG, e); + } + return false; + } + private boolean isOrganizationDiscoveryEnabled(AuthenticationContext context) throws AuthenticationFailedException { try { diff --git a/components/org.wso2.carbon.identity.application.authenticator.organization.login/src/test/java/org/wso2/carbon/identity/application/authenticator/organization/login/OrganizationAuthenticatorTest.java b/components/org.wso2.carbon.identity.application.authenticator.organization.login/src/test/java/org/wso2/carbon/identity/application/authenticator/organization/login/OrganizationAuthenticatorTest.java index 898f792..9ba7361 100644 --- a/components/org.wso2.carbon.identity.application.authenticator.organization.login/src/test/java/org/wso2/carbon/identity/application/authenticator/organization/login/OrganizationAuthenticatorTest.java +++ b/components/org.wso2.carbon.identity.application.authenticator.organization.login/src/test/java/org/wso2/carbon/identity/application/authenticator/organization/login/OrganizationAuthenticatorTest.java @@ -18,11 +18,13 @@ package org.wso2.carbon.identity.application.authenticator.organization.login; +import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import org.wso2.carbon.base.CarbonBaseConstants; import org.wso2.carbon.context.PrivilegedCarbonContext; @@ -43,7 +45,10 @@ import org.wso2.carbon.identity.oauth.OAuthAdminServiceImpl; import org.wso2.carbon.identity.oauth.dto.OAuthConsumerAppDTO; import org.wso2.carbon.identity.organization.config.service.OrganizationConfigManager; +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.AttributeBasedOrganizationDiscoveryHandler; +import org.wso2.carbon.identity.organization.discovery.service.OrganizationDiscoveryManager; import org.wso2.carbon.identity.organization.management.application.OrgApplicationManager; import org.wso2.carbon.identity.organization.management.service.OrganizationManager; import org.wso2.carbon.identity.organization.management.service.exception.OrganizationManagementServerException; @@ -57,6 +62,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -69,11 +75,15 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; import static org.wso2.carbon.base.MultitenantConstants.SUPER_TENANT_ID; import static org.wso2.carbon.identity.application.authenticator.organization.login.constant.AuthenticatorConstants.AUTHENTICATOR_FRIENDLY_NAME; import static org.wso2.carbon.identity.application.authenticator.organization.login.constant.AuthenticatorConstants.AUTHENTICATOR_NAME; +import static org.wso2.carbon.identity.application.authenticator.organization.login.constant.AuthenticatorConstants.ENABLE_CONFIG; import static org.wso2.carbon.identity.application.authenticator.organization.login.constant.AuthenticatorConstants.INBOUND_AUTH_TYPE_OAUTH; +import static org.wso2.carbon.identity.application.authenticator.organization.login.constant.AuthenticatorConstants.LOGIN_HINT_PARAMETER; import static org.wso2.carbon.identity.application.authenticator.organization.login.constant.AuthenticatorConstants.OIDC_CLAIM_DIALECT_URL; +import static org.wso2.carbon.identity.application.authenticator.organization.login.constant.AuthenticatorConstants.ORGANIZATION_DISCOVERY_TYPE; import static org.wso2.carbon.identity.application.authenticator.organization.login.constant.AuthenticatorConstants.ORG_ID_PARAMETER; import static org.wso2.carbon.identity.application.authenticator.organization.login.constant.AuthenticatorConstants.ORG_PARAMETER; import static org.wso2.carbon.identity.organization.management.service.constant.OrganizationManagementConstants.ErrorMessages.ERROR_CODE_APPLICATION_NOT_SHARED; @@ -100,6 +110,11 @@ public class OrganizationAuthenticatorTest { private static final String clientId = "3_TCRZ93rTQtPL8k02_trEYTfVca"; private static final String secretKey = "uW4q6dYgSaHJIv11Llqi1nvOQBUa"; + private static final String emailDomainDiscoveryType = "emailDomain"; + private static final String invalidDiscoveryType = "invalidDiscoveryType"; + private static final String userEmailWithValidDomain = "john@wso2.com"; + private static final String userEmailWithInvalidDomain = "john@incorrect.wso2.com"; + private static Map authenticatorParamProperties; private static Map authenticatorProperties; private static Map mockContextParam; @@ -126,6 +141,9 @@ public class OrganizationAuthenticatorTest { private DiscoveryConfig mockDiscoveryConfig; private MockedStatic mockedUtilities; + @Mock + private OrganizationDiscoveryManager mockOrganizationDiscoveryManager; + @BeforeClass public void setUp() { @@ -136,6 +154,7 @@ public void setUp() { @BeforeMethod public void init() throws UserStoreException { + initMocks(this); mockServletRequest = mock(HttpServletRequest.class); mockServletResponse = mock(HttpServletResponse.class); mockAuthenticationContext = mock(AuthenticationContext.class); @@ -167,6 +186,7 @@ public void init() throws UserStoreException { authenticatorDataHolder.setApplicationManagementService(mockApplicationManagementService); authenticatorDataHolder.setClaimMetadataManagementService(mockClaimMetadataManagementService); authenticatorDataHolder.setOrganizationConfigManager(mockOrganizationConfigManager); + authenticatorDataHolder.setOrganizationDiscoveryManager(mockOrganizationDiscoveryManager); Tenant tenant = mock(Tenant.class); TenantManager mockTenantManager = mock(TenantManager.class); when(mockRealmService.getTenantManager()).thenReturn(mockTenantManager); @@ -444,6 +464,60 @@ public void testInitiateAuthenticationRequestInvalidSharedApp() throws Exception mockAuthenticationContext); } + @DataProvider(name = "invalidOrgDiscoveryParams") + public Object[][] getInvalidOrgDiscoveryParams() { + + return new Object[][]{ + // When the given discovery type is not valid. + {userEmailWithValidDomain, invalidDiscoveryType, new ArrayList<>(Collections.singletonList( + new ConfigProperty(emailDomainDiscoveryType + ENABLE_CONFIG, "true")))}, + // When the given discovery type is valid but not enabled. + {userEmailWithValidDomain, emailDomainDiscoveryType, new ArrayList<>(Collections.singletonList( + new ConfigProperty(emailDomainDiscoveryType + ENABLE_CONFIG, "false")))}, + // When the given email domain of the user email is invalid. + {userEmailWithInvalidDomain, emailDomainDiscoveryType, new ArrayList<>(Collections.singletonList( + new ConfigProperty(emailDomainDiscoveryType + ENABLE_CONFIG, "true")))} + }; + } + + @Test(dataProvider = "invalidOrgDiscoveryParams") + public void testProcessWithInvalidOrgDiscoveryParam(String userEmail, String discoveryType, + List configProperties) throws Exception { + + Map mockParamMap = new HashMap<>(); + mockParamMap.put(LOGIN_HINT_PARAMETER, new String[]{userEmail}); + mockParamMap.put(ORGANIZATION_DISCOVERY_TYPE, new String[]{discoveryType}); + when(mockServletRequest.getParameterMap()).thenReturn(mockParamMap); + when(mockServletRequest.getParameter(LOGIN_HINT_PARAMETER)).thenReturn(userEmail); + when(mockServletRequest.getParameter(ORGANIZATION_DISCOVERY_TYPE)).thenReturn(discoveryType); + + when(authenticatorDataHolder.getOrganizationConfigManager().getDiscoveryConfiguration()) + .thenReturn(mockDiscoveryConfig); + when(mockDiscoveryConfig.getConfigProperties()).thenReturn(configProperties); + + Map discoveryHandlers = new HashMap<>(); + AttributeBasedOrganizationDiscoveryHandler discoveryHandler = + mock(AttributeBasedOrganizationDiscoveryHandler.class); + discoveryHandlers.put(emailDomainDiscoveryType, discoveryHandler); + when(authenticatorDataHolder.getOrganizationDiscoveryManager().getAttributeBasedOrganizationDiscoveryHandlers()) + .thenReturn(discoveryHandlers); + + when(mockAuthenticationContext.getLoginTenantDomain()).thenReturn(saasAppOwnedTenant); + when(authenticatorDataHolder.getOrganizationManager().resolveOrganizationId(saasAppOwnedTenant)).thenReturn( + saasAppOwnedOrgId); + when(authenticatorDataHolder.getOrganizationDiscoveryManager() + .getOrganizationIdByDiscoveryAttribute(discoveryType, userEmail, saasAppOwnedOrgId)).thenReturn(null); + + when(mockAuthenticationContext.getContextIdentifier()).thenReturn(contextIdentifier); + when(mockAuthenticationContext.getExternalIdP()).thenReturn(mockExternalIdPConfig); + when(mockAuthenticationContext.getServiceProviderResourceId()).thenReturn(saasAppResourceId); + when(mockExternalIdPConfig.getName()).thenReturn(AUTHENTICATOR_FRIENDLY_NAME); + + AuthenticatorFlowStatus status = organizationAuthenticator.process(mockServletRequest, mockServletResponse, + mockAuthenticationContext); + Assert.assertEquals(status, AuthenticatorFlowStatus.INCOMPLETE); + } + private void setMockContextParamForValidOrganization() { mockContextParam.put(ORG_PARAMETER, org);