Support for mutiple identity providers

Closes #28840

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-04-30 16:04:09 -03:00 committed by Alexander Schwartz
parent 051c0197db
commit 32d25f43d0
27 changed files with 991 additions and 372 deletions

View file

@ -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;
} }

View file

@ -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();

View file

@ -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);
}

View file

@ -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();
} }

View file

@ -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<>();

View file

@ -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;

View file

@ -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

View file

@ -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>

View file

@ -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.

View file

@ -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.
* *

View file

@ -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);
} }

View file

@ -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));

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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);
}
} }

View file

@ -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

View file

@ -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,55 +60,114 @@ 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() {
return session.getProvider(OrganizationProvider.class); return session.getProvider(OrganizationProvider.class);
} }
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) {

View file

@ -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;
} }
} }

View file

@ -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"));
} }
} }

View file

@ -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);
} }

View file

@ -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());

View file

@ -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()));
} }

View file

@ -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);

View file

@ -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();

View file

@ -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());

View file

@ -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());