From 1e3837421e9cea41aa91ef99800af1f82f91089b Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Mon, 15 Apr 2024 16:22:11 -0300 Subject: [PATCH] Organization member onboarding using the organization identity provider Closes #28273 Signed-off-by: Pedro Igor --- .../jpa/JpaOrganizationProvider.java | 5 + .../jpa/JpaOrganizationProviderFactory.java | 64 +++++- .../organization/jpa/OrganizationAdapter.java | 6 + .../forms/login/LoginFormsProvider.java | 4 + .../organization/OrganizationProvider.java | 7 + .../keycloak/models/OrganizationModel.java | 2 + .../IdentityProviderAuthenticator.java | 2 +- .../FreeMarkerLoginFormsProvider.java | 11 ++ .../OrganizationIdentityProviderResource.java | 23 ++- .../broker/IdpOrganizationAuthenticator.java | 101 ++++++++++ .../IdpOrganizationAuthenticatorFactory.java | 102 ++++++++++ .../browser/OrganizationAuthenticator.java | 103 ++++++++++ .../OrganizationAuthenticatorFactory.java | 59 ++++++ ...ycloak.authentication.AuthenticatorFactory | 2 + .../keycloak/testsuite/pages/LoginPage.java | 10 + .../broker/KcOidcBrokerConfiguration.java | 6 +- .../admin/AbstractOrganizationTest.java | 56 +++++- ...rganizationBrokerSelfRegistrationTest.java | 183 ++++++++++++++++++ .../OrganizationIdentityProviderTest.java | 73 ++++--- .../theme/base/login/login-username.ftl | 2 +- .../main/resources/theme/base/login/login.ftl | 2 +- 21 files changed, 767 insertions(+), 56 deletions(-) create mode 100644 services/src/main/java/org/keycloak/organization/authentication/authenticators/broker/IdpOrganizationAuthenticator.java create mode 100644 services/src/main/java/org/keycloak/organization/authentication/authenticators/broker/IdpOrganizationAuthenticatorFactory.java create mode 100644 services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java create mode 100644 services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticatorFactory.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationBrokerSelfRegistrationTest.java diff --git a/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java b/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java index 98a90a3402..db7556cc82 100644 --- a/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java +++ b/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java @@ -226,6 +226,11 @@ public class JpaOrganizationProvider implements OrganizationProvider { return true; } + @Override + public boolean isEnabled() { + return getAllStream().findAny().isPresent(); + } + @Override public void close() { } diff --git a/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProviderFactory.java b/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProviderFactory.java index 310db293a0..ee2b26f1f3 100644 --- a/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProviderFactory.java +++ b/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProviderFactory.java @@ -18,9 +18,14 @@ package org.keycloak.organization.jpa; 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.KeycloakSessionFactory; import org.keycloak.models.RealmModel; +import org.keycloak.models.RealmModel.RealmPostCreateEvent; import org.keycloak.models.RealmModel.RealmRemovedEvent; import org.keycloak.organization.OrganizationProvider; import org.keycloak.organization.OrganizationProviderFactory; @@ -40,7 +45,7 @@ public class JpaOrganizationProviderFactory implements OrganizationProviderFacto @Override public void postInit(KeycloakSessionFactory factory) { - factory.register(this::handleRealmRemovedEvent); + factory.register(this::handleEvents); } @Override @@ -53,11 +58,66 @@ public class JpaOrganizationProviderFactory implements OrganizationProviderFacto 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) { KeycloakSession session = ((RealmRemovedEvent) event).getKeycloakSession(); OrganizationProvider provider = session.getProvider(OrganizationProvider.class); 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); + } } diff --git a/model/jpa/src/main/java/org/keycloak/organization/jpa/OrganizationAdapter.java b/model/jpa/src/main/java/org/keycloak/organization/jpa/OrganizationAdapter.java index f4b9d0e29c..1103c5d7cc 100644 --- a/model/jpa/src/main/java/org/keycloak/organization/jpa/OrganizationAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/organization/jpa/OrganizationAdapter.java @@ -27,6 +27,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.ModelValidationException; import org.keycloak.models.OrganizationDomainModel; import org.keycloak.models.OrganizationModel; @@ -128,6 +129,11 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModelStian Thorgersen @@ -162,4 +164,6 @@ public interface LoginFormsProvider extends Provider { LoginFormsProvider setExecution(String execution); LoginFormsProvider setAuthContext(AuthenticationFlowContext context); + + LoginFormsProvider setAttributeMapper(Function, Map> configurer); } diff --git a/server-spi-private/src/main/java/org/keycloak/organization/OrganizationProvider.java b/server-spi-private/src/main/java/org/keycloak/organization/OrganizationProvider.java index 9485094fc8..9685d60470 100644 --- a/server-spi-private/src/main/java/org/keycloak/organization/OrganizationProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/organization/OrganizationProvider.java @@ -132,4 +132,11 @@ public interface OrganizationProvider extends Provider { * @return {@code true} if the link was removed, {@code false} otherwise */ boolean removeIdentityProvider(OrganizationModel organization); + + /** + * Indicates if the current realm supports organization. + * + * @return {@code true} if organization is supported. Otherwise, returns {@code false} + */ + boolean isEnabled(); } diff --git a/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java b/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java index bdf8054f49..1427004cd7 100644 --- a/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java +++ b/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java @@ -39,4 +39,6 @@ public interface OrganizationModel { Stream getDomains(); void setDomains(Set domains); + + IdentityProviderModel getIdentityProvider(); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java index 1e5ec16d70..0be7430a42 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java @@ -74,7 +74,7 @@ public class IdentityProviderAuthenticator implements Authenticator { } } - private void redirect(AuthenticationFlowContext context, String providerId) { + protected void redirect(AuthenticationFlowContext context, String providerId) { Optional idp = context.getRealm().getIdentityProvidersStream() .filter(IdentityProviderModel::isEnabled) .filter(identityProvider -> Objects.equals(providerId, identityProvider.getAlias())) diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java index 7c250ccd24..b4ff1b16e5 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -94,7 +94,9 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Properties; +import java.util.function.Function; import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD; @@ -133,6 +135,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { protected UserModel user; protected final Map attributes = new HashMap<>(); + private Function, Map> attributeMapper; public FreeMarkerLoginFormsProvider(KeycloakSession session) { this.session = session; @@ -547,6 +550,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { */ protected Response processTemplate(Theme theme, String templateName, Locale locale) { try { + Map attributes = Optional.ofNullable(attributeMapper).orElse(Function.identity()).apply(this.attributes); 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); for (Map.Entry entry : httpResponseHeaders.entrySet()) { @@ -903,11 +907,18 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return this; } + @Override public LoginFormsProvider setAuthContext(AuthenticationFlowContext context) { this.context = context; return this; } + @Override + public LoginFormsProvider setAttributeMapper(Function, Map> mapper) { + this.attributeMapper = mapper; + return this; + } + @Override public void close() { } diff --git a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationIdentityProviderResource.java b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationIdentityProviderResource.java index 5f979fe534..08eda71c06 100644 --- a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationIdentityProviderResource.java +++ b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationIdentityProviderResource.java @@ -28,6 +28,8 @@ 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; @@ -70,7 +72,7 @@ public class OrganizationIdentityProviderResource { @Consumes(MediaType.APPLICATION_JSON) public Response addIdentityProvider(IdentityProviderRepresentation providerRep) { - IdentityProviderModel identityProvider = organizationProvider.getIdentityProvider(organization); + IdentityProviderModel identityProvider = organization.getIdentityProvider(); if (identityProvider != null) { throw ErrorResponse.error("Organization already assigned with an identity provider.", Status.BAD_REQUEST); } @@ -101,8 +103,7 @@ public class OrganizationIdentityProviderResource { @GET @Produces(MediaType.APPLICATION_JSON) public IdentityProviderRepresentation getIdentityProvider() { - IdentityProviderModel identityProvider = organizationProvider.getIdentityProvider(organization); - return identityProvider == null ? null : toRepresentation(identityProvider); + return Optional.ofNullable(organization.getIdentityProvider()).map(this::toRepresentation).orElse(null); } @DELETE @@ -130,17 +131,21 @@ public class OrganizationIdentityProviderResource { @PUT @Consumes(MediaType.APPLICATION_JSON) - public Response update(IdentityProviderRepresentation providerRep) { + public Response update(IdentityProviderRepresentation rep) { 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 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 - identityProvider = realm.getIdentityProviderByAlias(providerRep.getAlias()); + identityProvider = realm.getIdentityProviderByAlias(rep.getAlias()); String errorMessage; try { @@ -167,10 +172,12 @@ public class OrganizationIdentityProviderResource { } private IdentityProviderModel getIdentityProviderModel() { - IdentityProviderModel identityProvider = organizationProvider.getIdentityProvider(organization); + IdentityProviderModel identityProvider = organization.getIdentityProvider(); + if (identityProvider == null) { throw ErrorResponse.error("Organization doesn't have assigned an identity provider.", Status.NOT_FOUND); } + return identityProvider; } } diff --git a/services/src/main/java/org/keycloak/organization/authentication/authenticators/broker/IdpOrganizationAuthenticator.java b/services/src/main/java/org/keycloak/organization/authentication/authenticators/broker/IdpOrganizationAuthenticator.java new file mode 100644 index 0000000000..4dad9c1d05 --- /dev/null +++ b/services/src/main/java/org/keycloak/organization/authentication/authenticators/broker/IdpOrganizationAuthenticator.java @@ -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); + } + +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/organization/authentication/authenticators/broker/IdpOrganizationAuthenticatorFactory.java b/services/src/main/java/org/keycloak/organization/authentication/authenticators/broker/IdpOrganizationAuthenticatorFactory.java new file mode 100644 index 0000000000..7a278b2cc4 --- /dev/null +++ b/services/src/main/java/org/keycloak/organization/authentication/authenticators/broker/IdpOrganizationAuthenticatorFactory.java @@ -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 getConfigProperties() { + return List.of(); + } + + @Override + public boolean isSupported(Scope config) { + return Profile.isFeatureEnabled(Feature.ORGANIZATION); + } +} diff --git a/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java b/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java new file mode 100644 index 0000000000..0d978dcb67 --- /dev/null +++ b/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java @@ -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 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); + } +} diff --git a/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticatorFactory.java b/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticatorFactory.java new file mode 100644 index 0000000000..a52a928689 --- /dev/null +++ b/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticatorFactory.java @@ -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 Stian Thorgersen + */ +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); + } +} \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory index fb70466f69..6e00f4cd0b 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -40,6 +40,7 @@ org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFac org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory org.keycloak.authentication.authenticators.broker.IdpAutoLinkAuthenticatorFactory +org.keycloak.organization.authentication.authenticators.broker.IdpOrganizationAuthenticatorFactory org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFactory 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.sessionlimits.UserSessionLimitsAuthenticatorFactory org.keycloak.authentication.authenticators.browser.RecoveryAuthnCodesFormAuthenticatorFactory +org.keycloak.organization.authentication.authenticators.browser.OrganizationAuthenticatorFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java index 339152df3b..10d9cdbf56 100755 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java @@ -87,6 +87,12 @@ public class LoginPage extends LanguageComboboxAwarePage { clickLink(submitButton); } + public void loginUsername(String username) { + clearUsernameInputAndWaitIfNecessary(); + usernameInput.sendKeys(username); + clickLink(submitButton); + } + private void clearUsernameInputAndWaitIfNecessary() { try { usernameInput.clear(); @@ -145,6 +151,10 @@ public class LoginPage extends LanguageComboboxAwarePage { return passwordInput.getAttribute("value"); } + public boolean isPasswordInputPresent() { + return !driver.findElements(By.id("password")).isEmpty(); + } + public void cancel() { cancelButton.click(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java index 963716d12a..3f5c088942 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java @@ -75,10 +75,10 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration { client.setSecret(CLIENT_SECRET); client.setRedirectUris(Collections.singletonList(getConsumerRoot() + - "/auth/realms/" + REALM_CONS_NAME + "/broker/" + getIDPAlias() + "/endpoint/*")); + "/auth/realms/" + consumerRealmName() + "/broker/" + getIDPAlias() + "/endpoint/*")); client.setAdminUrl(getConsumerRoot() + - "/auth/realms/" + REALM_CONS_NAME + "/broker/" + getIDPAlias() + "/endpoint"); + "/auth/realms/" + consumerRealmName() + "/broker/" + getIDPAlias() + "/endpoint"); OIDCAdvancedConfigWrapper.fromClientRepresentation(client).setPostLogoutRedirectUris(Collections.singletonList("+")); @@ -188,7 +188,7 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration { @Override public IdentityProviderRepresentation setUpIdentityProvider(IdentityProviderSyncMode syncMode) { - IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID); + IdentityProviderRepresentation idp = createIdentityProvider(getIDPAlias(), IDP_OIDC_PROVIDER_ID); Map config = idp.getConfig(); applyDefaultConfiguration(config, syncMode); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java index 8f7ec51f51..ea5b486624 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java @@ -20,15 +20,20 @@ package org.keycloak.testsuite.organization.admin; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import java.util.List; + import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import org.keycloak.admin.client.resource.OrganizationResource; import org.keycloak.representations.idm.OrganizationDomainRepresentation; import org.keycloak.representations.idm.OrganizationRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.admin.AbstractAdminTest; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.Users; +import org.keycloak.testsuite.broker.KcOidcBrokerConfiguration; +import org.keycloak.testsuite.util.UserBuilder; /** * @author Pedro Igor @@ -39,6 +44,52 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { protected String memberEmail = "jdoe@neworg.org"; 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 testRealms) { + testRealms.add(bc.createProviderRealm()); + super.addTestRealms(testRealms); + } + protected OrganizationRepresentation createOrganization() { return createOrganization(organizationName); } @@ -63,7 +114,10 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { 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()); return org; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationBrokerSelfRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationBrokerSelfRegistrationTest.java new file mode 100644 index 0000000000..09b77afd80 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationBrokerSelfRegistrationTest.java @@ -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 reps = users.searchByEmail(userEmail, true); + Assert.assertFalse(reps.isEmpty()); + Assert.assertEquals(1, reps.size()); + return reps.get(0); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationIdentityProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationIdentityProviderTest.java index a978cf6955..6fed8aad92 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationIdentityProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationIdentityProviderTest.java @@ -22,7 +22,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; import jakarta.ws.rs.core.Response; -import org.junit.Before; +import jakarta.ws.rs.core.Response.Status; import org.junit.Test; import org.keycloak.admin.client.resource.OrganizationIdentityProviderResource; import org.keycloak.admin.client.resource.OrganizationResource; @@ -34,35 +34,41 @@ import org.keycloak.testsuite.arquillian.annotation.EnableFeature; @EnableFeature(Feature.ORGANIZATION) 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 - public void addCleanups() { - addCleanupIdP(idpAlias); + String displayName = "My Org Broker"; + //update + 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 - public void testCRUD() { - OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(createOrganization().getId()).identityProvider(); + public void testFailUpdateAlias() { + 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 - idpRepresentation.setAlias(updatedIdpAlias); + idpRepresentation.setAlias("should-fail"); try (Response response = orgIdPResource.update(idpRepresentation)) { - assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode())); - addCleanupIdP(updatedIdpAlias); + assertThat(response.getStatus(), equalTo(Status.NOT_FOUND.getStatusCode())); } - 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()) { assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode())); } @@ -73,10 +79,7 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest { public void tryCreateSecondIdp() { OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(createOrganization().getId()).identityProvider(); - IdentityProviderRepresentation idpRepresentation = createRep(idpAlias, "oidc"); - try (Response response = orgIdPResource.create(idpRepresentation)) { - assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode())); - } + IdentityProviderRepresentation idpRepresentation = orgIdPResource.toRepresentation(); idpRepresentation.setAlias("another-idp"); try (Response response = orgIdPResource.create(idpRepresentation)) { @@ -89,18 +92,11 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest { OrganizationRepresentation orgRep = createOrganization(); 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()) { assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode())); } - testRealm().identityProviders().get(idpAlias).toRepresentation(); + testRealm().identityProviders().get(bc.getIDPAlias()).toRepresentation(); } @Test @@ -110,7 +106,7 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest { OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProvider(); - IdentityProviderRepresentation idpRepresentation = createRep(idpAlias, "oidc"); + IdentityProviderRepresentation idpRepresentation = createRep("some-broker", "oidc"); //create IdP in realm not bound to Org testRealm().identityProviders().create(idpRepresentation).close(); @@ -118,7 +114,10 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest { assertThat(response.getStatus(), equalTo(Response.Status.NOT_FOUND.getStatusCode())); } 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); return idp; } - - private void addCleanupIdP(String alias) { - getCleanup().addCleanup(() -> testRealm().identityProviders().get(alias).remove()); - } } diff --git a/themes/src/main/resources/theme/base/login/login-username.ftl b/themes/src/main/resources/theme/base/login/login-username.ftl index 02ced45a5c..be77ccbe4c 100755 --- a/themes/src/main/resources/theme/base/login/login-username.ftl +++ b/themes/src/main/resources/theme/base/login/login-username.ftl @@ -62,7 +62,7 @@ <#elseif section = "socialProviders" > - <#if realm.password && social.providers??> + <#if realm.password && social?? && social.providers??>

${msg("identity-provider-login-label")}

diff --git a/themes/src/main/resources/theme/base/login/login.ftl b/themes/src/main/resources/theme/base/login/login.ftl index d6008e0e4b..0b36ee9554 100755 --- a/themes/src/main/resources/theme/base/login/login.ftl +++ b/themes/src/main/resources/theme/base/login/login.ftl @@ -88,7 +88,7 @@
<#elseif section = "socialProviders" > - <#if realm.password && social.providers??> + <#if realm.password && social?? && social.providers??>

${msg("identity-provider-login-label")}