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:
parent
2055cf62f2
commit
77b58275ca
12 changed files with 421 additions and 116 deletions
|
@ -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());
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue