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 <mposolda@gmail.com>
This commit is contained in:
Martin Bartoš 2020-02-26 08:45:26 +01:00 committed by GitHub
parent 8436a88075
commit eaaff6e555
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 364 additions and 43 deletions

View file

@ -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);

View file

@ -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

View file

@ -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

View file

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

View file

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

View file

@ -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<WebAuthnCredentialProvider> {
public class WebAuthnCredentialProviderFactory implements CredentialProviderFactory<WebAuthnCredentialProvider>, 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);
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class WebAuthnPasswordlessCredentialProviderFactory implements CredentialProviderFactory<WebAuthnPasswordlessCredentialProvider> {
public class WebAuthnPasswordlessCredentialProviderFactory implements CredentialProviderFactory<WebAuthnPasswordlessCredentialProvider>, 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);
}
}

View file

@ -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

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class FeatureDeployerUtil {
private final static Map<Profile.Feature, Map<ProviderFactory, Spi>> initializer = new ConcurrentHashMap<>();
private final static Map<Profile.Feature, ProviderManager> 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<ProviderFactory, Spi> 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<ProviderFactory, Spi> factoriesBeforeEnable = initializer.remove(feature);
Map<ProviderFactory, Spi> factoriesAfterEnable = loadEnabledEnvironmentFactories();
Map<ProviderFactory, Spi> 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<ProviderFactory, Spi> factoriesBeforeDisable = initializer.remove(feature);
Map<ProviderFactory, Spi> factoriesAfterDisable = loadEnabledEnvironmentFactories();
Map<ProviderFactory, Spi> 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<ProviderFactory, Spi> getFactoriesDependentOnFeature(Map<ProviderFactory, Spi> factoriesDisabled, Map<ProviderFactory, Spi> factoriesEnabled) {
Set<Class<? extends ProviderFactory>> disabledFactoriesClasses = factoriesDisabled.keySet().stream()
.map(ProviderFactory::getClass)
.collect(Collectors.toSet());
Set<Class<? extends ProviderFactory>> enabledFactoriesClasses = factoriesEnabled.keySet().stream()
.map(ProviderFactory::getClass)
.collect(Collectors.toSet());
enabledFactoriesClasses.removeAll(disabledFactoriesClasses);
Map<ProviderFactory, Spi> 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<ProviderFactory, Spi> factories) {
KeycloakDeploymentInfo di = KeycloakDeploymentInfo.create();
for (Map.Entry<ProviderFactory, Spi> factory : factories.entrySet()) {
ProviderFactory pf = factory.getKey();
Class<? extends Spi> spiClass = factory.getValue().getClass();
di.addProvider(spiClass, pf);
}
return di;
}
private static Map<ProviderFactory, Spi> loadEnabledEnvironmentFactories() {
KeycloakDeploymentInfo di = KeycloakDeploymentInfo.create().services();
ClassLoader classLoader = DefaultKeycloakSession.class.getClassLoader();
DefaultProviderLoader loader = new DefaultProviderLoader(di, classLoader);
Map<ProviderFactory, Spi> providerFactories = new HashMap<>();
for (Spi spi : loader.loadSpis()) {
List<ProviderFactory> 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);
}
}

View file

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

View file

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

View file

@ -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<UpdateFeature> 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<UpdateFeature> 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()));
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@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());

View file

@ -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<String, String> params = new HashMap<>();

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@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<Map<String, Object>> providers = authMgmtResource.getAuthenticatorProviders();
providers = sortProviders(providers);
compareProviders(sortProviders(expectedAuthProviders()), providers);
compareProviders(expectedAuthProviders(), providers);
}
private List<Map<String, Object>> expectedAuthProviders() {
@ -234,7 +234,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
for (Map<String, Object> item: list) {
result.add(new HashMap(item));
}
return result;
return sortProviders(result);
}
private void addProviderInfo(List<Map<String, Object>> list, String id, String displayName, String description) {

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true)
public class RequiredActionsTest extends AbstractAuthenticationTest {
@Test

View file

@ -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.";

View file

@ -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));

View file

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

View file

@ -67,7 +67,7 @@ import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@AuthServerContainerExclude(AuthServer.REMOTE)
@EnableFeature(OPENSHIFT_INTEGRATION)
@EnableFeature(value = OPENSHIFT_INTEGRATION, skipRestart = true)
public final class OpenshiftClientStorageTest extends AbstractTestRealmKeycloakTest {
private static Undertow OPENSHIFT_API_SERVER;

View file

@ -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<String> 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);

View file

@ -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 <vmuzikar@redhat.com>
*/
@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";

View file

@ -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'

View file

@ -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");
});

View file

@ -19,6 +19,7 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>{{:: 'authentication' | translate}}</h1>
<span data-ng-init="redirectIfWebAuthnDisabled()"></span>
<kc-tabs-authentication></kc-tabs-authentication>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">

View file

@ -1,6 +1,7 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>{{:: 'authentication' | translate}}</h1>
<span data-ng-init="redirectIfWebAuthnDisabled()"></span>
<kc-tabs-authentication></kc-tabs-authentication>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">

View file

@ -4,11 +4,11 @@
<li ng-class="{active: path[3] == 'required-actions'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/required-actions">{{:: 'required-actions' | translate}}</a></li>
<li ng-class="{active: path[3] == 'password-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/password-policy">{{:: 'password-policy' | translate}}</a></li>
<li ng-class="{active: path[3] == 'otp-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/otp-policy">{{:: 'otp-policy' | translate}}</a></li>
<li ng-class="{active: path[3] == 'webauthn-policy'}" data-ng-show="access.viewRealm">
<li ng-class="{active: path[3] == 'webauthn-policy'}" data-ng-show="access.viewRealm && serverInfo.featureEnabled('WEB_AUTHN')">
<a href="#/realms/{{realm.realm}}/authentication/webauthn-policy">{{:: 'webauthn-policy' | translate}}</a>
<kc-tooltip>{{:: 'webauthn-policy.tooltip' | translate}}</kc-tooltip>
</li>
<li ng-class="{active: path[3] == 'webauthn-policy-passwordless'}" data-ng-show="access.viewRealm">
<li ng-class="{active: path[3] == 'webauthn-policy-passwordless'}" data-ng-show="access.viewRealm && serverInfo.featureEnabled('WEB_AUTHN')">
<a href="#/realms/{{realm.realm}}/authentication/webauthn-policy-passwordless">{{:: 'webauthn-policy-passwordless' | translate}}</a>
<kc-tooltip>{{:: 'webauthn-policy-passwordless.tooltip' | translate}}</kc-tooltip>
</li>