From eaaff6e55578501256c8b55617213fafa2be3d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Barto=C5=A1?= Date: Wed, 26 Feb 2020 08:45:26 +0100 Subject: [PATCH] KEYCLOAK-12958 Preview feature profile for WebAuthn (#6780) * KEYCLOAK-12958 Preview feature profile for WebAuthn * KEYCLOAK-12958 Ability to enable features having EnvironmentDependent providers without restart server * KEYCLOAK-12958 WebAuthn profile product/project Co-authored-by: Marek Posolda --- .../java/org/keycloak/common/Profile.java | 30 +++- .../java/org/keycloak/common/ProfileTest.java | 26 ++- .../authentication/AuthenticatorSpi.java | 4 +- .../browser/WebAuthnAuthenticatorFactory.java | 9 +- .../WebAuthnRegisterFactory.java | 8 +- .../WebAuthnCredentialProviderFactory.java | 9 +- ...PasswordlessCredentialProviderFactory.java | 9 +- .../rest/TestingResourceProvider.java | 22 ++- .../testsuite/util/FeatureDeployerUtil.java | 150 ++++++++++++++++++ .../arquillian/annotation/DisableFeature.java | 1 + .../arquillian/annotation/EnableFeature.java | 1 + .../KeycloakContainerFeaturesController.java | 17 +- .../account/AccountRestServiceTest.java | 21 ++- .../admin/authentication/ExecutionTest.java | 3 + .../admin/authentication/ProvidersTest.java | 8 +- .../authentication/RequiredActionsTest.java | 3 + .../testsuite/forms/BrowserFlowTest.java | 3 + .../forms/MultiFactorAuthenticationTest.java | 3 + .../OpenShiftTokenReviewEndpointTest.java | 2 +- .../openshift/OpenshiftClientStorageTest.java | 2 +- .../WebAuthnRegisterAndLoginTest.java | 27 ++++ .../testsuite/ui/account2/SigningInTest.java | 5 + .../theme/base/admin/resources/js/app.js | 12 +- .../admin/resources/js/controllers/realm.js | 26 ++- .../webauthn-policy-passwordless.html | 1 + .../resources/partials/webauthn-policy.html | 1 + .../templates/kc-tabs-authentication.html | 4 +- 27 files changed, 364 insertions(+), 43 deletions(-) create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/FeatureDeployerUtil.java diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index 0aeadc934d..696ee1701c 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -52,16 +52,31 @@ public class Profile { OPENSHIFT_INTEGRATION(Type.PREVIEW), SCRIPTS(Type.PREVIEW), TOKEN_EXCHANGE(Type.PREVIEW), - UPLOAD_SCRIPTS(DEPRECATED); + UPLOAD_SCRIPTS(DEPRECATED), + WEB_AUTHN(Type.DEFAULT, Type.PREVIEW); - private Type type; + private Type typeProject; + private Type typeProduct; Feature(Type type) { - this.type = type; + this(type, type); } - public Type getType() { - return type; + Feature(Type typeProject, Type typeProduct) { + this.typeProject = typeProject; + this.typeProduct = typeProduct; + } + + public Type getTypeProject() { + return typeProject; + } + + public Type getTypeProduct() { + return typeProduct; + } + + public boolean hasDifferentProductType() { + return typeProject != typeProduct; } } @@ -95,8 +110,9 @@ public class Profile { for (Feature f : Feature.values()) { Boolean enabled = config.getConfig(f); + Type type = product.equals(ProductValue.RHSSO) ? f.getTypeProduct() : f.getTypeProject(); - switch (f.getType()) { + switch (type) { case DEFAULT: if (enabled != null && !enabled) { disabledFeatures.add(f); @@ -107,7 +123,7 @@ public class Profile { case DISABLED_BY_DEFAULT: if (enabled == null || !enabled) { disabledFeatures.add(f); - } else if (DEPRECATED.equals(f.getType())) { + } else if (DEPRECATED.equals(type)) { logger.warnf("Deprecated feature enabled: " + f.name().toLowerCase()); if (Feature.UPLOAD_SCRIPTS.equals(f)) { previewFeatures.add(Feature.SCRIPTS); diff --git a/common/src/test/java/org/keycloak/common/ProfileTest.java b/common/src/test/java/org/keycloak/common/ProfileTest.java index 5d2c2ce3c8..3369ccad57 100644 --- a/common/src/test/java/org/keycloak/common/ProfileTest.java +++ b/common/src/test/java/org/keycloak/common/ProfileTest.java @@ -19,12 +19,36 @@ public class ProfileTest { public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test - public void checkDefaults() { + public void checkDefaultsKeycloak() { Assert.assertEquals("community", Profile.getName()); assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ACCOUNT2, Profile.Feature.ACCOUNT_API, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS); assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ACCOUNT_API, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION); assertEquals(Profile.getExperimentalFeatures(), Profile.Feature.ACCOUNT2); assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS); + + Assert.assertTrue(Profile.Feature.WEB_AUTHN.hasDifferentProductType()); + Assert.assertEquals(Profile.Feature.WEB_AUTHN.getTypeProject(), Profile.Type.DEFAULT); + } + + @Test + public void checkDefaultsRH_SSO() { + System.setProperty("keycloak.profile", "product"); + String backUpName = Version.NAME; + Version.NAME = "rh-sso"; + Profile.init(); + + Assert.assertEquals("product", Profile.getName()); + assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ACCOUNT2, Profile.Feature.ACCOUNT_API, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.WEB_AUTHN); + assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ACCOUNT_API, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.WEB_AUTHN); + assertEquals(Profile.getExperimentalFeatures(), Profile.Feature.ACCOUNT2); + assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS); + + Assert.assertTrue(Profile.Feature.WEB_AUTHN.hasDifferentProductType()); + Assert.assertEquals(Profile.Feature.WEB_AUTHN.getTypeProduct(), Profile.Type.PREVIEW); + + System.setProperty("keycloak.profile", "community"); + Version.NAME = backUpName; + Profile.init(); } @Test diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticatorSpi.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticatorSpi.java index 728a0adf0c..d4447c58d4 100755 --- a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticatorSpi.java +++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticatorSpi.java @@ -26,6 +26,8 @@ import org.keycloak.provider.Spi; */ public class AuthenticatorSpi implements Spi { + public static final String SPI_NAME = "authenticator"; + @Override public boolean isInternal() { return true; @@ -33,7 +35,7 @@ public class AuthenticatorSpi implements Spi { @Override public String getName() { - return "authenticator"; + return SPI_NAME; } @Override diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/WebAuthnAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/WebAuthnAuthenticatorFactory.java index 3543de5482..94597fff3e 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/WebAuthnAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/WebAuthnAuthenticatorFactory.java @@ -19,15 +19,17 @@ package org.keycloak.authentication.authenticators.browser; import org.keycloak.Config; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.common.Profile; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.credential.WebAuthnCredentialModel; +import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.ProviderConfigProperty; import java.util.List; -public class WebAuthnAuthenticatorFactory implements AuthenticatorFactory { +public class WebAuthnAuthenticatorFactory implements AuthenticatorFactory, EnvironmentDependentProviderFactory { public static final String PROVIDER_ID = "webauthn-authenticator"; @@ -91,4 +93,9 @@ public class WebAuthnAuthenticatorFactory implements AuthenticatorFactory { public String getId() { return PROVIDER_ID; } + + @Override + public boolean isSupported() { + return Profile.isFeatureEnabled(Profile.Feature.WEB_AUTHN); + } } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/WebAuthnRegisterFactory.java b/services/src/main/java/org/keycloak/authentication/requiredactions/WebAuthnRegisterFactory.java index 9b1bd7bf76..0d05c1bfd3 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/WebAuthnRegisterFactory.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/WebAuthnRegisterFactory.java @@ -22,8 +22,10 @@ import org.keycloak.Config.Scope; import org.keycloak.authentication.DisplayTypeRequiredActionFactory; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.common.Profile; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.truststore.TruststoreProvider; import com.webauthn4j.anchor.KeyStoreTrustAnchorsProvider; @@ -31,7 +33,7 @@ import com.webauthn4j.anchor.TrustAnchorsResolverImpl; import com.webauthn4j.validator.attestation.trustworthiness.certpath.NullCertPathTrustworthinessValidator; import com.webauthn4j.validator.attestation.trustworthiness.certpath.TrustAnchorCertPathTrustworthinessValidator; -public class WebAuthnRegisterFactory implements RequiredActionFactory, DisplayTypeRequiredActionFactory { +public class WebAuthnRegisterFactory implements RequiredActionFactory, DisplayTypeRequiredActionFactory, EnvironmentDependentProviderFactory { public static final String PROVIDER_ID = "webauthn-register"; @@ -88,4 +90,8 @@ public class WebAuthnRegisterFactory implements RequiredActionFactory, DisplayTy return "Webauthn Register"; } + @Override + public boolean isSupported() { + return Profile.isFeatureEnabled(Profile.Feature.WEB_AUTHN); + } } diff --git a/services/src/main/java/org/keycloak/credential/WebAuthnCredentialProviderFactory.java b/services/src/main/java/org/keycloak/credential/WebAuthnCredentialProviderFactory.java index 5f781da9ef..761af41622 100644 --- a/services/src/main/java/org/keycloak/credential/WebAuthnCredentialProviderFactory.java +++ b/services/src/main/java/org/keycloak/credential/WebAuthnCredentialProviderFactory.java @@ -16,11 +16,13 @@ package org.keycloak.credential; +import org.keycloak.common.Profile; import org.keycloak.models.KeycloakSession; import com.webauthn4j.converter.util.CborConverter; +import org.keycloak.provider.EnvironmentDependentProviderFactory; -public class WebAuthnCredentialProviderFactory implements CredentialProviderFactory { +public class WebAuthnCredentialProviderFactory implements CredentialProviderFactory, EnvironmentDependentProviderFactory { public static final String PROVIDER_ID = "keycloak-webauthn"; @@ -35,4 +37,9 @@ public class WebAuthnCredentialProviderFactory implements CredentialProviderFact public String getId() { return PROVIDER_ID; } + + @Override + public boolean isSupported() { + return Profile.isFeatureEnabled(Profile.Feature.WEB_AUTHN); + } } diff --git a/services/src/main/java/org/keycloak/credential/WebAuthnPasswordlessCredentialProviderFactory.java b/services/src/main/java/org/keycloak/credential/WebAuthnPasswordlessCredentialProviderFactory.java index 592572c029..c317567a41 100644 --- a/services/src/main/java/org/keycloak/credential/WebAuthnPasswordlessCredentialProviderFactory.java +++ b/services/src/main/java/org/keycloak/credential/WebAuthnPasswordlessCredentialProviderFactory.java @@ -19,12 +19,14 @@ package org.keycloak.credential; import com.webauthn4j.converter.util.CborConverter; +import org.keycloak.common.Profile; import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.EnvironmentDependentProviderFactory; /** * @author Marek Posolda */ -public class WebAuthnPasswordlessCredentialProviderFactory implements CredentialProviderFactory { +public class WebAuthnPasswordlessCredentialProviderFactory implements CredentialProviderFactory, EnvironmentDependentProviderFactory { public static final String PROVIDER_ID = "keycloak-webauthn-passwordless"; @@ -39,4 +41,9 @@ public class WebAuthnPasswordlessCredentialProviderFactory implements Credential public String getId() { return PROVIDER_ID; } + + @Override + public boolean isSupported() { + return Profile.isFeatureEnabled(Profile.Feature.WEB_AUTHN); + } } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java index 4b739dd664..9198c1c269 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java @@ -74,6 +74,7 @@ import org.keycloak.testsuite.rest.resource.TestingExportImportResource; import org.keycloak.testsuite.runonserver.FetchOnServer; import org.keycloak.testsuite.runonserver.RunOnServer; import org.keycloak.testsuite.runonserver.SerializationUtil; +import org.keycloak.testsuite.util.FeatureDeployerUtil; import org.keycloak.timer.TimerProvider; import org.keycloak.util.JsonSerialization; import org.keycloak.utils.MediaType; @@ -867,6 +868,8 @@ public class TestingResourceProvider implements RealmResourceProvider { if (Profile.isFeatureEnabled(featureProfile)) return Response.ok().build(); + FeatureDeployerUtil.initBeforeChangeFeature(featureProfile); + System.setProperty("keycloak.profile.feature." + featureProfile.toString().toLowerCase(), "enabled"); String jbossServerConfigDir = System.getProperty("jboss.server.config.dir"); @@ -877,6 +880,8 @@ public class TestingResourceProvider implements RealmResourceProvider { Profile.init(); + FeatureDeployerUtil.deployFactoriesAfterFeatureEnabled(featureProfile); + if (Profile.isFeatureEnabled(featureProfile)) return Response.ok().build(); else @@ -899,7 +904,9 @@ public class TestingResourceProvider implements RealmResourceProvider { if (!Profile.isFeatureEnabled(featureProfile)) return Response.ok().build(); - System.getProperties().remove("keycloak.profile.feature." + featureProfile.toString().toLowerCase()); + FeatureDeployerUtil.initBeforeChangeFeature(featureProfile); + + disableFeatureProperties(featureProfile); String jbossServerConfigDir = System.getProperty("jboss.server.config.dir"); // If we are in jboss-based container, we need to write profile.properties file, otherwise the change in system property will disappear after restart @@ -909,12 +916,25 @@ public class TestingResourceProvider implements RealmResourceProvider { Profile.init(); + FeatureDeployerUtil.undeployFactoriesAfterFeatureDisabled(featureProfile); + if (!Profile.isFeatureEnabled(featureProfile)) return Response.ok().build(); else return Response.status(Response.Status.NOT_FOUND).build(); } + /** + * KEYCLOAK-12958 + */ + private void disableFeatureProperties(Profile.Feature feature) { + Profile.Type type = Profile.getName().equals("product") ? feature.getTypeProduct() : feature.getTypeProject(); + if (type.equals(Profile.Type.DEFAULT)) { + System.setProperty("keycloak.profile.feature." + feature.toString().toLowerCase(), "disabled"); + } else { + System.getProperties().remove("keycloak.profile.feature." + feature.toString().toLowerCase()); + } + } /** * This will send POST request to specified URL with specified form parameters. It's not easily possible to "trick" web driver to send POST diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/FeatureDeployerUtil.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/FeatureDeployerUtil.java new file mode 100644 index 0000000000..4f9135d6cc --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/FeatureDeployerUtil.java @@ -0,0 +1,150 @@ +/* + * Copyright 2019 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.testsuite.util; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import org.jboss.logging.Logger; +import org.keycloak.common.Profile; +import org.keycloak.provider.DefaultProviderLoader; +import org.keycloak.provider.EnvironmentDependentProviderFactory; +import org.keycloak.provider.KeycloakDeploymentInfo; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.ProviderManager; +import org.keycloak.provider.ProviderManagerRegistry; +import org.keycloak.provider.Spi; +import org.keycloak.services.DefaultKeycloakSession; + +/** + * Used to dynamically reload EnvironmentDependentProviderFactories after some feature is enabled/disabled + * + * @author Marek Posolda + */ +public class FeatureDeployerUtil { + + private final static Map> initializer = new ConcurrentHashMap<>(); + + private final static Map deployersCache = new ConcurrentHashMap<>(); + + private static final Logger logger = Logger.getLogger(FeatureDeployerUtil.class); + + public static void initBeforeChangeFeature(Profile.Feature feature) { + if (deployersCache.containsKey(feature)) return; + + // Compute which provider factories are enabled before feature is enabled (disabled) + Map factoriesBefore = loadEnabledEnvironmentFactories(); + initializer.put(feature, factoriesBefore); + } + + public static void deployFactoriesAfterFeatureEnabled(Profile.Feature feature) { + ProviderManager manager = deployersCache.get(feature); + if (manager == null) { + // Need to figure which provider factories were enabled after feature was enabled. Create deployer based on it and save it to the cache + Map factoriesBeforeEnable = initializer.remove(feature); + Map factoriesAfterEnable = loadEnabledEnvironmentFactories(); + Map factories = getFactoriesDependentOnFeature(factoriesBeforeEnable, factoriesAfterEnable); + + logger.infof("New factories when enabling feature '%s': %s", feature, factories.keySet()); + + KeycloakDeploymentInfo di = createDeploymentInfo(factories); + + manager = new ProviderManager(di, FeatureDeployerUtil.class.getClassLoader()); + deployersCache.put(feature, manager); + } + ProviderManagerRegistry.SINGLETON.deploy(manager); + } + + public static void undeployFactoriesAfterFeatureDisabled(Profile.Feature feature) { + ProviderManager manager = deployersCache.get(feature); + if (manager == null) { + // This is used if some feature is enabled by default and then disabled + // Need to figure which provider factories were enabled after feature was enabled. Create deployer based on it and save it to the cache + Map factoriesBeforeDisable = initializer.remove(feature); + Map factoriesAfterDisable = loadEnabledEnvironmentFactories(); + Map factories = getFactoriesDependentOnFeature(factoriesAfterDisable, factoriesBeforeDisable); + + KeycloakDeploymentInfo di = createDeploymentInfo(factories); + + manager = new ProviderManager(di, FeatureDeployerUtil.class.getClassLoader()); + loadFactories(manager); + deployersCache.put(feature, manager); + } + ProviderManagerRegistry.SINGLETON.undeploy(manager); + } + + private static Map getFactoriesDependentOnFeature(Map factoriesDisabled, Map factoriesEnabled) { + Set> disabledFactoriesClasses = factoriesDisabled.keySet().stream() + .map(ProviderFactory::getClass) + .collect(Collectors.toSet()); + + Set> enabledFactoriesClasses = factoriesEnabled.keySet().stream() + .map(ProviderFactory::getClass) + .collect(Collectors.toSet()); + + enabledFactoriesClasses.removeAll(disabledFactoriesClasses); + + Map newFactories = factoriesEnabled.entrySet().stream() + .filter(entry -> enabledFactoriesClasses.contains(entry.getKey().getClass())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + return newFactories; + } + + private static KeycloakDeploymentInfo createDeploymentInfo(Map factories) { + KeycloakDeploymentInfo di = KeycloakDeploymentInfo.create(); + for (Map.Entry factory : factories.entrySet()) { + ProviderFactory pf = factory.getKey(); + Class spiClass = factory.getValue().getClass(); + di.addProvider(spiClass, pf); + } + return di; + } + + private static Map loadEnabledEnvironmentFactories() { + KeycloakDeploymentInfo di = KeycloakDeploymentInfo.create().services(); + ClassLoader classLoader = DefaultKeycloakSession.class.getClassLoader(); + DefaultProviderLoader loader = new DefaultProviderLoader(di, classLoader); + + Map providerFactories = new HashMap<>(); + for (Spi spi : loader.loadSpis()) { + List currentFactories = loader.load(spi); + for (ProviderFactory factory : currentFactories) { + if (factory instanceof EnvironmentDependentProviderFactory) { + if (((EnvironmentDependentProviderFactory) factory).isSupported()) { + providerFactories.put(factory, spi); + } + } + + } + } + + return providerFactories; + } + + private static void loadFactories(ProviderManager pm) { + KeycloakDeploymentInfo di = KeycloakDeploymentInfo.create().services(); + ClassLoader classLoader = DefaultKeycloakSession.class.getClassLoader(); + DefaultProviderLoader loader = new DefaultProviderLoader(di, classLoader); + loader.loadSpis().forEach(pm::load); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/DisableFeature.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/DisableFeature.java index 9c7298686f..df28e95f9e 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/DisableFeature.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/DisableFeature.java @@ -20,4 +20,5 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; public @interface DisableFeature { Profile.Feature value(); boolean skipRestart() default false; + boolean onlyForProduct() default false; } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/EnableFeature.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/EnableFeature.java index d29e814a42..421e61ca74 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/EnableFeature.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/EnableFeature.java @@ -20,4 +20,5 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; public @interface EnableFeature { Profile.Feature value(); boolean skipRestart() default false; + boolean onlyForProduct() default false; } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerFeaturesController.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerFeaturesController.java index da083f9743..ac8a4c665f 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerFeaturesController.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerFeaturesController.java @@ -72,11 +72,13 @@ public class KeycloakContainerFeaturesController { private Profile.Feature feature; private boolean skipRestart; private FeatureAction action; + private boolean onlyForProduct; - public UpdateFeature(Profile.Feature feature, boolean skipRestart, FeatureAction action) { + public UpdateFeature(Profile.Feature feature, boolean skipRestart, FeatureAction action, boolean onlyForProduct) { this.feature = feature; this.skipRestart = skipRestart; this.action = action; + this.onlyForProduct = onlyForProduct; } /** @@ -120,6 +122,10 @@ public class KeycloakContainerFeaturesController { } private void updateFeatures(List updateFeatures) throws Exception { + updateFeatures = updateFeatures.stream() + .filter(this::skipForProduct) + .collect(Collectors.toList()); + updateFeatures.forEach(UpdateFeature::performAction); if (updateFeatures.stream().anyMatch(updateFeature -> !updateFeature.skipRestart)) { @@ -130,20 +136,25 @@ public class KeycloakContainerFeaturesController { updateFeatures.forEach(UpdateFeature::assertPerformed); } + // KEYCLOAK-12958 WebAuthn profile product/project + private boolean skipForProduct(UpdateFeature feature) { + return !feature.onlyForProduct || Profile.getName().equals("product"); + } + private void checkAnnotatedElementForFeatureAnnotations(AnnotatedElement annotatedElement, State state) throws Exception { List updateFeatureList = new ArrayList<>(0); if (annotatedElement.isAnnotationPresent(EnableFeatures.class) || annotatedElement.isAnnotationPresent(EnableFeature.class)) { updateFeatureList.addAll(Arrays.stream(annotatedElement.getAnnotationsByType(EnableFeature.class)) .map(annotation -> new UpdateFeature(annotation.value(), annotation.skipRestart(), - state == State.BEFORE ? FeatureAction.ENABLE : FeatureAction.DISABLE)) + state == State.BEFORE ? FeatureAction.ENABLE : FeatureAction.DISABLE, annotation.onlyForProduct())) .collect(Collectors.toList())); } if (annotatedElement.isAnnotationPresent(DisableFeatures.class) || annotatedElement.isAnnotationPresent(DisableFeature.class)) { updateFeatureList.addAll(Arrays.stream(annotatedElement.getAnnotationsByType(DisableFeature.class)) .map(annotation -> new UpdateFeature(annotation.value(), annotation.skipRestart(), - state == State.BEFORE ? FeatureAction.DISABLE : FeatureAction.ENABLE)) + state == State.BEFORE ? FeatureAction.DISABLE : FeatureAction.ENABLE, annotation.onlyForProduct())) .collect(Collectors.toList())); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java index 230ba7be1c..e23d5b8245 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java @@ -26,6 +26,7 @@ import org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAu import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory; import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory; import org.keycloak.broker.provider.util.SimpleHttp; +import org.keycloak.common.Profile; import org.keycloak.credential.CredentialTypeMetadata; import org.keycloak.events.EventType; import org.keycloak.models.AuthenticationExecutionModel; @@ -50,11 +51,16 @@ import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.account.AccountCredentialResource; +import org.keycloak.services.resources.account.AccountCredentialResource.PasswordUpdate; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.authentication.AbstractAuthenticationTest; +import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; +import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.TokenUtil; +import javax.ws.rs.core.Response; import java.io.IOException; import java.util.Collections; import java.util.HashMap; @@ -62,22 +68,25 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import javax.ws.rs.core.Response; - import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.Assert.*; -import org.keycloak.services.resources.account.AccountCredentialResource.PasswordUpdate; -import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; -import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.keycloak.common.Profile.Feature.ACCOUNT_API; /** * @author Stian Thorgersen */ @AuthServerContainerExclude(AuthServer.REMOTE) +@EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true) +@EnableFeature(value = ACCOUNT_API, skipRestart = true) public class AccountRestServiceTest extends AbstractRestServiceTest { @Test public void testGetProfile() throws IOException { + UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class); assertEquals("Tom", user.getFirstName()); assertEquals("Brady", user.getLastName()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ExecutionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ExecutionTest.java index 4815e21bee..5401257b87 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ExecutionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ExecutionTest.java @@ -24,6 +24,7 @@ import org.keycloak.authentication.authenticators.browser.UsernameFormFactory; import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory; import org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory; import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator; +import org.keycloak.common.Profile; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; @@ -31,6 +32,7 @@ import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.AssertAdminEvents; @@ -320,6 +322,7 @@ public class ExecutionTest extends AbstractAuthenticationTest { } @Test + @EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true) @AuthServerContainerExclude(AuthServer.REMOTE) public void testRequirementsInExecution() { HashMap params = new HashMap<>(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java index 7a3553bcf5..aac352e759 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java @@ -34,12 +34,14 @@ import java.util.List; import java.util.Map; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import static org.hamcrest.Matchers.is; /** * @author Marko Strukelj */ +@EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true) public class ProvidersTest extends AbstractAuthenticationTest { @Test @@ -136,9 +138,7 @@ public class ProvidersTest extends AbstractAuthenticationTest { @Test public void testInitialAuthenticationProviders() { List> providers = authMgmtResource.getAuthenticatorProviders(); - providers = sortProviders(providers); - - compareProviders(sortProviders(expectedAuthProviders()), providers); + compareProviders(expectedAuthProviders(), providers); } private List> expectedAuthProviders() { @@ -234,7 +234,7 @@ public class ProvidersTest extends AbstractAuthenticationTest { for (Map item: list) { result.add(new HashMap(item)); } - return result; + return sortProviders(result); } private void addProviderInfo(List> list, String id, String displayName, String description) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RequiredActionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RequiredActionsTest.java index 00f949155c..cfa902be1e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RequiredActionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RequiredActionsTest.java @@ -19,11 +19,13 @@ package org.keycloak.testsuite.admin.authentication; import org.junit.Assert; import org.junit.Test; +import org.keycloak.common.Profile; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation; import org.keycloak.testsuite.actions.DummyRequiredActionFactory; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.util.AdminEventPaths; import javax.ws.rs.NotFoundException; @@ -37,6 +39,7 @@ import java.util.Map; /** * @author Marko Strukelj */ +@EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true) public class RequiredActionsTest extends AbstractAuthenticationTest { @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserFlowTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserFlowTest.java index d588f5badc..3d19e0c4cf 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserFlowTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserFlowTest.java @@ -16,6 +16,7 @@ import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorF import org.keycloak.authentication.authenticators.conditional.ConditionalRoleAuthenticatorFactory; import org.keycloak.authentication.authenticators.conditional.ConditionalUserConfiguredAuthenticatorFactory; import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory; +import org.keycloak.common.Profile; import org.keycloak.events.Details; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel.Requirement; @@ -33,6 +34,7 @@ import org.keycloak.testsuite.ActionURIUtils; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.authentication.AbstractAuthenticationTest; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.auth.page.login.OneTimeCode; import org.keycloak.testsuite.broker.SocialLoginTest; import org.keycloak.testsuite.pages.ErrorPage; @@ -61,6 +63,7 @@ import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITHUB; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITLAB; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE; +@EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true) public class BrowserFlowTest extends AbstractTestRealmKeycloakTest { private static final String INVALID_AUTH_CODE = "Invalid authenticator code."; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultiFactorAuthenticationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultiFactorAuthenticationTest.java index fa496a37ce..2400e4f8e4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultiFactorAuthenticationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultiFactorAuthenticationTest.java @@ -33,6 +33,7 @@ import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFa import org.keycloak.authentication.authenticators.browser.PasswordFormFactory; import org.keycloak.authentication.authenticators.browser.UsernameFormFactory; import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory; +import org.keycloak.common.Profile; import org.keycloak.events.Details; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.utils.TimeBasedOTP; @@ -40,6 +41,7 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.client.KeycloakTestingClient; import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginPage; @@ -241,6 +243,7 @@ public class MultiFactorAuthenticationTest extends AbstractTestRealmKeycloakTest // Test for the case when user can authenticate either with: WebAuthn OR (Password AND OTP) // WebAuthn is not enabled for the user, so he needs to use password AND OTP @Test + @EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true) public void testAlternativeMechanismsInDifferentSubflows_firstMechanismUnavailable() { final String newFlowAlias = "browser - alternative mechanisms"; testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias)); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenShiftTokenReviewEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenShiftTokenReviewEndpointTest.java index 81d60735e9..9c47909ea7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenShiftTokenReviewEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenShiftTokenReviewEndpointTest.java @@ -57,7 +57,7 @@ import static org.keycloak.testsuite.ProfileAssume.assumeFeatureEnabled; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; @AuthServerContainerExclude(AuthServer.REMOTE) -@EnableFeature(OPENSHIFT_INTEGRATION) +@EnableFeature(value = OPENSHIFT_INTEGRATION, skipRestart = true) public class OpenShiftTokenReviewEndpointTest extends AbstractTestRealmKeycloakTest { private static boolean flowConfigured; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenshiftClientStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenshiftClientStorageTest.java index aa812dead9..08b7cab743 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenshiftClientStorageTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenshiftClientStorageTest.java @@ -67,7 +67,7 @@ import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername; * @author Pedro Igor */ @AuthServerContainerExclude(AuthServer.REMOTE) -@EnableFeature(OPENSHIFT_INTEGRATION) +@EnableFeature(value = OPENSHIFT_INTEGRATION, skipRestart = true) public final class OpenshiftClientStorageTest extends AbstractTestRealmKeycloakTest { private static Undertow OPENSHIFT_API_SERVER; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/WebAuthnRegisterAndLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/WebAuthnRegisterAndLoginTest.java index 9e8e612fe3..eaf5e54b68 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/WebAuthnRegisterAndLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/WebAuthnRegisterAndLoginTest.java @@ -22,8 +22,11 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.keycloak.WebAuthnConstants; +import org.keycloak.authentication.AuthenticatorSpi; +import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory; import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory; import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory; +import org.keycloak.common.Profile; import org.keycloak.common.util.RandomString; import org.keycloak.events.Details; import org.keycloak.events.EventType; @@ -32,10 +35,14 @@ import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.info.ServerInfoRepresentation; import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.ProfileAssume; import org.keycloak.testsuite.admin.AbstractAdminTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.annotation.DisableFeature; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.RegisterPage; import org.keycloak.testsuite.pages.webauthn.WebAuthnLoginPage; @@ -48,10 +55,13 @@ import static org.junit.Assert.assertEquals; import java.util.Arrays; import java.util.List; +import java.util.Set; + import org.junit.Assume; import org.junit.BeforeClass; import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.AUTH_SERVER_SSL_REQUIRED; +@EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true) public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest { @Rule @@ -281,6 +291,23 @@ public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest } } + @Test + public void testWebAuthnEnabled() { + testWebAuthnAvailability(true); + } + + @Test + @DisableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true) + public void testWebAuthnDisabled() { + testWebAuthnAvailability(false); + } + + private void testWebAuthnAvailability(boolean expectedAvailability) { + ServerInfoRepresentation serverInfo = adminClient.serverInfo().getInfo(); + Set authenticatorProviderIds = serverInfo.getProviders().get(AuthenticatorSpi.SPI_NAME).getProviders().keySet(); + Assert.assertEquals(expectedAvailability, authenticatorProviderIds.contains(WebAuthnAuthenticatorFactory.PROVIDER_ID)); + } + private void assertUserRegistered(String userId, String username, String email) { UserRepresentation user = getUser(userId); Assert.assertNotNull(user); diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/SigningInTest.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/SigningInTest.java index 0d7deb725f..9f099a24af 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/SigningInTest.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/SigningInTest.java @@ -24,6 +24,7 @@ import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorF import org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory; import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory; import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory; +import org.keycloak.common.Profile; import org.keycloak.models.credential.OTPCredentialModel; import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.credential.WebAuthnCredentialModel; @@ -37,6 +38,7 @@ import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation; import org.keycloak.testsuite.WebAuthnAssume; import org.keycloak.testsuite.admin.Users; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.auth.page.login.OTPSetup; import org.keycloak.testsuite.auth.page.login.UpdatePassword; import org.keycloak.testsuite.pages.webauthn.WebAuthnRegisterPage; @@ -62,6 +64,9 @@ import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad; /** * @author Vaclav Muzikar */ +@EnableFeature(value = Profile.Feature.ACCOUNT2, skipRestart = true) +@EnableFeature(value = Profile.Feature.ACCOUNT_API, skipRestart = true) +@EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true) public class SigningInTest extends BaseAccountPageTest { public static final String PASSWORD_LABEL = "My Password"; public static final String WEBAUTHN_FLOW_ID = "75e2390e-f296-49e6-acf8-6d21071d7e10"; diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js index 804a414c3e..93c89680ba 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -1993,8 +1993,8 @@ module.config([ '$routeProvider', function($routeProvider) { realm : function(RealmLoader) { return RealmLoader(); }, - serverInfo : function(ServerInfo) { - return ServerInfo.delay; + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); } }, controller : 'RealmOtpPolicyCtrl' @@ -2005,8 +2005,8 @@ module.config([ '$routeProvider', function($routeProvider) { realm : function(RealmLoader) { return RealmLoader(); }, - serverInfo : function(ServerInfo) { - return ServerInfo.delay; + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); } }, controller : 'RealmWebAuthnPolicyCtrl' @@ -2017,8 +2017,8 @@ module.config([ '$routeProvider', function($routeProvider) { realm : function(RealmLoader) { return RealmLoader(); }, - serverInfo : function(ServerInfo) { - return ServerInfo.delay; + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); } }, controller : 'RealmWebAuthnPasswordlessPolicyCtrl' diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index 7ef5d4cae9..fb36bf7099 100644 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -398,30 +398,44 @@ module.controller('RealmOtpPolicyCtrl', function($scope, Current, Realm, realm, genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/otp-policy"); }); -module.controller('RealmWebAuthnPolicyCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) { +module.controller('RealmWebAuthnPolicyCtrl', function ($scope, Current, Realm, realm, serverInfo, $http, $route, $location, Dialog, Notifications) { $scope.deleteAcceptableAaguid = function(index) { $scope.realm.webAuthnPolicyAcceptableAaguids.splice(index, 1); - } + }; $scope.addAcceptableAaguid = function() { $scope.realm.webAuthnPolicyAcceptableAaguids.push($scope.newAcceptableAaguid); $scope.newAcceptableAaguid = ""; - } + }; + + // Just for case the user fill particular URL with disabled WebAuthn feature. + $scope.redirectIfWebAuthnDisabled = function () { + if (!serverInfo.featureEnabled('WEB_AUTHN')) { + $location.url("/realms/" + $scope.realm.realm + "/authentication"); + } + }; genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/webauthn-policy"); }); -module.controller('RealmWebAuthnPasswordlessPolicyCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) { +module.controller('RealmWebAuthnPasswordlessPolicyCtrl', function ($scope, Current, Realm, realm, serverInfo, $http, $route, $location, Dialog, Notifications) { $scope.deleteAcceptableAaguid = function(index) { $scope.realm.webAuthnPolicyPasswordlessAcceptableAaguids.splice(index, 1); - } + }; $scope.addAcceptableAaguid = function() { $scope.realm.webAuthnPolicyPasswordlessAcceptableAaguids.push($scope.newAcceptableAaguid); $scope.newAcceptableAaguid = ""; - } + }; + + // Just for case the user fill particular URL with disabled WebAuthn feature. + $scope.redirectIfWebAuthnDisabled = function () { + if (!serverInfo.featureEnabled('WEB_AUTHN')) { + $location.url("/realms/" + $scope.realm.realm + "/authentication"); + } + }; genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/webauthn-policy-passwordless"); }); diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/webauthn-policy-passwordless.html b/themes/src/main/resources/theme/base/admin/resources/partials/webauthn-policy-passwordless.html index 8317208e62..dbb1e0e83b 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/webauthn-policy-passwordless.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/webauthn-policy-passwordless.html @@ -19,6 +19,7 @@

{{:: 'authentication' | translate}}

+
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/webauthn-policy.html b/themes/src/main/resources/theme/base/admin/resources/partials/webauthn-policy.html index 37ccf082bc..432eb3de55 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/webauthn-policy.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/webauthn-policy.html @@ -1,6 +1,7 @@

{{:: 'authentication' | translate}}

+ diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-authentication.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-authentication.html index 6bc9471d42..e1b29660b4 100755 --- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-authentication.html +++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-authentication.html @@ -4,11 +4,11 @@
  • {{:: 'required-actions' | translate}}
  • {{:: 'password-policy' | translate}}
  • {{:: 'otp-policy' | translate}}
  • -
  • +
  • {{:: 'webauthn-policy' | translate}} {{:: 'webauthn-policy.tooltip' | translate}}
  • -
  • +
  • {{:: 'webauthn-policy-passwordless' | translate}} {{:: 'webauthn-policy-passwordless.tooltip' | translate}}