diff --git a/lib/keycloak-home-idp-discovery.jar b/lib/keycloak-home-idp-discovery.jar new file mode 100644 index 00000000..f677007c Binary files /dev/null and b/lib/keycloak-home-idp-discovery.jar differ 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..bc062791 --- /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 OperationalInfo.get(); + } + + + @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/main/resources/theme-resources/templates/hidpd-select-idp.ftl b/src/main/resources/theme-resources/templates/hidpd-select-idp.ftl deleted file mode 100644 index 8523f9cc..00000000 --- a/src/main/resources/theme-resources/templates/hidpd-select-idp.ftl +++ /dev/null @@ -1,28 +0,0 @@ -<#import "template.ftl" as layout> -<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username') displayInfo=(realm.password && realm.registrationAllowed && !registrationDisabled??); section> - <#if section = "header"> - ${msg("loginAccountTitle")} - <#elseif section = "socialProviders" > - <#if realm.password && hidpd.providers??> -

-
-

${msg("ext-auth-home-idp-discovery-identity-provider-login-label")}

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