From ac4113bfcd8b217baeb058b0e0a3d71c794b90c9 Mon Sep 17 00:00:00 2001 From: razvantufisi Date: Sat, 27 Jul 2024 14:37:55 +0300 Subject: [PATCH] #235 Implement own authenticator base on HomeIdp discovery Use some tricks --- lib/keycloak-home-idp-discovery.jar | Bin 0 -> 65102 bytes pom.xml | 7 + .../keycloak/authentication/hidpd/LICENCE.md} | 0 .../OrgsEmailHomeIdpDiscovererConfig.java} | 7 +- .../OrgsEmailHomeIdpDiscovererFactory.java | 13 +- .../hidpd/OrgsIdentityProviders.java | 25 +++ .../hidpd/PhaseTwoAuthenticator.java} | 9 +- .../hidpd/PhaseTwoAuthenticatorFactory.java | 110 ++++++++++++ ...tHomeIdpDiscoveryAuthenticatorFactory.java | 134 --------------- ...AlwaysSelectableIdentityProviderModel.java | 27 --- .../auth/idp/AuthenticationChallenge.java | 63 ------- .../auth/idp/BaseUriLoginFormsProvider.java | 29 ---- .../idp/HomeIdpAuthenticationFlowContext.java | 94 ---------- .../HomeIdpDiscoveryAuthenticatorFactory.java | 37 ---- .../auth/idp/HomeIdpForwarderConfig.java | 30 ---- .../idp/HomeIdpForwarderConfigProperties.java | 35 ---- .../auth/idp/IdpSelectorAuthenticator.java | 111 ------------ .../idp/IdpSelectorAuthenticatorFactory.java | 86 ---------- .../phasetwo/service/auth/idp/LoginForm.java | 54 ------ .../phasetwo/service/auth/idp/LoginHint.java | 56 ------ .../phasetwo/service/auth/idp/LoginPage.java | 52 ------ .../service/auth/idp/OperationalInfo.java | 16 -- .../phasetwo/service/auth/idp/PublicAPI.java | 53 ------ .../service/auth/idp/Reauthentication.java | 26 --- .../phasetwo/service/auth/idp/Redirector.java | 74 -------- .../phasetwo/service/auth/idp/RememberMe.java | 46 ----- .../io/phasetwo/service/auth/idp/Users.java | 30 ---- .../email/DefaultIdentityProviders.java | 27 --- .../auth/idp/discovery/email/Domain.java | 45 ----- .../idp/discovery/email/DomainExtractor.java | 46 ----- .../email/EmailHomeIdpDiscoverer.java | 161 ------------------ .../email/EmailHomeIdpDiscovererFactory.java | 51 ------ ...yAuthenticatorFactoryDiscovererConfig.java | 20 --- .../email/IdentityProviderModelConfig.java | 58 ------- .../discovery/email/IdentityProviders.java | 66 ------- .../orgs/domainhint/OrgsDomainDiscoverer.java | 44 ----- .../OrgsDomainDiscovererProviderFactory.java | 57 ------- ...nHomeIdpDiscoveryAuthenticatorFactory.java | 57 ------- .../OrgsEmailHomeIdpDiscovererConfig.java | 40 ----- ...lHomeIdpDiscoveryAuthenticatorFactory.java | 58 ------- .../orgs/email/OrgsIdentityProviders.java | 60 ------- .../idp/discovery/spi/HomeIdpDiscoverer.java | 63 ------- .../spi/HomeIdpDiscovererFactory.java | 13 -- .../discovery/spi/HomeIdpDiscoverySpi.java | 33 ---- .../service/auth/idp/package-info.java | 12 -- .../AbstractCypressOrganizationTest.java | 9 + .../service/AbstractOrganizationTest.java | 9 + 47 files changed, 172 insertions(+), 1981 deletions(-) create mode 100644 lib/keycloak-home-idp-discovery.jar rename src/main/java/{io/phasetwo/service/auth/idp/LICENSE.md => de/sventorben/keycloak/authentication/hidpd/LICENCE.md} (100%) rename src/main/java/{io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscovererConfig.java => de/sventorben/keycloak/authentication/hidpd/OrgsEmailHomeIdpDiscovererConfig.java} (93%) rename src/main/java/{io/phasetwo/service/auth/idp/discovery/orgs/email => de/sventorben/keycloak/authentication/hidpd}/OrgsEmailHomeIdpDiscovererFactory.java (67%) create mode 100644 src/main/java/de/sventorben/keycloak/authentication/hidpd/OrgsIdentityProviders.java rename src/main/java/{io/phasetwo/service/auth/idp/HomeIdpDiscoveryAuthenticator.java => de/sventorben/keycloak/authentication/hidpd/PhaseTwoAuthenticator.java} (95%) create mode 100755 src/main/java/de/sventorben/keycloak/authentication/hidpd/PhaseTwoAuthenticatorFactory.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/AbstractHomeIdpDiscoveryAuthenticatorFactory.java delete mode 100755 src/main/java/io/phasetwo/service/auth/idp/AlwaysSelectableIdentityProviderModel.java delete mode 100755 src/main/java/io/phasetwo/service/auth/idp/AuthenticationChallenge.java delete mode 100755 src/main/java/io/phasetwo/service/auth/idp/BaseUriLoginFormsProvider.java delete mode 100755 src/main/java/io/phasetwo/service/auth/idp/HomeIdpAuthenticationFlowContext.java delete mode 100755 src/main/java/io/phasetwo/service/auth/idp/HomeIdpDiscoveryAuthenticatorFactory.java delete mode 100755 src/main/java/io/phasetwo/service/auth/idp/HomeIdpForwarderConfig.java delete mode 100755 src/main/java/io/phasetwo/service/auth/idp/HomeIdpForwarderConfigProperties.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/IdpSelectorAuthenticator.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/IdpSelectorAuthenticatorFactory.java delete mode 100755 src/main/java/io/phasetwo/service/auth/idp/LoginForm.java delete mode 100755 src/main/java/io/phasetwo/service/auth/idp/LoginHint.java delete mode 100755 src/main/java/io/phasetwo/service/auth/idp/LoginPage.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/OperationalInfo.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/PublicAPI.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/Reauthentication.java delete mode 100755 src/main/java/io/phasetwo/service/auth/idp/Redirector.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/RememberMe.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/Users.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/email/DefaultIdentityProviders.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/email/Domain.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/email/DomainExtractor.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscoverer.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscovererFactory.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscoveryAuthenticatorFactoryDiscovererConfig.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/email/IdentityProviderModelConfig.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/email/IdentityProviders.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/domainhint/OrgsDomainDiscoverer.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/domainhint/OrgsDomainDiscovererProviderFactory.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/domainhint/OrgsDomainHomeIdpDiscoveryAuthenticatorFactory.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsEmailHomeIdpDiscovererConfig.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsEmailHomeIdpDiscoveryAuthenticatorFactory.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsIdentityProviders.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/spi/HomeIdpDiscoverer.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/spi/HomeIdpDiscovererFactory.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/discovery/spi/HomeIdpDiscoverySpi.java delete mode 100644 src/main/java/io/phasetwo/service/auth/idp/package-info.java diff --git a/lib/keycloak-home-idp-discovery.jar b/lib/keycloak-home-idp-discovery.jar new file mode 100644 index 0000000000000000000000000000000000000000..f677007cfd259607ba667ade1c06dfc093e856bc GIT binary patch literal 65102 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 Of5cVbnM5P<+y4PoIC7l; literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml index 0f6e1be6..b8e518cf 100644 --- a/pom.xml +++ b/pom.xml @@ -259,6 +259,13 @@ 3.5.3 true + + de.sventorben.keycloak + keycloak-home-idp-discovery + 25.0.0 + system + ${project.basedir}/lib/keycloak-home-idp-discovery.jar + diff --git a/src/main/java/io/phasetwo/service/auth/idp/LICENSE.md b/src/main/java/de/sventorben/keycloak/authentication/hidpd/LICENCE.md similarity index 100% rename from src/main/java/io/phasetwo/service/auth/idp/LICENSE.md rename to src/main/java/de/sventorben/keycloak/authentication/hidpd/LICENCE.md diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscovererConfig.java b/src/main/java/de/sventorben/keycloak/authentication/hidpd/OrgsEmailHomeIdpDiscovererConfig.java similarity index 93% rename from src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscovererConfig.java rename to src/main/java/de/sventorben/keycloak/authentication/hidpd/OrgsEmailHomeIdpDiscovererConfig.java index 6cedd4cf..c6905b4a 100644 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscovererConfig.java +++ b/src/main/java/de/sventorben/keycloak/authentication/hidpd/OrgsEmailHomeIdpDiscovererConfig.java @@ -1,5 +1,4 @@ -//package io.phasetwo.service.auth.idp.discovery.email; -package io.phasetwo.service.auth.idp.discovery.email; +package de.sventorben.keycloak.authentication.hidpd; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.provider.ProviderConfigProperty; @@ -11,7 +10,7 @@ import static org.keycloak.provider.ProviderConfigProperty.BOOLEAN_TYPE; import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE; -final class EmailHomeIdpDiscovererConfig { +final class OrgsEmailHomeIdpDiscovererConfig { private static final String FORWARD_TO_LINKED_IDP = "forwardToLinkedIdp"; private static final String USER_ATTRIBUTE = "userAttribute"; @@ -58,7 +57,7 @@ final class EmailHomeIdpDiscovererConfig { .build(); private final AuthenticatorConfigModel authenticatorConfigModel; - public EmailHomeIdpDiscovererConfig(AuthenticatorConfigModel authenticatorConfigModel) { + public OrgsEmailHomeIdpDiscovererConfig(AuthenticatorConfigModel authenticatorConfigModel) { this.authenticatorConfigModel = authenticatorConfigModel; } diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsEmailHomeIdpDiscovererFactory.java b/src/main/java/de/sventorben/keycloak/authentication/hidpd/OrgsEmailHomeIdpDiscovererFactory.java similarity index 67% rename from src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsEmailHomeIdpDiscovererFactory.java rename to src/main/java/de/sventorben/keycloak/authentication/hidpd/OrgsEmailHomeIdpDiscovererFactory.java index 2e83e05e..39d0b0d1 100644 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsEmailHomeIdpDiscovererFactory.java +++ b/src/main/java/de/sventorben/keycloak/authentication/hidpd/OrgsEmailHomeIdpDiscovererFactory.java @@ -1,23 +1,20 @@ -//package io.phasetwo.service.auth.idp.discovery.orgs.email; -package io.phasetwo.service.auth.idp.discovery.orgs.email; +package de.sventorben.keycloak.authentication.hidpd; import com.google.auto.service.AutoService; -import io.phasetwo.service.auth.idp.OperationalInfo; -import io.phasetwo.service.auth.idp.Users; -import io.phasetwo.service.auth.idp.discovery.email.EmailHomeIdpDiscoverer; -import io.phasetwo.service.auth.idp.discovery.spi.HomeIdpDiscoverer; +import de.sventorben.keycloak.authentication.hidpd.discovery.email.EmailHomeIdpDiscoverer; +import de.sventorben.keycloak.authentication.hidpd.discovery.spi.HomeIdpDiscoverer; +import de.sventorben.keycloak.authentication.hidpd.discovery.spi.HomeIdpDiscovererFactory; import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ServerInfoAwareProviderFactory; import java.util.Map; -import io.phasetwo.service.auth.idp.discovery.spi.HomeIdpDiscovererFactory; @AutoService(HomeIdpDiscovererFactory.class) public final class OrgsEmailHomeIdpDiscovererFactory implements HomeIdpDiscovererFactory, ServerInfoAwareProviderFactory { - static final String PROVIDER_ID = "orgs-email"; + static final String PROVIDER_ID = "orgs-ext-email"; @Override public HomeIdpDiscoverer create(KeycloakSession keycloakSession) { diff --git a/src/main/java/de/sventorben/keycloak/authentication/hidpd/OrgsIdentityProviders.java b/src/main/java/de/sventorben/keycloak/authentication/hidpd/OrgsIdentityProviders.java new file mode 100644 index 00000000..666034f9 --- /dev/null +++ b/src/main/java/de/sventorben/keycloak/authentication/hidpd/OrgsIdentityProviders.java @@ -0,0 +1,25 @@ +package de.sventorben.keycloak.authentication.hidpd; + +import de.sventorben.keycloak.authentication.hidpd.discovery.email.Domain; +import de.sventorben.keycloak.authentication.hidpd.discovery.email.IdentityProviders; +import io.phasetwo.service.model.OrganizationModel; +import io.phasetwo.service.model.OrganizationProvider; +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.models.IdentityProviderModel; + +import java.util.List; +import java.util.stream.Collectors; + +final class OrgsIdentityProviders implements IdentityProviders { + + @Override + public List withMatchingDomain(AuthenticationFlowContext context, List candidates, Domain domain) { + var orgs = context.getSession().getProvider(OrganizationProvider.class); + var config = new OrgsEmailHomeIdpDiscovererConfig(context.getAuthenticatorConfig()); + return orgs.getOrganizationsStreamForDomain( + context.getRealm(), domain.toString(), config.requireVerifiedDomain()) + .flatMap(OrganizationModel::getIdentityProvidersStream) + .filter(IdentityProviderModel::isEnabled) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/io/phasetwo/service/auth/idp/HomeIdpDiscoveryAuthenticator.java b/src/main/java/de/sventorben/keycloak/authentication/hidpd/PhaseTwoAuthenticator.java similarity index 95% rename from src/main/java/io/phasetwo/service/auth/idp/HomeIdpDiscoveryAuthenticator.java rename to src/main/java/de/sventorben/keycloak/authentication/hidpd/PhaseTwoAuthenticator.java index d8423e13..c37339cf 100755 --- a/src/main/java/io/phasetwo/service/auth/idp/HomeIdpDiscoveryAuthenticator.java +++ b/src/main/java/de/sventorben/keycloak/authentication/hidpd/PhaseTwoAuthenticator.java @@ -1,5 +1,4 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; +package de.sventorben.keycloak.authentication.hidpd; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; @@ -24,13 +23,13 @@ import static org.keycloak.protocol.oidc.OIDCLoginProtocol.LOGIN_HINT_PARAM; import static org.keycloak.services.validation.Validation.FIELD_USERNAME; -final class HomeIdpDiscoveryAuthenticator extends AbstractUsernameFormAuthenticator { +final class PhaseTwoAuthenticator extends AbstractUsernameFormAuthenticator { - private static final Logger LOG = Logger.getLogger(HomeIdpDiscoveryAuthenticator.class); + private static final Logger LOG = Logger.getLogger(PhaseTwoAuthenticator.class); private final AbstractHomeIdpDiscoveryAuthenticatorFactory.DiscovererConfig discovererConfig; - HomeIdpDiscoveryAuthenticator(AbstractHomeIdpDiscoveryAuthenticatorFactory.DiscovererConfig discovererConfig) { + PhaseTwoAuthenticator(AbstractHomeIdpDiscoveryAuthenticatorFactory.DiscovererConfig discovererConfig) { this.discovererConfig = discovererConfig; } diff --git a/src/main/java/de/sventorben/keycloak/authentication/hidpd/PhaseTwoAuthenticatorFactory.java b/src/main/java/de/sventorben/keycloak/authentication/hidpd/PhaseTwoAuthenticatorFactory.java new file mode 100755 index 00000000..455a6884 --- /dev/null +++ b/src/main/java/de/sventorben/keycloak/authentication/hidpd/PhaseTwoAuthenticatorFactory.java @@ -0,0 +1,110 @@ +package de.sventorben.keycloak.authentication.hidpd; + +import com.google.auto.service.AutoService; +import org.keycloak.Config; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ServerInfoAwareProviderFactory; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.keycloak.models.AuthenticationExecutionModel.Requirement.ALTERNATIVE; +import static org.keycloak.models.AuthenticationExecutionModel.Requirement.DISABLED; +import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED; + +@AutoService(AuthenticatorFactory.class) +public final class PhaseTwoAuthenticatorFactory implements AuthenticatorFactory, ServerInfoAwareProviderFactory { + + private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = new AuthenticationExecutionModel.Requirement[]{REQUIRED, ALTERNATIVE, DISABLED}; + + private static final String PROVIDER_ID = "ext-auth-home-idp-discovery"; + + public Authenticator create(KeycloakSession session) { + + //@xpg -this could be simplified if we could convince the HomeIDPProvider guy to remove final from the creation phase + // public final Authenticator create(KeycloakSession session) { + // return new HomeIdpDiscoveryAuthenticator(discovererConfig); + // } + return new PhaseTwoAuthenticator(new AbstractHomeIdpDiscoveryAuthenticatorFactory.DiscovererConfig() { + public List getProperties() { + return OrgsEmailHomeIdpDiscovererConfig.CONFIG_PROPERTIES; + } + + public String getProviderId() { + return "orgs-ext-email"; + } + }); + } + + @Override + public String getDisplayType() { + return "PhaseTwo Home IdP Discovery"; + } + + @Override + public String getReferenceCategory() { + return "Authorization"; + } + + @Override + public boolean isConfigurable() { + return true; + } + + @Override + public final AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public String getHelpText() { + return "Redirects users to their home identity provider"; + } + + @Override + public final List getConfigProperties() { + return Stream.concat( + HomeIdpForwarderConfigProperties.CONFIG_PROPERTIES.stream(), + OrgsEmailHomeIdpDiscovererConfig.CONFIG_PROPERTIES.stream()) + .collect(Collectors.toList()); + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public Map getOperationalInfo() { + return Map.of(); + } + + + @Override + public void init(Config.Scope scope) { + + } + + @Override + public void postInit(KeycloakSessionFactory keycloakSessionFactory) { + + } + + @Override + public void close() { + + } +} + diff --git a/src/main/java/io/phasetwo/service/auth/idp/AbstractHomeIdpDiscoveryAuthenticatorFactory.java b/src/main/java/io/phasetwo/service/auth/idp/AbstractHomeIdpDiscoveryAuthenticatorFactory.java deleted file mode 100644 index 744314d4..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/AbstractHomeIdpDiscoveryAuthenticatorFactory.java +++ /dev/null @@ -1,134 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import org.keycloak.Config; -import org.keycloak.authentication.Authenticator; -import org.keycloak.authentication.AuthenticatorFactory; -import org.keycloak.models.AuthenticationExecutionModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.provider.ServerInfoAwareProviderFactory; -import io.phasetwo.service.auth.idp.discovery.spi.HomeIdpDiscoverer; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.keycloak.models.AuthenticationExecutionModel.Requirement.ALTERNATIVE; -import static org.keycloak.models.AuthenticationExecutionModel.Requirement.DISABLED; -import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED; - -/** - * Provides a base implementation for authenticator factories that integrate custom identity provider - * discovery mechanisms within authentication flow of this extension. This abstract class simplifies - * the creation of authenticator instances by encapsulating common logic and providing a framework - * for extending the discovery functionality through custom {@link HomeIdpDiscoverer} implementations. - *

