Organization member onboarding using the organization identity provider
Closes #28273 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
e7dd5c1991
commit
1e3837421e
21 changed files with 767 additions and 56 deletions
|
@ -226,6 +226,11 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return getAllStream().findAny().isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,14 @@
|
||||||
package org.keycloak.organization.jpa;
|
package org.keycloak.organization.jpa;
|
||||||
|
|
||||||
import org.keycloak.Config.Scope;
|
import org.keycloak.Config.Scope;
|
||||||
|
import org.keycloak.organization.authentication.authenticators.broker.IdpOrganizationAuthenticatorFactory;
|
||||||
|
import org.keycloak.organization.authentication.authenticators.browser.OrganizationAuthenticatorFactory;
|
||||||
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.RealmModel.RealmPostCreateEvent;
|
||||||
import org.keycloak.models.RealmModel.RealmRemovedEvent;
|
import org.keycloak.models.RealmModel.RealmRemovedEvent;
|
||||||
import org.keycloak.organization.OrganizationProvider;
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
import org.keycloak.organization.OrganizationProviderFactory;
|
import org.keycloak.organization.OrganizationProviderFactory;
|
||||||
|
@ -40,7 +45,7 @@ public class JpaOrganizationProviderFactory implements OrganizationProviderFacto
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postInit(KeycloakSessionFactory factory) {
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
factory.register(this::handleRealmRemovedEvent);
|
factory.register(this::handleEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -53,11 +58,66 @@ public class JpaOrganizationProviderFactory implements OrganizationProviderFacto
|
||||||
return "jpa";
|
return "jpa";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleRealmRemovedEvent(ProviderEvent event) {
|
private void handleEvents(ProviderEvent event) {
|
||||||
|
if (event instanceof RealmPostCreateEvent) {
|
||||||
|
RealmModel realm = ((RealmPostCreateEvent) event).getCreatedRealm();
|
||||||
|
configureAuthenticationFlows(realm);
|
||||||
|
}
|
||||||
if (event instanceof RealmRemovedEvent) {
|
if (event instanceof RealmRemovedEvent) {
|
||||||
KeycloakSession session = ((RealmRemovedEvent) event).getKeycloakSession();
|
KeycloakSession session = ((RealmRemovedEvent) event).getKeycloakSession();
|
||||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||||
provider.removeAll();
|
provider.removeAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void configureAuthenticationFlows(RealmModel realm) {
|
||||||
|
addOrganizationFirstBrokerFlowStep(realm);
|
||||||
|
addOrganizationBrowserFlowStep(realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addOrganizationFirstBrokerFlowStep(RealmModel realm) {
|
||||||
|
AuthenticationFlowModel firstBrokerLoginFlow = realm.getFirstBrokerLoginFlow();
|
||||||
|
|
||||||
|
if (firstBrokerLoginFlow == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (realm.getAuthenticationExecutionsStream(firstBrokerLoginFlow.getId())
|
||||||
|
.map(AuthenticationExecutionModel::getAuthenticator)
|
||||||
|
.anyMatch(IdpOrganizationAuthenticatorFactory.ID::equals)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
|
||||||
|
execution.setParentFlow(firstBrokerLoginFlow.getId());
|
||||||
|
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||||
|
execution.setAuthenticator(IdpOrganizationAuthenticatorFactory.ID);
|
||||||
|
execution.setPriority(50);
|
||||||
|
execution.setAuthenticatorFlow(false);
|
||||||
|
realm.addAuthenticatorExecution(execution);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addOrganizationBrowserFlowStep(RealmModel realm) {
|
||||||
|
AuthenticationFlowModel browserFlow = realm.getBrowserFlow();
|
||||||
|
|
||||||
|
if (browserFlow == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (realm.getAuthenticationExecutionsStream(browserFlow.getId())
|
||||||
|
.map(AuthenticationExecutionModel::getAuthenticator)
|
||||||
|
.anyMatch(OrganizationAuthenticatorFactory.ID::equals)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
|
||||||
|
|
||||||
|
execution.setParentFlow(browserFlow.getId());
|
||||||
|
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
|
||||||
|
execution.setAuthenticator(OrganizationAuthenticatorFactory.ID);
|
||||||
|
execution.setPriority(26);
|
||||||
|
execution.setAuthenticatorFlow(false);
|
||||||
|
|
||||||
|
realm.addAuthenticatorExecution(execution);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ 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;
|
||||||
|
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.ModelValidationException;
|
import org.keycloak.models.ModelValidationException;
|
||||||
import org.keycloak.models.OrganizationDomainModel;
|
import org.keycloak.models.OrganizationDomainModel;
|
||||||
import org.keycloak.models.OrganizationModel;
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
@ -128,6 +129,11 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IdentityProviderModel getIdentityProvider() {
|
||||||
|
return provider.getIdentityProvider(this);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OrganizationEntity getEntity() {
|
public OrganizationEntity getEntity() {
|
||||||
return entity;
|
return entity;
|
||||||
|
|
|
@ -28,6 +28,8 @@ import jakarta.ws.rs.core.MultivaluedMap;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -162,4 +164,6 @@ public interface LoginFormsProvider extends Provider {
|
||||||
LoginFormsProvider setExecution(String execution);
|
LoginFormsProvider setExecution(String execution);
|
||||||
|
|
||||||
LoginFormsProvider setAuthContext(AuthenticationFlowContext context);
|
LoginFormsProvider setAuthContext(AuthenticationFlowContext context);
|
||||||
|
|
||||||
|
LoginFormsProvider setAttributeMapper(Function<Map<String, Object>, Map<String, Object>> configurer);
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,4 +132,11 @@ public interface OrganizationProvider extends 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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the current realm supports organization.
|
||||||
|
*
|
||||||
|
* @return {@code true} if organization is supported. Otherwise, returns {@code false}
|
||||||
|
*/
|
||||||
|
boolean isEnabled();
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,4 +39,6 @@ public interface OrganizationModel {
|
||||||
Stream<OrganizationDomainModel> getDomains();
|
Stream<OrganizationDomainModel> getDomains();
|
||||||
|
|
||||||
void setDomains(Set<OrganizationDomainModel> domains);
|
void setDomains(Set<OrganizationDomainModel> domains);
|
||||||
|
|
||||||
|
IdentityProviderModel getIdentityProvider();
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,7 @@ public class IdentityProviderAuthenticator implements Authenticator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void redirect(AuthenticationFlowContext context, String providerId) {
|
protected void redirect(AuthenticationFlowContext context, String providerId) {
|
||||||
Optional<IdentityProviderModel> idp = context.getRealm().getIdentityProvidersStream()
|
Optional<IdentityProviderModel> idp = context.getRealm().getIdentityProvidersStream()
|
||||||
.filter(IdentityProviderModel::isEnabled)
|
.filter(IdentityProviderModel::isEnabled)
|
||||||
.filter(identityProvider -> Objects.equals(providerId, identityProvider.getAlias()))
|
.filter(identityProvider -> Objects.equals(providerId, identityProvider.getAlias()))
|
||||||
|
|
|
@ -94,7 +94,9 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD;
|
import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD;
|
||||||
|
|
||||||
|
@ -133,6 +135,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
protected UserModel user;
|
protected UserModel user;
|
||||||
|
|
||||||
protected final Map<String, Object> attributes = new HashMap<>();
|
protected final Map<String, Object> attributes = new HashMap<>();
|
||||||
|
private Function<Map<String, Object>, Map<String, Object>> attributeMapper;
|
||||||
|
|
||||||
public FreeMarkerLoginFormsProvider(KeycloakSession session) {
|
public FreeMarkerLoginFormsProvider(KeycloakSession session) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
|
@ -547,6 +550,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
*/
|
*/
|
||||||
protected Response processTemplate(Theme theme, String templateName, Locale locale) {
|
protected Response processTemplate(Theme theme, String templateName, Locale locale) {
|
||||||
try {
|
try {
|
||||||
|
Map<String, Object> attributes = Optional.ofNullable(attributeMapper).orElse(Function.identity()).apply(this.attributes);
|
||||||
String result = freeMarker.processTemplate(attributes, templateName, theme);
|
String result = freeMarker.processTemplate(attributes, templateName, theme);
|
||||||
Response.ResponseBuilder builder = Response.status(status == null ? Response.Status.OK : status).type(MediaType.TEXT_HTML_UTF_8_TYPE).language(locale).entity(result);
|
Response.ResponseBuilder builder = Response.status(status == null ? Response.Status.OK : status).type(MediaType.TEXT_HTML_UTF_8_TYPE).language(locale).entity(result);
|
||||||
for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) {
|
for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) {
|
||||||
|
@ -903,11 +907,18 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public LoginFormsProvider setAuthContext(AuthenticationFlowContext context) {
|
public LoginFormsProvider setAuthContext(AuthenticationFlowContext context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoginFormsProvider setAttributeMapper(Function<Map<String, Object>, Map<String, Object>> mapper) {
|
||||||
|
this.attributeMapper = mapper;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,8 @@ import jakarta.ws.rs.core.Response;
|
||||||
import jakarta.ws.rs.core.Response.Status;
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import jakarta.ws.rs.ext.Provider;
|
import jakarta.ws.rs.ext.Provider;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
|
@ -70,7 +72,7 @@ public class OrganizationIdentityProviderResource {
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public Response addIdentityProvider(IdentityProviderRepresentation providerRep) {
|
public Response addIdentityProvider(IdentityProviderRepresentation providerRep) {
|
||||||
|
|
||||||
IdentityProviderModel identityProvider = organizationProvider.getIdentityProvider(organization);
|
IdentityProviderModel identityProvider = organization.getIdentityProvider();
|
||||||
if (identityProvider != null) {
|
if (identityProvider != null) {
|
||||||
throw ErrorResponse.error("Organization already assigned with an identity provider.", Status.BAD_REQUEST);
|
throw ErrorResponse.error("Organization already assigned with an identity provider.", Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
@ -101,8 +103,7 @@ public class OrganizationIdentityProviderResource {
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public IdentityProviderRepresentation getIdentityProvider() {
|
public IdentityProviderRepresentation getIdentityProvider() {
|
||||||
IdentityProviderModel identityProvider = organizationProvider.getIdentityProvider(organization);
|
return Optional.ofNullable(organization.getIdentityProvider()).map(this::toRepresentation).orElse(null);
|
||||||
return identityProvider == null ? null : toRepresentation(identityProvider);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
|
@ -130,17 +131,21 @@ public class OrganizationIdentityProviderResource {
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public Response update(IdentityProviderRepresentation providerRep) {
|
public Response update(IdentityProviderRepresentation rep) {
|
||||||
IdentityProviderModel identityProvider = getIdentityProviderModel();
|
IdentityProviderModel identityProvider = getIdentityProviderModel();
|
||||||
|
|
||||||
Response response = getIdentityProviderResource(identityProvider).update(providerRep);
|
if (!rep.getAlias().equals(identityProvider.getAlias())) {
|
||||||
|
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
|
//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() &&
|
if (Status.NO_CONTENT.getStatusCode() == response.getStatus() &&
|
||||||
! Objects.equals(identityProvider.getAlias(), providerRep.getAlias())) {
|
! Objects.equals(identityProvider.getAlias(), rep.getAlias())) {
|
||||||
|
|
||||||
//get the updated IdP from session
|
//get the updated IdP from session
|
||||||
identityProvider = realm.getIdentityProviderByAlias(providerRep.getAlias());
|
identityProvider = realm.getIdentityProviderByAlias(rep.getAlias());
|
||||||
|
|
||||||
String errorMessage;
|
String errorMessage;
|
||||||
try {
|
try {
|
||||||
|
@ -167,10 +172,12 @@ public class OrganizationIdentityProviderResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
private IdentityProviderModel getIdentityProviderModel() {
|
private IdentityProviderModel getIdentityProviderModel() {
|
||||||
IdentityProviderModel identityProvider = organizationProvider.getIdentityProvider(organization);
|
IdentityProviderModel identityProvider = organization.getIdentityProvider();
|
||||||
|
|
||||||
if (identityProvider == null) {
|
if (identityProvider == null) {
|
||||||
throw ErrorResponse.error("Organization doesn't have assigned an identity provider.", Status.NOT_FOUND);
|
throw ErrorResponse.error("Organization doesn't have assigned an identity provider.", Status.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
return identityProvider;
|
return identityProvider;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* 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.authentication.authenticators.broker;
|
||||||
|
|
||||||
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
|
import org.keycloak.authentication.AuthenticationFlowError;
|
||||||
|
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
||||||
|
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||||
|
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
|
|
||||||
|
public class IdpOrganizationAuthenticator extends AbstractIdpAuthenticator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
|
||||||
|
OrganizationProvider provider = context.getSession().getProvider(OrganizationProvider.class);
|
||||||
|
UserModel user = context.getUser();
|
||||||
|
OrganizationModel organization = (OrganizationModel) context.getSession().getAttribute(OrganizationModel.class.getName());
|
||||||
|
|
||||||
|
if (organization == null) {
|
||||||
|
context.attempted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IdentityProviderModel expectedBroker = organization.getIdentityProvider();
|
||||||
|
IdentityProviderModel currentBroker = brokerContext.getIdpConfig();
|
||||||
|
|
||||||
|
if (!expectedBroker.getAlias().equals(currentBroker.getAlias())) {
|
||||||
|
context.failure(AuthenticationFlowError.ACCESS_DENIED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.addMember(organization, user);
|
||||||
|
context.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean requiresUser() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||||
|
|
||||||
|
if (!provider.isEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String domain = getEmailDomain(user.getEmail());
|
||||||
|
|
||||||
|
if (domain == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OrganizationModel organization = provider.getByDomainName(domain);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* 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.authentication.authenticators.broker;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.Config.Scope;
|
||||||
|
import org.keycloak.authentication.Authenticator;
|
||||||
|
import org.keycloak.authentication.AuthenticatorFactory;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.common.Profile.Feature;
|
||||||
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
|
public class IdpOrganizationAuthenticatorFactory implements AuthenticatorFactory, EnvironmentDependentProviderFactory {
|
||||||
|
|
||||||
|
public static final String ID = "organization-broker";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authenticator create(KeycloakSession session) {
|
||||||
|
return new IdpOrganizationAuthenticator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getReferenceCategory() {
|
||||||
|
return "organization";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConfigurable() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||||
|
return REQUIREMENT_CHOICES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayType() {
|
||||||
|
return "Organization Member Link";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "Adds a federated user as a member of an organization";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUserSetupAllowed() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSupported(Scope config) {
|
||||||
|
return Profile.isFeatureEnabled(Feature.ORGANIZATION);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* 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.authentication.authenticators.browser;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.MultivaluedMap;
|
||||||
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
|
import org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator;
|
||||||
|
import org.keycloak.http.HttpRequest;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
|
|
||||||
|
public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
|
|
||||||
|
public OrganizationAuthenticator(KeycloakSession session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void authenticate(AuthenticationFlowContext context) {
|
||||||
|
OrganizationProvider provider = getOrganizationProvider();
|
||||||
|
|
||||||
|
if (!provider.isEnabled()) {
|
||||||
|
context.attempted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
challenge(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void action(AuthenticationFlowContext context) {
|
||||||
|
HttpRequest request = context.getHttpRequest();
|
||||||
|
MultivaluedMap<String, String> parameters = request.getDecodedFormParameters();
|
||||||
|
String username = parameters.getFirst(UserModel.USERNAME);
|
||||||
|
|
||||||
|
if (username == null) {
|
||||||
|
challenge(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String domain = getEmailDomain(username);
|
||||||
|
OrganizationProvider provider = getOrganizationProvider();
|
||||||
|
OrganizationModel organization = provider.getByDomainName(domain);
|
||||||
|
|
||||||
|
if (organization == null) {
|
||||||
|
context.attempted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IdentityProviderModel identityProvider = organization.getIdentityProvider();
|
||||||
|
|
||||||
|
if (identityProvider == null) {
|
||||||
|
context.attempted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(context, identityProvider.getAlias());
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrganizationProvider getOrganizationProvider() {
|
||||||
|
return session.getProvider(OrganizationProvider.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void challenge (AuthenticationFlowContext context){
|
||||||
|
context.challenge(context.form()
|
||||||
|
.setAttributeMapper(attributes -> {
|
||||||
|
// removes identity provider related attributes from forms
|
||||||
|
attributes.remove("social");
|
||||||
|
return attributes;
|
||||||
|
})
|
||||||
|
.createLoginUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getEmailDomain(String email) {
|
||||||
|
int domainSeparator = email.indexOf('@');
|
||||||
|
|
||||||
|
if (domainSeparator == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return email.substring(domainSeparator + 1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.organization.authentication.authenticators.browser;
|
||||||
|
|
||||||
|
import org.keycloak.Config.Scope;
|
||||||
|
import org.keycloak.authentication.Authenticator;
|
||||||
|
import org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticatorFactory;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.common.Profile.Feature;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class OrganizationAuthenticatorFactory extends IdentityProviderAuthenticatorFactory implements EnvironmentDependentProviderFactory {
|
||||||
|
|
||||||
|
public static final String ID = "organization";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayType() {
|
||||||
|
return "Organization Identity Provider Redirector";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "If organizations are enabled, automatically redirects users to the corresponding identity provider.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authenticator create(KeycloakSession session) {
|
||||||
|
return new OrganizationAuthenticator(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSupported(Scope config) {
|
||||||
|
return Profile.isFeatureEnabled(Feature.ORGANIZATION);
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,6 +40,7 @@ org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFac
|
||||||
org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
|
org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory
|
org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory
|
||||||
org.keycloak.authentication.authenticators.broker.IdpAutoLinkAuthenticatorFactory
|
org.keycloak.authentication.authenticators.broker.IdpAutoLinkAuthenticatorFactory
|
||||||
|
org.keycloak.organization.authentication.authenticators.broker.IdpOrganizationAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory
|
org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory
|
||||||
org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFactory
|
org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
|
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
|
||||||
|
@ -51,3 +52,4 @@ org.keycloak.authentication.authenticators.access.DenyAccessAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.access.AllowAccessAuthenticatorFactory
|
org.keycloak.authentication.authenticators.access.AllowAccessAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.sessionlimits.UserSessionLimitsAuthenticatorFactory
|
org.keycloak.authentication.authenticators.sessionlimits.UserSessionLimitsAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.browser.RecoveryAuthnCodesFormAuthenticatorFactory
|
org.keycloak.authentication.authenticators.browser.RecoveryAuthnCodesFormAuthenticatorFactory
|
||||||
|
org.keycloak.organization.authentication.authenticators.browser.OrganizationAuthenticatorFactory
|
|
@ -87,6 +87,12 @@ public class LoginPage extends LanguageComboboxAwarePage {
|
||||||
clickLink(submitButton);
|
clickLink(submitButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void loginUsername(String username) {
|
||||||
|
clearUsernameInputAndWaitIfNecessary();
|
||||||
|
usernameInput.sendKeys(username);
|
||||||
|
clickLink(submitButton);
|
||||||
|
}
|
||||||
|
|
||||||
private void clearUsernameInputAndWaitIfNecessary() {
|
private void clearUsernameInputAndWaitIfNecessary() {
|
||||||
try {
|
try {
|
||||||
usernameInput.clear();
|
usernameInput.clear();
|
||||||
|
@ -145,6 +151,10 @@ public class LoginPage extends LanguageComboboxAwarePage {
|
||||||
return passwordInput.getAttribute("value");
|
return passwordInput.getAttribute("value");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPasswordInputPresent() {
|
||||||
|
return !driver.findElements(By.id("password")).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
cancelButton.click();
|
cancelButton.click();
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,10 +75,10 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration {
|
||||||
client.setSecret(CLIENT_SECRET);
|
client.setSecret(CLIENT_SECRET);
|
||||||
|
|
||||||
client.setRedirectUris(Collections.singletonList(getConsumerRoot() +
|
client.setRedirectUris(Collections.singletonList(getConsumerRoot() +
|
||||||
"/auth/realms/" + REALM_CONS_NAME + "/broker/" + getIDPAlias() + "/endpoint/*"));
|
"/auth/realms/" + consumerRealmName() + "/broker/" + getIDPAlias() + "/endpoint/*"));
|
||||||
|
|
||||||
client.setAdminUrl(getConsumerRoot() +
|
client.setAdminUrl(getConsumerRoot() +
|
||||||
"/auth/realms/" + REALM_CONS_NAME + "/broker/" + getIDPAlias() + "/endpoint");
|
"/auth/realms/" + consumerRealmName() + "/broker/" + getIDPAlias() + "/endpoint");
|
||||||
|
|
||||||
OIDCAdvancedConfigWrapper.fromClientRepresentation(client).setPostLogoutRedirectUris(Collections.singletonList("+"));
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(client).setPostLogoutRedirectUris(Collections.singletonList("+"));
|
||||||
|
|
||||||
|
@ -188,7 +188,7 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IdentityProviderRepresentation setUpIdentityProvider(IdentityProviderSyncMode syncMode) {
|
public IdentityProviderRepresentation setUpIdentityProvider(IdentityProviderSyncMode syncMode) {
|
||||||
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
|
IdentityProviderRepresentation idp = createIdentityProvider(getIDPAlias(), IDP_OIDC_PROVIDER_ID);
|
||||||
|
|
||||||
Map<String, String> config = idp.getConfig();
|
Map<String, String> config = idp.getConfig();
|
||||||
applyDefaultConfiguration(config, syncMode);
|
applyDefaultConfiguration(config, syncMode);
|
||||||
|
|
|
@ -20,15 +20,20 @@ package org.keycloak.testsuite.organization.admin;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
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.keycloak.admin.client.resource.OrganizationResource;
|
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
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.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.admin.Users;
|
import org.keycloak.testsuite.admin.Users;
|
||||||
|
import org.keycloak.testsuite.broker.KcOidcBrokerConfiguration;
|
||||||
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
@ -39,6 +44,52 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
|
||||||
protected String memberEmail = "jdoe@neworg.org";
|
protected String memberEmail = "jdoe@neworg.org";
|
||||||
protected String memberPassword = "password";
|
protected String memberPassword = "password";
|
||||||
|
|
||||||
|
protected KcOidcBrokerConfiguration bc = new KcOidcBrokerConfiguration() {
|
||||||
|
@Override
|
||||||
|
public String consumerRealmName() {
|
||||||
|
return TEST_REALM_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RealmRepresentation createProviderRealm() {
|
||||||
|
RealmRepresentation providerRealm = super.createProviderRealm();
|
||||||
|
|
||||||
|
providerRealm.setClients(createProviderClients());
|
||||||
|
providerRealm.setUsers(List.of(
|
||||||
|
UserBuilder.create()
|
||||||
|
.username(getUserLogin())
|
||||||
|
.email(getUserEmail())
|
||||||
|
.password(getUserPassword())
|
||||||
|
.enabled(true).build())
|
||||||
|
);
|
||||||
|
|
||||||
|
return providerRealm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUserEmail() {
|
||||||
|
return getUserLogin() + "@" + organizationName + ".org";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIDPAlias() {
|
||||||
|
return "org-identity-provider";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
testRealm.getClients().addAll(bc.createConsumerClients());
|
||||||
|
testRealm.setSmtpServer(null);
|
||||||
|
super.configureTestRealm(testRealm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
|
testRealms.add(bc.createProviderRealm());
|
||||||
|
super.addTestRealms(testRealms);
|
||||||
|
}
|
||||||
|
|
||||||
protected OrganizationRepresentation createOrganization() {
|
protected OrganizationRepresentation createOrganization() {
|
||||||
return createOrganization(organizationName);
|
return createOrganization(organizationName);
|
||||||
}
|
}
|
||||||
|
@ -63,7 +114,10 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
|
||||||
id = ApiUtil.getCreatedId(response);
|
id = ApiUtil.getCreatedId(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
org.setId(id);
|
testRealm().organizations().get(id).identityProvider().create(bc.setUpIdentityProvider()).close();
|
||||||
|
|
||||||
|
org = testRealm().organizations().get(id).toRepresentation();
|
||||||
|
|
||||||
getCleanup().addCleanup(() -> testRealm().organizations().get(id).delete().close());
|
getCleanup().addCleanup(() -> testRealm().organizations().get(id).delete().close());
|
||||||
|
|
||||||
return org;
|
return org;
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
/*
|
||||||
|
* 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.testsuite.organization.admin;
|
||||||
|
|
||||||
|
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
|
import org.keycloak.admin.client.resource.UsersResource;
|
||||||
|
import org.keycloak.common.Profile.Feature;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.testsuite.Assert;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
import org.keycloak.testsuite.pages.AppPage;
|
||||||
|
import org.keycloak.testsuite.pages.IdpConfirmLinkPage;
|
||||||
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
|
import org.keycloak.testsuite.pages.UpdateAccountInformationPage;
|
||||||
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
|
||||||
|
@EnableFeature(Feature.ORGANIZATION)
|
||||||
|
public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganizationTest {
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected IdpConfirmLinkPage idpConfirmLinkPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected UpdateAccountInformationPage updateAccountInformationPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected AppPage appPage;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBrokerRegistration() {
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
assertBrokerRegistration(organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 testLinkExistingAccount() {
|
||||||
|
// create a realm user in the consumer realm
|
||||||
|
realmsResouce().realm(bc.consumerRealmName()).users()
|
||||||
|
.create(UserBuilder.create()
|
||||||
|
.username(bc.getUserLogin())
|
||||||
|
.email(bc.getUserEmail())
|
||||||
|
.password(bc.getUserPassword())
|
||||||
|
.enabled(true).build()
|
||||||
|
).close();
|
||||||
|
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
|
||||||
|
// login with email only
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
log.debug("Logging in");
|
||||||
|
loginPage.loginUsername(bc.getUserEmail());
|
||||||
|
|
||||||
|
// user automatically redirected to the organization identity provider
|
||||||
|
waitForPage(driver, "sign in to", true);
|
||||||
|
Assert.assertTrue("Driver should be on the provider realm page right now",
|
||||||
|
driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
|
||||||
|
|
||||||
|
// login to the organization identity provider and run the configured first broker login flow
|
||||||
|
loginPage.login(bc.getUserEmail(), bc.getUserPassword());
|
||||||
|
waitForPage(driver, "update account information", false);
|
||||||
|
updateAccountInformationPage.assertCurrent();
|
||||||
|
Assert.assertTrue("We must be on correct realm right now",
|
||||||
|
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
|
||||||
|
log.debug("Updating info on updateAccount page");
|
||||||
|
updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname");
|
||||||
|
|
||||||
|
// account with the same email exists in the realm, execute account linking
|
||||||
|
waitForPage(driver, "account already exists", false);
|
||||||
|
idpConfirmLinkPage.assertCurrent();
|
||||||
|
idpConfirmLinkPage.clickLinkAccount();
|
||||||
|
// confirm the link by authenticating
|
||||||
|
loginPage.login(bc.getUserEmail(), bc.getUserPassword());
|
||||||
|
assertIsMember(bc.getUserEmail(), organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMemberAlreadyExists() {
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
|
||||||
|
// add the member for the first time
|
||||||
|
assertBrokerRegistration(organization);
|
||||||
|
|
||||||
|
// logout to force the user to authenticate again
|
||||||
|
UserRepresentation account = getUserRepresentation(bc.getUserEmail());
|
||||||
|
realmsResouce().realm(bc.consumerRealmName()).users().get(account.getId()).logout();
|
||||||
|
|
||||||
|
// login with email only
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
log.debug("Logging in");
|
||||||
|
loginPage.loginUsername(bc.getUserEmail());
|
||||||
|
|
||||||
|
// 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 automatically redirects to the app as the account already exists
|
||||||
|
loginPage.login(bc.getUserEmail(), bc.getUserPassword());
|
||||||
|
appPage.assertCurrent();
|
||||||
|
assertIsMember(bc.getUserEmail(), organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertBrokerRegistration(OrganizationResource organization) {
|
||||||
|
// 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(bc.getUserEmail());
|
||||||
|
|
||||||
|
// user automatically redirected to the organization identity provider
|
||||||
|
waitForPage(driver, "sign in to", true);
|
||||||
|
Assert.assertTrue("Driver should be on the provider realm page right now",
|
||||||
|
driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
|
||||||
|
|
||||||
|
// login to the organization identity provider and run the configured first broker login flow
|
||||||
|
loginPage.login(bc.getUserEmail(), bc.getUserPassword());
|
||||||
|
waitForPage(driver, "update account information", false);
|
||||||
|
updateAccountInformationPage.assertCurrent();
|
||||||
|
Assert.assertTrue("We must be on correct realm right now",
|
||||||
|
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
|
||||||
|
log.debug("Updating info on updateAccount page");
|
||||||
|
updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname");
|
||||||
|
|
||||||
|
assertIsMember(bc.getUserEmail(), organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertIsMember(String userEmail, OrganizationResource organization) {
|
||||||
|
UserRepresentation account = getUserRepresentation(userEmail);
|
||||||
|
UserRepresentation member = organization.members().member(account.getId()).toRepresentation();
|
||||||
|
Assert.assertEquals(account.getId(), member.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserRepresentation getUserRepresentation(String userEmail) {
|
||||||
|
UsersResource users = adminClient.realm(bc.consumerRealmName()).users();
|
||||||
|
List<UserRepresentation> reps = users.searchByEmail(userEmail, true);
|
||||||
|
Assert.assertFalse(reps.isEmpty());
|
||||||
|
Assert.assertEquals(1, reps.size());
|
||||||
|
return reps.get(0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.nullValue;
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import org.junit.Before;
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import org.junit.Test;
|
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;
|
||||||
|
@ -34,35 +34,41 @@ import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
@EnableFeature(Feature.ORGANIZATION)
|
@EnableFeature(Feature.ORGANIZATION)
|
||||||
public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
||||||
|
|
||||||
private final String idpAlias = "org-identity-provider";
|
@Test
|
||||||
|
public void testUpdate() {
|
||||||
|
OrganizationRepresentation organization = createOrganization();
|
||||||
|
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(organization.getId()).identityProvider();
|
||||||
|
IdentityProviderRepresentation idpRepresentation = orgIdPResource.toRepresentation();
|
||||||
|
assertThat(idpRepresentation.getAlias(), equalTo(bc.getIDPAlias()));
|
||||||
|
|
||||||
@Before
|
String displayName = "My Org Broker";
|
||||||
public void addCleanups() {
|
//update
|
||||||
addCleanupIdP(idpAlias);
|
idpRepresentation.setDisplayName(displayName);
|
||||||
|
try (Response response = orgIdPResource.update(idpRepresentation)) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
|
||||||
|
}
|
||||||
|
assertThat(orgIdPResource.toRepresentation().getDisplayName(), equalTo(displayName));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCRUD() {
|
public void testFailUpdateAlias() {
|
||||||
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(createOrganization().getId()).identityProvider();
|
OrganizationRepresentation organization = createOrganization();
|
||||||
|
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(organization.getId()).identityProvider();
|
||||||
|
IdentityProviderRepresentation idpRepresentation = orgIdPResource.toRepresentation();
|
||||||
|
assertThat(idpRepresentation.getAlias(), equalTo(bc.getIDPAlias()));
|
||||||
|
|
||||||
//create, read
|
|
||||||
IdentityProviderRepresentation idpRepresentation = createRep(idpAlias, "oidc");
|
|
||||||
try (Response response = orgIdPResource.create(idpRepresentation)) {
|
|
||||||
assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
|
|
||||||
}
|
|
||||||
idpRepresentation = orgIdPResource.toRepresentation();
|
|
||||||
assertThat(idpRepresentation.getAlias(), equalTo(idpAlias));
|
|
||||||
|
|
||||||
String updatedIdpAlias = "updated-org-identity-provider";
|
|
||||||
//update
|
//update
|
||||||
idpRepresentation.setAlias(updatedIdpAlias);
|
idpRepresentation.setAlias("should-fail");
|
||||||
try (Response response = orgIdPResource.update(idpRepresentation)) {
|
try (Response response = orgIdPResource.update(idpRepresentation)) {
|
||||||
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
|
assertThat(response.getStatus(), equalTo(Status.NOT_FOUND.getStatusCode()));
|
||||||
addCleanupIdP(updatedIdpAlias);
|
|
||||||
}
|
}
|
||||||
assertThat(orgIdPResource.toRepresentation().getAlias(), equalTo(updatedIdpAlias));
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDelete() {
|
||||||
|
OrganizationRepresentation organization = createOrganization();
|
||||||
|
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(organization.getId()).identityProvider();
|
||||||
|
|
||||||
//delete
|
|
||||||
try (Response response = orgIdPResource.delete()) {
|
try (Response response = orgIdPResource.delete()) {
|
||||||
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
|
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
|
||||||
}
|
}
|
||||||
|
@ -73,10 +79,7 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
||||||
public void tryCreateSecondIdp() {
|
public void tryCreateSecondIdp() {
|
||||||
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(createOrganization().getId()).identityProvider();
|
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(createOrganization().getId()).identityProvider();
|
||||||
|
|
||||||
IdentityProviderRepresentation idpRepresentation = createRep(idpAlias, "oidc");
|
IdentityProviderRepresentation idpRepresentation = orgIdPResource.toRepresentation();
|
||||||
try (Response response = orgIdPResource.create(idpRepresentation)) {
|
|
||||||
assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
idpRepresentation.setAlias("another-idp");
|
idpRepresentation.setAlias("another-idp");
|
||||||
try (Response response = orgIdPResource.create(idpRepresentation)) {
|
try (Response response = orgIdPResource.create(idpRepresentation)) {
|
||||||
|
@ -89,18 +92,11 @@ 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();
|
|
||||||
|
|
||||||
IdentityProviderRepresentation idpRepresentation = createRep(idpAlias, "oidc");
|
|
||||||
try (Response response = orgIdPResource.create(idpRepresentation)) {
|
|
||||||
assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
try (Response response = orgResource.delete()) {
|
try (Response response = orgResource.delete()) {
|
||||||
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
|
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
testRealm().identityProviders().get(idpAlias).toRepresentation();
|
testRealm().identityProviders().get(bc.getIDPAlias()).toRepresentation();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -110,7 +106,7 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
||||||
|
|
||||||
OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProvider();
|
OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProvider();
|
||||||
|
|
||||||
IdentityProviderRepresentation idpRepresentation = createRep(idpAlias, "oidc");
|
IdentityProviderRepresentation idpRepresentation = createRep("some-broker", "oidc");
|
||||||
//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();
|
||||||
|
|
||||||
|
@ -118,7 +114,10 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
||||||
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(Response.Status.NOT_FOUND.getStatusCode()));
|
assertThat(response.getStatus(), equalTo(Status.NO_CONTENT.getStatusCode()));
|
||||||
|
}
|
||||||
|
try (Response response = orgIdPResource.delete()) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Status.NOT_FOUND.getStatusCode()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,8 +130,4 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
|
||||||
idp.setEnabled(true);
|
idp.setEnabled(true);
|
||||||
return idp;
|
return idp;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addCleanupIdP(String alias) {
|
|
||||||
getCleanup().addCleanup(() -> testRealm().identityProviders().get(alias).remove());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
</div>
|
</div>
|
||||||
</#if>
|
</#if>
|
||||||
<#elseif section = "socialProviders" >
|
<#elseif section = "socialProviders" >
|
||||||
<#if realm.password && social.providers??>
|
<#if realm.password && social?? && social.providers??>
|
||||||
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
|
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
|
||||||
<hr/>
|
<hr/>
|
||||||
<h4>${msg("identity-provider-login-label")}</h4>
|
<h4>${msg("identity-provider-login-label")}</h4>
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
</div>
|
</div>
|
||||||
</#if>
|
</#if>
|
||||||
<#elseif section = "socialProviders" >
|
<#elseif section = "socialProviders" >
|
||||||
<#if realm.password && social.providers??>
|
<#if realm.password && social?? && social.providers??>
|
||||||
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
|
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
|
||||||
<hr/>
|
<hr/>
|
||||||
<h2>${msg("identity-provider-login-label")}</h2>
|
<h2>${msg("identity-provider-login-label")}</h2>
|
||||||
|
|
Loading…
Reference in a new issue