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

View file

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

View file

@ -24,6 +24,8 @@ import jakarta.ws.rs.core.MultivaluedMap;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator; 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.forms.login.freemarker.model.IdentityProviderBean;
import org.keycloak.http.HttpRequest; import org.keycloak.http.HttpRequest;
import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.FederatedIdentityModel;
@ -32,8 +34,13 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationModel; import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.organization.OrganizationProvider; 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.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 { public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
@ -68,8 +75,6 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
return; return;
} }
OrganizationProvider provider = getOrganizationProvider();
OrganizationModel organization = null;
RealmModel realm = context.getRealm(); RealmModel realm = context.getRealm();
UserModel user = session.users().getUserByEmail(realm, username); UserModel user = session.users().getUserByEmail(realm, username);
@ -80,67 +85,31 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
return; return;
} }
organization = provider.getByMember(user); IdentityProviderModel broker = resolveBroker(user);
if (organization != null) { if (broker == null) {
if (provider.isManagedMember(organization, user)) { // not a managed member, continue with the regular flow
// user is a managed member, try to resolve the origin broker and redirect automatically context.attempted();
List<IdentityProviderModel> organizationBrokers = organization.getIdentityProviders().toList(); } else {
List<IdentityProviderModel> originBrokers = session.users().getFederatedIdentitiesStream(realm, user) // user is a managed member and associated with a broker, redirect automatically
.map(f -> { redirect(context, broker.getAlias(), user.getEmail());
IdentityProviderModel broker = realm.getIdentityProviderByAlias(f.getIdentityProvider());
if (!organizationBrokers.contains(broker)) {
return null;
}
FederatedIdentityModel identity = session.users().getFederatedIdentity(realm, user, broker.getAlias());
if (identity != null) {
return broker;
}
return null;
}).filter(Objects::nonNull)
.toList();
if (originBrokers.size() == 1) {
redirect(context, originBrokers.get(0).getAlias());
return;
}
} else {
context.attempted();
return;
}
} }
return;
} }
if (organization == null) { OrganizationProvider provider = getOrganizationProvider();
organization = provider.getByDomainName(emailDomain); OrganizationModel organization = provider.getByDomainName(emailDomain);
}
if (organization == null) { if (organization == null || !organization.isEnabled()) {
// request does not map to any organization, go to the next step/sub-flow // request does not map to any organization, go to the next step/sub-flow
context.attempted(); context.attempted();
return; return;
} }
List<IdentityProviderModel> domainBrokers = organization.getIdentityProviders().toList(); List<IdentityProviderModel> brokers = organization.getIdentityProviders().toList();
if (domainBrokers.isEmpty()) { for (IdentityProviderModel broker : brokers) {
// 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); String idpDomain = broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
if (emailDomain.equals(idpDomain)) { if (emailDomain.equals(idpDomain)) {
@ -150,31 +119,98 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
} }
} }
// the user is authenticating in the scope of the organization, show the identity-first login page and the 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 // public organization brokers for selection
context.challenge(context.form() LoginFormsProvider form = context.form()
.setAttributeMapper(attributes -> { .setAttributeMapper(attributes -> {
attributes.computeIfPresent("social", attributes.computeIfPresent("social",
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, true) (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; 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()); .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 (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> brokers = session.users().getFederatedIdentitiesStream(realm, user)
.map(f -> {
IdentityProviderModel broker = realm.getIdentityProviderByAlias(f.getIdentityProvider());
if (!organizationBrokers.contains(broker)) {
return null;
}
FederatedIdentityModel identity = session.users().getFederatedIdentity(realm, user, broker.getAlias());
if (identity != null) {
return broker;
}
return null;
}).filter(Objects::nonNull)
.toList();
return brokers.size() == 1 ? brokers.get(0) : null;
}
return null;
}
private OrganizationProvider getOrganizationProvider() { private OrganizationProvider getOrganizationProvider() {
return session.getProvider(OrganizationProvider.class); return session.getProvider(OrganizationProvider.class);
} }
private void challenge(AuthenticationFlowContext context){ 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 // 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 -> { .setAttributeMapper(attributes -> {
// removes identity provider related attributes from forms attributes.computeIfPresent("social",
attributes.remove("social"); (key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, false, true)
);
attributes.computeIfPresent("auth",
(key, bean) -> new OrganizationAwareAuthenticationContextBean((AuthenticationContextBean) bean, false)
);
return attributes; 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) { 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.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Predicate;
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean; import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
@ -32,8 +33,16 @@ public class OrganizationAwareIdentityProviderBean extends IdentityProviderBean
private final List<IdentityProvider> providers; private final List<IdentityProvider> providers;
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session, boolean onlyOrganizationBrokers) { 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; 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() providers = Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
.filter(this::isPublicOrganizationBroker) .filter(this::isPublicOrganizationBroker)
.toList(); .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, metadata.addAttribute(OrganizationModel.ORGANIZATION_ATTRIBUTE, -1,
new AttributeValidatorMetadata(OrganizationMemberValidator.ID), new AttributeValidatorMetadata(OrganizationMemberValidator.ID),
new AttributeValidatorMetadata(ImmutableAttributeValidator.ID)) new AttributeValidatorMetadata(ImmutableAttributeValidator.ID))
.addReadCondition(c -> USER_API.equals(c.getContext()))
.addWriteCondition(context -> { .addWriteCondition(context -> {
// the attribute can only be managed within the scope of the Organization API // 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 // 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 @Test
public void testDefaultAuthenticationMechanismIfNotOrganizationMember() { public void testDefaultAuthenticationIfUserDoesNotExistAndNoOrgMatch() {
testRealm().organizations().get(createOrganization().getId()); testRealm().organizations().get(createOrganization().getId());
oauth.clientId("broker-app"); oauth.clientId("broker-app");
@ -85,12 +85,171 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
log.debug("Logging in"); log.debug("Logging in");
Assert.assertFalse(loginPage.isPasswordInputPresent()); Assert.assertFalse(loginPage.isPasswordInputPresent());
loginPage.loginUsername("user@noorg.org"); 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 // check if the login page is shown
Assert.assertTrue(loginPage.isUsernameInputPresent()); Assert.assertTrue(loginPage.isUsernameInputPresent());
Assert.assertTrue(loginPage.isPasswordInputPresent()); 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 @Test
public void testRealmLevelBrokersAvailableIfEmailDoesNotMatchOrganization() { public void testRealmLevelBrokersAvailableIfEmailDoesNotMatchOrganization() {
testRealm().organizations().get(createOrganization().getId()); testRealm().organizations().get(createOrganization().getId());
@ -131,12 +290,20 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
).close(); ).close();
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); 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"); oauth.clientId("broker-app");
// login with email only // login with email only
loginPage.open(bc.consumerRealmName()); loginPage.open(bc.consumerRealmName());
log.debug("Logging in"); log.debug("Logging in");
loginPage.loginUsername(bc.getUserEmail()); 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 // user automatically redirected to the organization identity provider
waitForPage(driver, "sign in to", true); waitForPage(driver, "sign in to", true);
@ -162,7 +329,57 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
} }
@Test @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()); OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
// add the member for the first time // add the member for the first time
@ -281,47 +498,6 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
assertEquals(bc.getIDPAlias(), federatedIdentities.get(0).getIdentityProvider()); 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 @Test
public void testLoginUsingBrokerWithoutDomain() { public void testLoginUsingBrokerWithoutDomain() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); 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 // make sure the user can select this idp from the organization when authenticating
idpRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString()); idpRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
idpRep.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
testRealm().identityProviders().get(idpRep.getAlias()).update(idpRep); 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 // 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 // make sure the user can select this idp from the organization when authenticating
idpRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString()); idpRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
idpRep.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
testRealm().identityProviders().get(idpRep.getAlias()).update(idpRep); 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 // 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; package org.keycloak.testsuite.organization.admin;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage; 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 jakarta.ws.rs.core.Response.Status;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.keycloak.admin.client.resource.OrganizationResource; 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.admin.client.resource.UsersResource;
import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
@ -102,6 +105,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
id = ApiUtil.getCreatedId(response); id = ApiUtil.getCreatedId(response);
} }
IdentityProviderRepresentation broker = brokerConfigFunction.apply(name).setUpIdentityProvider(); IdentityProviderRepresentation broker = brokerConfigFunction.apply(name).setUpIdentityProvider();
broker.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, org.getDomains().iterator().next().getName());
testRealm().identityProviders().create(broker).close(); testRealm().identityProviders().create(broker).close();
getCleanup().addCleanup(testRealm().identityProviders().get(broker.getAlias())::remove); getCleanup().addCleanup(testRealm().identityProviders().get(broker.getAlias())::remove);
testRealm().organizations().get(id).identityProviders().addIdentityProvider(broker.getAlias()).close(); 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", Assert.assertTrue("We must be on correct realm right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
log.debug("Updating info on updateAccount page"); log.debug("Updating info on updateAccount page");
assertFalse(driver.getPageSource().contains("kc.org"));
updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), email, "Firstname", "Lastname"); updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), email, "Firstname", "Lastname");
assertIsMember(email, organization); assertIsMember(email, organization);

View file

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

View file

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

View file

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