- * Implementors of this class need to provide their own {@link DiscovererConfig}, which includes - * the discovery logic specifics and configuration properties. This approach ensures flexibility and - * customizability, enabling developers to tailor the identity provider discovery process to specific - * organizational needs or authentication scenarios. - *

- *

- * By inheriting from this class, developers can focus on the specifics of their discovery logic - * without worrying about the boilerplate associated with UI integration and redirection logic. - *

- * - * @apiNote This interface is part of the public API, but is currently unstable and may change in future releases. - * - * @see HomeIdpDiscoverer - * @see DiscovererConfig - */ -@PublicAPI(unstable = true) -public abstract class AbstractHomeIdpDiscoveryAuthenticatorFactory implements AuthenticatorFactory, ServerInfoAwareProviderFactory { - private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = new AuthenticationExecutionModel.Requirement[]{REQUIRED, ALTERNATIVE, DISABLED}; - - private final DiscovererConfig discovererConfig; - - protected AbstractHomeIdpDiscoveryAuthenticatorFactory(DiscovererConfig discovererConfig) { - this.discovererConfig = discovererConfig; - } - - @Override - public final boolean isConfigurable() { - return true; - } - - @Override - public final AuthenticationExecutionModel.Requirement[] getRequirementChoices() { - return REQUIREMENT_CHOICES; - } - - @Override - public final boolean isUserSetupAllowed() { - return false; - } - - @Override - public final List getConfigProperties() { - return Stream.concat( - HomeIdpForwarderConfigProperties.CONFIG_PROPERTIES.stream(), - discovererConfig.getProperties().stream()) - .collect(Collectors.toList()); - } - - @Override - public final Authenticator create(KeycloakSession session) { - return new HomeIdpDiscoveryAuthenticator(discovererConfig); - } - - @Override - public final void init(Config.Scope config) { - } - - @Override - public final void postInit(KeycloakSessionFactory factory) { - } - - @Override - public final void close() { - } - - @Override - public final Map getOperationalInfo() { - return OperationalInfo.get(); - } - - /** - * Represents the configuration settings for a {@link HomeIdpDiscoverer} implementation. This interface - * is designed to allow for dynamic specification of configuration properties necessary for the - * discovery of home Identity Providers (IdPs). The configurations defined by an implementation of - * this interface provide the parameters and metadata required by a discoverer to properly integrate - * with {@link HomeIdpDiscoveryAuthenticator}. - * - * @apiNote This interface is part of the public API, but is currently unstable and may change in future releases. - * - * @see HomeIdpDiscoverer - */ - @PublicAPI(unstable = true) - public interface DiscovererConfig { - /** - * Retrieves a list of {@link ProviderConfigProperty} objects that define the configuration - * properties available for the discoverer. Each {@code ProviderConfigProperty} includes metadata - * such as the property name, type, label, default value, and other attributes necessary for - * configuring the discoverer identified by {@link #getProviderId()} dynamically at runtime. - * - * @return a list of {@link ProviderConfigProperty} that describes each configuration property - * required by the discoverer. If no home properties are need for configuration, this method must - * return an empty list. - */ - List getProperties(); - - /** - * Returns the unique provider ID associated with the discoverer. This ID is used to uniquely - * identify and reference the specific discoverer implementation within the Keycloak system. - * The provider ID should be unique across all discoverer configurations to prevent conflicts - * and ensure correct operation. - * - * @return the unique string identifier for the discoverer provider. - */ - String getProviderId(); - } -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/AlwaysSelectableIdentityProviderModel.java b/src/main/java/io/phasetwo/service/auth/idp/AlwaysSelectableIdentityProviderModel.java deleted file mode 100755 index 8dfe446d..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/AlwaysSelectableIdentityProviderModel.java +++ /dev/null @@ -1,27 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import org.keycloak.models.IdentityProviderModel; - -import java.util.HashMap; -import java.util.Map; - -final class AlwaysSelectableIdentityProviderModel extends IdentityProviderModel { - - AlwaysSelectableIdentityProviderModel(IdentityProviderModel delegate) { - super(delegate); - } - - @Override - public boolean isHideOnLogin() { - return false; - } - - @Override - public Map getConfig() { - Map superConfig = new HashMap<>(super.getConfig()); - superConfig.put("hideOnLoginPage", Boolean.FALSE.toString()); - return superConfig; - } - -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/AuthenticationChallenge.java b/src/main/java/io/phasetwo/service/auth/idp/AuthenticationChallenge.java deleted file mode 100755 index 02e90759..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/AuthenticationChallenge.java +++ /dev/null @@ -1,63 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import jakarta.ws.rs.core.MultivaluedHashMap; -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.core.Response; -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.services.managers.AuthenticationManager; - -import java.util.List; - -final class AuthenticationChallenge { - - private final AuthenticationFlowContext context; - private final RememberMe rememberMe; - private final LoginHint loginHint; - private final LoginForm loginForm; - private final Reauthentication reauthentication; - - AuthenticationChallenge(AuthenticationFlowContext context, RememberMe rememberMe, LoginHint loginHint, LoginForm loginForm, Reauthentication reauthentication) { - this.context = context; - this.rememberMe = rememberMe; - this.loginHint = loginHint; - this.loginForm = loginForm; - this.reauthentication = reauthentication; - } - - void forceChallenge() { - MultivaluedMap formData = new MultivaluedHashMap<>(); - String loginHintUsername = loginHint.getFromSession(); - - String rememberMeUsername = rememberMe.getUserName(); - - if (reauthentication.required() && context.getUser() != null) { - String attribute = context.getAuthenticatorConfig().getConfig().getOrDefault("userAttribute", "username"); - formData.add(AuthenticationManager.FORM_USERNAME, context.getUser().getFirstAttribute(attribute)); - } else { - if (loginHintUsername != null || rememberMeUsername != null) { - if (loginHintUsername != null) { - formData.add(AuthenticationManager.FORM_USERNAME, loginHintUsername); - } else { - formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername); - formData.add("rememberMe", "on"); - } - } - } - - Response challengeResponse; - if (reauthentication.required()) { - challengeResponse = loginForm.createWithSignInButtonOnly(formData); - } else { - challengeResponse = loginForm.create(formData); - } - - context.challenge(challengeResponse); - } - - void forceChallenge(List homeIdps) { - context.forceChallenge(loginForm.create(homeIdps)); - } - -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/BaseUriLoginFormsProvider.java b/src/main/java/io/phasetwo/service/auth/idp/BaseUriLoginFormsProvider.java deleted file mode 100755 index 23e40023..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/BaseUriLoginFormsProvider.java +++ /dev/null @@ -1,29 +0,0 @@ -// package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import jakarta.ws.rs.core.UriBuilder; -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.forms.login.freemarker.FreeMarkerLoginFormsProvider; -import org.keycloak.services.resources.LoginActionsService; - -import java.net.URI; - -/** - * Workaround to reuse the logic in FreeMarkerLoginFormsProvider.prepareBaseUriBuilder, so no need to reimplement it. - */ -final class BaseUriLoginFormsProvider extends FreeMarkerLoginFormsProvider { - - public BaseUriLoginFormsProvider(AuthenticationFlowContext context) { - super(context.getSession()); - super.setAuthenticationSession(context.getAuthenticationSession()); - super.setClientSessionCode(context.generateAccessCode()); - } - - public URI getBaseUriWithCodeAndClientId() { - UriBuilder baseUriBuilder = super.prepareBaseUriBuilder(false); - if (accessCode != null) { - baseUriBuilder.queryParam(LoginActionsService.SESSION_CODE, accessCode); - } - return baseUriBuilder.build(); - } -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/HomeIdpAuthenticationFlowContext.java b/src/main/java/io/phasetwo/service/auth/idp/HomeIdpAuthenticationFlowContext.java deleted file mode 100755 index 69b1c11e..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/HomeIdpAuthenticationFlowContext.java +++ /dev/null @@ -1,94 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import io.phasetwo.service.auth.idp.discovery.spi.HomeIdpDiscoverer; -import org.keycloak.authentication.AuthenticationFlowContext; - -final class HomeIdpAuthenticationFlowContext { - - private final AuthenticationFlowContext context; - private HomeIdpForwarderConfig config; - private LoginPage loginPage; - private LoginHint loginHint; - private HomeIdpDiscoverer discoverer; - private RememberMe rememberMe; - private AuthenticationChallenge authenticationChallenge; - private Redirector redirector; - private BaseUriLoginFormsProvider loginFormsProvider; - private LoginForm loginForm; - private Reauthentication reauthentication; - - HomeIdpAuthenticationFlowContext(AuthenticationFlowContext context) { - this.context = context; - } - - HomeIdpForwarderConfig config() { - if (config == null) { - config = new HomeIdpForwarderConfig(context.getAuthenticatorConfig()); - } - return config; - } - - LoginPage loginPage() { - if (loginPage == null) { - loginPage = new LoginPage(context, config(), reauthentication()); - } - return loginPage; - } - - LoginHint loginHint() { - if (loginHint == null) { - loginHint = new LoginHint(context, new Users(context.getSession())); - } - return loginHint; - } - - HomeIdpDiscoverer discoverer(AbstractHomeIdpDiscoveryAuthenticatorFactory.DiscovererConfig discovererConfig) { - if (discoverer == null) { - discoverer = context.getSession().getProvider(HomeIdpDiscoverer.class, discovererConfig.getProviderId()); - } - return discoverer; - } - - RememberMe rememberMe() { - if (rememberMe == null) { - rememberMe = new RememberMe(context); - } - return rememberMe; - } - - AuthenticationChallenge authenticationChallenge() { - if (authenticationChallenge == null) { - authenticationChallenge = new AuthenticationChallenge(context, rememberMe(), loginHint(), loginForm(), reauthentication()); - } - return authenticationChallenge; - } - - Redirector redirector() { - if (redirector == null) { - redirector = new Redirector(context); - } - return redirector; - } - - LoginForm loginForm() { - if (loginForm == null) { - loginForm = new LoginForm(context, loginFormsProvider()); - } - return loginForm; - } - - BaseUriLoginFormsProvider loginFormsProvider() { - if (loginFormsProvider == null) { - loginFormsProvider = new BaseUriLoginFormsProvider(context); - } - return loginFormsProvider; - } - - Reauthentication reauthentication() { - if (reauthentication == null) { - reauthentication = new Reauthentication(context); - } - return reauthentication; - } -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/HomeIdpDiscoveryAuthenticatorFactory.java b/src/main/java/io/phasetwo/service/auth/idp/HomeIdpDiscoveryAuthenticatorFactory.java deleted file mode 100755 index 515ac587..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/HomeIdpDiscoveryAuthenticatorFactory.java +++ /dev/null @@ -1,37 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import com.google.auto.service.AutoService; -import io.phasetwo.service.auth.idp.discovery.email.EmailHomeIdpDiscoveryAuthenticatorFactoryDiscovererConfig; -import org.keycloak.authentication.AuthenticatorFactory; - -@AutoService(AuthenticatorFactory.class) -public final class HomeIdpDiscoveryAuthenticatorFactory extends AbstractHomeIdpDiscoveryAuthenticatorFactory { - - private static final String PROVIDER_ID = "ext-auth-home-idp-discovery"; - - public HomeIdpDiscoveryAuthenticatorFactory() { - super(new EmailHomeIdpDiscoveryAuthenticatorFactoryDiscovererConfig()); - } - - @Override - public String getDisplayType() { - return "Home IdP Discovery"; - } - - @Override - public String getReferenceCategory() { - return "Authorization"; - } - - @Override - public String getHelpText() { - return "Redirects users to their home identity provider"; - } - - @Override - public String getId() { - return PROVIDER_ID; - } -} - diff --git a/src/main/java/io/phasetwo/service/auth/idp/HomeIdpForwarderConfig.java b/src/main/java/io/phasetwo/service/auth/idp/HomeIdpForwarderConfig.java deleted file mode 100755 index 270c4743..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/HomeIdpForwarderConfig.java +++ /dev/null @@ -1,30 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import org.keycloak.models.AuthenticatorConfigModel; - -import java.util.Optional; - -final class HomeIdpForwarderConfig { - - static final String BYPASS_LOGIN_PAGE = "bypassLoginPage"; - static final String FORWARD_TO_FIRST_MATCH = "forwardToFirstMatch"; - - private final AuthenticatorConfigModel authenticatorConfigModel; - - HomeIdpForwarderConfig(AuthenticatorConfigModel authenticatorConfigModel) { - this.authenticatorConfigModel = authenticatorConfigModel; - } - - boolean bypassLoginPage() { - return Optional.ofNullable(authenticatorConfigModel) - .map(it -> Boolean.parseBoolean(it.getConfig().getOrDefault(BYPASS_LOGIN_PAGE, "false"))) - .orElse(false); - } - - boolean forwardToFirstMatch() { - return Optional.ofNullable(authenticatorConfigModel) - .map(it -> Boolean.parseBoolean(it.getConfig().getOrDefault(FORWARD_TO_FIRST_MATCH, "true"))) - .orElse(true); - } -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/HomeIdpForwarderConfigProperties.java b/src/main/java/io/phasetwo/service/auth/idp/HomeIdpForwarderConfigProperties.java deleted file mode 100755 index 1eb391bd..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/HomeIdpForwarderConfigProperties.java +++ /dev/null @@ -1,35 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.provider.ProviderConfigurationBuilder; - -import java.util.List; - - -import static io.phasetwo.service.auth.idp.HomeIdpForwarderConfig.BYPASS_LOGIN_PAGE; -import static io.phasetwo.service.auth.idp.HomeIdpForwarderConfig.FORWARD_TO_FIRST_MATCH; -import static org.keycloak.provider.ProviderConfigProperty.BOOLEAN_TYPE; -final class HomeIdpForwarderConfigProperties { - private static final ProviderConfigProperty BYPASS_LOGIN_PAGE_PROPERTY = new ProviderConfigProperty( - BYPASS_LOGIN_PAGE, - "Bypass login page", - "If OIDC login_hint parameter is present, whether to bypass the login page for managed domains or not.", - BOOLEAN_TYPE, - false, - false); - - private static final ProviderConfigProperty FORWARD_TO_FIRST_MATCH_PROPERTY = new ProviderConfigProperty( - FORWARD_TO_FIRST_MATCH, - "Forward to first matched IdP", - "When multiple IdPs match the domain, whether to forward to the first IdP found or let the user choose.", - BOOLEAN_TYPE, - true, - false); - - static final List CONFIG_PROPERTIES = ProviderConfigurationBuilder.create() - .property(BYPASS_LOGIN_PAGE_PROPERTY) - .property(FORWARD_TO_FIRST_MATCH_PROPERTY) - .build(); - -} \ No newline at end of file diff --git a/src/main/java/io/phasetwo/service/auth/idp/IdpSelectorAuthenticator.java b/src/main/java/io/phasetwo/service/auth/idp/IdpSelectorAuthenticator.java deleted file mode 100644 index 38104398..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/IdpSelectorAuthenticator.java +++ /dev/null @@ -1,111 +0,0 @@ -package io.phasetwo.service.auth.idp; - -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.core.Response; -import lombok.extern.jbosslog.JBossLog; -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.authentication.Authenticator; -import org.keycloak.models.AuthenticationExecutionModel; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; - -/** */ -@JBossLog -public class IdpSelectorAuthenticator implements Authenticator { - - protected static final String ACCEPTS_PROMPT_NONE = "acceptsPromptNoneForwardFromClient"; - - private final KeycloakSession session; - - public IdpSelectorAuthenticator(KeycloakSession session) { - this.session = session; - } - - @Override - public void authenticate(AuthenticationFlowContext context) { - Response challenge = context.form().createForm("login-select-idp.ftl"); - context.challenge(challenge); - return; - } - - private void redirect(AuthenticationFlowContext context, String providerId) { - IdentityProviderModel identityProvider = context.getRealm().getIdentityProviderByAlias(providerId); - if (identityProvider != null && identityProvider.isEnabled()) { - new Redirector(context).redirectTo(identityProvider); - /* - String accessCode = - new ClientSessionCode<>( - context.getSession(), context.getRealm(), context.getAuthenticationSession()) - .getOrGenerateCode(); - String clientId = context.getAuthenticationSession().getClient().getClientId(); - String tabId = context.getAuthenticationSession().getTabId(); - URI location = - Urls.identityProviderAuthnRequest( - context.getUriInfo().getBaseUri(), - providerId, - context.getRealm().getName(), - accessCode, - clientId, - tabId); - if (context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY) != null) { - location = - UriBuilder.fromUri(location) - .queryParam( - OAuth2Constants.DISPLAY, - context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY)) - .build(); - } - Response response = Response.seeOther(location).build(); - // will forward the request to the IDP with prompt=none if the IDP accepts forwards with - // prompt=none. - if ("none" - .equals( - context - .getAuthenticationSession() - .getClientNote(OIDCLoginProtocol.PROMPT_PARAM)) - && Boolean.valueOf(identityProvider.getConfig().get(ACCEPTS_PROMPT_NONE))) { - context - .getAuthenticationSession() - .setAuthNote(AuthenticationProcessor.FORWARDED_PASSIVE_LOGIN, "true"); - } - - log.debugf("Redirecting to %s", providerId); - context.forceChallenge(response); - */ - return; - } - - log.warnf("Provider not found or not enabled for realm %s", providerId); - if (context.getExecution().getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) { - context.success(); - } else { - context.attempted(); - } - } - - @Override - public void action(AuthenticationFlowContext context) { - MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); - String providerId = formData.getFirst("providerId"); - log.infof("Redirecting to %s", providerId); - redirect(context, providerId); - } - - @Override - public boolean requiresUser() { - return false; - } - - @Override - public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { - return true; - } - - @Override - public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {} - - @Override - public void close() {} -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/IdpSelectorAuthenticatorFactory.java b/src/main/java/io/phasetwo/service/auth/idp/IdpSelectorAuthenticatorFactory.java deleted file mode 100644 index 690a0922..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/IdpSelectorAuthenticatorFactory.java +++ /dev/null @@ -1,86 +0,0 @@ -package io.phasetwo.service.auth.idp; - -import com.google.auto.service.AutoService; -import java.util.ArrayList; -import java.util.List; -import lombok.extern.jbosslog.JBossLog; -import org.keycloak.Config; -import org.keycloak.authentication.Authenticator; -import org.keycloak.authentication.AuthenticatorFactory; -import org.keycloak.authentication.ConfigurableAuthenticatorFactory; -import org.keycloak.models.AuthenticationExecutionModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.provider.ProviderConfigProperty; - -/** */ -@JBossLog -@AutoService(AuthenticatorFactory.class) -public class IdpSelectorAuthenticatorFactory - implements AuthenticatorFactory, ConfigurableAuthenticatorFactory { - - public static final String PROVIDER_ID = "ext-auth-idp-selector"; - - @Override - public String getId() { - return PROVIDER_ID; - } - - @Override - public Authenticator create(KeycloakSession session) { - return new IdpSelectorAuthenticator(session); - } - - private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { - AuthenticationExecutionModel.Requirement.REQUIRED, - AuthenticationExecutionModel.Requirement.ALTERNATIVE, - AuthenticationExecutionModel.Requirement.DISABLED - }; - - @Override - public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { - return REQUIREMENT_CHOICES; - } - - @Override - public boolean isUserSetupAllowed() { - return false; // must return true for the Authenticator to call setRequiredActions() - } - - @Override - public boolean isConfigurable() { - return false; // only if we add a config property - } - - @Override - public List getConfigProperties() { - return configProperties; - } - - private static final List configProperties = - new ArrayList(); - - @Override - public String getHelpText() { - return "Allows a user to select an IdP by alias and be redirected."; - } - - @Override - public String getDisplayType() { - return "IdP Selector"; - } - - @Override - public String getReferenceCategory() { - return null; - } - - @Override - public void init(Config.Scope config) {} - - @Override - public void postInit(KeycloakSessionFactory factory) {} - - @Override - public void close() {} -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/LoginForm.java b/src/main/java/io/phasetwo/service/auth/idp/LoginForm.java deleted file mode 100755 index b417e028..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/LoginForm.java +++ /dev/null @@ -1,54 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.core.Response; -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.forms.login.LoginFormsProvider; -import org.keycloak.forms.login.freemarker.model.IdentityProviderBean; -import org.keycloak.models.IdentityProviderModel; - -import java.net.URI; -import java.util.List; -import java.util.stream.Collectors; - -final class LoginForm { - - private final AuthenticationFlowContext context; - private final BaseUriLoginFormsProvider loginFormsProvider; - - LoginForm(AuthenticationFlowContext context, BaseUriLoginFormsProvider loginFormsProvider) { - this.context = context; - this.loginFormsProvider = loginFormsProvider; - } - - Response createWithSignInButtonOnly(MultivaluedMap formData) { - LoginFormsProvider form = createForm(formData); - form.setAttribute(LoginFormsProvider.USERNAME_HIDDEN, "true"); - form.setAttribute(LoginFormsProvider.REGISTRATION_DISABLED, "true"); - return form.createLoginUsername(); - } - - Response create(MultivaluedMap formData) { - LoginFormsProvider forms = createForm(formData); - return forms.createLoginUsername(); - } - - private LoginFormsProvider createForm(MultivaluedMap formData) { - LoginFormsProvider forms = context.form(); - if (!formData.isEmpty()) { - forms.setFormData(formData); - } - return forms; - } - - Response create(List idps) { - URI baseUriWithCodeAndClientId = loginFormsProvider.getBaseUriWithCodeAndClientId(); - LoginFormsProvider forms = context.form(); - forms.setAttribute("hidpd", new IdentityProviderBean(context.getRealm(), - context.getSession(), - idps.stream().map(AlwaysSelectableIdentityProviderModel::new).collect(Collectors.toList()), - baseUriWithCodeAndClientId)); - return forms.createForm("hidpd-select-idp.ftl"); - } -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/LoginHint.java b/src/main/java/io/phasetwo/service/auth/idp/LoginHint.java deleted file mode 100755 index 36f04af9..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/LoginHint.java +++ /dev/null @@ -1,56 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.models.FederatedIdentityModel; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.UserModel; -import org.keycloak.protocol.oidc.OIDCLoginProtocol; -import org.keycloak.services.managers.ClientSessionCode; -import org.keycloak.sessions.AuthenticationSessionModel; - -import java.util.Map; -import java.util.stream.Collectors; - -import static org.keycloak.protocol.oidc.OIDCLoginProtocol.LOGIN_HINT_PARAM; - -final class LoginHint { - - private final AuthenticationFlowContext context; - private final Users users; - - LoginHint(AuthenticationFlowContext context, Users users) { - this.context = context; - this.users = users; - } - - void setInAuthSession(IdentityProviderModel homeIdp, String username) { - String loginHint = username; - UserModel user = users.lookupBy(username); - if (user != null) { - Map idpToUsername = context.getSession().users() - .getFederatedIdentitiesStream(context.getRealm(), user) - .collect( - Collectors.toMap(FederatedIdentityModel::getIdentityProvider, - FederatedIdentityModel::getUserName)); - String alias = homeIdp == null ? "" : homeIdp.getAlias(); - loginHint = idpToUsername.getOrDefault(alias, username); - } - setInAuthSession(loginHint); - } - - void setInAuthSession(String loginHint) { - context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint); - } - - String getFromSession() { - return context.getAuthenticationSession().getClientNote(LOGIN_HINT_PARAM); - } - - void copyTo(ClientSessionCode clientSessionCode) { - String loginHint = getFromSession(); - if (clientSessionCode.getClientSession() != null && loginHint != null) { - clientSessionCode.getClientSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint); - } - } -} \ No newline at end of file diff --git a/src/main/java/io/phasetwo/service/auth/idp/LoginPage.java b/src/main/java/io/phasetwo/service/auth/idp/LoginPage.java deleted file mode 100755 index d5b67d4a..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/LoginPage.java +++ /dev/null @@ -1,52 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import org.jboss.logging.Logger; -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.sessions.AuthenticationSessionModel; -import org.keycloak.util.TokenUtil; - -import java.util.Set; - -import static org.keycloak.protocol.oidc.OIDCLoginProtocol.*; -import static org.keycloak.protocol.saml.SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT; -import static org.keycloak.protocol.saml.SamlProtocol.SAML_LOGIN_REQUEST_FORCEAUTHN; - -final class LoginPage { - - private static final Logger LOG = Logger.getLogger(LoginPage.class); - private static final Set OIDC_PROMPT_NO_BYPASS = - Set.of(PROMPT_VALUE_LOGIN, PROMPT_VALUE_CONSENT, PROMPT_VALUE_SELECT_ACCOUNT); - - private final AuthenticationFlowContext context; - private final HomeIdpForwarderConfig config; - private final Reauthentication reauthentication; - - LoginPage(AuthenticationFlowContext context, HomeIdpForwarderConfig config, Reauthentication reauthentication) { - this.context = context; - this.config = config; - this.reauthentication = reauthentication; - } - - boolean shouldByPass() { - boolean bypassLoginPage = config.bypassLoginPage(); - if (bypassLoginPage) { - AuthenticationSessionModel authenticationSession = context.getAuthenticationSession(); - String prompt = authenticationSession.getClientNote(PROMPT_PARAM); - if (OIDC_PROMPT_NO_BYPASS.stream().anyMatch(it -> TokenUtil.hasPrompt(prompt, it))) { - LOG.debugf("OIDC: Forced by prompt=%s", prompt); - return false; - } - if (SAML_FORCEAUTHN_REQUIREMENT.equalsIgnoreCase( - authenticationSession.getAuthNote(SAML_LOGIN_REQUEST_FORCEAUTHN))) { - LOG.debugf("SAML: Forced authentication"); - return false; - } - if (reauthentication.required()) { - LOG.debugf("Forced, cause reauthentication is required"); - return false; - } - } - return bypassLoginPage; - } -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/OperationalInfo.java b/src/main/java/io/phasetwo/service/auth/idp/OperationalInfo.java deleted file mode 100644 index 35b65472..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/OperationalInfo.java +++ /dev/null @@ -1,16 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import java.util.Map; - -public final class OperationalInfo { - - public static Map get() { - String version = OperationalInfo.class.getPackage().getImplementationVersion(); - if (version == null) { - version = "dev-snapshot"; - } - return Map.of("Version", version); - } - -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/PublicAPI.java b/src/main/java/io/phasetwo/service/auth/idp/PublicAPI.java deleted file mode 100644 index 5ebd715d..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/PublicAPI.java +++ /dev/null @@ -1,53 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - - -/** - * Marks a class or method as part of the public API of this extension. This annotation - * serves to clearly indicate which components of the extension are designed for external - * use and are supported according to the project's compatibility and deprecation policies. - * - *

Elements marked with this annotation are considered stable and safe for use in production - * environments, unless specified otherwise by the {@code unstable} attribute. Developers - * using these APIs can expect them to follow semantically versioned paths for updates, - * including deprecations and removals.

- * - *

Usage Guidelines

- *
    - *
  • Stable API: By default, APIs annotated with {@code @PublicAPI} without - * the {@code unstable} flag set to {@code true} are stable. These APIs are suitable for - * long-term use and should maintain backward compatibility according to the project's versioning - * policy.
  • - *
  • Unstable API: APIs marked as unstable with {@code @PublicAPI(unstable = true)} - * are in a state of flux and may undergo significant changes including backwards incompatible - * modifications. They are intended for testing, experimental use, or to gain feedback before - * becoming part of the stable public API.
  • - *
- */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR}) -public @interface PublicAPI { - /** - * Indicates whether this API is unstable. Unstable APIs are subject to change - * and may not maintain backward compatibility. Default is {@code false}, indicating - * the API is stable and intended for widespread use in production environments. - *

- * Unstable APIs are intended for early - * access to features for feedback and may change based on that feedback or - * be removed in future versions. - *

- *

- * Default value: {@code false}, meaning the API is stable. - *

- * - * @return {@code true} if the API is unstable, {@code false} if it is stable. - */ - boolean unstable() default false; -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/Reauthentication.java b/src/main/java/io/phasetwo/service/auth/idp/Reauthentication.java deleted file mode 100644 index 27551665..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/Reauthentication.java +++ /dev/null @@ -1,26 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.models.UserSessionModel; -import org.keycloak.protocol.LoginProtocol; -import org.keycloak.services.managers.AuthenticationManager; - -final class Reauthentication { - - private final AuthenticationFlowContext context; - - Reauthentication(AuthenticationFlowContext context) { - this.context = context; - } - - boolean required() { - AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(context.getSession(), context.getRealm(), true); - UserSessionModel userSessionModel = null; - if (authResult != null) { - userSessionModel = authResult.getSession(); - } - LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getAuthenticationSession().getProtocol()); - return protocol.requireReauthentication(userSessionModel, context.getAuthenticationSession()); - } -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/Redirector.java b/src/main/java/io/phasetwo/service/auth/idp/Redirector.java deleted file mode 100755 index 1283017a..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/Redirector.java +++ /dev/null @@ -1,74 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import jakarta.ws.rs.core.Response; -import org.jboss.logging.Logger; -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.authentication.AuthenticationProcessor; -import org.keycloak.broker.provider.AuthenticationRequest; -import org.keycloak.broker.provider.IdentityProvider; -import org.keycloak.broker.provider.IdentityProviderFactory; -import org.keycloak.broker.provider.util.IdentityBrokerState; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakUriInfo; -import org.keycloak.models.RealmModel; -import org.keycloak.services.Urls; -import org.keycloak.services.managers.ClientSessionCode; -import org.keycloak.sessions.AuthenticationSessionModel; - -import static org.keycloak.services.resources.IdentityBrokerService.getIdentityProviderFactory; - -final class Redirector { - - private static final Logger LOG = Logger.getLogger(Redirector.class); - - private final AuthenticationFlowContext context; - - Redirector(AuthenticationFlowContext context) { - this.context = context; - } - - void redirectTo(IdentityProviderModel idp) { - String providerAlias = idp.getAlias(); - RealmModel realm = context.getRealm(); - AuthenticationSessionModel authenticationSession = context.getAuthenticationSession(); - KeycloakSession keycloakSession = context.getSession(); - ClientSessionCode clientSessionCode = - new ClientSessionCode<>(keycloakSession, realm, authenticationSession); - clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name()); - if (!idp.isEnabled()) { - LOG.warnf("Identity Provider %s is disabled.", providerAlias); - return; - } - if (idp.isLinkOnly()) { - LOG.warnf("Identity Provider %s is not allowed to perform a login.", providerAlias); - return; - } - new HomeIdpAuthenticationFlowContext(context).loginHint().copyTo(clientSessionCode); - IdentityProviderFactory providerFactory = getIdentityProviderFactory(keycloakSession, idp); - IdentityProvider identityProvider = providerFactory.create(keycloakSession, idp); - - Response response = identityProvider.performLogin(createAuthenticationRequest(providerAlias, identityProvider, clientSessionCode)); - context.forceChallenge(response); - } - - private AuthenticationRequest createAuthenticationRequest(String providerAlias, IdentityProvider identityProvider, ClientSessionCode clientSessionCode) { - AuthenticationSessionModel authSession = null; - IdentityBrokerState encodedState = null; - - if (clientSessionCode != null) { - authSession = clientSessionCode.getClientSession(); - String relayState = clientSessionCode.getOrGenerateCode(); - String clientData = identityProvider.supportsLongStateParameter() ? AuthenticationProcessor.getClientData(context.getSession(), authSession) : null; - encodedState = IdentityBrokerState.decoded(relayState, authSession.getClient().getId(), authSession.getClient().getClientId(), authSession.getTabId(), clientData); - } - - KeycloakSession keycloakSession = context.getSession(); - KeycloakUriInfo keycloakUriInfo = keycloakSession.getContext().getUri(); - RealmModel realm = context.getRealm(); - String redirectUri = Urls.identityProviderAuthnResponse(keycloakUriInfo.getBaseUri(), providerAlias, realm.getName()).toString(); - return new AuthenticationRequest(keycloakSession, realm, authSession, context.getHttpRequest(), keycloakUriInfo, encodedState, redirectUri); - } - -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/RememberMe.java b/src/main/java/io/phasetwo/service/auth/idp/RememberMe.java deleted file mode 100644 index 1e145404..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/RememberMe.java +++ /dev/null @@ -1,46 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import jakarta.ws.rs.core.MultivaluedMap; -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.events.Details; -import org.keycloak.models.RealmModel; -import org.keycloak.services.managers.AuthenticationManager; - -final class RememberMe { - - private final AuthenticationFlowContext context; - - RememberMe(AuthenticationFlowContext context) { - this.context = context; - } - - void remember(String username) { - String rememberMe = context.getAuthenticationSession().getAuthNote(Details.REMEMBER_ME); - RealmModel realm = context.getRealm(); - boolean remember = realm.isRememberMe() && "true".equalsIgnoreCase(rememberMe); - if (remember) { - AuthenticationManager.createRememberMeCookie(username, context.getUriInfo(), context.getSession()); - } else { - AuthenticationManager.expireRememberMeCookie(context.getSession()); - } - } - - /* - * Sets session notes for interoperability with other authenticators and Keycloak defaults - */ - void handleAction(MultivaluedMap formData) { - boolean remember = context.getRealm().isRememberMe() && - "on".equalsIgnoreCase(formData.getFirst("rememberMe")); - if (remember) { - context.getAuthenticationSession().setAuthNote(Details.REMEMBER_ME, "true"); - context.getEvent().detail(Details.REMEMBER_ME, "true"); - } else { - context.getAuthenticationSession().removeAuthNote(Details.REMEMBER_ME); - } - } - - String getUserName() { - return AuthenticationManager.getRememberMeUsername(context.getSession()); - } -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/Users.java b/src/main/java/io/phasetwo/service/auth/idp/Users.java deleted file mode 100644 index 7bcc14e1..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/Users.java +++ /dev/null @@ -1,30 +0,0 @@ -//package de.sventorben.keycloak.authentication.hidpd; -package io.phasetwo.service.auth.idp; - -import org.jboss.logging.Logger; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ModelDuplicateException; -import org.keycloak.models.UserModel; -import org.keycloak.models.utils.KeycloakModelUtils; - -public final class Users { - - private static final Logger LOG = Logger.getLogger(Users.class); - - private final KeycloakSession keycloakSession; - - public Users(KeycloakSession keycloakSession) { - this.keycloakSession = keycloakSession; - } - - public UserModel lookupBy(String username) { - UserModel user = null; - try { - user = KeycloakModelUtils.findUserByNameOrEmail(keycloakSession, keycloakSession.getContext().getRealm(), username); - } catch (ModelDuplicateException ex) { - LOG.warnf(ex, "Could not uniquely identify the user. Multiple users with name or email '%s' found.", username); - } - return user; - } - -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/DefaultIdentityProviders.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/email/DefaultIdentityProviders.java deleted file mode 100644 index 267e2025..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/DefaultIdentityProviders.java +++ /dev/null @@ -1,27 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.email; -package io.phasetwo.service.auth.idp.discovery.email; - -import org.jboss.logging.Logger; -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.models.IdentityProviderModel; - -import java.util.List; -import java.util.stream.Collectors; - -final class DefaultIdentityProviders implements IdentityProviders { - - private static final Logger LOG = Logger.getLogger(DefaultIdentityProviders.class); - - @Override - public List withMatchingDomain(AuthenticationFlowContext context, List candidates, Domain domain) { - EmailHomeIdpDiscovererConfig config = new EmailHomeIdpDiscovererConfig(context.getAuthenticatorConfig()); - String userAttributeName = config.userAttribute(); - List idpsWithMatchingDomain = candidates.stream() - .filter(it -> new IdentityProviderModelConfig(it).supportsDomain(userAttributeName, domain)) - .collect(Collectors.toList()); - LOG.tracef("IdPs with matching domain '%s' for attribute '%s': %s", domain, userAttributeName, - idpsWithMatchingDomain.stream().map(IdentityProviderModel::getAlias).collect(Collectors.joining(","))); - return idpsWithMatchingDomain; - } - -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/Domain.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/email/Domain.java deleted file mode 100644 index 2faa32b7..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/Domain.java +++ /dev/null @@ -1,45 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.email; -package io.phasetwo.service.auth.idp.discovery.email; - -import java.util.Objects; -import io.phasetwo.service.auth.idp.PublicAPI; - -@PublicAPI(unstable = true) -public final class Domain { - - private final String value; - - Domain(String value) { - Objects.requireNonNull(value); - this.value = value.toLowerCase(); - } - - boolean isSubDomainOf(Domain domain) { - return this.value.endsWith("." + domain.value); - } - - public String getRawValue() { - return this.value; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) - return false; - if (!(obj instanceof Domain)) - return false; - if (this == obj) - return true; - return this.value.equalsIgnoreCase(((Domain) obj).value); - } - - @Override - public int hashCode() { - return this.value.hashCode(); - } - - @Override - public String toString() { - return this.value; - } -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/DomainExtractor.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/email/DomainExtractor.java deleted file mode 100644 index e4a8dac2..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/DomainExtractor.java +++ /dev/null @@ -1,46 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.email; -package io.phasetwo.service.auth.idp.discovery.email; - -import org.jboss.logging.Logger; -import org.keycloak.models.UserModel; - -import java.util.Optional; - -final class DomainExtractor { - - private static final Logger LOG = Logger.getLogger(DomainExtractor.class); - - private final EmailHomeIdpDiscovererConfig config; - - DomainExtractor(EmailHomeIdpDiscovererConfig config) { - this.config = config; - } - - Optional extractFrom(UserModel user) { - if (!user.isEnabled()) { - LOG.warnf("User '%s' not enabled", user.getId()); - return Optional.empty(); - } - String userAttribute = user.getFirstAttribute(config.userAttribute()); - if (userAttribute == null) { - LOG.warnf("Could not find user attribute '%s' for user '%s'", config.userAttribute(), user.getId()); - return Optional.empty(); - } - return extractFrom(userAttribute); - } - - Optional extractFrom(String usernameOrEmail) { - Domain domain = null; - if (usernameOrEmail != null) { - int atIndex = usernameOrEmail.trim().lastIndexOf("@"); - if (atIndex >= 0) { - String strDomain = usernameOrEmail.trim().substring(atIndex + 1); - if (strDomain.length() > 0) { - domain = new Domain(strDomain); - } - } - } - return Optional.ofNullable(domain); - } - -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscoverer.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscoverer.java deleted file mode 100644 index dbbe689d..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscoverer.java +++ /dev/null @@ -1,161 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.email; -package io.phasetwo.service.auth.idp.discovery.email; - -import io.phasetwo.service.auth.idp.PublicAPI; -import io.phasetwo.service.auth.idp.Users; -import io.phasetwo.service.auth.idp.discovery.spi.HomeIdpDiscoverer; -import io.phasetwo.service.model.OrganizationModel; -import io.phasetwo.service.model.OrganizationProvider; -import org.jboss.logging.Logger; -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.models.FederatedIdentityModel; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.UserModel; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - -import static java.util.Collections.emptyList; - -@PublicAPI(unstable = true) -public final class EmailHomeIdpDiscoverer implements HomeIdpDiscoverer { - - private static final Logger LOG = Logger.getLogger(EmailHomeIdpDiscoverer.class); - private static final String EMAIL_ATTRIBUTE = "email"; - private final Users users; - private final IdentityProviders identityProviders; - - @PublicAPI(unstable = true) - public EmailHomeIdpDiscoverer(Users users, IdentityProviders identityProviders) { - this.users = users; - this.identityProviders = identityProviders; - } - - @Override - public List discoverForUser(AuthenticationFlowContext context, String username) { - EmailHomeIdpDiscovererConfig config = new EmailHomeIdpDiscovererConfig(context.getAuthenticatorConfig()); - DomainExtractor domainExtractor = new DomainExtractor(config); - - String realmName = context.getRealm().getName(); - LOG.tracef("Trying to discover home IdP for username '%s' in realm '%s' with authenticator config '%s'", - username, realmName, config.getAlias()); - - List homeIdps = new ArrayList<>(); - - final Optional emailDomain; - UserModel user = users.lookupBy(username); - if (user == null) { - LOG.tracef("No user found in AuthenticationFlowContext. Extracting domain from provided username '%s'.", - username); - emailDomain = domainExtractor.extractFrom(username); - } else { - LOG.tracef("User found in AuthenticationFlowContext. Extracting domain from stored user '%s'.", - user.getId()); - if (EMAIL_ATTRIBUTE.equalsIgnoreCase(config.userAttribute()) && !user.isEmailVerified() - && !config.forwardUserWithUnverifiedEmail()) { - LOG.warnf("Email address of user '%s' is not verified and forwarding not enabled", user.getId()); - emailDomain = Optional.empty(); - } else { - emailDomain = domainExtractor.extractFrom(user); - } - } - - if (emailDomain.isPresent()) { - Domain domain = emailDomain.get(); - homeIdps = discoverHomeIdps(context, domain, user, username); - if (homeIdps.isEmpty()) { - LOG.infof("Could not find home IdP for domain '%s' and user '%s' in realm '%s'", - domain, username, realmName); - } - } else { - LOG.warnf("Could not extract domain from email address '%s'", username); - } - - return homeIdps; - } - - private List discoverHomeIdps(AuthenticationFlowContext context, Domain domain, UserModel user, String username) { - final Map linkedIdps; - - EmailHomeIdpDiscovererConfig config = new EmailHomeIdpDiscovererConfig(context.getAuthenticatorConfig()); - if (user == null || !config.forwardToLinkedIdp()) { - linkedIdps = Collections.emptyMap(); - LOG.tracef( - "User '%s' is not stored locally or forwarding to linked IdP is disabled. Skipping discovery of linked IdPs.", - username); - } else { - LOG.tracef( - "Found local user '%s' and forwarding to linked IdP is enabled. Discovering linked IdPs.", - username); - linkedIdps = context.getSession().users() - .getFederatedIdentitiesStream(context.getRealm(), user) - .collect( - Collectors.toMap(FederatedIdentityModel::getIdentityProvider, FederatedIdentityModel::getUserName)); - } - - - //Todo: This logic should be moved in a custom identity provider class. see OrgsIdentityProviders - List candidateIdps = identityProviders.candidatesForHomeIdp(context, user); - if (candidateIdps == null) { - candidateIdps = emptyList(); - } - // Original; lookup mechanism from https://github.com/sventorben/keycloak-home-idp-discovery - /* - List idpsWithMatchingDomain = identityProviders.withMatchingDomain(context, candidateIdps, domain); - if (idpsWithMatchingDomain == null) { - idpsWithMatchingDomain = emptyList(); - } - */ - // Overidden lookup mechanism to lookup via organization domain - OrganizationProvider orgs = context.getSession().getProvider(OrganizationProvider.class); - List idpsWithMatchingDomain = - orgs.getOrganizationsStreamForDomain( - context.getRealm(), domain.toString(), config.requireVerifiedDomain()) - .flatMap(OrganizationModel::getIdentityProvidersStream) - .filter(IdentityProviderModel::isEnabled) - .collect(Collectors.toList()); - - // Prefer linked IdP with matching domain first - List homeIdps = getLinkedIdpsFrom(idpsWithMatchingDomain, linkedIdps); - - if (homeIdps.isEmpty()) { - if (!linkedIdps.isEmpty()) { - // Prefer linked and enabled IdPs without matching domain in favor of not linked IdPs with matching domain - homeIdps = getLinkedIdpsFrom(candidateIdps, linkedIdps); - } - if (homeIdps.isEmpty()) { - // Fallback to not linked IdPs with matching domain (general case if user logs in for the first time) - homeIdps = idpsWithMatchingDomain; - logFoundIdps("non-linked", "matching", homeIdps, domain, username); - } else { - logFoundIdps("non-linked", "non-matching", homeIdps, domain, username); - } - } else { - logFoundIdps("linked", "matching", homeIdps, domain, username); - } - - return homeIdps; - } - - private void logFoundIdps(String idpQualifier, String domainQualifier, List homeIdps, Domain domain, String username) { - String homeIdpsString = homeIdps.stream() - .map(IdentityProviderModel::getAlias) - .collect(Collectors.joining(",")); - LOG.tracef("Found %s IdPs [%s] with %s domain '%s' for user '%s'", - idpQualifier, homeIdpsString, domainQualifier, domain, username); - } - - private List getLinkedIdpsFrom(List enabledIdpsWithMatchingDomain, Map linkedIdps) { - return enabledIdpsWithMatchingDomain.stream() - .filter(it -> linkedIdps.containsKey(it.getAlias())) - .collect(Collectors.toList()); - } - - @Override - public void close() { - } -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscovererFactory.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscovererFactory.java deleted file mode 100644 index 1fc6d93c..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscovererFactory.java +++ /dev/null @@ -1,51 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.email; -package io.phasetwo.service.auth.idp.discovery.email; - - -import com.google.auto.service.AutoService; -import io.phasetwo.service.auth.idp.OperationalInfo; -import io.phasetwo.service.auth.idp.Users; -import io.phasetwo.service.auth.idp.discovery.spi.HomeIdpDiscoverer; -import io.phasetwo.service.auth.idp.discovery.spi.HomeIdpDiscovererFactory; -import org.keycloak.Config; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.provider.ServerInfoAwareProviderFactory; - -import java.util.Map; - -@AutoService(HomeIdpDiscovererFactory.class) -public final class EmailHomeIdpDiscovererFactory implements HomeIdpDiscovererFactory, ServerInfoAwareProviderFactory { - - static final String PROVIDER_ID = "email"; - - @Override - public HomeIdpDiscoverer create(KeycloakSession keycloakSession) { - return new EmailHomeIdpDiscoverer(new Users(keycloakSession), new DefaultIdentityProviders()); - } - - @Override - public void init(Config.Scope scope) { - - } - - @Override - public void postInit(KeycloakSessionFactory keycloakSessionFactory) { - - } - - @Override - public void close() { - - } - - @Override - public String getId() { - return PROVIDER_ID; - } - - @Override - public Map getOperationalInfo() { - return OperationalInfo.get(); - } -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscoveryAuthenticatorFactoryDiscovererConfig.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscoveryAuthenticatorFactoryDiscovererConfig.java deleted file mode 100644 index 149980df..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/EmailHomeIdpDiscoveryAuthenticatorFactoryDiscovererConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.email; -package io.phasetwo.service.auth.idp.discovery.email; - -import io.phasetwo.service.auth.idp.AbstractHomeIdpDiscoveryAuthenticatorFactory; -import org.keycloak.provider.ProviderConfigProperty; - -import java.util.List; - -public final class EmailHomeIdpDiscoveryAuthenticatorFactoryDiscovererConfig implements AbstractHomeIdpDiscoveryAuthenticatorFactory.DiscovererConfig { - @Override - public List getProperties() { - return EmailHomeIdpDiscovererConfig.CONFIG_PROPERTIES; - } - - @Override - public String getProviderId() { - return EmailHomeIdpDiscovererFactory.PROVIDER_ID; - } - -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/IdentityProviderModelConfig.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/email/IdentityProviderModelConfig.java deleted file mode 100644 index 0661ea24..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/IdentityProviderModelConfig.java +++ /dev/null @@ -1,58 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.email; -package io.phasetwo.service.auth.idp.discovery.email; - -import org.keycloak.models.Constants; -import org.keycloak.models.IdentityProviderModel; - -import java.util.Arrays; -import java.util.stream.Stream; - -final class IdentityProviderModelConfig { - - private static final String DOMAINS_ATTRIBUTE_KEY = "home.idp.discovery.domains"; - private static final String SUBDOMAINS_ATTRIBUTE_KEY = "home.idp.discovery.matchSubdomains"; - - private final IdentityProviderModel identityProviderModel; - - IdentityProviderModelConfig(IdentityProviderModel identityProviderModel) { - this.identityProviderModel = identityProviderModel; - } - - boolean supportsDomain(String userAttributeName, Domain domain) { - boolean shouldMatchSubdomains = shouldMatchSubdomains(userAttributeName); - return getDomains(userAttributeName).anyMatch(it -> - it.equals(domain) || - (shouldMatchSubdomains && domain.isSubDomainOf(it))); - } - - private boolean shouldMatchSubdomains(String userAttributeName) { - String key = getSubdomainConfigKey(userAttributeName); - return Boolean.parseBoolean(identityProviderModel.getConfig().getOrDefault(key, "false")); - } - - private Stream getDomains(String userAttributeName) { - String key = getDomainConfigKey(userAttributeName); - String domainsAttribute = identityProviderModel.getConfig().getOrDefault(key, ""); - return Arrays.stream(Constants.CFG_DELIMITER_PATTERN.split(domainsAttribute)).map(Domain::new); - } - - private String getDomainConfigKey(String userAttributeName) { - return getConfigKey(DOMAINS_ATTRIBUTE_KEY, userAttributeName); - } - - private String getSubdomainConfigKey(String userAttributeName) { - return getConfigKey(SUBDOMAINS_ATTRIBUTE_KEY, userAttributeName); - } - - private String getConfigKey(String attributeKey, String userAttributeName) { - String key = attributeKey; - if (userAttributeName != null) { - final String candidateKey = attributeKey + "." + userAttributeName; - if (identityProviderModel.getConfig().containsKey(candidateKey)) { - key = candidateKey; - } - } - return key; - } - -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/IdentityProviders.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/email/IdentityProviders.java deleted file mode 100644 index 69233ce4..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/email/IdentityProviders.java +++ /dev/null @@ -1,66 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.email; -package io.phasetwo.service.auth.idp.discovery.email; - -import io.phasetwo.service.auth.idp.PublicAPI; -import org.jboss.logging.Logger; -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * Defines the contract for filtering and retrieving identity providers based on domain-specific - * criteria within the authentication process. This interface allows for the dynamic - * selection of identity providers (IdPs) that match certain conditions, enhancing the flexibility - * and precision of home IdP discovery mechanisms. - *

- * Implementations of this interface should provide logic to filter identity providers based on - * custom criteria such as the domain associated with the user or other relevant factors. - *

- * - * @apiNote This interface is part of the public API but is marked as unstable and may be subject - * to changes in future releases. - */ -@PublicAPI(unstable = true) -public interface IdentityProviders { - - Logger LOG = Logger.getLogger(IdentityProviders.class); - - /** - * Filters the given list of identity provider candidates to return those that match a specified - * domain within the context of an authentication flow. - * - * @param context The authentication flow context providing runtime information about the - * current authentication process. - * @param candidates A list of potentially eligible identity providers that may be suitable - * for the user based on initial criteria (see {@code #candidatesForHomeIdp}). - * @param domain The domain criteria used to match identity providers. - * @return A filtered list of {@link IdentityProviderModel} that match the specified domain criteria. - * May be empty but not {@code null}. - */ - List withMatchingDomain(AuthenticationFlowContext context, List candidates, Domain domain); - - /** - * Retrieves a list of identity providers that are candidates for being the user's home IdP. - *

- * This default method filters out and collects only those providers that are enabled. - *

- * @param context The authentication flow context providing runtime information about the - * current authentication process. - * @return A list of {@link IdentityProviderModel} from the realm. May be empty but not {@code null}. - */ - default List candidatesForHomeIdp(AuthenticationFlowContext context, UserModel user) { - RealmModel realm = context.getRealm(); - List enabledIdps = realm.getIdentityProvidersStream() - .filter(IdentityProviderModel::isEnabled) - .collect(Collectors.toList()); - LOG.tracef("Enabled IdPs in realm '%s': %s", - realm.getName(), - enabledIdps.stream().map(IdentityProviderModel::getAlias).collect(Collectors.joining(","))); - return enabledIdps; - } - -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/domainhint/OrgsDomainDiscoverer.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/domainhint/OrgsDomainDiscoverer.java deleted file mode 100644 index b41bcfe1..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/domainhint/OrgsDomainDiscoverer.java +++ /dev/null @@ -1,44 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.orgs.domainhint; -package io.phasetwo.service.auth.idp.discovery.orgs.domainhint; - -import io.phasetwo.service.auth.idp.discovery.spi.HomeIdpDiscoverer; -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.OrganizationModel; -import org.keycloak.organization.OrganizationProvider; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -final class OrgsDomainDiscoverer implements HomeIdpDiscoverer { - - private final KeycloakSession keycloakSession; - - OrgsDomainDiscoverer(KeycloakSession keycloakSession) { - this.keycloakSession = keycloakSession; - } - - @Override - public List discoverForUser(AuthenticationFlowContext context, String username) { - String domain = username; - OrganizationProvider orgProvider = keycloakSession.getProvider(OrganizationProvider.class); - - if (!orgProvider.isEnabled()) { - return Collections.emptyList(); - } - - OrganizationModel org = orgProvider.getByDomainName(domain); - if (org != null) { - return org.getIdentityProviders() - .filter(IdentityProviderModel::isEnabled) - .collect(Collectors.toList()); - } - return Collections.emptyList(); - } - - @Override - public void close() { - - } -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/domainhint/OrgsDomainDiscovererProviderFactory.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/domainhint/OrgsDomainDiscovererProviderFactory.java deleted file mode 100644 index 8b1711b4..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/domainhint/OrgsDomainDiscovererProviderFactory.java +++ /dev/null @@ -1,57 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.orgs.domainhint; -package io.phasetwo.service.auth.idp.discovery.orgs.domainhint; - -import com.google.auto.service.AutoService; -import io.phasetwo.service.auth.idp.OperationalInfo; -import io.phasetwo.service.auth.idp.discovery.spi.HomeIdpDiscoverer; -import io.phasetwo.service.auth.idp.discovery.spi.HomeIdpDiscovererFactory; -import org.keycloak.Config; -import org.keycloak.common.Profile; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.provider.EnvironmentDependentProviderFactory; -import org.keycloak.provider.ServerInfoAwareProviderFactory; - -import java.util.Map; - -@AutoService(HomeIdpDiscovererFactory.class) -public final class OrgsDomainDiscovererProviderFactory implements HomeIdpDiscovererFactory, EnvironmentDependentProviderFactory, ServerInfoAwareProviderFactory { - - static final String PROVIDER_ID = "orgs-domain"; - - @Override - public boolean isSupported(Config.Scope config) { - return Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION); - } - - @Override - public HomeIdpDiscoverer create(KeycloakSession keycloakSession) { - return new OrgsDomainDiscoverer(keycloakSession); - } - - @Override - public void init(Config.Scope scope) { - - } - - @Override - public void postInit(KeycloakSessionFactory keycloakSessionFactory) { - - } - - @Override - public void close() { - - } - - @Override - public String getId() { - return PROVIDER_ID; - } - - @Override - public final Map getOperationalInfo() { - return OperationalInfo.get(); - } - -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/domainhint/OrgsDomainHomeIdpDiscoveryAuthenticatorFactory.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/domainhint/OrgsDomainHomeIdpDiscoveryAuthenticatorFactory.java deleted file mode 100644 index 9047bc13..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/domainhint/OrgsDomainHomeIdpDiscoveryAuthenticatorFactory.java +++ /dev/null @@ -1,57 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.orgs.domainhint; -package io.phasetwo.service.auth.idp.discovery.orgs.domainhint; - -import com.google.auto.service.AutoService; -import io.phasetwo.service.auth.idp.AbstractHomeIdpDiscoveryAuthenticatorFactory; -import org.keycloak.Config; -import org.keycloak.authentication.AuthenticatorFactory; -import org.keycloak.common.Profile; -import org.keycloak.provider.EnvironmentDependentProviderFactory; -import org.keycloak.provider.ProviderConfigProperty; - -import java.util.Collections; -import java.util.List; - -@AutoService(AuthenticatorFactory.class) -public final class OrgsDomainHomeIdpDiscoveryAuthenticatorFactory extends AbstractHomeIdpDiscoveryAuthenticatorFactory implements EnvironmentDependentProviderFactory { - private static final String PROVIDER_ID = "orgs-domain"; - - @Override - public boolean isSupported(Config.Scope config) { - return Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION); - } - - public OrgsDomainHomeIdpDiscoveryAuthenticatorFactory() { - super(new DiscovererConfig() { - @Override - public List getProperties() { - return Collections.emptyList(); - } - - @Override - public String getProviderId() { - return OrgsDomainDiscovererProviderFactory.PROVIDER_ID; - } - }); - } - - @Override - public String getId() { - return PROVIDER_ID; - } - - @Override - public String getDisplayType() { - return "Home IdP Discovery - Organization via Domain Hint"; - } - - @Override - public String getReferenceCategory() { - return null; - } - - @Override - public String getHelpText() { - return "Redirects users to their organization's identity provider which will be discovered based on a domain hint"; - } -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsEmailHomeIdpDiscovererConfig.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsEmailHomeIdpDiscovererConfig.java deleted file mode 100644 index 9c21ef8a..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsEmailHomeIdpDiscovererConfig.java +++ /dev/null @@ -1,40 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.orgs.email; -package io.phasetwo.service.auth.idp.discovery.orgs.email; - -import org.keycloak.models.AuthenticatorConfigModel; -import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.provider.ProviderConfigurationBuilder; - -import java.util.List; -import java.util.Optional; - -import static org.keycloak.provider.ProviderConfigProperty.BOOLEAN_TYPE; - -final class OrgsEmailHomeIdpDiscovererConfig { - - private static final String FORWARD_UNVERIFIED_ATTRIBUTE = "forwardUnverifiedEmail"; - - private static final ProviderConfigProperty FORWARD_UNVERIFIED_PROPERTY = new ProviderConfigProperty( - FORWARD_UNVERIFIED_ATTRIBUTE, - "Forward users with unverified email", - "If 'User attribute' is set to 'email', whether to forward existing user if user's email is not verified.", - BOOLEAN_TYPE, - false, - false); - - static final List CONFIG_PROPERTIES = ProviderConfigurationBuilder.create() - .property(FORWARD_UNVERIFIED_PROPERTY) - .build(); - private final AuthenticatorConfigModel authenticatorConfigModel; - - public OrgsEmailHomeIdpDiscovererConfig(AuthenticatorConfigModel authenticatorConfigModel) { - this.authenticatorConfigModel = authenticatorConfigModel; - } - - boolean forwardUserWithUnverifiedEmail() { - return Optional.ofNullable(authenticatorConfigModel) - .map(it -> Boolean.parseBoolean(it.getConfig().getOrDefault(FORWARD_UNVERIFIED_ATTRIBUTE, "false"))) - .orElse(false); - } - -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsEmailHomeIdpDiscoveryAuthenticatorFactory.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsEmailHomeIdpDiscoveryAuthenticatorFactory.java deleted file mode 100644 index 4e8cd9a7..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsEmailHomeIdpDiscoveryAuthenticatorFactory.java +++ /dev/null @@ -1,58 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.orgs.email; -package io.phasetwo.service.auth.idp.discovery.orgs.email; - -import com.google.auto.service.AutoService; -import io.phasetwo.service.auth.idp.AbstractHomeIdpDiscoveryAuthenticatorFactory; -import org.keycloak.Config; -import org.keycloak.authentication.AuthenticatorFactory; -import org.keycloak.common.Profile; -import org.keycloak.provider.EnvironmentDependentProviderFactory; -import org.keycloak.provider.ProviderConfigProperty; - -import java.util.List; - -@AutoService(AuthenticatorFactory.class) -public final class OrgsEmailHomeIdpDiscoveryAuthenticatorFactory extends AbstractHomeIdpDiscoveryAuthenticatorFactory implements EnvironmentDependentProviderFactory { - - private static final String PROVIDER_ID = "orgs-email"; - - @Override - public boolean isSupported(Config.Scope config) { - return Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION); - } - - public OrgsEmailHomeIdpDiscoveryAuthenticatorFactory() { - super(new DiscovererConfig() { - @Override - public List getProperties() { - return OrgsEmailHomeIdpDiscovererConfig.CONFIG_PROPERTIES; - } - - @Override - public String getProviderId() { - return PROVIDER_ID; - } - }); - } - - @Override - public String getId() { - return PROVIDER_ID; - } - - @Override - public String getDisplayType() { - return "Home IdP Discovery - Organization via Email"; - } - - @Override - public String getReferenceCategory() { - return null; - } - - @Override - public String getHelpText() { - return "Redirects users to their organization's identity provider which will be discovered based on the domain of the user's email address."; - } - -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsIdentityProviders.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsIdentityProviders.java deleted file mode 100644 index 2ce69864..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/orgs/email/OrgsIdentityProviders.java +++ /dev/null @@ -1,60 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.orgs.email; -package io.phasetwo.service.auth.idp.discovery.orgs.email; - -import io.phasetwo.service.auth.idp.discovery.email.Domain; -import io.phasetwo.service.auth.idp.discovery.email.IdentityProviders; -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.OrganizationDomainModel; -import org.keycloak.models.OrganizationModel; -import org.keycloak.models.UserModel; -import org.keycloak.organization.OrganizationProvider; - -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -final class OrgsIdentityProviders implements IdentityProviders { - - @Override - public List candidatesForHomeIdp(AuthenticationFlowContext context, UserModel user) { - OrganizationProvider orgProvider = context.getSession().getProvider(OrganizationProvider.class); - if (user == null) { - return Collections.emptyList(); - } - if (orgProvider.isEnabled()) { - OrganizationModel org = orgProvider.getByMember(user); - if (org != null && org.isEnabled()) { - return org.getIdentityProviders() - .filter(IdentityProviderModel::isEnabled) - .collect(Collectors.toList()); - } - } else { - // TODO: Log a warning - } - return Collections.emptyList(); - } - - @Override - public List withMatchingDomain(AuthenticationFlowContext context, List candidates, Domain domain) { - OrganizationProvider orgProvider = context.getSession().getProvider(OrganizationProvider.class); - if (orgProvider.isEnabled()) { - OrganizationModel org = orgProvider.getByDomainName(domain.getRawValue()); - if (org != null && org.isEnabled()) { - boolean verified = org.getDomains() - .filter(it -> domain.getRawValue().equalsIgnoreCase(it.getName())) - .anyMatch(OrganizationDomainModel::isVerified); - if (verified) { - return org.getIdentityProviders() - .filter(IdentityProviderModel::isEnabled) - // TODO: Filter based on domain - should only be one - .collect(Collectors.toList()); - } - } - } else { - // TODO: Log a warning - } - return Collections.emptyList(); - } - -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/spi/HomeIdpDiscoverer.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/spi/HomeIdpDiscoverer.java deleted file mode 100644 index 7ba27d98..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/spi/HomeIdpDiscoverer.java +++ /dev/null @@ -1,63 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.spi; -package io.phasetwo.service.auth.idp.discovery.spi; - -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.provider.Provider; -import io.phasetwo.service.auth.idp.PublicAPI; - -import java.util.List; - -/** - * The {@link HomeIdpDiscoverer} defines the contract for implementations - * responsible for discovering the Home Identity Provider(s) for a given user. - * This interface is a part of the Service Provider Interface (SPI) extension for the - * Home IdP Discovery Keycloak extension, aimed at enabling dynamic discovery of a user's - * Home IdP based on custom logic and criteria. - *

- * Implementations of this interface should provide the logic to determine the appropriate - * Home IdP(s) for a user, potentially based on attributes such as the username, domain, or - * other identifiers associated with the user's account. This is particularly useful in - * scenarios where users may belong to different IdPs based on their organization, domain, - * or other factors, and an automated method is required to direct the user to their - * respective IdP for authentication. - *

- * - * @apiNote This interface is part of the public API, but is currently unstable and may change in future releases. - * - * @see IdentityProviderModel - * @see AuthenticationFlowContext - */ -@PublicAPI(unstable = true) -public interface HomeIdpDiscoverer extends Provider { - - /** - * Discovers and returns a list of {@link IdentityProviderModel} instances representing - * the Home Identity Provider(s) for the specified user. The method takes the username - * of the user as a parameter and returns a list of IdP models that are considered the - * user's home IdPs. If no home IdP is found for the user, this method may return an - * empty list. - *

- * Implementors should ensure that the logic for discovering the home IdPs is efficient - * and accounts for various criteria that may determine the user's Home IdP(s). The - * criteria and the discovery logic are dependent on the specific implementation. - *

- * @param context the {@link AuthenticationFlowContext} providing the current state and parameters - * of the authentication flow. This context can include various details such as the - * client, session, and other relevant information that can be utilized to determine - * the most appropriate home IdP(s) for the user. Implementors can use this context - * to access additional attributes or perform more complex logic based on the current - * authentication flow. - * @param username the unvalidated username provided by the user, serving as the primary identifier - * for the discovery of the user's Home IdP(s). Given that this username is unvalidated - * input, implementors should apply appropriate validation or sanitization measures - * to mitigate potential security risks or logic errors. This consideration is - * crucial, especially in scenarios where multiple users across different realms or - * IdPs might share the same username, necessitating the use of the authentication - * flow context to resolve such ambiguities. - * @return a list of {@link IdentityProviderModel} instances representing the discovered - * home IdP(s) for the user. The list may be empty if no home IdP is associated - * with the user. Do not return {@code null}. - */ - List discoverForUser(AuthenticationFlowContext context, String username); -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/spi/HomeIdpDiscovererFactory.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/spi/HomeIdpDiscovererFactory.java deleted file mode 100644 index ad6aa55d..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/spi/HomeIdpDiscovererFactory.java +++ /dev/null @@ -1,13 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.spi; -package io.phasetwo.service.auth.idp.discovery.spi; - -import io.phasetwo.service.auth.idp.PublicAPI; -import org.keycloak.provider.ProviderFactory; - -/** - * @apiNote This interface is part of the public API, but is currently unstable and may change in future releases. - */ -@PublicAPI(unstable = true) - -public interface HomeIdpDiscovererFactory extends ProviderFactory { -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/discovery/spi/HomeIdpDiscoverySpi.java b/src/main/java/io/phasetwo/service/auth/idp/discovery/spi/HomeIdpDiscoverySpi.java deleted file mode 100644 index 435e0925..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/discovery/spi/HomeIdpDiscoverySpi.java +++ /dev/null @@ -1,33 +0,0 @@ -//package io.phasetwo.service.auth.idp.discovery.spi; -package io.phasetwo.service.auth.idp.discovery.spi; - -import com.google.auto.service.AutoService; -import org.keycloak.provider.Provider; -import org.keycloak.provider.ProviderFactory; -import org.keycloak.provider.Spi; - -@AutoService(Spi.class) -public final class HomeIdpDiscoverySpi implements Spi { - - private static final String SPI_NAME = "hidpd-discovery"; - - @Override - public boolean isInternal() { - return true; - } - - @Override - public String getName() { - return SPI_NAME; - } - - @Override - public Class getProviderClass() { - return HomeIdpDiscoverer.class; - } - - @Override - public Class> getProviderFactoryClass() { - return HomeIdpDiscovererFactory.class; - } -} diff --git a/src/main/java/io/phasetwo/service/auth/idp/package-info.java b/src/main/java/io/phasetwo/service/auth/idp/package-info.java deleted file mode 100644 index 2aadcc21..00000000 --- a/src/main/java/io/phasetwo/service/auth/idp/package-info.java +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Forked from @sventorben's keycloak-home-idp-discovery - * https://github.com/sventorben/keycloak-home-idp-discovery - * - *

License: MIT https://github.com/sventorben/keycloak-home-idp-discovery/blob/main/LICENSE.md - * - *

Includes patches for loading from ATTEMPTED_USERNAME and looking up IdPs by an organization - * domains table. - * - *

Forked on September 4, 2023 from 04b9becfb37df63784559c936f0b49609686439e - */ -package io.phasetwo.service.auth.idp; diff --git a/src/test/java/io/phasetwo/service/AbstractCypressOrganizationTest.java b/src/test/java/io/phasetwo/service/AbstractCypressOrganizationTest.java index 24700fb8..bb4eb083 100644 --- a/src/test/java/io/phasetwo/service/AbstractCypressOrganizationTest.java +++ b/src/test/java/io/phasetwo/service/AbstractCypressOrganizationTest.java @@ -55,11 +55,20 @@ public class AbstractCypressOrganizationTest { "io.phasetwo.keycloak:keycloak-events" }; + static final String[] internalDeps = { + "lib/keycloak-home-idp-discovery.jar" + }; + static List getDeps() { List dependencies = new ArrayList(); for (String dep : deps) { dependencies.addAll(getDep(dep)); } + + for (String dep: internalDeps){ + File f = new File(dep); + dependencies.add(f); + } return dependencies; } diff --git a/src/test/java/io/phasetwo/service/AbstractOrganizationTest.java b/src/test/java/io/phasetwo/service/AbstractOrganizationTest.java index b2d84728..f22418df 100644 --- a/src/test/java/io/phasetwo/service/AbstractOrganizationTest.java +++ b/src/test/java/io/phasetwo/service/AbstractOrganizationTest.java @@ -65,11 +65,20 @@ public abstract class AbstractOrganizationTest { "io.phasetwo.keycloak:keycloak-events" }; + static final String[] internalDeps = { + "lib/keycloak-home-idp-discovery.jar" + }; + static List getDeps() { List dependencies = new ArrayList(); for (String dep : deps) { dependencies.addAll(getDep(dep)); } + + for (String dep: internalDeps){ + File f = new File(dep); + dependencies.add(f); + } return dependencies; }