diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 67be1d9980..e617e15ec0 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -3191,4 +3191,6 @@ linkUpdatedSuccessful=Identity provider link successfully updated linkUpdateError=Could not update link to identity provider\: {{error}} noResultsFound=No results found linkedOrganization=Linked organization -send=Send \ No newline at end of file +send=Send +redirectWhenEmailMatches=Redirect when email domain matches +redirectWhenEmailMatchesHelp=Automatically redirect the user to this identity provider when the email domain matches the domain \ No newline at end of file diff --git a/js/apps/admin-ui/src/organizations/LinkIdentityProviderModal.tsx b/js/apps/admin-ui/src/organizations/LinkIdentityProviderModal.tsx index 22531614d5..44c0f6f594 100644 --- a/js/apps/admin-ui/src/organizations/LinkIdentityProviderModal.tsx +++ b/js/apps/admin-ui/src/organizations/LinkIdentityProviderModal.tsx @@ -145,6 +145,14 @@ export const LinkIdentityProviderModal = ({ labelIcon={t("shownOnLoginPageHelp")} stringify /> + diff --git a/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java b/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java index 88a689843a..baa8a5315b 100644 --- a/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java +++ b/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java @@ -29,6 +29,24 @@ public interface OrganizationModel { String ORGANIZATION_DOMAIN_ATTRIBUTE = "kc.org.domain"; String BROKER_PUBLIC = "kc.org.broker.public"; + enum IdentityProviderRedirectMode { + EMAIL_MATCH("kc.org.broker.redirect.mode.email-matches"); + + private final String key; + + IdentityProviderRedirectMode(String key) { + this.key = key; + } + + public boolean isSet(IdentityProviderModel broker) { + return Boolean.parseBoolean(broker.getConfig().get(key)); + } + + public String getKey() { + return key; + } + } + String getId(); void setName(String name); diff --git a/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java b/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java index 4045ddacd6..a5b174a73b 100644 --- a/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java +++ b/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java @@ -32,6 +32,7 @@ import org.keycloak.http.HttpRequest; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.OrganizationModel; +import org.keycloak.models.OrganizationModel.IdentityProviderRedirectMode; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; @@ -109,14 +110,8 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator { List brokers = organization.getIdentityProviders().toList(); - for (IdentityProviderModel broker : brokers) { - String idpDomain = broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE); - - if (emailDomain.equals(idpDomain)) { - // redirect the user using the broker that matches the email domain - redirect(context, broker.getAlias(), username); - return; - } + if (redirect(context, brokers, username, emailDomain)) { + return; } if (!hasPublicBrokers(brokers)) { @@ -195,4 +190,20 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator { public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return realm.isOrganizationsEnabled(); } + + protected boolean redirect(AuthenticationFlowContext context, List brokers, String username, String emailDomain) { + for (IdentityProviderModel broker : brokers) { + if (IdentityProviderRedirectMode.EMAIL_MATCH.isSet(broker)) { + String idpDomain = broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE); + + if (emailDomain.equals(idpDomain)) { + // redirect the user using the broker that matches the email domain + redirect(context, broker.getAlias(), username); + return true; + } + } + } + + return false; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractBrokerSelfRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractBrokerSelfRegistrationTest.java index ed54987275..5d9d9b6224 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractBrokerSelfRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractBrokerSelfRegistrationTest.java @@ -34,6 +34,7 @@ import org.keycloak.admin.client.resource.OrganizationResource; import org.keycloak.admin.client.resource.UsersResource; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.OrganizationModel; +import org.keycloak.models.OrganizationModel.IdentityProviderRedirectMode; import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.ErrorRepresentation; @@ -498,6 +499,32 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz assertEquals(bc.getIDPAlias(), federatedIdentities.get(0).getIdentityProvider()); } + @Test + public void testDoNotRedirectToIdentityProviderAssociatedWithOrganizationDomain() { + OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); + IdentityProviderRepresentation idp = organization.identityProviders().get(bc.getIDPAlias()).toRepresentation(); + idp.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "neworg.org"); + idp.getConfig().put(IdentityProviderRedirectMode.EMAIL_MATCH.getKey(), Boolean.FALSE.toString()); + idp.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString()); + testRealm().identityProviders().get(bc.getIDPAlias()).update(idp); + + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + log.debug("Logging in"); + Assert.assertFalse(loginPage.isPasswordInputPresent()); + Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias())); + Assert.assertFalse(loginPage.isSocialButtonPresent(idp.getAlias())); + loginPage.loginUsername(bc.getUserEmail()); + + // user not automatically redirected to the organization identity provider + waitForPage(driver, "sign in to", true); + Assert.assertTrue("Driver should be on the consumer realm page right now", + driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); + Assert.assertFalse(loginPage.isPasswordInputPresent()); + Assert.assertTrue(driver.getPageSource().contains("Your email domain matches the " + organizationName + " organization but you don't have an account yet.")); + Assert.assertTrue(loginPage.isSocialButtonPresent(bc.getIDPAlias())); + } + @Test public void testLoginUsingBrokerWithoutDomain() { OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java index 52be10cfbd..628049026a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java @@ -32,6 +32,7 @@ import org.jboss.arquillian.graphene.page.Page; import org.keycloak.admin.client.resource.OrganizationResource; import org.keycloak.models.OrganizationModel; import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.models.OrganizationModel.IdentityProviderRedirectMode; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.representations.idm.GroupRepresentation; @@ -117,6 +118,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { } // set the idp domain to the first domain used to create the org. broker.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, orgDomains[0]); + broker.getConfig().put(IdentityProviderRedirectMode.EMAIL_MATCH.getKey(), Boolean.TRUE.toString()); testRealm.identityProviders().create(broker).close(); testCleanup.addCleanup(testRealm.identityProviders().get(broker.getAlias())::remove); testRealm.organizations().get(id).identityProviders().addIdentityProvider(broker.getAlias()).close(); @@ -201,7 +203,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { assertFalse(driver.getPageSource().contains("kc.org")); updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), email, "Firstname", "Lastname"); assertThat(appPage.getRequestType(),is(AppPage.RequestType.AUTH_RESPONSE)); - + assertIsMember(email, organization); }