diff --git a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/cache/SessionContextCache.java b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/cache/SessionContextCache.java
index 93573f7c72d8..e8589af2aa5c 100644
--- a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/cache/SessionContextCache.java
+++ b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/cache/SessionContextCache.java
@@ -82,6 +82,25 @@ public void addToCache(SessionContextCacheKey key, SessionContextCacheEntry entr
optimizeAndStoreSessionData(key, entry);
}
+ /**
+ * Add the given session context entry to the cache only, without persisting it to the session data store.
+ *
+ * @param key Key which the cache entry is indexed by.
+ * @param entry Value to be stored in the cache.
+ * @param loginTenantDomain Login tenant domain under which the cache entry is stored.
+ */
+ public void addToCacheWithoutPersisting(SessionContextCacheKey key, SessionContextCacheEntry entry,
+ String loginTenantDomain) {
+
+ if (log.isDebugEnabled()) {
+ log.debug("Adding session context to cache without updating the session data store corresponding " +
+ "to the key : " + key.getContextId() + " with accessed time " + entry.getAccessedTime() +
+ " and validity time " + entry.getValidityPeriod());
+ }
+ entry.setAccessedTime();
+ super.addToCache(key, entry, resolveLoginTenantDomain(loginTenantDomain));
+ }
+
/**
* Add a cache entry during a READ operation.
*
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/DefaultAuthenticationRequestHandler.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/DefaultAuthenticationRequestHandler.java
index ca5ef1da9c59..083fe202eee3 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/DefaultAuthenticationRequestHandler.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/DefaultAuthenticationRequestHandler.java
@@ -65,6 +65,7 @@
import org.wso2.carbon.identity.core.URLBuilderException;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.core.util.IdentityUtil;
+import org.wso2.carbon.identity.organization.management.service.exception.OrganizationManagementException;
import org.wso2.carbon.idp.mgt.IdentityProviderManagementException;
import org.wso2.carbon.idp.mgt.IdentityProviderManager;
import org.wso2.carbon.idp.mgt.util.IdPManagementUtil;
@@ -81,8 +82,10 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -512,27 +515,17 @@ protected void concludeFlow(HttpServletRequest request, HttpServletResponse resp
SessionContext loadedSessionContext = FrameworkUtils.getSessionContextFromCache(
sessionContextKey, context.getLoginTenantDomain());
if (loadedSessionContext != null) {
- if (context.isSharedAppLogin()) {
- String authenticatedOrgId =
- context.getOrganizationLoginData().getAccessingOrganization().getId();
- if (MapUtils.isNotEmpty(loadedSessionContext.getAuthenticatedOrgData()) &&
- loadedSessionContext.getAuthenticatedOrgData().get(authenticatedOrgId) != null) {
- sessionContext = loadedSessionContext;
- }
- } else if (context.isOrgApplicationLogin()) {
- String accessingOrgId = PrivilegedCarbonContext.getThreadLocalCarbonContext()
- .getAccessingOrganizationId();
- if (MapUtils.isNotEmpty(loadedSessionContext.getAuthenticatedOrgData()) &&
- loadedSessionContext.getAuthenticatedOrgData().get(accessingOrgId) != null) {
- sessionContext = loadedSessionContext;
- }
- } else {
+ boolean isSubOrgLogin = context.isSharedAppLogin() || context.isOrgApplicationLogin();
+ boolean isPreviousSessionApplicable = !isSubOrgLogin
+ || isAuthenticatedOrgSessionsApplicable(context, loadedSessionContext);
+
+ if (isPreviousSessionApplicable) {
sessionContext = loadedSessionContext;
- }
- if (sessionContext == null) {
+ } else {
if (log.isDebugEnabled()) {
- log.debug("Previous session context is not applicable for the current authentication." +
- " Hence, a new session context will be created for the authentication flow.");
+ log.debug("Previous session context is not applicable for the current " +
+ "authentication. Hence, a new session context will be created for the " +
+ "authentication flow.");
}
FrameworkUtils.removeSessionContextFromCache(sessionContextKey,
context.getLoginTenantDomain());
@@ -557,16 +550,31 @@ protected void concludeFlow(HttpServletRequest request, HttpServletResponse resp
}
AuthenticatedOrgData authenticatedOrgData =
sessionContext.getAuthenticatedOrgData().get(organizationId);
- authenticatedOrgData.getAuthenticatedSequences()
- .put(appConfig.getApplicationName(), sequenceConfig);
- authenticatedOrgData.getAuthenticatedIdPs().putAll(context.getCurrentAuthenticatedIdPs());
- if (!context.isPassiveAuthenticate()) {
- setAuthenticatedIDPsOfApp(authenticatedOrgData, context.getCurrentAuthenticatedIdPs(),
- appConfig.getApplicationName());
- }
if (context.isSharedAppLogin()) {
sessionContext.setAuthenticatedSharedAppOrgId(organizationId);
}
+ if (authenticatedOrgData != null) {
+ authenticatedOrgData.getAuthenticatedSequences()
+ .put(appConfig.getApplicationName(), sequenceConfig);
+ authenticatedOrgData.getAuthenticatedIdPs().putAll(context.getCurrentAuthenticatedIdPs());
+ if (!context.isPassiveAuthenticate()) {
+ setAuthenticatedIDPsOfApp(authenticatedOrgData, context.getCurrentAuthenticatedIdPs(),
+ appConfig.getApplicationName());
+ }
+ } else {
+ // currently accessing org is not previously authenticated.
+ AuthenticatedOrgData accessingOrgAuthenticatedOrgData = new AuthenticatedOrgData();
+ accessingOrgAuthenticatedOrgData.getAuthenticatedSequences().put(appConfig.getApplicationName(),
+ sequenceConfig);
+ // Consider both the previous and the current authenticated IdPs when an IdP with the same
+ // name is present in both.
+ accessingOrgAuthenticatedOrgData.setAuthenticatedIdPs(mergeAuthenticatedIdPs(
+ context.getPreviousAuthenticatedIdPs(), context.getCurrentAuthenticatedIdPs()));
+ setAuthenticatedIDPsOfApp(accessingOrgAuthenticatedOrgData,
+ context.getCurrentAuthenticatedIdPs(), appConfig.getApplicationName());
+ accessingOrgAuthenticatedOrgData.setRememberMe(context.isRememberMe());
+ sessionContext.getAuthenticatedOrgData().put(organizationId, accessingOrgAuthenticatedOrgData);
+ }
} else {
sessionContext.getAuthenticatedSequences().put(appConfig.getApplicationName(), sequenceConfig);
sessionContext.getAuthenticatedIdPs().putAll(context.getCurrentAuthenticatedIdPs());
@@ -658,8 +666,8 @@ protected void concludeFlow(HttpServletRequest request, HttpServletResponse resp
request, response, context);
// TODO add to cache?
// store again. when replicate cache is used. this may be needed.
- FrameworkUtils.addSessionContextToCache(sessionContextKey, sessionContext, applicationTenantDomain,
- context.getLoginTenantDomain(), organizationId);
+ addSessionContextToCache(sessionContextKey, sessionContext,
+ applicationTenantDomain, context, organizationId);
// Since the session context is already available, audit log will be added with updated details.
addAuditLogs(SessionMgtConstants.UPDATE_SESSION_ACTION, authenticationResult.getSubject(),
sessionContextKey, FrameworkUtils.getCorrelation(),
@@ -856,6 +864,47 @@ protected void concludeFlow(HttpServletRequest request, HttpServletResponse resp
sendResponse(request, response, context);
}
+ /**
+ * Adds the session context to the cache against the login tenant domain (also persisting it to the session data
+ * store) and additionally caches it against all other tenant domains the session spans across.
+ *
+ * @param sessionContextKey Session context cache key.
+ * @param sessionContext Session context to be cached.
+ * @param applicationTenantDomain Application tenant domain used to resolve session timeout configurations.
+ * @param context Authentication context.
+ * @param organizationId Organization id used to resolve authenticated sequences for cache cleanup.
+ */
+ private void addSessionContextToCache(String sessionContextKey, SessionContext sessionContext,
+ String applicationTenantDomain,
+ AuthenticationContext context, String organizationId) {
+
+ String loginTenantDomain = context.getLoginTenantDomain();
+ FrameworkUtils.addSessionContextToCache(sessionContextKey, sessionContext, applicationTenantDomain,
+ loginTenantDomain, organizationId);
+ Set authenticatedOrganizations = new HashSet<>();
+ if (MapUtils.isNotEmpty(sessionContext.getAuthenticatedOrgData())) {
+ for (String authenticatedOrgId : sessionContext.getAuthenticatedOrgData().keySet()) {
+ try {
+ authenticatedOrganizations.add(FrameworkServiceDataHolder.getInstance()
+ .getOrganizationManager().resolveTenantDomain(authenticatedOrgId));
+ } catch (OrganizationManagementException e) {
+ log.error("Error while resolving the tenant domain of the organization with id: " +
+ authenticatedOrgId, e);
+ }
+ }
+ }
+
+ if (context.getOrganizationLoginData() != null && StringUtils.isNotBlank(
+ context.getOrganizationLoginData().getRootOrganizationTenantDomain())) {
+ authenticatedOrganizations.add(context.getOrganizationLoginData().getRootOrganizationTenantDomain());
+ }
+ authenticatedOrganizations.remove(loginTenantDomain);
+ for (String tenantDomain : authenticatedOrganizations) {
+ FrameworkUtils.addSessionContextToCacheWithoutPersisting(sessionContextKey, sessionContext,
+ applicationTenantDomain, tenantDomain, organizationId);
+ }
+ }
+
private void storeFedAuthSessionMapping(String sessionContextKey, AuthHistory authHistory)
throws UserSessionException {
@@ -1387,6 +1436,71 @@ private void setAuthenticatedIDPsOfApp(AuthenticatedOrgData authenticatedOrgData
authenticatedOrgData.setAuthenticatedIdPsOfApp(applicationName, authenticatedIdPDataMap);
}
+ /**
+ * Merges the given authenticated IdP data maps into a single map
+ * adding only the authenticators which are not already present.
+ *
+ * @param previousAuthenticatedIdPs The authenticated IdP data to be used as the base.
+ * @param currentAuthenticatedIdPs The authenticated IdP data to be merged into the base.
+ * @return A new map containing the merged authenticated IdP data.
+ * @throws FrameworkException If an error occurs while cloning the authenticated IdP data.
+ */
+ private Map mergeAuthenticatedIdPs(
+ Map previousAuthenticatedIdPs,
+ Map currentAuthenticatedIdPs) throws FrameworkException {
+
+ Map mergedAuthenticatedIdPs = new HashMap<>();
+ mergeAuthenticatedIdPsInto(mergedAuthenticatedIdPs, previousAuthenticatedIdPs);
+ mergeAuthenticatedIdPsInto(mergedAuthenticatedIdPs, currentAuthenticatedIdPs);
+ return mergedAuthenticatedIdPs;
+ }
+
+ /**
+ * Merges the authenticated IdP data of the given source map into the target merged map. When an IdP with the same
+ * name is already present in the merged map, only the authenticators which are not already present are added to
+ * the existing entry.
+ *
+ * @param mergedAuthenticatedIdPs The target map to merge into.
+ * @param authenticatedIdPs The source authenticated IdP data to merge.
+ * @throws FrameworkException If an error occurs while cloning the authenticated IdP data.
+ */
+ private void mergeAuthenticatedIdPsInto(Map mergedAuthenticatedIdPs,
+ Map authenticatedIdPs)
+ throws FrameworkException {
+
+ if (MapUtils.isEmpty(authenticatedIdPs)) {
+ return;
+ }
+
+ for (Map.Entry entry : authenticatedIdPs.entrySet()) {
+ String idpName = entry.getKey();
+ AuthenticatedIdPData authenticatedIdPData = entry.getValue();
+ AuthenticatedIdPData existingAuthenticatedIdPData = mergedAuthenticatedIdPs.get(idpName);
+ if (existingAuthenticatedIdPData == null) {
+ try {
+ mergedAuthenticatedIdPs.put(idpName, (AuthenticatedIdPData) authenticatedIdPData.clone());
+ } catch (CloneNotSupportedException e) {
+ throw new FrameworkException("Error while cloning AuthenticatedIdPData object.", e);
+ }
+ } else {
+ // An IdP with the same name is already added. Add only the authenticators which are not already
+ // present in the existing entry.
+ List existingAuthenticators = existingAuthenticatedIdPData.getAuthenticators();
+ List newAuthenticators = authenticatedIdPData.getAuthenticators();
+ if (CollectionUtils.isNotEmpty(newAuthenticators)) {
+ for (AuthenticatorConfig newAuthenticator : newAuthenticators) {
+ boolean alreadyPresent = existingAuthenticators != null && existingAuthenticators.stream()
+ .anyMatch(existing -> StringUtils.equals(existing.getName(),
+ newAuthenticator.getName()));
+ if (!alreadyPresent) {
+ existingAuthenticatedIdPData.addAuthenticator(newAuthenticator);
+ }
+ }
+ }
+ }
+ }
+ }
+
private void addAuditLogs(String sessionAction, AuthenticatedUser authenticatedUser, String sessionKey,
String traceId, Long lastAccessedTimestamp, boolean isRememberMe) {
@@ -1446,4 +1560,56 @@ private List getFederatedAuthenticatorName(List federate
}
return federatedTokens.stream().map(FederatedToken::getIdp).collect(Collectors.toList());
}
+
+ /**
+ * Determines whether the organization sessions held in an already existing session context are applicable to
+ * the current organization login, and can therefore be reused instead of creating a new session context.
+ *
+ * @param context Authentication context of the current authentication flow.
+ * @param sessionContext Existing session context loaded from the cache.
+ * @return {@code true} if the organization sessions in the existing session context are applicable to the
+ * current login; {@code false} otherwise.
+ */
+ private boolean isAuthenticatedOrgSessionsApplicable(AuthenticationContext context,
+ SessionContext sessionContext) {
+
+ Map authenticatedOrgDataMap = sessionContext.getAuthenticatedOrgData();
+ if (MapUtils.isEmpty(authenticatedOrgDataMap)) {
+ return false;
+ }
+
+ AuthenticatedUser currentAuthenticatedUser = context.getSequenceConfig().getAuthenticatedUser();
+ String accessingOrgId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getAccessingOrganizationId();
+ if (context.isSharedAppLogin()) {
+ accessingOrgId = context.getOrganizationLoginData().getAccessingOrganization().getId();
+ }
+
+ if (accessingOrgId == null) {
+ return false;
+ }
+
+ if (!currentAuthenticatedUser.isSharedUser() &&
+ sessionContext.getAuthenticatedOrgData().get(accessingOrgId) == null) {
+ return false;
+ }
+
+ try {
+ String currentAuthenticatedUserId = currentAuthenticatedUser.getUserId();
+ for (AuthenticatedOrgData authenticatedOrgData : authenticatedOrgDataMap.values()) {
+ for (SequenceConfig sequenceConfig : authenticatedOrgData.getAuthenticatedSequences().values()) {
+ if (sequenceConfig != null && sequenceConfig.getAuthenticatedUser() != null &&
+ !StringUtils.equals(currentAuthenticatedUserId,
+ sequenceConfig.getAuthenticatedUser().getUserId())) {
+ return false;
+ }
+ }
+ }
+ } catch (UserIdNotFoundException e) {
+ if (log.isDebugEnabled()) {
+ log.debug("Error while resolving userId for user: " +
+ currentAuthenticatedUser.getLoggableMaskedUserId());
+ }
+ }
+ return true;
+ }
}
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/DefaultRequestCoordinator.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/DefaultRequestCoordinator.java
index eb052f7f6976..ff2635579c89 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/DefaultRequestCoordinator.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/DefaultRequestCoordinator.java
@@ -83,6 +83,8 @@
import org.wso2.carbon.identity.core.model.IdentityErrorMsgContext;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.core.util.IdentityUtil;
+import org.wso2.carbon.identity.organization.management.organization.user.sharing.OrganizationUserSharingService;
+import org.wso2.carbon.identity.organization.management.organization.user.sharing.models.UserAssociation;
import org.wso2.carbon.identity.organization.management.service.exception.OrganizationManagementException;
import org.wso2.carbon.identity.organization.management.service.util.OrganizationManagementUtil;
import org.wso2.carbon.user.api.Tenant;
@@ -106,6 +108,7 @@
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -1225,26 +1228,35 @@ protected void findPreviousAuthenticatedSession(HttpServletRequest request, Auth
if (previousSessionOrgId.isPresent()) {
orgIdForSessionDataLookup = previousSessionOrgId.get();
sessionContext = loadedSessionContext;
- }
-
- // Handle session context loading for root sessions if organization sessions are not applicable.
- boolean hasRootSessionContext = (loadedSessionContext.getAuthenticatedIdPs() != null);
- if (previousSessionOrgId.isEmpty() && hasRootSessionContext) {
- boolean invalidSessionInCurrentContext = false;
- if (applicationConfig != null && !applicationConfig.isSaaSApp()
- && loadedSessionContext.getProperty(FrameworkUtils.TENANT_DOMAIN) != null) {
- /*
- Consider the session context as invalid if the tenant domain in the session context is
- different from the login tenant domain in the current context for non-SaaS applications.
- */
- invalidSessionInCurrentContext = !StringUtils.equals(
- loadedSessionContext.getProperty(FrameworkUtils.TENANT_DOMAIN).toString(),
- context.getLoginTenantDomain());
- }
- if (invalidSessionInCurrentContext) {
- request.setAttribute(FrameworkConstants.REMOVE_COMMONAUTH_COOKIE, true);
- } else {
- sessionContext = loadedSessionContext;
+ } else if (context.isOrgApplicationLogin()
+ && MapUtils.isNotEmpty(loadedSessionContext.getAuthenticatedOrgData())) {
+ // The accessing org has no session in the loaded session context. Reuse the authenticators
+ // already satisfied in other organizations to honor SSO for the current organization
+ // application login.
+ String accessingOrgId =
+ PrivilegedCarbonContext.getThreadLocalCarbonContext().getAccessingOrganizationId();
+ populateContextWithPreviousAuthenticatedOrganizationSessions(accessingOrgId, context,
+ effectiveSequence, loadedSessionContext);
+ } else {
+ // Handle session context loading for root sessions if organization sessions are not applicable.
+ boolean hasRootSessionContext = (loadedSessionContext.getAuthenticatedIdPs() != null);
+ if (hasRootSessionContext) {
+ boolean invalidSessionInCurrentContext = false;
+ if (applicationConfig != null && !applicationConfig.isSaaSApp()
+ && loadedSessionContext.getProperty(FrameworkUtils.TENANT_DOMAIN) != null) {
+ /*
+ Consider the session context as invalid if the tenant domain in the session context is
+ different from the login tenant domain in the current context for non-SaaS applications.
+ */
+ invalidSessionInCurrentContext = !StringUtils.equals(
+ loadedSessionContext.getProperty(FrameworkUtils.TENANT_DOMAIN).toString(),
+ context.getLoginTenantDomain());
+ }
+ if (invalidSessionInCurrentContext) {
+ request.setAttribute(FrameworkConstants.REMOVE_COMMONAUTH_COOKIE, true);
+ } else {
+ sessionContext = loadedSessionContext;
+ }
}
}
}
@@ -1263,6 +1275,23 @@ protected void findPreviousAuthenticatedSession(HttpServletRequest request, Auth
context.setSequenceConfig(effectiveSequence);
}
+ /**
+ * Checks whether the request carries any organization discovery input parameter.
+ *
+ * @param request The HTTP servlet request.
+ * @return {@code true} if at least one organization discovery parameter is present in the request.
+ */
+ private boolean hasOrganizationDiscoveryParameters(HttpServletRequest request) {
+
+ return StringUtils.isNotEmpty(request.getParameter(FrameworkConstants.OrgDiscoveryInputParameters.ORG_ID))
+ || StringUtils.isNotEmpty(
+ request.getParameter(FrameworkConstants.OrgDiscoveryInputParameters.ORG_HANDLE))
+ || StringUtils.isNotEmpty(
+ request.getParameter(FrameworkConstants.OrgDiscoveryInputParameters.ORG_NAME))
+ || StringUtils.isNotEmpty(
+ request.getParameter(FrameworkConstants.OrgDiscoveryInputParameters.LOGIN_HINT));
+ }
+
protected void findPreviousOrganizationSession(HttpServletRequest request, AuthenticationContext context)
throws FrameworkException {
@@ -1284,6 +1313,12 @@ protected void findPreviousOrganizationSession(HttpServletRequest request, Authe
if (accessingOrgId != null) {
if (loadedSessionContext.getAuthenticatedOrgData().get(accessingOrgId) != null) {
sessionContext = loadedSessionContext;
+ } else {
+ // The accessing org has no session in the loaded session context. Reuse the authenticators
+ // already satisfied in other organizations to honor SSO for the current organization
+ // application login.
+ populateContextWithPreviousAuthenticatedOrganizationSessions(accessingOrgId,
+ context, effectiveSequence, loadedSessionContext);
}
}
}
@@ -1300,6 +1335,178 @@ protected void findPreviousOrganizationSession(HttpServletRequest request, Authe
context.setSequenceConfig(effectiveSequence);
}
+ /**
+ * Updates the effective sequence config to reflect the authenticators which the shared user has already
+ * authenticated with in other sub-organizations, providing SSO capabilities for shared user logins across
+ * sub-organizations.
+ *
+ * @param context The authentication context.
+ * @param effectiveSequence The effective sequence config of the current authentication flow.
+ * @param loadedSessionContext The loaded session context holding the authenticated organization data.
+ */
+ private void populateContextWithPreviousAuthenticatedOrganizationSessions(String accessingOrgId,
+ AuthenticationContext context,
+ SequenceConfig effectiveSequence,
+ SessionContext loadedSessionContext) {
+
+ Map authenticatedOrgData = loadedSessionContext.getAuthenticatedOrgData();
+ if (MapUtils.isEmpty(authenticatedOrgData)) {
+ if (log.isDebugEnabled()) {
+ log.debug("No authenticated organization data found in the session context. Skipping the " +
+ "population of the sequence config with previously authenticated organization sessions.");
+ }
+ return;
+ }
+
+ if (!isAuthenticatedUserSharedToAccessingOrg(loadedSessionContext, accessingOrgId)) {
+ if (log.isDebugEnabled()) {
+ log.debug("The authenticated user is not shared to the accessing organization: " + accessingOrgId +
+ ". Skipping the population of the sequence config with previously authenticated " +
+ "organization sessions.");
+ }
+ return;
+ }
+
+ Map previousAuthenticatedIdPs = new HashMap<>();
+
+ for (StepConfig stepConfig : effectiveSequence.getStepMap().values()) {
+ for (Map.Entry orgDataEntry : authenticatedOrgData.entrySet()) {
+ Map orgAuthenticatedIdPs = orgDataEntry.getValue().getAuthenticatedIdPs();
+ Map authenticatedStepIdPs =
+ FrameworkUtils.getAuthenticatedStepIdPs(stepConfig, orgAuthenticatedIdPs);
+ if (authenticatedStepIdPs.isEmpty()) {
+ continue;
+ }
+
+ Map.Entry authenticatedStepIdP =
+ authenticatedStepIdPs.entrySet().iterator().next();
+ String authenticatedIdPName = authenticatedStepIdP.getKey();
+ AuthenticatorConfig authenticatedAuthenticator = authenticatedStepIdP.getValue();
+ AuthenticatedIdPData authenticatedIdPData = orgAuthenticatedIdPs.get(authenticatedIdPName);
+
+ stepConfig.setAuthenticatedAutenticator(authenticatedAuthenticator);
+ stepConfig.setAuthenticatedAuthenticatorName(authenticatedAuthenticator.getName());
+ stepConfig.setAuthenticatedIdP(authenticatedIdPName);
+
+ AuthenticatedUser authenticatedUser = authenticatedIdPData.getUser();
+ // Switch the accessing org of the authenticated user to the current accessing org.
+ // Copying to avoid modifying the cache entry.
+ boolean updateAccessingOrganization = authenticatedUser != null
+ && StringUtils.isNotBlank(authenticatedUser.getAccessingOrganization());
+ if (updateAccessingOrganization) {
+ authenticatedUser = new AuthenticatedUser(authenticatedUser);
+ authenticatedUser.setAccessingOrganization(accessingOrgId);
+
+ // Clone the authenticated IdP data and switch its user's accessing org similarly.
+ try {
+ AuthenticatedIdPData clonedAuthenticatedIdPData =
+ (AuthenticatedIdPData) authenticatedIdPData.clone();
+ clonedAuthenticatedIdPData.setUser(authenticatedUser);
+ authenticatedIdPData = clonedAuthenticatedIdPData;
+ } catch (CloneNotSupportedException e) {
+ log.error("Error while cloning the authenticated IdP data of the IdP: " +
+ authenticatedIdPName + ". Proceeding with the loaded authenticated IdP data.", e);
+ }
+ }
+ stepConfig.setAuthenticatedUser(authenticatedUser);
+
+ // Carry over the authenticated IdP data so the framework can honor the SSO downstream.
+ AuthenticatedIdPData existingAuthenticatedIdPData = previousAuthenticatedIdPs.get(authenticatedIdPName);
+ if (existingAuthenticatedIdPData == null) {
+ previousAuthenticatedIdPs.put(authenticatedIdPName, authenticatedIdPData);
+ } else {
+ // An entry for the same IdP is already carried over (from another organization). Append only
+ // the authenticators which are not already present in the existing entry.
+ List existingAuthenticators =
+ existingAuthenticatedIdPData.getAuthenticators();
+ List newAuthenticators = authenticatedIdPData.getAuthenticators();
+ if (CollectionUtils.isNotEmpty(newAuthenticators)) {
+ for (AuthenticatorConfig newAuthenticator : newAuthenticators) {
+ boolean alreadyPresent = existingAuthenticators != null && existingAuthenticators.stream()
+ .anyMatch(existing -> StringUtils.equals(existing.getName(),
+ newAuthenticator.getName()));
+ if (!alreadyPresent) {
+ existingAuthenticatedIdPData.addAuthenticator(newAuthenticator);
+ }
+ }
+ }
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug(String.format("Marking step %d as authenticated with the authenticator '%s' of the " +
+ "IdP '%s' from the previously authenticated organization '%s'.",
+ stepConfig.getOrder(), authenticatedAuthenticator.getName(), authenticatedIdPName,
+ orgDataEntry.getKey()));
+ }
+ break;
+ }
+ }
+
+ if (!previousAuthenticatedIdPs.isEmpty()) {
+ context.setPreviousSessionFound(true);
+ Map existingPreviousAuthenticatedIdPs =
+ context.getPreviousAuthenticatedIdPs();
+ if (MapUtils.isNotEmpty(existingPreviousAuthenticatedIdPs)) {
+ existingPreviousAuthenticatedIdPs.putAll(previousAuthenticatedIdPs);
+ } else {
+ context.setPreviousAuthenticatedIdPs(previousAuthenticatedIdPs);
+ }
+ }
+ }
+
+ /**
+ * Checks whether the authenticated user held in the loaded session context's authenticated organization data is
+ * shared to the accessing organization.
+ *
+ * @param loadedSessionContext The loaded session context holding the authenticated organization data.
+ * @param accessingOrgId The organization ID the user is currently accessing.
+ * @return {@code true} if the authenticated user is shared to the accessing organization, {@code false} otherwise.
+ */
+ private boolean isAuthenticatedUserSharedToAccessingOrg(SessionContext loadedSessionContext,
+ String accessingOrgId) {
+
+ if (StringUtils.isBlank(accessingOrgId)) {
+ return false;
+ }
+
+ AuthenticatedUser authenticatedUser = null;
+ Iterator authenticatedOrgDataIterator =
+ loadedSessionContext.getAuthenticatedOrgData().values().iterator();
+ if (authenticatedOrgDataIterator.hasNext()) {
+ AuthenticatedOrgData firstAuthenticatedOrgData = authenticatedOrgDataIterator.next();
+ for (SequenceConfig sequenceConfig : firstAuthenticatedOrgData.getAuthenticatedSequences().values()) {
+ if (sequenceConfig != null && sequenceConfig.getAuthenticatedUser() != null) {
+ authenticatedUser = sequenceConfig.getAuthenticatedUser();
+ break;
+ }
+ }
+ }
+
+ if (authenticatedUser == null) {
+ if (log.isDebugEnabled()) {
+ log.debug("No authenticated user found in the authenticated organization data of the loaded " +
+ "session context.");
+ }
+ return false;
+ }
+
+ try {
+ OrganizationUserSharingService organizationUserSharingService =
+ FrameworkServiceDataHolder.getInstance().getOrganizationUserSharingService();
+ UserAssociation userAssociation = organizationUserSharingService
+ .getUserAssociationOfAssociatedUserByOrgId(authenticatedUser.getUserId(), accessingOrgId);
+ return userAssociation != null;
+ } catch (OrganizationManagementException e) {
+ log.error("Error while resolving the shared user association of the authenticated user for the " +
+ "accessing organization: " + accessingOrgId, e);
+ return false;
+ } catch (UserIdNotFoundException e) {
+ log.error("Authenticated user ID not found while resolving the shared user association for the " +
+ "accessing organization: " + accessingOrgId, e);
+ return false;
+ }
+ }
+
/**
* Processes ACR (Authentication Context Class Reference) values from the request and adds them to the
* authentication context.
@@ -1424,6 +1631,10 @@ private Optional handleOrganizationSessions(SessionContext loadedSession
ApplicationConfig applicationConfig)
throws FrameworkException {
+ if (hasOrganizationDiscoveryParameters(request)) {
+ return Optional.empty();
+ }
+
boolean sharedAppLoginSession = (loadedSessionContext.getAuthenticatedSharedAppOrgId() != null) &&
(MapUtils.isNotEmpty(loadedSessionContext.getAuthenticatedOrgData()));
String accessingOrgId = PrivilegedCarbonContext.getThreadLocalCarbonContext()
@@ -1553,10 +1764,17 @@ private void populateContextWithPreviousSession(HttpServletRequest request, Auth
}
}
}
+ String primaryApplicationTenantDomain = context.getTenantDomain();
+ if (context.getOrganizationLoginData() != null &&
+ context.getOrganizationLoginData().getPrimaryAppData() != null &&
+ StringUtils.isNotBlank(context.getOrganizationLoginData().getPrimaryAppData().getTenantDomain())) {
+ primaryApplicationTenantDomain =
+ context.getOrganizationLoginData().getPrimaryAppData().getTenantDomain();
+ }
// This is done to reflect the changes done in SP to the sequence config. So, the requested claim
// updates, authentication step updates will be reflected.
refreshAppConfig(effectiveSequence, request.getParameter(FrameworkConstants.RequestParams.ISSUER),
- context.getRequestType(), context.getTenantDomain());
+ context.getRequestType(), primaryApplicationTenantDomain);
Map authenticatedIdPsOfApp;
if (orgId != null) {
@@ -2005,6 +2223,7 @@ private void updateContextForOrganizationLogin(HttpServletRequest request, Authe
int primaryAppId = applicationConfig.getApplicationID();
PrimaryAppData primaryAppData = new PrimaryAppData();
primaryAppData.setId(primaryAppId);
+ primaryAppData.setTenantDomain(applicationConfig.getServiceProvider().getTenantDomain());
context.getOrganizationLoginData().setPrimaryAppData(primaryAppData);
// Setting the sequence config of the shared application to the context.
diff --git a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/model/PrimaryAppData.java b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/model/PrimaryAppData.java
index 4151a71531fe..120f6f7efc61 100644
--- a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/model/PrimaryAppData.java
+++ b/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/main/java/org/wso2/carbon/identity/application/authentication/framework/model/PrimaryAppData.java
@@ -28,6 +28,7 @@ public class PrimaryAppData implements Serializable {
private static final long serialVersionUID = -3925617004643909936L;
private int id;
+ private String tenantDomain;
public int getId() {
@@ -38,4 +39,14 @@ public void setId(int id) {
this.id = id;
}
+
+ public String getTenantDomain() {
+
+ return tenantDomain;
+ }
+
+ public void setTenantDomain(String tenantDomain) {
+
+ this.tenantDomain = tenantDomain;
+ }
}
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 06bf59759b68..52a7eede2248 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
@@ -1324,6 +1324,46 @@ public static void addSessionContextToCache(String key, SessionContext sessionCo
String loginTenantDomain, String orgId) {
SessionContextCacheKey cacheKey = new SessionContextCacheKey(key);
+ SessionContextCacheEntry cacheEntry = buildSessionContextCacheEntry(key, sessionContext, tenantDomain, orgId);
+ SessionContextCache.getInstance().addToCache(cacheKey, cacheEntry, loginTenantDomain);
+ }
+
+ /**
+ * Adds the given session context to the session context cache only, without persisting it to the session data
+ * store.
+ *
+ * @param key Session context cache key.
+ * @param sessionContext Session context to be cached.
+ * @param tenantDomain Application tenant domain used to resolve session timeout configurations.
+ * @param loginTenantDomain Login tenant domain under which the cache entry is stored.
+ * @param orgId Organization id used to look up authenticated sequences from
+ * {@link SessionContext#getAuthenticatedOrgData()}.
+ */
+ public static void addSessionContextToCacheWithoutPersisting(String key, SessionContext sessionContext,
+ String tenantDomain, String loginTenantDomain,
+ String orgId) {
+
+ SessionContextCacheKey cacheKey = new SessionContextCacheKey(key);
+ SessionContextCacheEntry cacheEntry = buildSessionContextCacheEntry(key, sessionContext, tenantDomain, orgId);
+ SessionContextCache.getInstance().addToCacheWithoutPersisting(cacheKey, cacheEntry, loginTenantDomain);
+ }
+
+ /**
+ * Builds a {@link SessionContextCacheEntry} for the given session context. When an {@code orgId} is provided and
+ * the session context holds an {@link AuthenticatedOrgData} entry for that organization, the authenticated
+ * sequences of that organization are used for cleanup (clearing user attributes and the authentication graph).
+ * Otherwise, the top-level authenticated sequences of the session context are used.
+ *
+ * @param key Session context cache key.
+ * @param sessionContext Session context to be cached.
+ * @param tenantDomain Application tenant domain used to resolve session timeout configurations.
+ * @param orgId Organization id used to look up authenticated sequences from
+ * {@link SessionContext#getAuthenticatedOrgData()}.
+ * @return The built session context cache entry.
+ */
+ private static SessionContextCacheEntry buildSessionContextCacheEntry(String key, SessionContext sessionContext,
+ String tenantDomain, String orgId) {
+
SessionContextCacheEntry cacheEntry = new SessionContextCacheEntry();
cacheEntry.setContextIdentifier(key);
@@ -1364,7 +1404,7 @@ public static void addSessionContextToCache(String key, SessionContext sessionCo
cacheEntry.setContext(sessionContext);
cacheEntry.setValidityPeriod(timeoutPeriod);
- SessionContextCache.getInstance().addToCache(cacheKey, cacheEntry, loginTenantDomain);
+ return cacheEntry;
}
/**
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/DefaultAuthenticationRequestHandlerTest.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/DefaultAuthenticationRequestHandlerTest.java
index 9525cf548987..7ca781994d51 100644
--- a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/test/java/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/DefaultAuthenticationRequestHandlerTest.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/DefaultAuthenticationRequestHandlerTest.java
@@ -28,12 +28,14 @@
import org.testng.annotations.Test;
import org.wso2.carbon.identity.application.authentication.framework.cache.AuthenticationResultCacheEntry;
import org.wso2.carbon.identity.application.authentication.framework.config.model.ApplicationConfig;
+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.context.AuthenticationContext;
import org.wso2.carbon.identity.application.authentication.framework.exception.FrameworkException;
import org.wso2.carbon.identity.application.authentication.framework.exception.PostAuthenticationFailedException;
import org.wso2.carbon.identity.application.authentication.framework.handler.sequence.impl.DefaultStepBasedSequenceHandler;
import org.wso2.carbon.identity.application.authentication.framework.internal.FrameworkServiceDataHolder;
+import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedIdPData;
import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser;
import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticationResult;
import org.wso2.carbon.identity.application.authentication.framework.model.CommonAuthResponseWrapper;
@@ -51,7 +53,9 @@
import org.wso2.carbon.identity.common.testng.WithCarbonHome;
import java.io.IOException;
+import java.lang.reflect.Method;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -75,6 +79,7 @@
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNotSame;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import static org.wso2.carbon.base.MultitenantConstants.SUPER_TENANT_DOMAIN_NAME;
@@ -633,5 +638,90 @@ public void testConcludeFlowAllowsTenantMismatchForSharedUser() throws Exception
+ "when user is a shared user");
}
}
+
+ @SuppressWarnings("unchecked")
+ private Map invokeMergeAuthenticatedIdPs(
+ Map previous,
+ Map current) throws Exception {
+
+ Method method = DefaultAuthenticationRequestHandler.class.getDeclaredMethod(
+ "mergeAuthenticatedIdPs", Map.class, Map.class);
+ method.setAccessible(true);
+ return (Map) method.invoke(
+ new DefaultAuthenticationRequestHandler(), previous, current);
+ }
+
+ private AuthenticatedIdPData buildAuthenticatedIdPData(String idpName, String... authenticatorNames) {
+
+ AuthenticatedIdPData authenticatedIdPData = new AuthenticatedIdPData();
+ authenticatedIdPData.setIdpName(idpName);
+ authenticatedIdPData.setUser(new AuthenticatedUser());
+ List authenticators = new ArrayList<>();
+ for (String authenticatorName : authenticatorNames) {
+ authenticators.add(new AuthenticatorConfig(authenticatorName, true, null));
+ }
+ authenticatedIdPData.setAuthenticators(authenticators);
+ return authenticatedIdPData;
+ }
+
+ private List authenticatorNames(AuthenticatedIdPData authenticatedIdPData) {
+
+ List names = new ArrayList<>();
+ for (AuthenticatorConfig authenticatorConfig : authenticatedIdPData.getAuthenticators()) {
+ names.add(authenticatorConfig.getName());
+ }
+ return names;
+ }
+
+ @Test(description = "Merging disjoint authenticated IdP maps keeps all IdPs from both maps.")
+ public void testMergeAuthenticatedIdPsWithDisjointIdPs() throws Exception {
+
+ Map previous = new HashMap<>();
+ previous.put("LOCAL", buildAuthenticatedIdPData("LOCAL", "BasicAuthenticator"));
+ Map current = new HashMap<>();
+ current.put("FederatedIdP", buildAuthenticatedIdPData("FederatedIdP", "OpenIDConnectAuthenticator"));
+
+ Map merged = invokeMergeAuthenticatedIdPs(previous, current);
+
+ assertEquals(merged.size(), 2);
+ assertTrue(merged.containsKey("LOCAL"));
+ assertTrue(merged.containsKey("FederatedIdP"));
+ assertNotNull(merged.get("LOCAL"));
+ assertNotSame(merged.get("LOCAL"), previous.get("LOCAL"));
+ }
+
+ @Test(description = "Merging IdP maps that share an IdP appends only the authenticators not already present.")
+ public void testMergeAuthenticatedIdPsAppendsMissingAuthenticators() throws Exception {
+
+ Map previous = new HashMap<>();
+ previous.put("LOCAL", buildAuthenticatedIdPData("LOCAL", "BasicAuthenticator"));
+ Map current = new HashMap<>();
+ // Same IdP with a duplicate authenticator and a new authenticator.
+ current.put("LOCAL", buildAuthenticatedIdPData("LOCAL", "BasicAuthenticator", "TOTPAuthenticator"));
+
+ Map merged = invokeMergeAuthenticatedIdPs(previous, current);
+
+ assertEquals(merged.size(), 1);
+ List mergedAuthenticators = authenticatorNames(merged.get("LOCAL"));
+ assertEquals(mergedAuthenticators.size(), 2, "Duplicate authenticator should not be added twice.");
+ assertTrue(mergedAuthenticators.contains("BasicAuthenticator"));
+ assertTrue(mergedAuthenticators.contains("TOTPAuthenticator"));
+ }
+
+ @Test(description = "Merging handles null and empty authenticated IdP maps gracefully.")
+ public void testMergeAuthenticatedIdPsWithNullAndEmptyMaps() throws Exception {
+
+ Map current = new HashMap<>();
+ current.put("LOCAL", buildAuthenticatedIdPData("LOCAL", "BasicAuthenticator"));
+
+ // Previous map is null.
+ Map merged = invokeMergeAuthenticatedIdPs(null, current);
+ assertEquals(merged.size(), 1);
+ assertTrue(merged.containsKey("LOCAL"));
+
+ // Both maps empty/null.
+ Map emptyMerge = invokeMergeAuthenticatedIdPs(null, new HashMap<>());
+ assertTrue(emptyMerge.isEmpty());
+ }
}
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/DefaultRequestCoordinatorTest.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/DefaultRequestCoordinatorTest.java
index bae187323bfc..dd443fb743ae 100644
--- a/components/authentication-framework/org.wso2.carbon.identity.application.authentication.framework/src/test/java/org/wso2/carbon/identity/application/authentication/framework/handler/request/impl/DefaultRequestCoordinatorTest.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/DefaultRequestCoordinatorTest.java
@@ -26,15 +26,21 @@
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import org.wso2.carbon.context.PrivilegedCarbonContext;
+import org.wso2.carbon.identity.application.authentication.framework.ApplicationAuthenticator;
import org.wso2.carbon.identity.application.authentication.framework.config.ConfigurationFacade;
import org.wso2.carbon.identity.application.authentication.framework.config.model.ApplicationConfig;
+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.config.model.graph.AuthGraphNode;
import org.wso2.carbon.identity.application.authentication.framework.config.model.graph.ShowPromptNode;
import org.wso2.carbon.identity.application.authentication.framework.context.AuthenticationContext;
import org.wso2.carbon.identity.application.authentication.framework.context.SessionContext;
import org.wso2.carbon.identity.application.authentication.framework.exception.CookieValidationFailedException;
import org.wso2.carbon.identity.application.authentication.framework.exception.FrameworkException;
+import org.wso2.carbon.identity.application.authentication.framework.internal.FrameworkServiceDataHolder;
+import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedIdPData;
+import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedOrgData;
import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser;
import org.wso2.carbon.identity.application.authentication.framework.model.CommonAuthRequestWrapper;
import org.wso2.carbon.identity.application.authentication.framework.model.CommonAuthResponseWrapper;
@@ -55,6 +61,9 @@
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.identity.event.services.IdentityEventService;
+import org.wso2.carbon.identity.organization.management.organization.user.sharing.OrganizationUserSharingService;
+import org.wso2.carbon.identity.organization.management.organization.user.sharing.models.UserAssociation;
+import org.wso2.carbon.identity.organization.management.service.exception.OrganizationManagementException;
import org.wso2.carbon.identity.testutil.IdentityBaseTest;
import java.io.IOException;
@@ -62,7 +71,10 @@
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import javax.servlet.http.Cookie;
@@ -84,6 +96,8 @@
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNotSame;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants.AUTHENTICATOR;
@@ -988,4 +1002,384 @@ public void testUpdateServiceProviderUuidInOutboundQueryParams(String initialQue
assertEquals(context.getQueryParams(), expectedQueryParams);
}
}
+
+ @DataProvider(name = "orgDiscoveryParamProvider")
+ public Object[][] provideOrgDiscoveryParams() {
+
+ return new Object[][]{
+ // orgId, orgHandle, org, login_hint, expected.
+ {null, null, null, null, false},
+ {"org-id-123", null, null, null, true},
+ {null, "org.example.com", null, null, true},
+ {null, null, "exampleOrg", null, true},
+ {null, null, null, "user@example.com", true},
+ {"", "", "", "", false},
+ };
+ }
+
+ @Test(dataProvider = "orgDiscoveryParamProvider")
+ public void testHasOrganizationDiscoveryParameters(String orgId, String orgHandle, String org, String loginHint,
+ boolean expected) throws Exception {
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ when(request.getParameter(FrameworkConstants.OrgDiscoveryInputParameters.ORG_ID)).thenReturn(orgId);
+ when(request.getParameter(FrameworkConstants.OrgDiscoveryInputParameters.ORG_HANDLE)).thenReturn(orgHandle);
+ when(request.getParameter(FrameworkConstants.OrgDiscoveryInputParameters.ORG_NAME)).thenReturn(org);
+ when(request.getParameter(FrameworkConstants.OrgDiscoveryInputParameters.LOGIN_HINT)).thenReturn(loginHint);
+
+ Method method = DefaultRequestCoordinator.class.getDeclaredMethod(
+ "hasOrganizationDiscoveryParameters", HttpServletRequest.class);
+ method.setAccessible(true);
+ boolean result = (boolean) method.invoke(requestCoordinator, request);
+
+ assertEquals(result, expected);
+ }
+
+ private boolean invokeIsAuthenticatedUserSharedToAccessingOrg(SessionContext sessionContext, String accessingOrgId)
+ throws Exception {
+
+ Method method = DefaultRequestCoordinator.class.getDeclaredMethod(
+ "isAuthenticatedUserSharedToAccessingOrg", SessionContext.class, String.class);
+ method.setAccessible(true);
+ return (boolean) method.invoke(requestCoordinator, sessionContext, accessingOrgId);
+ }
+
+ /**
+ * Builds a session context whose first authenticated organization data holds a sequence config carrying the
+ * given authenticated user.
+ */
+ private SessionContext buildSessionContextWithAuthenticatedUser(AuthenticatedUser authenticatedUser) {
+
+ SequenceConfig sequenceConfig = new SequenceConfig();
+ sequenceConfig.setAuthenticatedUser(authenticatedUser);
+ AuthenticatedOrgData orgData = new AuthenticatedOrgData();
+ orgData.getAuthenticatedSequences().put("app", sequenceConfig);
+ SessionContext sessionContext = new SessionContext();
+ Map authenticatedOrgData = new HashMap<>();
+ authenticatedOrgData.put("source-org-id", orgData);
+ sessionContext.setAuthenticatedOrgData(authenticatedOrgData);
+ return sessionContext;
+ }
+
+ @Test(description = "The shared-user check returns false when the accessing organization ID is blank.")
+ public void testIsAuthenticatedUserSharedToAccessingOrgWithBlankOrgId() throws Exception {
+
+ SessionContext sessionContext = buildSessionContextWithAuthenticatedUser(new AuthenticatedUser());
+ assertFalse(invokeIsAuthenticatedUserSharedToAccessingOrg(sessionContext, " "));
+ }
+
+ @Test(description = "The shared-user check returns false when no authenticated user is found in the session.")
+ public void testIsAuthenticatedUserSharedToAccessingOrgWithoutAuthenticatedUser() throws Exception {
+
+ SessionContext sessionContext = new SessionContext();
+ sessionContext.setAuthenticatedOrgData(new HashMap<>());
+ assertFalse(invokeIsAuthenticatedUserSharedToAccessingOrg(sessionContext, "accessing-org-id"));
+ }
+
+ @Test(description = "The shared-user check returns true when the user has an association in the accessing org.")
+ public void testIsAuthenticatedUserSharedToAccessingOrgWhenShared() throws Exception {
+
+ AuthenticatedUser authenticatedUser = new AuthenticatedUser();
+ authenticatedUser.setUserId("user-id-123");
+ SessionContext sessionContext = buildSessionContextWithAuthenticatedUser(authenticatedUser);
+
+ OrganizationUserSharingService sharingService = mock(OrganizationUserSharingService.class);
+ when(sharingService.getUserAssociationOfAssociatedUserByOrgId("user-id-123", "accessing-org-id"))
+ .thenReturn(mock(UserAssociation.class));
+ FrameworkServiceDataHolder.getInstance().setOrganizationUserSharingService(sharingService);
+ try {
+ assertTrue(invokeIsAuthenticatedUserSharedToAccessingOrg(sessionContext, "accessing-org-id"));
+ } finally {
+ FrameworkServiceDataHolder.getInstance().setOrganizationUserSharingService(null);
+ }
+ }
+
+ @Test(description = "The shared-user check returns false when the user has no association in the accessing org.")
+ public void testIsAuthenticatedUserSharedToAccessingOrgWhenNotShared() throws Exception {
+
+ AuthenticatedUser authenticatedUser = new AuthenticatedUser();
+ authenticatedUser.setUserId("user-id-123");
+ SessionContext sessionContext = buildSessionContextWithAuthenticatedUser(authenticatedUser);
+
+ OrganizationUserSharingService sharingService = mock(OrganizationUserSharingService.class);
+ when(sharingService.getUserAssociationOfAssociatedUserByOrgId("user-id-123", "accessing-org-id"))
+ .thenReturn(null);
+ FrameworkServiceDataHolder.getInstance().setOrganizationUserSharingService(sharingService);
+ try {
+ assertFalse(invokeIsAuthenticatedUserSharedToAccessingOrg(sessionContext, "accessing-org-id"));
+ } finally {
+ FrameworkServiceDataHolder.getInstance().setOrganizationUserSharingService(null);
+ }
+ }
+
+ @Test(description = "The shared-user check returns false and swallows OrganizationManagementException.")
+ public void testIsAuthenticatedUserSharedToAccessingOrgOnException() throws Exception {
+
+ AuthenticatedUser authenticatedUser = new AuthenticatedUser();
+ authenticatedUser.setUserId("user-id-123");
+ SessionContext sessionContext = buildSessionContextWithAuthenticatedUser(authenticatedUser);
+
+ OrganizationUserSharingService sharingService = mock(OrganizationUserSharingService.class);
+ when(sharingService.getUserAssociationOfAssociatedUserByOrgId(anyString(), anyString()))
+ .thenThrow(new OrganizationManagementException("error"));
+ FrameworkServiceDataHolder.getInstance().setOrganizationUserSharingService(sharingService);
+ try {
+ assertFalse(invokeIsAuthenticatedUserSharedToAccessingOrg(sessionContext, "accessing-org-id"));
+ } finally {
+ FrameworkServiceDataHolder.getInstance().setOrganizationUserSharingService(null);
+ }
+ }
+
+ @Test(description = "Populating context with previous org sessions is a no-op when the session holds no " +
+ "authenticated organization data.")
+ public void testPopulateContextWithNoAuthenticatedOrgData() throws Exception {
+
+ AuthenticationContext context = new AuthenticationContext();
+ SessionContext sessionContext = new SessionContext();
+ sessionContext.setAuthenticatedOrgData(new HashMap<>());
+
+ invokePopulateContext("accessing-org-id", context, new SequenceConfig(), sessionContext);
+
+ assertFalse(context.isPreviousSessionFound());
+ assertTrue(context.getPreviousAuthenticatedIdPs().isEmpty());
+ }
+
+ @Test(description = "Populating context with previous org sessions is a no-op when the user is not shared to the " +
+ "accessing organization.")
+ public void testPopulateContextWhenUserNotSharedToAccessingOrg() throws Exception {
+
+ AuthenticatedUser authenticatedUser = new AuthenticatedUser();
+ authenticatedUser.setUserId("user-id-123");
+ SessionContext sessionContext = buildSessionContextWithAuthenticatedUser(authenticatedUser);
+
+ OrganizationUserSharingService sharingService = mock(OrganizationUserSharingService.class);
+ when(sharingService.getUserAssociationOfAssociatedUserByOrgId(anyString(), anyString())).thenReturn(null);
+ FrameworkServiceDataHolder.getInstance().setOrganizationUserSharingService(sharingService);
+
+ AuthenticationContext context = new AuthenticationContext();
+ try {
+ invokePopulateContext("accessing-org-id", context, new SequenceConfig(), sessionContext);
+ assertFalse(context.isPreviousSessionFound());
+ } finally {
+ FrameworkServiceDataHolder.getInstance().setOrganizationUserSharingService(null);
+ }
+ }
+
+ @Test(description = "Populating context marks the matching step as authenticated and carries over the " +
+ "authenticated IdP data when the shared user has a previous organization session.")
+ public void testPopulateContextCarriesOverPreviousOrganizationSession() throws Exception {
+
+ // Step requiring the federated IdP.
+ AuthenticatorConfig stepAuthenticator = new AuthenticatorConfig("OIDCAuthenticator", true, null);
+ stepAuthenticator.setIdPNames(Arrays.asList("FederatedIdP"));
+ StepConfig stepConfig = new StepConfig();
+ stepConfig.setOrder(1);
+ stepConfig.setAuthenticatorList(Arrays.asList(stepAuthenticator));
+ SequenceConfig effectiveSequence = new SequenceConfig();
+ Map stepMap = new HashMap<>();
+ stepMap.put(1, stepConfig);
+ effectiveSequence.setStepMap(stepMap);
+
+ // Authenticated user shared to the accessing org.
+ AuthenticatedUser authenticatedUser = new AuthenticatedUser();
+ authenticatedUser.setUserId("user-id-123");
+
+ // Authenticated IdP data already satisfied in another organization.
+ AuthenticatedIdPData authenticatedIdPData = new AuthenticatedIdPData();
+ authenticatedIdPData.setIdpName("FederatedIdP");
+ authenticatedIdPData.setUser(authenticatedUser);
+ authenticatedIdPData.addAuthenticator(new AuthenticatorConfig("OIDCAuthenticator", true, null));
+
+ SequenceConfig sourceSequenceConfig = new SequenceConfig();
+ sourceSequenceConfig.setAuthenticatedUser(authenticatedUser);
+ AuthenticatedOrgData orgData = new AuthenticatedOrgData();
+ orgData.getAuthenticatedSequences().put("app", sourceSequenceConfig);
+ orgData.getAuthenticatedIdPs().put("FederatedIdP", authenticatedIdPData);
+
+ SessionContext sessionContext = new SessionContext();
+ Map authenticatedOrgData = new HashMap<>();
+ authenticatedOrgData.put("source-org-id", orgData);
+ sessionContext.setAuthenticatedOrgData(authenticatedOrgData);
+
+ OrganizationUserSharingService sharingService = mock(OrganizationUserSharingService.class);
+ when(sharingService.getUserAssociationOfAssociatedUserByOrgId("user-id-123", "accessing-org-id"))
+ .thenReturn(mock(UserAssociation.class));
+ FrameworkServiceDataHolder.getInstance().setOrganizationUserSharingService(sharingService);
+
+ AuthenticationContext context = new AuthenticationContext();
+ try {
+ invokePopulateContext("accessing-org-id", context, effectiveSequence, sessionContext);
+
+ assertTrue(context.isPreviousSessionFound());
+ Map previousAuthenticatedIdPs = context.getPreviousAuthenticatedIdPs();
+ assertNotNull(previousAuthenticatedIdPs);
+ assertTrue(previousAuthenticatedIdPs.containsKey("FederatedIdP"));
+
+ // The matching step should be marked as authenticated with the carried over authenticator.
+ assertEquals(stepConfig.getAuthenticatedIdP(), "FederatedIdP");
+ assertNotNull(stepConfig.getAuthenticatedAutenticator());
+ assertEquals(stepConfig.getAuthenticatedAutenticator().getName(), "OIDCAuthenticator");
+ } finally {
+ FrameworkServiceDataHolder.getInstance().setOrganizationUserSharingService(null);
+ }
+ }
+
+ @Test(description = "Populating context switches the accessing organization of the carried over authenticated " +
+ "user (and its IdP data) to the current accessing organization without mutating the cached entry.")
+ public void testPopulateContextSwitchesAccessingOrganizationForSharedUser() throws Exception {
+
+ // Step requiring the federated IdP.
+ AuthenticatorConfig stepAuthenticator = new AuthenticatorConfig("OIDCAuthenticator", true, null);
+ stepAuthenticator.setIdPNames(List.of("FederatedIdP"));
+ StepConfig stepConfig = new StepConfig();
+ stepConfig.setOrder(1);
+ stepConfig.setAuthenticatorList(List.of(stepAuthenticator));
+ SequenceConfig effectiveSequence = new SequenceConfig();
+ Map stepMap = new HashMap<>();
+ stepMap.put(1, stepConfig);
+ effectiveSequence.setStepMap(stepMap);
+
+ // Authenticated user that was accessing another (source) organization.
+ AuthenticatedUser authenticatedUser = new AuthenticatedUser();
+ authenticatedUser.setUserId("user-id-123");
+ authenticatedUser.setAccessingOrganization("source-org-id");
+
+ AuthenticatedIdPData authenticatedIdPData = new AuthenticatedIdPData();
+ authenticatedIdPData.setIdpName("FederatedIdP");
+ authenticatedIdPData.setUser(authenticatedUser);
+ authenticatedIdPData.addAuthenticator(new AuthenticatorConfig("OIDCAuthenticator", true, null));
+
+ SequenceConfig sourceSequenceConfig = new SequenceConfig();
+ sourceSequenceConfig.setAuthenticatedUser(authenticatedUser);
+ AuthenticatedOrgData orgData = new AuthenticatedOrgData();
+ orgData.getAuthenticatedSequences().put("app", sourceSequenceConfig);
+ orgData.getAuthenticatedIdPs().put("FederatedIdP", authenticatedIdPData);
+
+ SessionContext sessionContext = new SessionContext();
+ Map authenticatedOrgData = new HashMap<>();
+ authenticatedOrgData.put("source-org-id", orgData);
+ sessionContext.setAuthenticatedOrgData(authenticatedOrgData);
+
+ OrganizationUserSharingService sharingService = mock(OrganizationUserSharingService.class);
+ when(sharingService.getUserAssociationOfAssociatedUserByOrgId("user-id-123", "accessing-org-id"))
+ .thenReturn(mock(UserAssociation.class));
+ FrameworkServiceDataHolder.getInstance().setOrganizationUserSharingService(sharingService);
+
+ AuthenticationContext context = new AuthenticationContext();
+ try {
+ invokePopulateContext("accessing-org-id", context, effectiveSequence, sessionContext);
+
+ // The step's authenticated user should now point to the current accessing organization.
+ assertNotNull(stepConfig.getAuthenticatedUser());
+ assertEquals(stepConfig.getAuthenticatedUser().getAccessingOrganization(), "accessing-org-id");
+
+ // The carried over IdP data should be a clone (not the cached instance) whose user's accessing
+ // organization was switched as well.
+ AuthenticatedIdPData carriedOverIdPData = context.getPreviousAuthenticatedIdPs().get("FederatedIdP");
+ assertNotNull(carriedOverIdPData);
+ assertNotSame(carriedOverIdPData, authenticatedIdPData);
+ assertEquals(carriedOverIdPData.getUser().getAccessingOrganization(), "accessing-org-id");
+
+ // The cached entry should remain untouched.
+ assertEquals(authenticatedUser.getAccessingOrganization(), "source-org-id");
+ } finally {
+ FrameworkServiceDataHolder.getInstance().setOrganizationUserSharingService(null);
+ }
+ }
+
+ @Test(description = "Populating context merges authenticators for the same IdP that the shared user satisfied " +
+ "across different organizations, appending only the authenticators not already carried over.")
+ public void testPopulateContextMergesAuthenticatorsAcrossOrganizations() throws Exception {
+
+ // Two LOCAL steps, each requiring a distinct local authenticator.
+ ApplicationAuthenticator basicAppAuthenticator = mock(ApplicationAuthenticator.class);
+ when(basicAppAuthenticator.getAuthMechanism()).thenReturn("basic");
+ AuthenticatorConfig step1Authenticator = new AuthenticatorConfig("BasicAuthenticator", true, null);
+ step1Authenticator.setIdPNames(List.of(FrameworkConstants.LOCAL));
+ step1Authenticator.setApplicationAuthenticator(basicAppAuthenticator);
+ StepConfig step1 = new StepConfig();
+ step1.setOrder(1);
+ step1.setAuthenticatorList(List.of(step1Authenticator));
+
+ ApplicationAuthenticator totpAppAuthenticator = mock(ApplicationAuthenticator.class);
+ when(totpAppAuthenticator.getAuthMechanism()).thenReturn("totp");
+ AuthenticatorConfig step2Authenticator = new AuthenticatorConfig("TOTPAuthenticator", true, null);
+ step2Authenticator.setIdPNames(List.of(FrameworkConstants.LOCAL));
+ step2Authenticator.setApplicationAuthenticator(totpAppAuthenticator);
+ StepConfig step2 = new StepConfig();
+ step2.setOrder(2);
+ step2.setAuthenticatorList(List.of(step2Authenticator));
+
+ SequenceConfig effectiveSequence = new SequenceConfig();
+ Map stepMap = new LinkedHashMap<>();
+ stepMap.put(1, step1);
+ stepMap.put(2, step2);
+ effectiveSequence.setStepMap(stepMap);
+
+ AuthenticatedUser authenticatedUser = new AuthenticatedUser();
+ authenticatedUser.setUserId("user-id-123");
+
+ // Org A satisfied the BasicAuthenticator only.
+ AuthenticatedIdPData orgALocalIdPData = new AuthenticatedIdPData();
+ orgALocalIdPData.setIdpName(FrameworkConstants.LOCAL);
+ orgALocalIdPData.setUser(authenticatedUser);
+ orgALocalIdPData.addAuthenticator(new AuthenticatorConfig("BasicAuthenticator", true, null));
+ SequenceConfig orgASequenceConfig = new SequenceConfig();
+ orgASequenceConfig.setAuthenticatedUser(authenticatedUser);
+ AuthenticatedOrgData orgAData = new AuthenticatedOrgData();
+ orgAData.getAuthenticatedSequences().put("app", orgASequenceConfig);
+ orgAData.getAuthenticatedIdPs().put(FrameworkConstants.LOCAL, orgALocalIdPData);
+
+ // Org B satisfied the TOTPAuthenticator only.
+ AuthenticatedIdPData orgBLocalIdPData = new AuthenticatedIdPData();
+ orgBLocalIdPData.setIdpName(FrameworkConstants.LOCAL);
+ orgBLocalIdPData.setUser(authenticatedUser);
+ orgBLocalIdPData.addAuthenticator(new AuthenticatorConfig("TOTPAuthenticator", true, null));
+ AuthenticatedOrgData orgBData = new AuthenticatedOrgData();
+ orgBData.getAuthenticatedIdPs().put(FrameworkConstants.LOCAL, orgBLocalIdPData);
+
+ // Preserve iteration order so org A wins step 1 and org B wins step 2.
+ SessionContext sessionContext = new SessionContext();
+ Map authenticatedOrgData = new LinkedHashMap<>();
+ authenticatedOrgData.put("org-a-id", orgAData);
+ authenticatedOrgData.put("org-b-id", orgBData);
+ sessionContext.setAuthenticatedOrgData(authenticatedOrgData);
+
+ OrganizationUserSharingService sharingService = mock(OrganizationUserSharingService.class);
+ when(sharingService.getUserAssociationOfAssociatedUserByOrgId("user-id-123", "accessing-org-id"))
+ .thenReturn(mock(UserAssociation.class));
+ FrameworkServiceDataHolder.getInstance().setOrganizationUserSharingService(sharingService);
+
+ AuthenticationContext context = new AuthenticationContext();
+ try {
+ invokePopulateContext("accessing-org-id", context, effectiveSequence, sessionContext);
+
+ assertTrue(context.isPreviousSessionFound());
+ AuthenticatedIdPData mergedLocalIdPData =
+ context.getPreviousAuthenticatedIdPs().get(FrameworkConstants.LOCAL);
+ assertNotNull(mergedLocalIdPData);
+
+ // The authenticator satisfied in org B should have been appended to org A's carried over entry.
+ List mergedAuthenticatorNames = new ArrayList<>();
+ for (AuthenticatorConfig authenticatorConfig : mergedLocalIdPData.getAuthenticators()) {
+ mergedAuthenticatorNames.add(authenticatorConfig.getName());
+ }
+ assertEquals(mergedAuthenticatorNames.size(), 2);
+ assertTrue(mergedAuthenticatorNames.contains("BasicAuthenticator"));
+ assertTrue(mergedAuthenticatorNames.contains("TOTPAuthenticator"));
+ } finally {
+ FrameworkServiceDataHolder.getInstance().setOrganizationUserSharingService(null);
+ }
+ }
+
+ private void invokePopulateContext(String accessingOrgId, AuthenticationContext context,
+ SequenceConfig effectiveSequence, SessionContext loadedSessionContext)
+ throws Exception {
+
+ Method method = DefaultRequestCoordinator.class.getDeclaredMethod(
+ "populateContextWithPreviousAuthenticatedOrganizationSessions", String.class,
+ AuthenticationContext.class, SequenceConfig.class, SessionContext.class);
+ method.setAccessible(true);
+ method.invoke(requestCoordinator, accessingOrgId, context, effectiveSequence, loadedSessionContext);
+ }
}