Improvements to the organization authentication flow

Closes #29416
Closes #29417
Closes #29418

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-05-07 19:20:54 -03:00
parent 2055cf62f2
commit 77b58275ca
12 changed files with 421 additions and 116 deletions

View file

@ -17,6 +17,7 @@
package org.keycloak.models.utils;
import static java.util.Optional.ofNullable;
import static org.keycloak.models.utils.StripSecretsUtils.stripSecrets;
import org.jboss.logging.Logger;
@ -55,13 +56,13 @@ import org.keycloak.utils.StringUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -463,7 +464,7 @@ public class ModelToRepresentation {
rep.setWebAuthnPolicyPasswordlessExtraOrigins(webAuthnPolicy.getExtraOrigins());
CibaConfig cibaPolicy = realm.getCibaPolicy();
Map<String, String> attrMap = Optional.ofNullable(rep.getAttributes()).orElse(new HashMap<>());
Map<String, String> attrMap = ofNullable(rep.getAttributes()).orElse(new HashMap<>());
attrMap.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE, cibaPolicy.getBackchannelTokenDeliveryMode());
attrMap.put(CibaConfig.CIBA_EXPIRES_IN, String.valueOf(cibaPolicy.getExpiresIn()));
attrMap.put(CibaConfig.CIBA_INTERVAL, String.valueOf(cibaPolicy.getPoolingInterval()));
@ -826,7 +827,7 @@ public class ModelToRepresentation {
ProtocolMapperRepresentation rep = new ProtocolMapperRepresentation();
rep.setId(model.getId());
rep.setProtocol(model.getProtocol());
Map<String, String> config = new HashMap<>(model.getConfig());
Map<String, String> config = new HashMap<>(ofNullable(model.getConfig()).orElse(Collections.emptyMap()));
rep.setConfig(config);
rep.setName(model.getName());
rep.setProtocolMapper(model.getProtocolMapper());

View file

@ -17,8 +17,8 @@
package org.keycloak.organization.admin.resource;
import java.util.Comparator;
import java.util.Optional;
import static java.util.Optional.ofNullable;
import java.util.Set;
import java.util.Objects;
import java.util.stream.Collectors;
@ -78,7 +78,7 @@ public class OrganizationResource {
throw ErrorResponse.error("Organization cannot be null.", Response.Status.BAD_REQUEST);
}
Set<String> domains = organization.getDomains().stream().map(OrganizationDomainRepresentation::getName).collect(Collectors.toSet());
Set<String> domains = ofNullable(organization.getDomains()).orElse(Set.of()).stream().map(OrganizationDomainRepresentation::getName).collect(Collectors.toSet());
OrganizationModel model = provider.create(organization.getName(), domains);
toModel(organization, model);
@ -198,7 +198,7 @@ public class OrganizationResource {
model.setEnabled(rep.isEnabled());
model.setDescription(rep.getDescription());
model.setAttributes(rep.getAttributes());
model.setDomains(Optional.ofNullable(rep.getDomains()).orElse(Set.of()).stream()
model.setDomains(ofNullable(rep.getDomains()).orElse(Set.of()).stream()
.filter(Objects::nonNull)
.map(this::toModel)
.collect(Collectors.toSet()));

View file

@ -24,6 +24,8 @@ import jakarta.ws.rs.core.MultivaluedMap;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.forms.login.freemarker.model.AuthenticationContextBean;
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
import org.keycloak.http.HttpRequest;
import org.keycloak.models.FederatedIdentityModel;
@ -32,8 +34,13 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareAuthenticationContextBean;
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareIdentityProviderBean;
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareRealmBean;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation;
public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
@ -68,8 +75,6 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
return;
}
OrganizationProvider provider = getOrganizationProvider();
OrganizationModel organization = null;
RealmModel realm = context.getRealm();
UserModel user = session.users().getUserByEmail(realm, username);
@ -80,13 +85,83 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
return;
}
organization = provider.getByMember(user);
IdentityProviderModel broker = resolveBroker(user);
if (broker == null) {
// not a managed member, continue with the regular flow
context.attempted();
} else {
// user is a managed member and associated with a broker, redirect automatically
redirect(context, broker.getAlias(), user.getEmail());
}
return;
}
OrganizationProvider provider = getOrganizationProvider();
OrganizationModel organization = provider.getByDomainName(emailDomain);
if (organization == null || !organization.isEnabled()) {
// request does not map to any organization, go to the next step/sub-flow
context.attempted();
return;
}
List<IdentityProviderModel> 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 (!hasPublicBrokers(brokers)) {
// the user does not exist, and there is no broker available for selection, redirect the user to the identity-first login page at the realm
challenge(username, context);
return;
}
// the user does not exist and is authenticating in the scope of the organization, show the identity-first login page and the
// public organization brokers for selection
LoginFormsProvider form = context.form()
.setAttributeMapper(attributes -> {
attributes.computeIfPresent("social",
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, true)
);
attributes.computeIfPresent("auth",
(key, bean) -> new OrganizationAwareAuthenticationContextBean((AuthenticationContextBean) bean, false)
);
attributes.computeIfPresent("realm",
(key, bean) -> new OrganizationAwareRealmBean(realm)
);
return attributes;
});
form.addError(new FormMessage("Your email domain matches the " + organization.getName() + " organization but you don't have an account yet."));
context.challenge(form
.createLoginUsername());
}
private static boolean hasPublicBrokers(List<IdentityProviderModel> brokers) {
return brokers.stream().anyMatch(p -> Boolean.parseBoolean(p.getConfig().getOrDefault(OrganizationModel.BROKER_PUBLIC, Boolean.FALSE.toString())));
}
private IdentityProviderModel resolveBroker(UserModel user) {
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
RealmModel realm = session.getContext().getRealm();
OrganizationModel organization = provider.getByMember(user);
if (organization == null || !organization.isEnabled()) {
return null;
}
if (organization != null) {
if (provider.isManagedMember(organization, user)) {
// user is a managed member, try to resolve the origin broker and redirect automatically
List<IdentityProviderModel> organizationBrokers = organization.getIdentityProviders().toList();
List<IdentityProviderModel> originBrokers = session.users().getFederatedIdentitiesStream(realm, user)
List<IdentityProviderModel> brokers = session.users().getFederatedIdentitiesStream(realm, user)
.map(f -> {
IdentityProviderModel broker = realm.getIdentityProviderByAlias(f.getIdentityProvider());
@ -104,62 +179,10 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
}).filter(Objects::nonNull)
.toList();
if (originBrokers.size() == 1) {
redirect(context, originBrokers.get(0).getAlias());
return;
}
} else {
context.attempted();
return;
}
}
return brokers.size() == 1 ? brokers.get(0) : null;
}
if (organization == null) {
organization = provider.getByDomainName(emailDomain);
}
if (organization == null) {
// request does not map to any organization, go to the next step/sub-flow
context.attempted();
return;
}
List<IdentityProviderModel> domainBrokers = organization.getIdentityProviders().toList();
if (domainBrokers.isEmpty()) {
// no organization brokers to automatically redirect the user, go to the next step/sub-flow
context.attempted();
return;
}
if (domainBrokers.size() == 1) {
// there is a single broker, redirect the user to authenticate
redirect(context, domainBrokers.get(0).getAlias(), username);
return;
}
for (IdentityProviderModel broker : domainBrokers) {
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;
}
}
// the user is authenticating in the scope of the organization, show the identity-first login page and the
// public organization brokers for selection
context.challenge(context.form()
.setAttributeMapper(attributes -> {
attributes.computeIfPresent("social",
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, true)
);
return attributes;
})
.createLoginUsername());
return null;
}
private OrganizationProvider getOrganizationProvider() {
@ -167,14 +190,27 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
}
private void challenge(AuthenticationFlowContext context) {
challenge(null, context);
}
private void challenge(String username, AuthenticationFlowContext context){
// the default challenge won't show any broker but just the identity-first login page and the option to try a different authentication mechanism
context.challenge(context.form()
LoginFormsProvider form = context.form()
.setAttributeMapper(attributes -> {
// removes identity provider related attributes from forms
attributes.remove("social");
attributes.computeIfPresent("social",
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, false, true)
);
attributes.computeIfPresent("auth",
(key, bean) -> new OrganizationAwareAuthenticationContextBean((AuthenticationContextBean) bean, false)
);
return attributes;
})
.createLoginUsername());
});
if (username != null) {
form.addError(new FormMessage(Validation.FIELD_USERNAME, Messages.INVALID_USER));
}
context.challenge(form.createLoginUsername());
}
private String getEmailDomain(String email) {

View file

@ -0,0 +1,59 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.organization.forms.login.freemarker.model;
import java.util.List;
import org.keycloak.authentication.AuthenticationSelectionOption;
import org.keycloak.forms.login.freemarker.model.AuthenticationContextBean;
public class OrganizationAwareAuthenticationContextBean extends AuthenticationContextBean {
private final AuthenticationContextBean delegate;
private final boolean showTryAnotherWayLink;
public OrganizationAwareAuthenticationContextBean(AuthenticationContextBean delegate, boolean showTryAnotherWayLink) {
super(null, null);
this.delegate = delegate;
this.showTryAnotherWayLink = showTryAnotherWayLink;
}
@Override
public List<AuthenticationSelectionOption> getAuthenticationSelections() {
return delegate.getAuthenticationSelections();
}
public boolean showTryAnotherWayLink() {
if (showTryAnotherWayLink) {
return delegate.showTryAnotherWayLink();
}
return false;
}
public boolean showUsername() {
return delegate.showUsername();
}
public boolean showResetCredentials() {
return delegate.showResetCredentials();
}
public String getAttemptedUsername() {
return delegate.getAttemptedUsername();
}
}

View file

@ -19,6 +19,7 @@ package org.keycloak.organization.forms.login.freemarker.model;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
import org.keycloak.models.IdentityProviderModel;
@ -32,8 +33,16 @@ public class OrganizationAwareIdentityProviderBean extends IdentityProviderBean
private final List<IdentityProvider> providers;
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session, boolean onlyOrganizationBrokers) {
this(delegate, session, onlyOrganizationBrokers, false);
}
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session, boolean onlyOrganizationBrokers, boolean onlyRealmBrokers) {
this.session = session;
if (onlyOrganizationBrokers) {
if (onlyRealmBrokers) {
providers = Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
.filter(Predicate.not(this::isPublicOrganizationBroker))
.toList();
} else if (onlyOrganizationBrokers) {
providers = Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
.filter(this::isPublicOrganizationBroker)
.toList();

View file

@ -0,0 +1,16 @@
package org.keycloak.organization.forms.login.freemarker.model;
import org.keycloak.forms.login.freemarker.model.RealmBean;
import org.keycloak.models.RealmModel;
public class OrganizationAwareRealmBean extends RealmBean {
public OrganizationAwareRealmBean(RealmModel realmModel) {
super(realmModel);
}
@Override
public boolean isRegistrationAllowed() {
return false;
}
}

View file

@ -358,6 +358,7 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide
metadata.addAttribute(OrganizationModel.ORGANIZATION_ATTRIBUTE, -1,
new AttributeValidatorMetadata(OrganizationMemberValidator.ID),
new AttributeValidatorMetadata(ImmutableAttributeValidator.ID))
.addReadCondition(c -> USER_API.equals(c.getContext()))
.addWriteCondition(context -> {
// the attribute can only be managed within the scope of the Organization API
// we assume, for now, that if the organization is set as a session attribute, we are operating within the scope if the Organization API

View file

@ -76,7 +76,7 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
}
@Test
public void testDefaultAuthenticationMechanismIfNotOrganizationMember() {
public void testDefaultAuthenticationIfUserDoesNotExistAndNoOrgMatch() {
testRealm().organizations().get(createOrganization().getId());
oauth.clientId("broker-app");
@ -85,12 +85,171 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
log.debug("Logging in");
Assert.assertFalse(loginPage.isPasswordInputPresent());
loginPage.loginUsername("user@noorg.org");
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() + "/"));
// check if the login page is shown
Assert.assertTrue(loginPage.isUsernameInputPresent());
Assert.assertTrue(loginPage.isPasswordInputPresent());
}
@Test
public void testIdentityFirstIfUserNotExistsAndEmailMatchOrgDomain() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
IdentityProviderRepresentation idpRep = organization.identityProviders().getIdentityProviders().get(0);
idpRep.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
testRealm().identityProviders().get(idpRep.getAlias()).update(idpRep);
oauth.clientId("broker-app");
// login with email only
loginPage.open(bc.consumerRealmName());
log.debug("Logging in");
Assert.assertFalse(loginPage.isPasswordInputPresent());
loginPage.loginUsername("user@neworg.org");
// should stay at the identity-first login page
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.assertTrue(loginPage.isUsernameInputPresent());
// registration link shown
Assert.assertTrue(loginPage.isRegisterLinkPresent());
// no need for password because the user does not exist
Assert.assertFalse(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(idpRep.getAlias()));
}
@Test
public void testIdentityFirstUserNotExistEmailMatchBrokerDomainAndBrokerIsPublic() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
IdentityProviderRepresentation idpRep = organization.identityProviders().getIdentityProviders().get(0);
idpRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
idpRep.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
testRealm().identityProviders().get(idpRep.getAlias()).update(idpRep);
oauth.clientId("broker-app");
// login with email only
loginPage.open(bc.consumerRealmName());
log.debug("Logging in");
Assert.assertFalse(loginPage.isPasswordInputPresent());
Assert.assertTrue(loginPage.isRegisterLinkPresent());
loginPage.loginUsername("user@neworg.org");
// should stay at the identity-first login page
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.assertEquals("Your email domain matches the neworg organization but you don't have an account yet.", loginPage.getError());
Assert.assertTrue(loginPage.isUsernameInputPresent());
Assert.assertFalse(loginPage.isPasswordInputPresent());
Assert.assertTrue(loginPage.isSocialButtonPresent(idpRep.getAlias()));
// no self-registration link because the user should register through the broker
Assert.assertFalse(loginPage.isRegisterLinkPresent());
}
@Test
public void testIdentityFirstUserNotExistEmailMatchBrokerDomainNoPublicBroker() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
IdentityProviderRepresentation idpRep = organization.identityProviders().getIdentityProviders().get(0);
idpRep.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
testRealm().identityProviders().get(idpRep.getAlias()).update(idpRep);
oauth.clientId("broker-app");
// login with email only
loginPage.open(bc.consumerRealmName());
log.debug("Logging in");
Assert.assertFalse(loginPage.isPasswordInputPresent());
Assert.assertTrue(loginPage.isRegisterLinkPresent());
loginPage.loginUsername("user@neworg.org");
// should stay at the identity-first login page
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(driver.getPageSource().contains("Your email domain matches the neworg organization but you don't have an account yet."));
Assert.assertTrue(loginPage.isUsernameInputPresent());
Assert.assertFalse(loginPage.isPasswordInputPresent());
// self-registration link shown because there is no public broker and user can choose to register
Assert.assertTrue(loginPage.isRegisterLinkPresent());
}
@Test
public void testDefaultAuthenticationShowsPublicOrganizationBrokers() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
OrganizationRepresentation representation = organization.toRepresentation();
representation.addDomain(new OrganizationDomainRepresentation("other.org"));
organization.update(representation).close();
IdentityProviderRepresentation idp = organization.identityProviders().get(bc.getIDPAlias()).toRepresentation();
idp.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "neworg.org");
// set a domain to the existing broker
testRealm().identityProviders().get(bc.getIDPAlias()).update(idp);
idp = bc.setUpIdentityProvider();
idp.setAlias("second-idp");
idp.setInternalId(null);
idp.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
idp.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
// create a second broker without a domain set
testRealm().identityProviders().create(idp).close();
getCleanup().addCleanup(testRealm().identityProviders().get("second-idp")::remove);
organization.identityProviders().addIdentityProvider(idp.getAlias()).close();
idp = organization.identityProviders().get(idp.getAlias()).toRepresentation();
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("external@user.org");
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.assertTrue(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
Assert.assertTrue(loginPage.isSocialButtonPresent(idp.getAlias()));
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() + "/"));
idp.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.FALSE.toString());
testRealm().identityProviders().get(idp.getAlias()).update(idp);
driver.navigate().refresh();
Assert.assertTrue(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
Assert.assertFalse(loginPage.isSocialButtonPresent(idp.getAlias()));
}
@Test
public void testDefaultAuthenticationWhenUserExistEmailMatchOrgDomain() {
realmsResouce().realm(bc.consumerRealmName()).users()
.create(UserBuilder.create()
.username("user@neworg.org")
.email("user@neworg.org")
.password(bc.getUserPassword())
.enabled(true).build()
).close();
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
IdentityProviderRepresentation idpRep = organization.identityProviders().getIdentityProviders().get(0);
idpRep.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
testRealm().identityProviders().get(idpRep.getAlias()).update(idpRep);
oauth.clientId("broker-app");
// login with email only
loginPage.open(bc.consumerRealmName());
log.debug("Logging in");
Assert.assertFalse(loginPage.isPasswordInputPresent());
loginPage.loginUsername("user@neworg.org");
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.assertTrue(loginPage.isUsernameInputPresent());
Assert.assertTrue(loginPage.isPasswordInputPresent());
Assert.assertTrue(loginPage.isRegisterLinkPresent());
}
@Test
public void testRealmLevelBrokersAvailableIfEmailDoesNotMatchOrganization() {
testRealm().organizations().get(createOrganization().getId());
@ -131,12 +290,20 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
).close();
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
OrganizationIdentityProviderResource broker = organization.identityProviders().get(bc.getIDPAlias());
IdentityProviderRepresentation brokerRep = broker.toRepresentation();
brokerRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
testRealm().identityProviders().get(brokerRep.getAlias()).update(brokerRep);
oauth.clientId("broker-app");
// login with email only
loginPage.open(bc.consumerRealmName());
log.debug("Logging in");
loginPage.loginUsername(bc.getUserEmail());
waitForPage(driver, "sign in to", true);
Assert.assertTrue("Driver should be on the provider realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
loginPage.clickSocial(bc.getIDPAlias());
// user automatically redirected to the organization identity provider
waitForPage(driver, "sign in to", true);
@ -162,7 +329,57 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
}
@Test
public void testReAuthenticateWhenAlreadyMember() {
public void testExistingUserUsingOrgDomain() {
// create a realm user in the consumer realm
realmsResouce().realm(bc.consumerRealmName()).users()
.create(UserBuilder.create()
.username(bc.getUserLogin())
.email(bc.getUserEmail())
.password(bc.getUserPassword())
.enabled(true).build()
).close();
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
OrganizationIdentityProviderResource broker = organization.identityProviders().get(bc.getIDPAlias());
IdentityProviderRepresentation brokerRep = broker.toRepresentation();
brokerRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
testRealm().identityProviders().get(brokerRep.getAlias()).update(brokerRep);
oauth.clientId("broker-app");
// login with email only
loginPage.open(bc.consumerRealmName());
log.debug("Logging in");
loginPage.loginUsername(bc.getUserEmail());
waitForPage(driver, "sign in to", true);
Assert.assertTrue("Driver should be on the provider realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
loginPage.clickSocial(bc.getIDPAlias());
// user automatically redirected to the organization identity provider
waitForPage(driver, "sign in to", true);
Assert.assertTrue("Driver should be on the provider realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
// login to the organization identity provider and run the configured first broker login flow
loginPage.login(bc.getUserEmail(), bc.getUserPassword());
waitForPage(driver, "update account information", false);
updateAccountInformationPage.assertCurrent();
Assert.assertTrue("We must be on correct realm right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
log.debug("Updating info on updateAccount page");
updateAccountInformationPage.updateAccountInformation(bc.getUserEmail(), bc.getUserEmail(), "Firstname", "Lastname");
// account with the same email exists in the realm, execute account linking
waitForPage(driver, "account already exists", false);
idpConfirmLinkPage.assertCurrent();
idpConfirmLinkPage.clickLinkAccount();
// confirm the link by authenticating
loginPage.login(bc.getUserEmail(), bc.getUserPassword());
assertIsMember(bc.getUserEmail(), organization);
}
@Test
public void testRedirectBrokerWhenManagedMember() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
// add the member for the first time
@ -281,47 +498,6 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
assertEquals(bc.getIDPAlias(), federatedIdentities.get(0).getIdentityProvider());
}
@Test
public void testIdentityFirstLoginShowsPublicOrganizationBrokers() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
OrganizationRepresentation representation = organization.toRepresentation();
representation.addDomain(new OrganizationDomainRepresentation("other.org"));
organization.update(representation).close();
IdentityProviderRepresentation idp = organization.identityProviders().get(bc.getIDPAlias()).toRepresentation();
idp.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "neworg.org");
// set a domain to the existing broker
testRealm().identityProviders().get(bc.getIDPAlias()).update(idp);
idp = bc.setUpIdentityProvider();
idp.setAlias("second-idp");
idp.setInternalId(null);
idp.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
idp.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
// create a second broker without a domain set
testRealm().identityProviders().create(idp).close();
getCleanup().addCleanup(testRealm().identityProviders().get("second-idp")::remove);
organization.identityProviders().addIdentityProvider(idp.getAlias()).close();
idp = organization.identityProviders().get(idp.getAlias()).toRepresentation();
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("external@user.org");
Assert.assertTrue(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
Assert.assertTrue(loginPage.isSocialButtonPresent(idp.getAlias()));
idp.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.FALSE.toString());
testRealm().identityProviders().get(idp.getAlias()).update(idp);
driver.navigate().refresh();
Assert.assertTrue(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
Assert.assertFalse(loginPage.isSocialButtonPresent(idp.getAlias()));
}
@Test
public void testLoginUsingBrokerWithoutDomain() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
@ -506,6 +682,7 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
// make sure the user can select this idp from the organization when authenticating
idpRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
idpRep.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
testRealm().identityProviders().get(idpRep.getAlias()).update(idpRep);
// create a user to the provider realm using a email that does not share the same domain as the org
@ -544,6 +721,7 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
// make sure the user can select this idp from the organization when authenticating
idpRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
idpRep.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
testRealm().identityProviders().get(idpRep.getAlias()).update(idpRep);
// create a user to the provider realm using a email that does not share the same domain as the org

View file

@ -18,6 +18,7 @@
package org.keycloak.testsuite.organization.admin;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
@ -28,6 +29,8 @@ import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.jboss.arquillian.graphene.page.Page;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.models.OrganizationModel;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.admin.client.resource.RealmResource;
@ -102,6 +105,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
id = ApiUtil.getCreatedId(response);
}
IdentityProviderRepresentation broker = brokerConfigFunction.apply(name).setUpIdentityProvider();
broker.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, org.getDomains().iterator().next().getName());
testRealm().identityProviders().create(broker).close();
getCleanup().addCleanup(testRealm().identityProviders().get(broker.getAlias())::remove);
testRealm().organizations().get(id).identityProviders().addIdentityProvider(broker.getAlias()).close();
@ -183,6 +187,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
Assert.assertTrue("We must be on correct realm right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
log.debug("Updating info on updateAccount page");
assertFalse(driver.getPageSource().contains("kc.org"));
updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), email, "Firstname", "Lastname");
assertIsMember(email, organization);

View file

@ -21,6 +21,6 @@ import org.keycloak.common.Profile.Feature;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
@EnableFeature(Feature.ORGANIZATION)
public class OrganizationBrokerSelfRegistrationTest extends AbstractBrokerSelfRegistrationTest {
public class OrganizationOIDCBrokerSelfRegistrationTest extends AbstractBrokerSelfRegistrationTest {
}

View file

@ -62,7 +62,7 @@
</div>
</#if>
<#elseif section = "socialProviders" >
<#if realm.password && social?? && social.providers??>
<#if realm.password && social?? && social.providers?has_content>
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
<hr/>
<h4>${msg("identity-provider-login-label")}</h4>

View file

@ -88,7 +88,7 @@
</div>
</#if>
<#elseif section = "socialProviders" >
<#if realm.password && social?? && social.providers??>
<#if realm.password && social?? && social.providers?has_content>
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
<hr/>
<h2>${msg("identity-provider-login-label")}</h2>