Support for mutiple identity providers
Closes #28840 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
051c0197db
commit
32d25f43d0
27 changed files with 991 additions and 372 deletions
|
@ -27,6 +27,14 @@ public class OrganizationDomainRepresentation {
|
||||||
private String name;
|
private String name;
|
||||||
private boolean verified;
|
private boolean verified;
|
||||||
|
|
||||||
|
public OrganizationDomainRepresentation() {
|
||||||
|
// for reflection
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrganizationDomainRepresentation(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ package org.keycloak.admin.client.resource;
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
import jakarta.ws.rs.DELETE;
|
import jakarta.ws.rs.DELETE;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.POST;
|
|
||||||
import jakarta.ws.rs.PUT;
|
import jakarta.ws.rs.PUT;
|
||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
@ -29,10 +28,6 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
|
|
||||||
public interface OrganizationIdentityProviderResource {
|
public interface OrganizationIdentityProviderResource {
|
||||||
|
|
||||||
@POST
|
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
|
||||||
Response create(IdentityProviderRepresentation idpRepresentation);
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
IdentityProviderRepresentation toRepresentation();
|
IdentityProviderRepresentation toRepresentation();
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* 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.admin.client.resource;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
|
|
||||||
|
public interface OrganizationIdentityProvidersResource {
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
Response create(IdentityProviderRepresentation idpRepresentation);
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
List<IdentityProviderRepresentation> getIdentityProviders();
|
||||||
|
|
||||||
|
@Path("{id}")
|
||||||
|
OrganizationIdentityProviderResource get(@PathParam("id") String id);
|
||||||
|
}
|
|
@ -43,6 +43,6 @@ public interface OrganizationResource {
|
||||||
@Path("members")
|
@Path("members")
|
||||||
OrganizationMembersResource members();
|
OrganizationMembersResource members();
|
||||||
|
|
||||||
@Path("identity-provider")
|
@Path("identity-providers")
|
||||||
OrganizationIdentityProviderResource identityProvider();
|
OrganizationIdentityProvidersResource identityProviders();
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,9 +57,6 @@ public class OrganizationEntity {
|
||||||
@Column(name = "GROUP_ID")
|
@Column(name = "GROUP_ID")
|
||||||
private String groupId;
|
private String groupId;
|
||||||
|
|
||||||
@Column(name = "IDP_ALIAS")
|
|
||||||
private String idpAlias;
|
|
||||||
|
|
||||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy="organization")
|
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy="organization")
|
||||||
protected Set<OrganizationDomainEntity> domains = new HashSet<>();
|
protected Set<OrganizationDomainEntity> domains = new HashSet<>();
|
||||||
|
|
||||||
|
@ -95,14 +92,6 @@ public class OrganizationEntity {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getIdpAlias() {
|
|
||||||
return idpAlias;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIdpAlias(String idpAlias) {
|
|
||||||
this.idpAlias = idpAlias;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<OrganizationDomainEntity> getDomains() {
|
public Collection<OrganizationDomainEntity> getDomains() {
|
||||||
if (this.domains == null) {
|
if (this.domains == null) {
|
||||||
this.domains = new HashSet<>();
|
this.domains = new HashSet<>();
|
||||||
|
|
|
@ -23,6 +23,7 @@ import static org.keycloak.utils.StreamsUtil.closing;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@ -98,8 +99,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
//TODO: won't scale, requires a better mechanism for bulk deleting users
|
//TODO: won't scale, requires a better mechanism for bulk deleting users
|
||||||
userProvider.getGroupMembersStream(realm, group).forEach(userModel -> removeMember(organization, userModel));
|
userProvider.getGroupMembersStream(realm, group).forEach(userModel -> removeMember(organization, userModel));
|
||||||
groupProvider.removeGroup(realm, group);
|
groupProvider.removeGroup(realm, group);
|
||||||
|
organization.getIdentityProviders().forEach((model) -> realm.removeIdentityProviderByAlias(model.getAlias()));
|
||||||
realm.removeIdentityProviderByAlias(entity.getIdpAlias());
|
|
||||||
|
|
||||||
em.remove(entity);
|
em.remove(entity);
|
||||||
|
|
||||||
|
@ -215,29 +215,35 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
throwExceptionIfObjectIsNull(identityProvider, "Identity provider");
|
throwExceptionIfObjectIsNull(identityProvider, "Identity provider");
|
||||||
|
|
||||||
OrganizationEntity organizationEntity = getEntity(organization.getId());
|
OrganizationEntity organizationEntity = getEntity(organization.getId());
|
||||||
organizationEntity.setIdpAlias(identityProvider.getAlias());
|
|
||||||
identityProvider.getConfig().put(ORGANIZATION_ATTRIBUTE, organization.getId());
|
identityProvider.setOrganizationId(organizationEntity.getId());
|
||||||
realm.updateIdentityProvider(identityProvider);
|
realm.updateIdentityProvider(identityProvider);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IdentityProviderModel getIdentityProvider(OrganizationModel organization) {
|
public Stream<IdentityProviderModel> getIdentityProviders(OrganizationModel organization) {
|
||||||
throwExceptionIfObjectIsNull(organization, "Organization");
|
throwExceptionIfObjectIsNull(organization, "Organization");
|
||||||
throwExceptionIfObjectIsNull(organization.getId(), "Organization ID");
|
throwExceptionIfObjectIsNull(organization.getId(), "Organization ID");
|
||||||
|
|
||||||
OrganizationEntity organizationEntity = getEntity(organization.getId());
|
OrganizationEntity organizationEntity = getEntity(organization.getId());
|
||||||
// realm and its IDPs are cached
|
|
||||||
return realm.getIdentityProviderByAlias(organizationEntity.getIdpAlias());
|
return realm.getIdentityProvidersStream().filter(model -> organizationEntity.getId().equals(model.getOrganizationId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean removeIdentityProvider(OrganizationModel organization) {
|
public boolean removeIdentityProvider(OrganizationModel organization, IdentityProviderModel identityProvider) {
|
||||||
throwExceptionIfObjectIsNull(organization, "Organization");
|
throwExceptionIfObjectIsNull(organization, "Organization");
|
||||||
|
|
||||||
OrganizationEntity organizationEntity = getEntity(organization.getId());
|
OrganizationEntity organizationEntity = getEntity(organization.getId());
|
||||||
organizationEntity.setIdpAlias(null);
|
|
||||||
|
if (!organizationEntity.getId().equals(identityProvider.getOrganizationId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
realm.removeIdentityProviderByAlias(identityProvider.getAlias());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,15 +255,19 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
IdentityProviderModel identityProvider = organization.getIdentityProvider();
|
List<IdentityProviderModel> brokers = organization.getIdentityProviders().toList();
|
||||||
|
|
||||||
if (identityProvider == null) {
|
if (brokers.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FederatedIdentityModel federatedIdentity = userProvider.getFederatedIdentity(realm, member, identityProvider.getAlias());
|
List<FederatedIdentityModel> federatedIdentities = userProvider.getFederatedIdentitiesStream(realm, member)
|
||||||
|
.map(federatedIdentityModel -> realm.getIdentityProviderByAlias(federatedIdentityModel.getIdentityProvider()))
|
||||||
|
.filter(brokers::contains)
|
||||||
|
.map(m -> userProvider.getFederatedIdentity(realm, member, m.getAlias()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
return federatedIdentity != null;
|
return !federatedIdentities.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -310,7 +320,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!realm.getId().equals(entity.getRealmId())) {
|
if (!realm.getId().equals(entity.getRealmId())) {
|
||||||
throw new ModelException("Organization [" + entity.getId() + " does not belong to realm [" + realm.getId() + "]");
|
throw new ModelException("Organization [" + entity.getId() + "] does not belong to realm [" + realm.getId() + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
return entity;
|
return entity;
|
||||||
|
|
|
@ -130,8 +130,8 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IdentityProviderModel getIdentityProvider() {
|
public Stream<IdentityProviderModel> getIdentityProviders() {
|
||||||
return provider.getIdentityProvider(this);
|
return provider.getIdentityProviders(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -89,7 +89,6 @@
|
||||||
<column name="GROUP_ID" type="VARCHAR(255)">
|
<column name="GROUP_ID" type="VARCHAR(255)">
|
||||||
<constraints nullable="false"/>
|
<constraints nullable="false"/>
|
||||||
</column>
|
</column>
|
||||||
<column name="IDP_ALIAS" type="VARCHAR(255)" />
|
|
||||||
<column name="NAME" type="VARCHAR(255)">
|
<column name="NAME" type="VARCHAR(255)">
|
||||||
<constraints nullable="false"/>
|
<constraints nullable="false"/>
|
||||||
</column>
|
</column>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.organization;
|
package org.keycloak.organization;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
@ -140,15 +141,16 @@ public interface OrganizationProvider extends Provider {
|
||||||
* @param organization the organization
|
* @param organization the organization
|
||||||
* @return The identityProvider associated with a given {@code organization} or {@code null} if there is none.
|
* @return The identityProvider associated with a given {@code organization} or {@code null} if there is none.
|
||||||
*/
|
*/
|
||||||
IdentityProviderModel getIdentityProvider(OrganizationModel organization);
|
Stream<IdentityProviderModel> getIdentityProviders(OrganizationModel organization);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the link between the given {@link OrganizationModel} and identity provider associated with it if such a link exists.
|
* Removes the link between the given {@link OrganizationModel} and identity provider associated with it if such a link exists.
|
||||||
*
|
*
|
||||||
* @param organization the organization
|
* @param organization the organization
|
||||||
|
* @param identityProvider the identity provider
|
||||||
* @return {@code true} if the link was removed, {@code false} otherwise
|
* @return {@code true} if the link was removed, {@code false} otherwise
|
||||||
*/
|
*/
|
||||||
boolean removeIdentityProvider(OrganizationModel organization);
|
boolean removeIdentityProvider(OrganizationModel organization, IdentityProviderModel identityProvider);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if the current realm supports organization.
|
* Indicates if the current realm supports organization.
|
||||||
|
|
|
@ -220,6 +220,14 @@ public class IdentityProviderModel implements Serializable {
|
||||||
return displayIconClasses;
|
return displayIconClasses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getOrganizationId() {
|
||||||
|
return getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrganizationId(String organizationId) {
|
||||||
|
getConfig().put(OrganizationModel.ORGANIZATION_ATTRIBUTE, organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Validates this configuration.
|
* <p>Validates this configuration.
|
||||||
*
|
*
|
||||||
|
|
|
@ -25,6 +25,8 @@ import java.util.stream.Stream;
|
||||||
public interface OrganizationModel {
|
public interface OrganizationModel {
|
||||||
|
|
||||||
String ORGANIZATION_ATTRIBUTE = "kc.org";
|
String ORGANIZATION_ATTRIBUTE = "kc.org";
|
||||||
|
String ORGANIZATION_DOMAIN_ATTRIBUTE = "kc.org.domain";
|
||||||
|
String BROKER_PUBLIC = "kc.org.broker.public";
|
||||||
|
|
||||||
String getId();
|
String getId();
|
||||||
|
|
||||||
|
@ -40,7 +42,7 @@ public interface OrganizationModel {
|
||||||
|
|
||||||
void setDomains(Set<OrganizationDomainModel> domains);
|
void setDomains(Set<OrganizationDomainModel> domains);
|
||||||
|
|
||||||
IdentityProviderModel getIdentityProvider();
|
Stream<IdentityProviderModel> getIdentityProviders();
|
||||||
|
|
||||||
boolean isManaged(UserModel user);
|
boolean isManaged(UserModel user);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@ import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator;
|
||||||
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
|
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
|
||||||
import org.keycloak.authentication.requiredactions.util.UserUpdateProfileContext;
|
import org.keycloak.authentication.requiredactions.util.UserUpdateProfileContext;
|
||||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.common.Profile.Feature;
|
||||||
import org.keycloak.common.util.ObjectUtil;
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
import org.keycloak.forms.login.LoginFormsPages;
|
import org.keycloak.forms.login.LoginFormsPages;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
|
@ -64,6 +66,7 @@ import org.keycloak.models.KeycloakSession;
|
||||||
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.models.utils.FormMessage;
|
||||||
|
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareIdentityProviderBean;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.rar.AuthorizationDetails;
|
import org.keycloak.rar.AuthorizationDetails;
|
||||||
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||||
|
@ -71,7 +74,6 @@ import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.LoginActionsService;
|
import org.keycloak.services.resources.LoginActionsService;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.sessions.CommonClientSessionModel;
|
|
||||||
import org.keycloak.theme.FreeMarkerException;
|
import org.keycloak.theme.FreeMarkerException;
|
||||||
import org.keycloak.theme.Theme;
|
import org.keycloak.theme.Theme;
|
||||||
import org.keycloak.theme.beans.AdvancedMessageFormatterMethod;
|
import org.keycloak.theme.beans.AdvancedMessageFormatterMethod;
|
||||||
|
@ -474,8 +476,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
|
|
||||||
List<IdentityProviderModel> identityProviders = LoginFormsUtil
|
List<IdentityProviderModel> identityProviders = LoginFormsUtil
|
||||||
.filterIdentityProvidersForTheme(realm.getIdentityProvidersStream(), session, context);
|
.filterIdentityProvidersForTheme(realm.getIdentityProvidersStream(), session, context);
|
||||||
attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUriWithCodeAndClientId));
|
IdentityProviderBean idpBean = new IdentityProviderBean(realm, session, identityProviders, baseUriWithCodeAndClientId);
|
||||||
|
|
||||||
|
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||||
|
idpBean = new OrganizationAwareIdentityProviderBean(idpBean, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes.put("social", idpBean);
|
||||||
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
|
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
|
||||||
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
|
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
|
||||||
attributes.put("auth", new AuthenticationContextBean(context, page));
|
attributes.put("auth", new AuthenticationContextBean(context, page));
|
||||||
|
|
|
@ -1,188 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.admin.resource;
|
|
||||||
|
|
||||||
import jakarta.ws.rs.Consumes;
|
|
||||||
import jakarta.ws.rs.DELETE;
|
|
||||||
import jakarta.ws.rs.GET;
|
|
||||||
import jakarta.ws.rs.POST;
|
|
||||||
import jakarta.ws.rs.PUT;
|
|
||||||
import jakarta.ws.rs.Produces;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
import jakarta.ws.rs.core.Response;
|
|
||||||
import jakarta.ws.rs.core.Response.Status;
|
|
||||||
import jakarta.ws.rs.ext.Provider;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.ModelException;
|
|
||||||
import org.keycloak.models.OrganizationModel;
|
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
|
||||||
import org.keycloak.organization.OrganizationProvider;
|
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
|
||||||
import org.keycloak.services.ErrorResponse;
|
|
||||||
import org.keycloak.services.resources.admin.AdminEventBuilder;
|
|
||||||
import org.keycloak.services.resources.admin.IdentityProviderResource;
|
|
||||||
import org.keycloak.services.resources.admin.IdentityProvidersResource;
|
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
|
||||||
|
|
||||||
@Provider
|
|
||||||
public class OrganizationIdentityProviderResource {
|
|
||||||
|
|
||||||
private final KeycloakSession session;
|
|
||||||
private final RealmModel realm;
|
|
||||||
private final OrganizationProvider organizationProvider;
|
|
||||||
private final OrganizationModel organization;
|
|
||||||
private final AdminPermissionEvaluator auth;
|
|
||||||
private final AdminEventBuilder adminEvent;
|
|
||||||
|
|
||||||
public OrganizationIdentityProviderResource() {
|
|
||||||
// needed for registering to the JAX-RS stack
|
|
||||||
this(null, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public OrganizationIdentityProviderResource(KeycloakSession session, OrganizationModel organization, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
|
||||||
this.session = session;
|
|
||||||
this.realm = session == null ? null : session.getContext().getRealm();
|
|
||||||
this.organizationProvider = session == null ? null : session.getProvider(OrganizationProvider.class);
|
|
||||||
this.organization = organization;
|
|
||||||
this.auth = auth;
|
|
||||||
this.adminEvent = adminEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
|
||||||
public Response addIdentityProvider(IdentityProviderRepresentation providerRep) {
|
|
||||||
|
|
||||||
auth.realm().requireManageRealm();
|
|
||||||
|
|
||||||
IdentityProviderModel identityProvider = organization.getIdentityProvider();
|
|
||||||
if (identityProvider != null) {
|
|
||||||
throw ErrorResponse.error("Organization already assigned with an identity provider.", Status.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
//create IdP within the realm
|
|
||||||
Response response = new IdentityProvidersResource(realm, session, auth, adminEvent).create(providerRep);
|
|
||||||
|
|
||||||
if (Status.CREATED.getStatusCode() == response.getStatus()) {
|
|
||||||
|
|
||||||
//get the created IdP from session
|
|
||||||
identityProvider = realm.getIdentityProviderByAlias(providerRep.getAlias());
|
|
||||||
|
|
||||||
String errorMessage;
|
|
||||||
try {
|
|
||||||
if (organizationProvider.addIdentityProvider(organization, identityProvider)) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
errorMessage = "Assigning the Identity provider with the organization was not succesful.";
|
|
||||||
} catch (ModelException me) {
|
|
||||||
errorMessage = me.getMessage();
|
|
||||||
}
|
|
||||||
throw ErrorResponse.error(errorMessage, Status.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public IdentityProviderRepresentation getIdentityProvider() {
|
|
||||||
auth.realm().requireManageRealm();
|
|
||||||
return Optional.ofNullable(organization.getIdentityProvider()).map(this::toRepresentation).orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DELETE
|
|
||||||
public Response delete() {
|
|
||||||
auth.realm().requireManageRealm();
|
|
||||||
IdentityProviderModel identityProvider = getIdentityProviderModel();
|
|
||||||
|
|
||||||
Response response = getIdentityProviderResource(identityProvider).delete();
|
|
||||||
|
|
||||||
// remove link between IdP and the organization if the IdP deletetion was successful
|
|
||||||
if (Status.NO_CONTENT.getStatusCode() == response.getStatus()) {
|
|
||||||
String errorMessage;
|
|
||||||
try {
|
|
||||||
if (organizationProvider.removeIdentityProvider(organization)) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
errorMessage = "Removing the Identity provider from the organization was not succesful.";
|
|
||||||
} catch (ModelException me) {
|
|
||||||
errorMessage = me.getMessage();
|
|
||||||
}
|
|
||||||
throw ErrorResponse.error(errorMessage, Status.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PUT
|
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
|
||||||
public Response update(IdentityProviderRepresentation rep) {
|
|
||||||
auth.realm().requireManageRealm();
|
|
||||||
IdentityProviderModel identityProvider = getIdentityProviderModel();
|
|
||||||
|
|
||||||
if (!rep.getAlias().equals(identityProvider.getAlias()) || (rep.getInternalId() != null && !Objects.equals(rep.getInternalId(), identityProvider.getInternalId()))) {
|
|
||||||
throw ErrorResponse.error("Identity provider not assigned to the organization.", Status.NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
Response response = getIdentityProviderResource(identityProvider).update(rep);
|
|
||||||
|
|
||||||
//update link between IdP and the organization if the update of IdP was successful and the IdP alias differs
|
|
||||||
if (Status.NO_CONTENT.getStatusCode() == response.getStatus() &&
|
|
||||||
! Objects.equals(identityProvider.getAlias(), rep.getAlias())) {
|
|
||||||
|
|
||||||
//get the updated IdP from session
|
|
||||||
identityProvider = realm.getIdentityProviderByAlias(rep.getAlias());
|
|
||||||
|
|
||||||
String errorMessage;
|
|
||||||
try {
|
|
||||||
if (organizationProvider.removeIdentityProvider(organization) &&
|
|
||||||
organizationProvider.addIdentityProvider(organization, identityProvider)) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
errorMessage = "Updating the Identity provider was not succesful.";
|
|
||||||
} catch (ModelException me) {
|
|
||||||
errorMessage = me.getMessage();
|
|
||||||
}
|
|
||||||
throw ErrorResponse.error(errorMessage, Status.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IdentityProviderRepresentation toRepresentation(IdentityProviderModel idp) {
|
|
||||||
return ModelToRepresentation.toRepresentation(realm, idp);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IdentityProviderResource getIdentityProviderResource(IdentityProviderModel idp) {
|
|
||||||
return new IdentityProviderResource(auth, realm, session, idp, adminEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IdentityProviderModel getIdentityProviderModel() {
|
|
||||||
IdentityProviderModel identityProvider = organization.getIdentityProvider();
|
|
||||||
|
|
||||||
if (identityProvider == null) {
|
|
||||||
throw ErrorResponse.error("Organization doesn't have assigned an identity provider.", Status.NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
return identityProvider;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* 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.admin.resource;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
|
import jakarta.ws.rs.ext.Provider;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.models.OrganizationDomainModel;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
|
import org.keycloak.services.ErrorResponse;
|
||||||
|
import org.keycloak.services.resources.admin.AdminEventBuilder;
|
||||||
|
import org.keycloak.services.resources.admin.IdentityProviderResource;
|
||||||
|
import org.keycloak.services.resources.admin.IdentityProvidersResource;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
|
||||||
|
@Provider
|
||||||
|
public class OrganizationIdentityProvidersResource {
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private final RealmModel realm;
|
||||||
|
private final OrganizationProvider organizationProvider;
|
||||||
|
private final OrganizationModel organization;
|
||||||
|
private final AdminPermissionEvaluator auth;
|
||||||
|
private final AdminEventBuilder adminEvent;
|
||||||
|
|
||||||
|
public OrganizationIdentityProvidersResource() {
|
||||||
|
// needed for registering to the JAX-RS stack
|
||||||
|
this(null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrganizationIdentityProvidersResource(KeycloakSession session, OrganizationModel organization, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
||||||
|
this.session = session;
|
||||||
|
this.realm = session == null ? null : session.getContext().getRealm();
|
||||||
|
this.organizationProvider = session == null ? null : session.getProvider(OrganizationProvider.class);
|
||||||
|
this.organization = organization;
|
||||||
|
this.auth = auth;
|
||||||
|
this.adminEvent = adminEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
public Response addIdentityProvider(IdentityProviderRepresentation providerRep) {
|
||||||
|
auth.realm().requireManageRealm();
|
||||||
|
|
||||||
|
Response response = new IdentityProvidersResource(realm, session, auth, adminEvent).create(providerRep);
|
||||||
|
|
||||||
|
if (Status.CREATED.getStatusCode() == response.getStatus()) {
|
||||||
|
try {
|
||||||
|
IdentityProviderModel identityProvider = realm.getIdentityProviderByAlias(providerRep.getAlias());
|
||||||
|
|
||||||
|
if (organizationProvider.addIdentityProvider(organization, identityProvider)) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ErrorResponse.error("Identity provider already associated to the organization", Status.BAD_REQUEST);
|
||||||
|
} catch (ModelException me) {
|
||||||
|
throw ErrorResponse.error(me.getMessage(), Status.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Stream<IdentityProviderRepresentation> getIdentityProviders() {
|
||||||
|
auth.realm().requireManageRealm();
|
||||||
|
return organization.getIdentityProviders().map(this::toRepresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("{alias}")
|
||||||
|
public IdentityProviderResource getIdentityProvider(@PathParam("alias") String alias) {
|
||||||
|
IdentityProviderModel broker = realm.getIdentityProviderByAlias(alias);
|
||||||
|
return new IdentityProviderResource(auth, realm, session, broker, adminEvent) {
|
||||||
|
@Override
|
||||||
|
public Response delete() {
|
||||||
|
Response response = super.delete();
|
||||||
|
|
||||||
|
if (organizationProvider.removeIdentityProvider(organization, broker)) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ErrorResponse.error("Identity provider not associated with the organization", Status.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response update(IdentityProviderRepresentation providerRep) {
|
||||||
|
if (organization.getIdentityProviders().noneMatch(model -> model.getInternalId().equals(providerRep.getInternalId()) || model.getAlias().equals(providerRep.getAlias()))) {
|
||||||
|
return Response.status(Status.NOT_FOUND).build();
|
||||||
|
}
|
||||||
|
String domain = providerRep.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
|
||||||
|
|
||||||
|
if (domain != null && organization.getDomains().map(OrganizationDomainModel::getName).noneMatch(domain::equals)) {
|
||||||
|
return Response.status(Status.BAD_REQUEST).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.update(providerRep);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private IdentityProviderRepresentation toRepresentation(IdentityProviderModel idp) {
|
||||||
|
return ModelToRepresentation.toRepresentation(realm, idp);
|
||||||
|
}
|
||||||
|
}
|
|
@ -142,9 +142,9 @@ public class OrganizationResource {
|
||||||
return new OrganizationMemberResource(session, organization, auth, adminEvent);
|
return new OrganizationMemberResource(session, organization, auth, adminEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path("{id}/identity-provider")
|
@Path("{id}/identity-providers")
|
||||||
public OrganizationIdentityProviderResource identityProvider(@PathParam("id") String id) {
|
public OrganizationIdentityProvidersResource identityProvider(@PathParam("id") String id) {
|
||||||
return new OrganizationIdentityProviderResource(session, getOrganization(id), auth, adminEvent);
|
return new OrganizationIdentityProvidersResource(session, getOrganization(id), auth, adminEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private OrganizationModel getOrganization(String id) {
|
private OrganizationModel getOrganization(String id) {
|
||||||
|
|
|
@ -17,6 +17,9 @@
|
||||||
|
|
||||||
package org.keycloak.organization.authentication.authenticators.broker;
|
package org.keycloak.organization.authentication.authenticators.broker;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
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.broker.AbstractIdpAuthenticator;
|
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
||||||
|
@ -29,7 +32,7 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.organization.OrganizationProvider;
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
|
|
||||||
public class IdpOrganizationAuthenticator extends AbstractIdpAuthenticator {
|
public class IdpAddOrganizationMemberAuthenticator extends AbstractIdpAuthenticator {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
|
protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
|
||||||
|
@ -46,10 +49,10 @@ public class IdpOrganizationAuthenticator extends AbstractIdpAuthenticator {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IdentityProviderModel expectedBroker = organization.getIdentityProvider();
|
Stream<IdentityProviderModel> expectedBrokers = organization.getIdentityProviders();
|
||||||
IdentityProviderModel currentBroker = brokerContext.getIdpConfig();
|
IdentityProviderModel broker = brokerContext.getIdpConfig();
|
||||||
|
|
||||||
if (!expectedBroker.getAlias().equals(currentBroker.getAlias())) {
|
if (expectedBrokers.noneMatch(broker::equals)) {
|
||||||
context.failure(AuthenticationFlowError.ACCESS_DENIED);
|
context.failure(AuthenticationFlowError.ACCESS_DENIED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -71,31 +74,12 @@ public class IdpOrganizationAuthenticator extends AbstractIdpAuthenticator {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String domain = getEmailDomain(user.getEmail());
|
OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());
|
||||||
|
|
||||||
if (domain == null) {
|
if (organization == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
OrganizationModel organization = provider.getByDomainName(domain);
|
return provider.getIdentityProviders(organization).findAny().isPresent();
|
||||||
|
|
||||||
if (organization == null || provider.getIdentityProvider(organization) == null) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
session.setAttribute(OrganizationModel.class.getName(), organization);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getEmailDomain(String email) {
|
|
||||||
int domainSeparator = email.indexOf('@');
|
|
||||||
|
|
||||||
if (domainSeparator == -1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return email.substring(domainSeparator + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -33,11 +33,11 @@ import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
public class IdpOrganizationAuthenticatorFactory implements AuthenticatorFactory, EnvironmentDependentProviderFactory {
|
public class IdpOrganizationAuthenticatorFactory implements AuthenticatorFactory, EnvironmentDependentProviderFactory {
|
||||||
|
|
||||||
public static final String ID = "organization-broker";
|
public static final String ID = "idp-add-organization-member";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Authenticator create(KeycloakSession session) {
|
public Authenticator create(KeycloakSession session) {
|
||||||
return new IdpOrganizationAuthenticator();
|
return new IdpAddOrganizationMemberAuthenticator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -77,7 +77,7 @@ public class IdpOrganizationAuthenticatorFactory implements AuthenticatorFactory
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getDisplayType() {
|
public String getDisplayType() {
|
||||||
return "Organization Member Link";
|
return "Organization";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -87,7 +87,7 @@ public class IdpOrganizationAuthenticatorFactory implements AuthenticatorFactory
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isUserSetupAllowed() {
|
public boolean isUserSetupAllowed() {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -17,18 +17,23 @@
|
||||||
|
|
||||||
package org.keycloak.organization.authentication.authenticators.browser;
|
package org.keycloak.organization.authentication.authenticators.browser;
|
||||||
|
|
||||||
import java.util.function.BiFunction;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import jakarta.ws.rs.core.MultivaluedMap;
|
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.authenticators.browser.IdentityProviderAuthenticator;
|
import org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator;
|
||||||
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.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.OrganizationModel;
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.organization.OrganizationProvider;
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
|
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareIdentityProviderBean;
|
||||||
|
|
||||||
public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
|
|
||||||
|
@ -43,7 +48,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
OrganizationProvider provider = getOrganizationProvider();
|
OrganizationProvider provider = getOrganizationProvider();
|
||||||
|
|
||||||
if (!provider.isEnabled()) {
|
if (!provider.isEnabled()) {
|
||||||
attempted(context);
|
context.attempted();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,48 +60,106 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
HttpRequest request = context.getHttpRequest();
|
HttpRequest request = context.getHttpRequest();
|
||||||
MultivaluedMap<String, String> parameters = request.getDecodedFormParameters();
|
MultivaluedMap<String, String> parameters = request.getDecodedFormParameters();
|
||||||
String username = parameters.getFirst(UserModel.USERNAME);
|
String username = parameters.getFirst(UserModel.USERNAME);
|
||||||
|
String emailDomain = getEmailDomain(username);
|
||||||
|
|
||||||
if (username == null) {
|
if (emailDomain == null) {
|
||||||
challenge(context);
|
// username does not map to any email domain, go to the next authentication step/sub-flow
|
||||||
return;
|
context.attempted();
|
||||||
}
|
|
||||||
|
|
||||||
String domain = getEmailDomain(username);
|
|
||||||
|
|
||||||
if (domain == null) {
|
|
||||||
attempted(context);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
OrganizationProvider provider = getOrganizationProvider();
|
OrganizationProvider provider = getOrganizationProvider();
|
||||||
OrganizationModel organization = provider.getByDomainName(domain);
|
OrganizationModel organization = null;
|
||||||
|
RealmModel realm = context.getRealm();
|
||||||
|
UserModel user = session.users().getUserByEmail(realm, username);
|
||||||
|
|
||||||
|
if (user != null) {
|
||||||
|
// user exists, check if enabled
|
||||||
|
if (!user.isEnabled()) {
|
||||||
|
context.failure(AuthenticationFlowError.INVALID_USER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
organization = provider.getByMember(user);
|
||||||
|
|
||||||
|
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)
|
||||||
|
.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();
|
||||||
|
|
||||||
|
|
||||||
|
if (originBrokers.size() == 1) {
|
||||||
|
redirect(context, originBrokers.get(0).getAlias());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.attempted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (organization == null) {
|
if (organization == null) {
|
||||||
attempted(context);
|
organization = provider.getByDomainName(emailDomain);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IdentityProviderModel identityProvider = organization.getIdentityProvider();
|
if (organization == null) {
|
||||||
|
// request does not map to any organization, go to the next step/sub-flow
|
||||||
if (identityProvider == null) {
|
|
||||||
attempted(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
redirect(context, identityProvider.getAlias(), username);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void attempted(AuthenticationFlowContext context) {
|
|
||||||
context.form()
|
|
||||||
.setAttributeMapper(attributes -> {
|
|
||||||
attributes.computeIfPresent("social", createOrganizationAwareSocialBean());
|
|
||||||
return attributes;
|
|
||||||
});
|
|
||||||
context.attempted();
|
context.attempted();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BiFunction<String, Object, IdentityProviderBean> createOrganizationAwareSocialBean() {
|
List<IdentityProviderModel> domainBrokers = organization.getIdentityProviders().toList();
|
||||||
return (key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session);
|
|
||||||
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
private OrganizationProvider getOrganizationProvider() {
|
private OrganizationProvider getOrganizationProvider() {
|
||||||
|
@ -104,6 +167,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void challenge(AuthenticationFlowContext context){
|
private void challenge(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()
|
context.challenge(context.form()
|
||||||
.setAttributeMapper(attributes -> {
|
.setAttributeMapper(attributes -> {
|
||||||
// removes identity provider related attributes from forms
|
// removes identity provider related attributes from forms
|
||||||
|
@ -114,6 +178,10 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getEmailDomain(String email) {
|
private String getEmailDomain(String email) {
|
||||||
|
if (email == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
int domainSeparator = email.indexOf('@');
|
int domainSeparator = email.indexOf('@');
|
||||||
|
|
||||||
if (domainSeparator == -1) {
|
if (domainSeparator == -1) {
|
||||||
|
|
|
@ -15,10 +15,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.organization.authentication.authenticators.browser;
|
package org.keycloak.organization.forms.login.freemarker.model;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
|
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
|
||||||
|
@ -29,30 +28,50 @@ import org.keycloak.models.RealmModel;
|
||||||
|
|
||||||
public class OrganizationAwareIdentityProviderBean extends IdentityProviderBean {
|
public class OrganizationAwareIdentityProviderBean extends IdentityProviderBean {
|
||||||
|
|
||||||
private final IdentityProviderBean delegate;
|
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
|
private final List<IdentityProvider> providers;
|
||||||
|
|
||||||
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session) {
|
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session, boolean onlyOrganizationBrokers) {
|
||||||
this.delegate = delegate;
|
|
||||||
this.session = session;
|
this.session = session;
|
||||||
|
if (onlyOrganizationBrokers) {
|
||||||
|
providers = Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
|
||||||
|
.filter(this::isPublicOrganizationBroker)
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
providers = Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
|
||||||
|
.filter(p -> isRealmBroker(p) || isPublicOrganizationBroker(p))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session) {
|
||||||
|
this(delegate, session, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<IdentityProvider> getProviders() {
|
public List<IdentityProvider> getProviders() {
|
||||||
return Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
|
return providers;
|
||||||
.filter(this::filterOrganizationalIdentityProvider)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isDisplayInfo() {
|
public boolean isDisplayInfo() {
|
||||||
return delegate.isDisplayInfo();
|
return !providers.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean filterOrganizationalIdentityProvider(IdentityProvider idp) {
|
private boolean isPublicOrganizationBroker(IdentityProvider idp) {
|
||||||
RealmModel realm = session.getContext().getRealm();
|
RealmModel realm = session.getContext().getRealm();
|
||||||
IdentityProviderModel model = realm.getIdentityProviderByAlias(idp.getAlias());
|
IdentityProviderModel model = realm.getIdentityProviderByAlias(idp.getAlias());
|
||||||
Map<String, String> config = model.getConfig();
|
|
||||||
return !config.containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
if (model.getOrganizationId() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean.parseBoolean(model.getConfig().getOrDefault(OrganizationModel.BROKER_PUBLIC, Boolean.FALSE.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRealmBroker(IdentityProvider idp) {
|
||||||
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
IdentityProviderModel model = realm.getIdentityProviderByAlias(idp.getAlias());
|
||||||
|
|
||||||
|
return model.getOrganizationId() == null;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -19,11 +19,13 @@ package org.keycloak.organization.validator;
|
||||||
|
|
||||||
import static org.keycloak.validate.BuiltinValidators.emailValidator;
|
import static org.keycloak.validate.BuiltinValidators.emailValidator;
|
||||||
|
|
||||||
import java.util.stream.Stream;
|
import java.util.List;
|
||||||
|
|
||||||
import org.keycloak.Config.Scope;
|
import org.keycloak.Config.Scope;
|
||||||
|
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.Profile.Feature;
|
import org.keycloak.common.Profile.Feature;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.OrganizationDomainModel;
|
import org.keycloak.models.OrganizationDomainModel;
|
||||||
import org.keycloak.models.OrganizationModel;
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
@ -32,6 +34,7 @@ import org.keycloak.organization.OrganizationProvider;
|
||||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||||
import org.keycloak.userprofile.AttributeContext;
|
import org.keycloak.userprofile.AttributeContext;
|
||||||
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
|
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
|
||||||
|
import org.keycloak.userprofile.UserProfileContext;
|
||||||
import org.keycloak.utils.StringUtil;
|
import org.keycloak.utils.StringUtil;
|
||||||
import org.keycloak.validate.AbstractSimpleValidator;
|
import org.keycloak.validate.AbstractSimpleValidator;
|
||||||
import org.keycloak.validate.ValidationContext;
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
@ -83,15 +86,38 @@ public class OrganizationMemberValidator extends AbstractSimpleValidator impleme
|
||||||
UserProfileAttributeValidationContext upContext = (UserProfileAttributeValidationContext) context;
|
UserProfileAttributeValidationContext upContext = (UserProfileAttributeValidationContext) context;
|
||||||
AttributeContext attributeContext = upContext.getAttributeContext();
|
AttributeContext attributeContext = upContext.getAttributeContext();
|
||||||
UserModel user = attributeContext.getUser();
|
UserModel user = attributeContext.getUser();
|
||||||
|
String emailDomain = email.substring(email.indexOf('@') + 1);
|
||||||
|
List<String> expectedDomains = organization.getDomains().map(OrganizationDomainModel::getName).toList();
|
||||||
|
|
||||||
if (!organization.isManaged(user)) {
|
if (UserProfileContext.IDP_REVIEW.equals(attributeContext.getContext())) {
|
||||||
|
KeycloakSession session = attributeContext.getSession();
|
||||||
|
BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) session.getAttribute(BrokeredIdentityContext.class.getName());
|
||||||
|
|
||||||
|
if (brokerContext != null) {
|
||||||
|
String alias = brokerContext.getIdpConfig().getAlias();
|
||||||
|
IdentityProviderModel broker = organization.getIdentityProviders().filter((p) -> p.getAlias().equals(alias)).findAny().orElse(null);
|
||||||
|
|
||||||
|
if (broker == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String domain = email.substring(email.indexOf('@') + 1);
|
String brokerDomain = broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
|
||||||
Stream<OrganizationDomainModel> expectedDomains = organization.getDomains();
|
|
||||||
|
|
||||||
if (expectedDomains.map(OrganizationDomainModel::getName).noneMatch(domain::equals)) {
|
if (brokerDomain == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedDomains = List.of(brokerDomain);
|
||||||
|
}
|
||||||
|
} else if (!organization.isManaged(user)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedDomains.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expectedDomains.contains(emailDomain)) {
|
||||||
context.addError(new ValidationError(ID, inputHint, "Email domain does not match any domain from the organization"));
|
context.addError(new ValidationError(ID, inputHint, "Email domain does not match any domain from the organization"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
package org.keycloak.services.resources;
|
package org.keycloak.services.resources;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.common.Profile.Feature;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.forms.login.MessageType;
|
import org.keycloak.forms.login.MessageType;
|
||||||
import org.keycloak.forms.login.freemarker.DetachedInfoStateChecker;
|
import org.keycloak.forms.login.freemarker.DetachedInfoStateChecker;
|
||||||
|
@ -52,6 +54,7 @@ import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.exceptions.TokenNotActiveException;
|
import org.keycloak.exceptions.TokenNotActiveException;
|
||||||
import org.keycloak.models.KeycloakContext;
|
import org.keycloak.models.KeycloakContext;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
import org.keycloak.models.SingleUseObjectKeyModel;
|
import org.keycloak.models.SingleUseObjectKeyModel;
|
||||||
import org.keycloak.models.AuthenticationFlowModel;
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
@ -67,6 +70,7 @@ import org.keycloak.models.utils.AuthenticationFlowResolver;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.models.utils.SystemClientUtil;
|
import org.keycloak.models.utils.SystemClientUtil;
|
||||||
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
import org.keycloak.protocol.AuthorizationEndpointBase;
|
import org.keycloak.protocol.AuthorizationEndpointBase;
|
||||||
import org.keycloak.protocol.LoginProtocol;
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.LoginProtocol.Error;
|
import org.keycloak.protocol.LoginProtocol.Error;
|
||||||
|
@ -77,7 +81,6 @@ import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||||
import org.keycloak.representations.JsonWebToken;
|
import org.keycloak.representations.JsonWebToken;
|
||||||
import org.keycloak.services.ErrorPage;
|
import org.keycloak.services.ErrorPage;
|
||||||
import org.keycloak.services.ErrorPageException;
|
import org.keycloak.services.ErrorPageException;
|
||||||
import org.keycloak.services.ErrorResponseException;
|
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
@ -925,9 +928,23 @@ public class LoginActionsService {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
configureOrganization(brokerContext);
|
||||||
|
|
||||||
return processFlow(checks.isActionRequest(), execution, authSession, flowPath, brokerLoginFlow, null, processor);
|
return processFlow(checks.isActionRequest(), execution, authSession, flowPath, brokerLoginFlow, null, processor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void configureOrganization(BrokeredIdentityContext brokerContext) {
|
||||||
|
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||||
|
String organizationId = brokerContext.getIdpConfig().getOrganizationId();
|
||||||
|
|
||||||
|
if (organizationId != null) {
|
||||||
|
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||||
|
session.setAttribute(OrganizationModel.class.getName(), provider.getById(organizationId));
|
||||||
|
session.setAttribute(BrokeredIdentityContext.class.getName(), brokerContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Response redirectToAfterBrokerLoginEndpoint(AuthenticationSessionModel authSession, boolean firstBrokerLogin) {
|
private Response redirectToAfterBrokerLoginEndpoint(AuthenticationSessionModel authSession, boolean firstBrokerLogin) {
|
||||||
return redirectToAfterBrokerLoginEndpoint(session, realm, session.getContext().getUri(), authSession, firstBrokerLogin);
|
return redirectToAfterBrokerLoginEndpoint(session, realm, session.getContext().getUri(), authSession, firstBrokerLogin);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ 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.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
@ -65,7 +66,15 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
|
||||||
.username(getUserLogin())
|
.username(getUserLogin())
|
||||||
.email(getUserEmail())
|
.email(getUserEmail())
|
||||||
.password(getUserPassword())
|
.password(getUserPassword())
|
||||||
.enabled(true).build())
|
.enabled(true)
|
||||||
|
.build(),
|
||||||
|
UserBuilder.create()
|
||||||
|
.username("external")
|
||||||
|
.email("external@user.org")
|
||||||
|
.password("password")
|
||||||
|
.enabled(true)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return providerRealm;
|
return providerRealm;
|
||||||
|
@ -80,6 +89,13 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
|
||||||
public String getIDPAlias() {
|
public String getIDPAlias() {
|
||||||
return name + "-identity-provider";
|
return name + "-identity-provider";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ClientRepresentation> createProviderClients() {
|
||||||
|
List<ClientRepresentation> clients = super.createProviderClients();
|
||||||
|
clients.get(0).setRedirectUris(List.of("*"));
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@Page
|
@Page
|
||||||
|
@ -126,7 +142,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
|
||||||
id = ApiUtil.getCreatedId(response);
|
id = ApiUtil.getCreatedId(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
testRealm().organizations().get(id).identityProvider().create(brokerConfigFunction.apply(name).setUpIdentityProvider()).close();
|
testRealm().organizations().get(id).identityProviders().create(brokerConfigFunction.apply(name).setUpIdentityProvider()).close();
|
||||||
org = testRealm().organizations().get(id).toRepresentation();
|
org = testRealm().organizations().get(id).toRepresentation();
|
||||||
getCleanup().addCleanup(() -> testRealm().organizations().get(id).delete().close());
|
getCleanup().addCleanup(() -> testRealm().organizations().get(id).delete().close());
|
||||||
|
|
||||||
|
|
|
@ -107,33 +107,33 @@ public class OrganizationAdminPermissionsTest extends AbstractOrganizationTest {
|
||||||
idpRep.setProviderId("oidc");
|
idpRep.setProviderId("oidc");
|
||||||
//create IdP
|
//create IdP
|
||||||
try (
|
try (
|
||||||
Response userResponse = realmUserResource.organizations().get(orgId).identityProvider().create(idpRep);
|
Response userResponse = realmUserResource.organizations().get(orgId).identityProviders().create(idpRep);
|
||||||
Response adminResponse = realmAdminResource.organizations().get(orgId).identityProvider().create(idpRep)
|
Response adminResponse = realmAdminResource.organizations().get(orgId).identityProviders().create(idpRep)
|
||||||
) {
|
) {
|
||||||
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
||||||
assertThat(adminResponse.getStatus(), equalTo(Status.CREATED.getStatusCode()));
|
assertThat(adminResponse.getStatus(), equalTo(Status.CREATED.getStatusCode()));
|
||||||
getCleanup().addCleanup(() -> testRealm().organizations().get(orgId).identityProvider().delete().close());
|
getCleanup().addCleanup(() -> testRealm().organizations().get(orgId).identityProviders().get(idpRep.getAlias()).delete().close());
|
||||||
}
|
}
|
||||||
|
|
||||||
//get IdP
|
//get IdP
|
||||||
try {
|
try {
|
||||||
//we should get 403, not 400 or 404 etc.
|
//we should get 403, not 400 or 404 etc.
|
||||||
realmUserResource.organizations().get("non-existing").identityProvider().toRepresentation();
|
realmUserResource.organizations().get("non-existing").identityProviders().get(idpRep.getAlias()).toRepresentation();
|
||||||
fail("Expected ForbiddenException");
|
fail("Expected ForbiddenException");
|
||||||
} catch (ForbiddenException expected) {}
|
} catch (ForbiddenException expected) {}
|
||||||
try {
|
try {
|
||||||
realmUserResource.organizations().get(orgId).identityProvider().toRepresentation();
|
realmUserResource.organizations().get(orgId).identityProviders().get(idpRep.getAlias()).toRepresentation();
|
||||||
fail("Expected ForbiddenException");
|
fail("Expected ForbiddenException");
|
||||||
} catch (ForbiddenException expected) {}
|
} catch (ForbiddenException expected) {}
|
||||||
assertThat(realmAdminResource.organizations().get(orgId).identityProvider().toRepresentation(), Matchers.notNullValue());
|
assertThat(realmAdminResource.organizations().get(orgId).identityProviders().get(idpRep.getAlias()).toRepresentation(), Matchers.notNullValue());
|
||||||
|
|
||||||
//update IdP
|
//update IdP
|
||||||
try (Response userResponse = realmUserResource.organizations().get(orgId).identityProvider().update(idpRep)) {
|
try (Response userResponse = realmUserResource.organizations().get(orgId).identityProviders().get(idpRep.getAlias()).update(idpRep)) {
|
||||||
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
//delete IdP
|
//delete IdP
|
||||||
try (Response userResponse = realmUserResource.organizations().get(orgId).identityProvider().delete()) {
|
try (Response userResponse = realmUserResource.organizations().get(orgId).identityProviders().get(idpRep.getAlias()).delete()) {
|
||||||
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
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.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
||||||
|
|
||||||
|
@ -26,15 +28,20 @@ import java.util.List;
|
||||||
import jakarta.ws.rs.BadRequestException;
|
import jakarta.ws.rs.BadRequestException;
|
||||||
import jakarta.ws.rs.NotFoundException;
|
import jakarta.ws.rs.NotFoundException;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.admin.client.resource.OrganizationIdentityProviderResource;
|
||||||
import org.keycloak.admin.client.resource.OrganizationMemberResource;
|
import org.keycloak.admin.client.resource.OrganizationMemberResource;
|
||||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
import org.keycloak.admin.client.resource.UsersResource;
|
import org.keycloak.admin.client.resource.UsersResource;
|
||||||
import org.keycloak.common.Profile.Feature;
|
import org.keycloak.common.Profile.Feature;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||||
|
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
|
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
||||||
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.testsuite.Assert;
|
import org.keycloak.testsuite.Assert;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
@ -44,17 +51,17 @@ import org.keycloak.testsuite.util.UserBuilder;
|
||||||
public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganizationTest {
|
public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganizationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBrokerRegistration() {
|
public void testRegistrationRedirectWhenSingleBroker() {
|
||||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
assertBrokerRegistration(organization);
|
assertBrokerRegistration(organization, bc.getUserEmail());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLoginHint() {
|
public void testLoginHintSentToBrokerWhenEnabled() {
|
||||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
IdentityProviderRepresentation idp = organization.identityProvider().toRepresentation();
|
IdentityProviderRepresentation idp = organization.identityProviders().get(bc.getIDPAlias()).toRepresentation();
|
||||||
idp.getConfig().put(IdentityProviderModel.LOGIN_HINT, "true");
|
idp.getConfig().put(IdentityProviderModel.LOGIN_HINT, "true");
|
||||||
organization.identityProvider().update(idp).close();
|
organization.identityProviders().get(bc.getIDPAlias()).update(idp).close();
|
||||||
|
|
||||||
oauth.clientId("broker-app");
|
oauth.clientId("broker-app");
|
||||||
loginPage.open(bc.consumerRealmName());
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
@ -71,6 +78,50 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
|
||||||
Assert.assertEquals(bc.getUserEmail(), loginPage.getUsername());
|
Assert.assertEquals(bc.getUserEmail(), loginPage.getUsername());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDefaultAuthenticationMechanismIfNotOrganizationMember() {
|
||||||
|
testRealm().organizations().get(createOrganization().getId());
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
|
||||||
|
// login with email only
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
log.debug("Logging in");
|
||||||
|
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||||
|
loginPage.loginUsername("user@noorg.org");
|
||||||
|
|
||||||
|
// check if the login page is shown
|
||||||
|
Assert.assertTrue(loginPage.isUsernameInputPresent());
|
||||||
|
Assert.assertTrue(loginPage.isPasswordInputPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRealmLevelBrokersAvailableIfEmailDoesNotMatchOrganization() {
|
||||||
|
testRealm().organizations().get(createOrganization().getId());
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
|
||||||
|
// login with email only
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
log.debug("Logging in");
|
||||||
|
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||||
|
loginPage.loginUsername("user");
|
||||||
|
|
||||||
|
// check if the login page is shown
|
||||||
|
Assert.assertTrue(loginPage.isUsernameInputPresent());
|
||||||
|
Assert.assertTrue(loginPage.isPasswordInputPresent());
|
||||||
|
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
|
||||||
|
|
||||||
|
IdentityProviderRepresentation idp = bc.setUpIdentityProvider();
|
||||||
|
idp.setAlias("realm-level-idp");
|
||||||
|
Assert.assertFalse(loginPage.isSocialButtonPresent(idp.getAlias()));
|
||||||
|
testRealm().identityProviders().create(idp).close();
|
||||||
|
|
||||||
|
driver.navigate().refresh();
|
||||||
|
|
||||||
|
Assert.assertTrue(loginPage.isUsernameInputPresent());
|
||||||
|
Assert.assertTrue(loginPage.isPasswordInputPresent());
|
||||||
|
Assert.assertTrue(loginPage.isSocialButtonPresent(idp.getAlias()));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLinkExistingAccount() {
|
public void testLinkExistingAccount() {
|
||||||
// create a realm user in the consumer realm
|
// create a realm user in the consumer realm
|
||||||
|
@ -102,7 +153,7 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
|
||||||
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");
|
||||||
updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname");
|
updateAccountInformationPage.updateAccountInformation(bc.getUserEmail(), bc.getUserEmail(), "Firstname", "Lastname");
|
||||||
|
|
||||||
// account with the same email exists in the realm, execute account linking
|
// account with the same email exists in the realm, execute account linking
|
||||||
waitForPage(driver, "account already exists", false);
|
waitForPage(driver, "account already exists", false);
|
||||||
|
@ -114,11 +165,11 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMemberAlreadyExists() {
|
public void testReAuthenticateWhenAlreadyMember() {
|
||||||
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
|
||||||
assertBrokerRegistration(organization);
|
assertBrokerRegistration(organization, bc.getUserEmail());
|
||||||
|
|
||||||
// logout to force the user to authenticate again
|
// logout to force the user to authenticate again
|
||||||
UserRepresentation account = getUserRepresentation(bc.getUserEmail());
|
UserRepresentation account = getUserRepresentation(bc.getUserEmail());
|
||||||
|
@ -141,11 +192,15 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFailUpdateEmailWithDomainDifferentThanOrganization() {
|
public void testFailUpdateEmailNotAssociatedOrganizationUsingAdminAPI() {
|
||||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
OrganizationIdentityProviderResource idp = organization.identityProviders().get(bc.getIDPAlias());
|
||||||
|
IdentityProviderRepresentation idpRep = idp.toRepresentation();
|
||||||
|
idpRep.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "neworg.org");
|
||||||
|
idp.update(idpRep).close();
|
||||||
|
|
||||||
// add the member for the first time
|
// add the member for the first time
|
||||||
assertBrokerRegistration(organization);
|
assertBrokerRegistration(organization, bc.getUserEmail());
|
||||||
UserRepresentation member = getUserRepresentation(bc.getUserEmail());
|
UserRepresentation member = getUserRepresentation(bc.getUserEmail());
|
||||||
|
|
||||||
member.setEmail(KeycloakModelUtils.generateId() + "@user.org");
|
member.setEmail(KeycloakModelUtils.generateId() + "@user.org");
|
||||||
|
@ -165,13 +220,12 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDelete() {
|
public void testDeleteManagedMember() {
|
||||||
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
|
||||||
assertBrokerRegistration(organization);
|
assertBrokerRegistration(organization, bc.getUserEmail());
|
||||||
UserRepresentation member = getUserRepresentation(bc.getUserEmail());
|
UserRepresentation member = getUserRepresentation(bc.getUserEmail());
|
||||||
member.setEmail(KeycloakModelUtils.generateId() + "@user.org");
|
|
||||||
OrganizationMemberResource organizationMember = organization.members().member(member.getId());
|
OrganizationMemberResource organizationMember = organization.members().member(member.getId());
|
||||||
|
|
||||||
organizationMember.delete().close();
|
organizationMember.delete().close();
|
||||||
|
@ -189,29 +243,368 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertBrokerRegistration(OrganizationResource organization) {
|
@Test
|
||||||
// login with email only
|
public void testRedirectToIdentityProviderAssociatedWithOrganizationDomain() {
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
IdentityProviderRepresentation idp = organization.identityProviders().get(bc.getIDPAlias()).toRepresentation();
|
||||||
|
idp.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "neworg.org");
|
||||||
|
organization.identityProviders().get(bc.getIDPAlias()).update(idp).close();
|
||||||
|
idp.setAlias("second-idp");
|
||||||
|
idp.setInternalId(null);
|
||||||
|
idp.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
|
||||||
|
organization.identityProviders().create(idp).close();
|
||||||
|
|
||||||
oauth.clientId("broker-app");
|
oauth.clientId("broker-app");
|
||||||
loginPage.open(bc.consumerRealmName());
|
loginPage.open(bc.consumerRealmName());
|
||||||
log.debug("Logging in");
|
log.debug("Logging in");
|
||||||
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||||
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
|
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
|
||||||
|
Assert.assertFalse(loginPage.isSocialButtonPresent(idp.getAlias()));
|
||||||
loginPage.loginUsername(bc.getUserEmail());
|
loginPage.loginUsername(bc.getUserEmail());
|
||||||
|
|
||||||
// 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);
|
||||||
Assert.assertTrue("Driver should be on the provider realm page right now",
|
Assert.assertTrue("Driver should be on the provider realm page right now",
|
||||||
driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
|
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());
|
loginPage.login(bc.getUserEmail(), bc.getUserPassword());
|
||||||
waitForPage(driver, "update account information", false);
|
waitForPage(driver, "update account information", false);
|
||||||
updateAccountInformationPage.assertCurrent();
|
updateAccountInformationPage.assertCurrent();
|
||||||
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");
|
||||||
updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname");
|
updateAccountInformationPage.updateAccountInformation(bc.getUserEmail(), bc.getUserEmail(), "Firstname", "Lastname");
|
||||||
|
appPage.assertCurrent();
|
||||||
assertIsMember(bc.getUserEmail(), organization);
|
assertIsMember(bc.getUserEmail(), organization);
|
||||||
|
UserRepresentation user = testRealm().users().search(bc.getUserEmail()).get(0);
|
||||||
|
List<FederatedIdentityRepresentation> federatedIdentities = testRealm().users().get(user.getId()).getFederatedIdentity();
|
||||||
|
assertEquals(1, federatedIdentities.size());
|
||||||
|
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
|
||||||
|
organization.identityProviders().get(bc.getIDPAlias()).update(idp).close();
|
||||||
|
|
||||||
|
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
|
||||||
|
organization.identityProviders().create(idp).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());
|
||||||
|
organization.identityProviders().get(idp.getAlias()).update(idp).close();
|
||||||
|
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());
|
||||||
|
IdentityProviderRepresentation idp = organization.identityProviders().get(bc.getIDPAlias()).toRepresentation();
|
||||||
|
idp.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "neworg.org");
|
||||||
|
// set a domain to the existing broker
|
||||||
|
organization.identityProviders().get(bc.getIDPAlias()).update(idp).close();
|
||||||
|
|
||||||
|
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
|
||||||
|
organization.identityProviders().create(idp).close();
|
||||||
|
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
log.debug("Logging in");
|
||||||
|
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||||
|
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
|
||||||
|
String email = "external@user.org";
|
||||||
|
loginPage.loginUsername(email);
|
||||||
|
loginPage.clickSocial(idp.getAlias());
|
||||||
|
|
||||||
|
// 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() + "/"));
|
||||||
|
loginPage.login("external", "password");
|
||||||
|
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(email, email, "Firstname", "Lastname");
|
||||||
|
appPage.assertCurrent();
|
||||||
|
assertIsMember(email, organization);
|
||||||
|
|
||||||
|
// make sure the federated identity matches the expected broker
|
||||||
|
UserRepresentation user = testRealm().users().search(email).get(0);
|
||||||
|
List<FederatedIdentityRepresentation> federatedIdentities = testRealm().users().get(user.getId()).getFederatedIdentity();
|
||||||
|
assertEquals(1, federatedIdentities.size());
|
||||||
|
assertEquals(idp.getAlias(), federatedIdentities.get(0).getIdentityProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmailDomainDoesNotMatchBrokerDomain() {
|
||||||
|
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
|
||||||
|
organization.identityProviders().get(bc.getIDPAlias()).update(idp).close();
|
||||||
|
|
||||||
|
idp = bc.setUpIdentityProvider();
|
||||||
|
idp.setAlias("second-idp");
|
||||||
|
idp.setInternalId(null);
|
||||||
|
idp.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "other.org");
|
||||||
|
idp.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
|
||||||
|
// create a second broker without a domain set
|
||||||
|
organization.identityProviders().create(idp).close();
|
||||||
|
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
log.debug("Logging in");
|
||||||
|
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||||
|
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
|
||||||
|
String email = "external@user.org";
|
||||||
|
loginPage.loginUsername(email);
|
||||||
|
loginPage.clickSocial(idp.getAlias());
|
||||||
|
|
||||||
|
// 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() + "/"));
|
||||||
|
loginPage.login(email, "password");
|
||||||
|
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(email, email, "Firstname", "Lastname");
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains("Email domain does not match any domain from the organization"));
|
||||||
|
assertIsNotMember(email, organization);
|
||||||
|
updateAccountInformationPage.updateAccountInformation("external@other.org", "external@other.org", "Firstname", "Lastname");
|
||||||
|
appPage.assertCurrent();
|
||||||
|
assertIsMember("external@other.org", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAnyEmailFromBrokerWithoutDomainSet() {
|
||||||
|
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
|
||||||
|
organization.identityProviders().get(bc.getIDPAlias()).update(idp).close();
|
||||||
|
|
||||||
|
idp = bc.setUpIdentityProvider();
|
||||||
|
idp.setAlias("second-idp");
|
||||||
|
idp.setInternalId(null);
|
||||||
|
idp.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
|
||||||
|
// create a second broker without a domain set
|
||||||
|
organization.identityProviders().create(idp).close();
|
||||||
|
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
log.debug("Logging in");
|
||||||
|
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||||
|
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
|
||||||
|
String email = "external@user.org";
|
||||||
|
loginPage.loginUsername(email);
|
||||||
|
loginPage.clickSocial(idp.getAlias());
|
||||||
|
|
||||||
|
// 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() + "/"));
|
||||||
|
loginPage.login(email, "password");
|
||||||
|
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("external@unknown.org", "external@unknown.org", "Firstname", "Lastname");
|
||||||
|
appPage.assertCurrent();
|
||||||
|
assertIsMember("external@unknown.org", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRealmLevelBrokerNotImpactedByOrganizationFlow() {
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
IdentityProviderRepresentation idp = bc.setUpIdentityProvider();
|
||||||
|
idp.setAlias("realm-idp");
|
||||||
|
idp.setInternalId(null);
|
||||||
|
// create a second broker without a domain set
|
||||||
|
testRealm().identityProviders().create(idp).close();
|
||||||
|
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
log.debug("Logging in");
|
||||||
|
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||||
|
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
|
||||||
|
loginPage.loginUsername("some@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() + "/"));
|
||||||
|
loginPage.clickSocial(idp.getAlias());
|
||||||
|
|
||||||
|
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() + "/"));
|
||||||
|
loginPage.login("external", "password");
|
||||||
|
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");
|
||||||
|
appPage.assertCurrent();
|
||||||
|
assertTrue(organization.members().getAll().isEmpty());
|
||||||
|
|
||||||
|
UserRepresentation user = testRealm().users().search(bc.getUserEmail()).get(0);
|
||||||
|
testRealm().users().get(user.getId()).remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMemberRegistrationUsingDifferentDomainThanOrganization() {
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
IdentityProviderRepresentation idpRep = organization.identityProviders().getIdentityProviders().get(0);
|
||||||
|
|
||||||
|
// make sure the user can select this idp from the organization when authenticating
|
||||||
|
idpRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
|
||||||
|
organization.identityProviders().get(idpRep.getAlias()).update(idpRep).close();
|
||||||
|
|
||||||
|
// create a user to the provider realm using a email that does not share the same domain as the org
|
||||||
|
UserRepresentation user = UserBuilder.create()
|
||||||
|
.username("user")
|
||||||
|
.email("user@different.org")
|
||||||
|
.password("password")
|
||||||
|
.enabled(true)
|
||||||
|
.build();
|
||||||
|
realmsResouce().realm(bc.providerRealmName()).users().create(user).close();
|
||||||
|
|
||||||
|
// select the organization broker to authenticate
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
loginPage.loginUsername("user@different.org");
|
||||||
|
loginPage.clickSocial(idpRep.getAlias());
|
||||||
|
|
||||||
|
// login through the organization broker
|
||||||
|
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() + "/"));
|
||||||
|
loginPage.login("user@different.org", "password");
|
||||||
|
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(user.getUsername(), user.getEmail(), "Firstname", "Lastname");
|
||||||
|
appPage.assertCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMemberFromBrokerRedirectedToOriginBroker() {
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
IdentityProviderRepresentation idpRep = organization.identityProviders().getIdentityProviders().get(0);
|
||||||
|
|
||||||
|
// make sure the user can select this idp from the organization when authenticating
|
||||||
|
idpRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
|
||||||
|
organization.identityProviders().get(idpRep.getAlias()).update(idpRep).close();
|
||||||
|
|
||||||
|
// create a user to the provider realm using a email that does not share the same domain as the org
|
||||||
|
UserRepresentation user = UserBuilder.create()
|
||||||
|
.username("user")
|
||||||
|
.email("user@different.org")
|
||||||
|
.password("password")
|
||||||
|
.enabled(true)
|
||||||
|
.build();
|
||||||
|
realmsResouce().realm(bc.providerRealmName()).users().create(user).close();
|
||||||
|
|
||||||
|
// execute the identity-first login
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
loginPage.loginUsername(user.getEmail());
|
||||||
|
|
||||||
|
waitForPage(driver, "sign in to", true);
|
||||||
|
// select the organization broker to authenticate
|
||||||
|
assertTrue(loginPage.isPasswordInputPresent());
|
||||||
|
assertTrue(loginPage.isUsernameInputPresent());
|
||||||
|
loginPage.clickSocial(idpRep.getAlias());
|
||||||
|
|
||||||
|
// login through the organization broker
|
||||||
|
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() + "/"));
|
||||||
|
loginPage.login("user@different.org", "password");
|
||||||
|
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(user.getUsername(), user.getEmail(), "Firstname", "Lastname");
|
||||||
|
UserRepresentation account = getUserRepresentation(user.getEmail());
|
||||||
|
realmsResouce().realm(bc.consumerRealmName()).users().get(account.getId()).logout();
|
||||||
|
|
||||||
|
// the flow now changed and the user should be automatically redirected to the origin broker
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
waitForPage(driver, "sign in to", true);
|
||||||
|
loginPage.loginUsername(user.getEmail());
|
||||||
|
Assert.assertTrue("Driver should be on the provider realm page right now",
|
||||||
|
driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
|
||||||
|
loginPage.login("user@different.org", "password");
|
||||||
|
appPage.assertCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertBrokerRegistration(OrganizationResource organization, String email) {
|
||||||
|
// login with email only
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
log.debug("Logging in");
|
||||||
|
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||||
|
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
|
||||||
|
loginPage.loginUsername(email);
|
||||||
|
|
||||||
|
// 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(email, 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.getUserLogin(), email, "Firstname", "Lastname");
|
||||||
|
|
||||||
|
assertIsMember(email, organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertIsMember(String userEmail, OrganizationResource organization) {
|
private void assertIsMember(String userEmail, OrganizationResource organization) {
|
||||||
|
@ -220,6 +613,23 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
|
||||||
Assert.assertEquals(account.getId(), member.getId());
|
Assert.assertEquals(account.getId(), member.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void assertIsNotMember(String userEmail, OrganizationResource organization) {
|
||||||
|
UsersResource users = adminClient.realm(bc.consumerRealmName()).users();
|
||||||
|
List<UserRepresentation> reps = users.searchByEmail(userEmail, true);
|
||||||
|
|
||||||
|
if (reps.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(1, reps.size());
|
||||||
|
UserRepresentation account = reps.get(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
assertNull(organization.members().member(account.getId()).toRepresentation());
|
||||||
|
} catch (NotFoundException ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private UserRepresentation getUserRepresentation(String userEmail) {
|
private UserRepresentation getUserRepresentation(String userEmail) {
|
||||||
UsersResource users = adminClient.realm(bc.consumerRealmName()).users();
|
UsersResource users = adminClient.realm(bc.consumerRealmName()).users();
|
||||||
List<UserRepresentation> reps = users.searchByEmail(userEmail, true);
|
List<UserRepresentation> reps = users.searchByEmail(userEmail, true);
|
||||||
|
|
|
@ -19,8 +19,8 @@ package org.keycloak.testsuite.organization.admin;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.nullValue;
|
|
||||||
|
|
||||||
|
import jakarta.ws.rs.NotFoundException;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jakarta.ws.rs.core.Response.Status;
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
@ -28,6 +28,7 @@ import org.junit.Test;
|
||||||
import org.keycloak.admin.client.resource.OrganizationIdentityProviderResource;
|
import org.keycloak.admin.client.resource.OrganizationIdentityProviderResource;
|
||||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
import org.keycloak.common.Profile.Feature;
|
import org.keycloak.common.Profile.Feature;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
@ -38,61 +39,90 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
||||||
@Test
|
@Test
|
||||||
public void testUpdate() {
|
public void testUpdate() {
|
||||||
OrganizationRepresentation organization = createOrganization();
|
OrganizationRepresentation organization = createOrganization();
|
||||||
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(organization.getId()).identityProvider();
|
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(organization.getId())
|
||||||
|
.identityProviders().get(bc.getIDPAlias());
|
||||||
IdentityProviderRepresentation actual = orgIdPResource.toRepresentation();
|
IdentityProviderRepresentation actual = orgIdPResource.toRepresentation();
|
||||||
IdentityProviderRepresentation expected = actual;
|
IdentityProviderRepresentation expected = actual;
|
||||||
assertThat(expected.getAlias(), equalTo(bc.getIDPAlias()));
|
assertThat(expected.getAlias(), equalTo(bc.getIDPAlias()));
|
||||||
|
|
||||||
//update
|
//update
|
||||||
|
expected.setAlias("changed-alias");
|
||||||
expected.setDisplayName("My Org Broker");
|
expected.setDisplayName("My Org Broker");
|
||||||
expected.getConfig().put("test", "value");
|
expected.getConfig().put("test", "value");
|
||||||
try (Response response = orgIdPResource.update(expected)) {
|
try (Response response = orgIdPResource.update(expected)) {
|
||||||
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
|
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
orgIdPResource.toRepresentation();
|
||||||
|
Assert.fail("should fail because the alias changed");
|
||||||
|
} catch (NotFoundException ignore) {
|
||||||
|
|
||||||
|
}
|
||||||
|
orgIdPResource = testRealm().organizations().get(organization.getId()).identityProviders().get(expected.getAlias());
|
||||||
actual = orgIdPResource.toRepresentation();
|
actual = orgIdPResource.toRepresentation();
|
||||||
|
assertThat(expected.getAlias(), equalTo(actual.getAlias()));
|
||||||
assertThat(expected.getDisplayName(), equalTo(actual.getDisplayName()));
|
assertThat(expected.getDisplayName(), equalTo(actual.getDisplayName()));
|
||||||
Assert.assertEquals(expected.getConfig().get("test"), actual.getConfig().get("test"));
|
Assert.assertEquals(expected.getConfig().get("test"), actual.getConfig().get("test"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testFailUpdateAlias() {
|
|
||||||
OrganizationRepresentation organization = createOrganization();
|
|
||||||
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(organization.getId()).identityProvider();
|
|
||||||
IdentityProviderRepresentation idpRepresentation = orgIdPResource.toRepresentation();
|
|
||||||
assertThat(idpRepresentation.getAlias(), equalTo(bc.getIDPAlias()));
|
|
||||||
|
|
||||||
//update
|
|
||||||
idpRepresentation.setAlias("should-fail");
|
|
||||||
try (Response response = orgIdPResource.update(idpRepresentation)) {
|
|
||||||
assertThat(response.getStatus(), equalTo(Status.NOT_FOUND.getStatusCode()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDelete() {
|
public void testDelete() {
|
||||||
OrganizationRepresentation organization = createOrganization();
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(organization.getId()).identityProvider();
|
IdentityProviderRepresentation idpTemplate = organization
|
||||||
|
.identityProviders().get(bc.getIDPAlias()).toRepresentation();
|
||||||
|
|
||||||
try (Response response = orgIdPResource.delete()) {
|
for (int i = 0; i < 5; i++) {
|
||||||
|
idpTemplate.setAlias("idp-" + i);
|
||||||
|
idpTemplate.setInternalId(null);
|
||||||
|
organization.identityProviders().create(idpTemplate).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.assertEquals(6, organization.identityProviders().getIdentityProviders().size());
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
OrganizationIdentityProviderResource idpResource = organization.identityProviders().get("idp-" + i);
|
||||||
|
|
||||||
|
try (Response response = idpResource.delete()) {
|
||||||
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
|
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
|
||||||
}
|
}
|
||||||
assertThat(orgIdPResource.toRepresentation(), nullValue());
|
|
||||||
|
try {
|
||||||
|
idpResource.toRepresentation();
|
||||||
|
Assert.fail("should be removed");
|
||||||
|
} catch (NotFoundException expected) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
organization.identityProviders().get(bc.getIDPAlias()).delete().close();
|
||||||
|
|
||||||
|
Assert.assertTrue(testRealm().identityProviders().findAll().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void tryCreateSecondIdp() {
|
public void testCreatingExistingIdentityProvider() {
|
||||||
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(createOrganization().getId()).identityProvider();
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
OrganizationIdentityProviderResource orgIdPResource = organization
|
||||||
|
.identityProviders().get(bc.getIDPAlias());
|
||||||
|
|
||||||
IdentityProviderRepresentation idpRepresentation = orgIdPResource.toRepresentation();
|
IdentityProviderRepresentation idpRepresentation = orgIdPResource.toRepresentation();
|
||||||
|
|
||||||
|
String alias = idpRepresentation.getAlias();
|
||||||
idpRepresentation.setAlias("another-idp");
|
idpRepresentation.setAlias("another-idp");
|
||||||
try (Response response = orgIdPResource.create(idpRepresentation)) {
|
|
||||||
assertThat(response.getStatus(), equalTo(Response.Status.BAD_REQUEST.getStatusCode()));
|
try (Response response = organization.identityProviders().create(idpRepresentation)) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Status.CONFLICT.getStatusCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
idpRepresentation.setAlias(alias);
|
||||||
|
idpRepresentation.setInternalId(null);
|
||||||
|
|
||||||
|
try (Response response = organization.identityProviders().create(idpRepresentation)) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Status.CONFLICT.getStatusCode()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = jakarta.ws.rs.NotFoundException.class)
|
@Test(expected = jakarta.ws.rs.NotFoundException.class)
|
||||||
public void removingOrgShouldRemoveIdP() {
|
public void testRemovingOrgShouldRemoveIdP() {
|
||||||
OrganizationRepresentation orgRep = createOrganization();
|
OrganizationRepresentation orgRep = createOrganization();
|
||||||
OrganizationResource orgResource = testRealm().organizations().get(orgRep.getId());
|
OrganizationResource orgResource = testRealm().organizations().get(orgRep.getId());
|
||||||
|
|
||||||
|
@ -101,25 +131,27 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
testRealm().identityProviders().get(bc.getIDPAlias()).toRepresentation();
|
testRealm().identityProviders().get(bc.getIDPAlias()).toRepresentation();
|
||||||
|
Assert.assertTrue(testRealm().identityProviders().findAll().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void tryUpdateAndRemoveIdPNotAssignedToOrg() {
|
public void testUpdateOrDeleteIdentityProviderNotAssignedToOrganization() {
|
||||||
OrganizationRepresentation orgRep = createOrganization();
|
OrganizationRepresentation orgRep = createOrganization();
|
||||||
OrganizationResource orgResource = testRealm().organizations().get(orgRep.getId());
|
OrganizationResource orgResource = testRealm().organizations().get(orgRep.getId());
|
||||||
|
OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProviders().get(bc.getIDPAlias());
|
||||||
OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProvider();
|
|
||||||
|
|
||||||
IdentityProviderRepresentation idpRepresentation = createRep("some-broker", "oidc");
|
IdentityProviderRepresentation idpRepresentation = createRep("some-broker", "oidc");
|
||||||
|
getCleanup().addCleanup(() -> testRealm().identityProviders().get(idpRepresentation.getAlias()).remove());
|
||||||
//create IdP in realm not bound to Org
|
//create IdP in realm not bound to Org
|
||||||
testRealm().identityProviders().create(idpRepresentation).close();
|
testRealm().identityProviders().create(idpRepresentation).close();
|
||||||
|
|
||||||
try (Response response = orgIdPResource.update(idpRepresentation)) {
|
try (Response response = orgIdPResource.update(idpRepresentation)) {
|
||||||
assertThat(response.getStatus(), equalTo(Response.Status.NOT_FOUND.getStatusCode()));
|
assertThat(response.getStatus(), equalTo(Response.Status.NOT_FOUND.getStatusCode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
try (Response response = orgIdPResource.delete()) {
|
try (Response response = orgIdPResource.delete()) {
|
||||||
assertThat(response.getStatus(), equalTo(Status.NO_CONTENT.getStatusCode()));
|
assertThat(response.getStatus(), equalTo(Status.NO_CONTENT.getStatusCode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
try (Response response = orgIdPResource.delete()) {
|
try (Response response = orgIdPResource.delete()) {
|
||||||
assertThat(response.getStatus(), equalTo(Status.NOT_FOUND.getStatusCode()));
|
assertThat(response.getStatus(), equalTo(Status.NOT_FOUND.getStatusCode()));
|
||||||
}
|
}
|
||||||
|
@ -130,21 +162,41 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
||||||
OrganizationRepresentation orgRep = createOrganization();
|
OrganizationRepresentation orgRep = createOrganization();
|
||||||
OrganizationResource orgResource = testRealm().organizations().get(orgRep.getId());
|
OrganizationResource orgResource = testRealm().organizations().get(orgRep.getId());
|
||||||
|
|
||||||
OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProvider();
|
OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProviders().get(bc.getIDPAlias());
|
||||||
|
|
||||||
IdentityProviderRepresentation idpRepresentation = createRep("some-broker", "oidc");
|
IdentityProviderRepresentation idpRepresentation = createRep("some-broker", "oidc");
|
||||||
//create IdP in realm not bound to Org and get created internalId
|
//create IdP in realm not bound to Org and get created internalId
|
||||||
testRealm().identityProviders().create(idpRepresentation).close();
|
testRealm().identityProviders().create(idpRepresentation).close();
|
||||||
|
getCleanup().addCleanup(() -> testRealm().identityProviders().get(idpRepresentation.getAlias()).remove());
|
||||||
String internalId = testRealm().identityProviders().get("some-broker").toRepresentation().getInternalId();
|
String internalId = testRealm().identityProviders().get("some-broker").toRepresentation().getInternalId();
|
||||||
|
|
||||||
IdentityProviderRepresentation orgIdPRep = orgIdPResource.toRepresentation();
|
IdentityProviderRepresentation orgIdPRep = orgIdPResource.toRepresentation();
|
||||||
orgIdPRep.setInternalId(internalId);
|
orgIdPRep.setInternalId(internalId);
|
||||||
|
|
||||||
|
try (Response response = orgIdPResource.update(orgIdPRep)) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Status.CONFLICT.getStatusCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
orgIdPRep.setAlias("some-broker-alias");
|
||||||
|
|
||||||
try (Response response = orgIdPResource.update(orgIdPRep)) {
|
try (Response response = orgIdPResource.update(orgIdPRep)) {
|
||||||
assertThat(response.getStatus(), equalTo(Response.Status.NOT_FOUND.getStatusCode()));
|
assertThat(response.getStatus(), equalTo(Response.Status.NOT_FOUND.getStatusCode()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAssignDomainNotBoundToOrganization() {
|
||||||
|
OrganizationRepresentation orgRep = createOrganization();
|
||||||
|
OrganizationResource orgResource = testRealm().organizations().get(orgRep.getId());
|
||||||
|
OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProviders().get(bc.getIDPAlias());
|
||||||
|
IdentityProviderRepresentation idpRep = orgIdPResource.toRepresentation();
|
||||||
|
idpRep.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "unknown.org");
|
||||||
|
|
||||||
|
try (Response response = orgIdPResource.update(idpRep)) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Status.BAD_REQUEST.getStatusCode()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private IdentityProviderRepresentation createRep(String alias, String providerId) {
|
private IdentityProviderRepresentation createRep(String alias, String providerId) {
|
||||||
IdentityProviderRepresentation idp = new IdentityProviderRepresentation();
|
IdentityProviderRepresentation idp = new IdentityProviderRepresentation();
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ public class OrganizationMemberAuthenticationTest extends AbstractOrganizationTe
|
||||||
|
|
||||||
// the email does not match an organization so redirect to the realm's default authentication mechanism
|
// the email does not match an organization so redirect to the realm's default authentication mechanism
|
||||||
waitForPage(driver, "sign in to", true);
|
waitForPage(driver, "sign in to", true);
|
||||||
Assert.assertTrue("Driver should be on the provider realm page right now",
|
Assert.assertTrue("Driver should be on the consumer realm page right now",
|
||||||
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
|
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
|
||||||
Assert.assertTrue(loginPage.isPasswordInputPresent());
|
Assert.assertTrue(loginPage.isPasswordInputPresent());
|
||||||
Assert.assertEquals(member.getEmail(), loginPage.getUsername());
|
Assert.assertEquals(member.getEmail(), loginPage.getUsername());
|
||||||
|
|
|
@ -164,7 +164,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDelete() {
|
public void testDeleteUnmanagedMember() {
|
||||||
UPConfig upConfig = testRealm().users().userProfile().getConfiguration();
|
UPConfig upConfig = testRealm().users().userProfile().getConfiguration();
|
||||||
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
|
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
|
||||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
@ -188,6 +188,19 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateEmailUnmanagedMember() {
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
UserRepresentation expected = addMember(organization);
|
||||||
|
expected.setEmail("some@unknown.org");
|
||||||
|
UserResource userResource = testRealm().users().get(expected.getId());
|
||||||
|
userResource.update(expected);
|
||||||
|
UserRepresentation actual = userResource.toRepresentation();
|
||||||
|
assertEquals(expected.getId(), actual.getId());
|
||||||
|
assertEquals(expected.getEmail(), actual.getEmail());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDeleteMembersOnOrganizationRemoval() {
|
public void testDeleteMembersOnOrganizationRemoval() {
|
||||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
|
Loading…
Reference in a new issue