mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-15 03:39:12 -06:00
Make passkeys feature supported
Closes #41556 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
@@ -124,7 +124,7 @@ public class Profile {
|
||||
|
||||
ORGANIZATION("Organization support within realms", Type.DEFAULT),
|
||||
|
||||
PASSKEYS("Passkeys", Type.PREVIEW, Feature.WEB_AUTHN),
|
||||
PASSKEYS("Passkeys", Type.DEFAULT, Feature.WEB_AUTHN),
|
||||
PASSKEYS_CONDITIONAL_UI_AUTHENTICATOR("Passkeys conditional UI authenticator", Type.DEPRECATED, FeatureUpdatePolicy.ROLLING_NO_UPGRADE, Feature.PASSKEYS),
|
||||
|
||||
USER_EVENT_METRICS("Collect metrics based on user events", Type.DEFAULT),
|
||||
|
||||
@@ -59,11 +59,11 @@ public class StartDevCommandDistTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({ "start-dev", "--debug", "--features=passkeys:v1" })
|
||||
@Launch({ "start-dev", "--debug", "--features=oid4vc-vci:v1" })
|
||||
void testStartDevShouldStartTwoJVMs(CLIResult cliResult) {
|
||||
cliResult.assertMessageWasShownExactlyNumberOfTimes("Listening for transport dt_socket at address:", 2);
|
||||
cliResult.assertStartedDevMode();
|
||||
cliResult.assertMessage("passkeys");
|
||||
cliResult.assertMessage("oid4vc-vci");
|
||||
// ensure consistency with build-time properties
|
||||
cliResult.assertNoMessage("Build time property cannot");
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.keycloak.authentication;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.ConfiguredProvider;
|
||||
|
||||
/**
|
||||
@@ -50,9 +51,10 @@ public interface ConfigurableAuthenticatorFactory extends ConfiguredProvider {
|
||||
/**
|
||||
* Optional categories that this authenticator can have (for example passkeys in username/form).
|
||||
* Optional categories are not taken into account by LoA.
|
||||
* @param session The current session in the request
|
||||
* @return Set of extra optional categories, empty by default
|
||||
*/
|
||||
default Set<String> getOptionalReferenceCategories() {
|
||||
default Set<String> getOptionalReferenceCategories(KeycloakSession session) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
|
||||
@@ -375,19 +375,21 @@ public class DefaultAuthenticationFlows {
|
||||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
AuthenticatorConfigModel configModel = new AuthenticatorConfigModel();
|
||||
configModel.setAlias("browser-conditional-credential");
|
||||
configModel.setConfig(Map.of("credentials", WebAuthnCredentialModel.TYPE_PASSWORDLESS));
|
||||
configModel = realm.addAuthenticatorConfig(configModel);
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.PASSKEYS)) {
|
||||
AuthenticatorConfigModel configModel = new AuthenticatorConfigModel();
|
||||
configModel.setAlias("browser-conditional-credential");
|
||||
configModel.setConfig(Map.of("credentials", WebAuthnCredentialModel.TYPE_PASSWORDLESS));
|
||||
configModel = realm.addAuthenticatorConfig(configModel);
|
||||
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(conditionalOTP.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||
execution.setAuthenticator("conditional-credential");
|
||||
execution.setPriority(20);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
execution.setAuthenticatorConfig(configModel.getId());
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(conditionalOTP.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||
execution.setAuthenticator("conditional-credential");
|
||||
execution.setPriority(20);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
execution.setAuthenticatorConfig(configModel.getId());
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
}
|
||||
|
||||
// otp processing
|
||||
execution = new AuthenticationExecutionModel();
|
||||
@@ -676,19 +678,21 @@ public class DefaultAuthenticationFlows {
|
||||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
AuthenticatorConfigModel configModel = new AuthenticatorConfigModel();
|
||||
configModel.setAlias("first-broker-login-conditional-credential");
|
||||
configModel.setConfig(Map.of("credentials", WebAuthnCredentialModel.TYPE_PASSWORDLESS));
|
||||
configModel = realm.addAuthenticatorConfig(configModel);
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.PASSKEYS)) {
|
||||
AuthenticatorConfigModel configModel = new AuthenticatorConfigModel();
|
||||
configModel.setAlias("first-broker-login-conditional-credential");
|
||||
configModel.setConfig(Map.of("credentials", WebAuthnCredentialModel.TYPE_PASSWORDLESS));
|
||||
configModel = realm.addAuthenticatorConfig(configModel);
|
||||
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(conditionalOTP.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||
execution.setAuthenticator("conditional-credential");
|
||||
execution.setPriority(20);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
execution.setAuthenticatorConfig(configModel.getId());
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(conditionalOTP.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||
execution.setAuthenticator("conditional-credential");
|
||||
execution.setPriority(20);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
execution.setAuthenticatorConfig(configModel.getId());
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
}
|
||||
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(conditionalOTP.getId());
|
||||
|
||||
@@ -22,7 +22,6 @@ import java.util.Set;
|
||||
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;
|
||||
@@ -71,10 +70,10 @@ public class UsernameFormFactory implements AuthenticatorFactory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getOptionalReferenceCategories() {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.PASSKEYS)
|
||||
public Set<String> getOptionalReferenceCategories(KeycloakSession session) {
|
||||
return WebAuthnConditionalUIAuthenticator.isPasskeysEnabled(session)
|
||||
? Collections.singleton(WebAuthnCredentialModel.TYPE_PASSWORDLESS)
|
||||
: AuthenticatorFactory.super.getOptionalReferenceCategories();
|
||||
: AuthenticatorFactory.super.getOptionalReferenceCategories(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -25,12 +25,11 @@ import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
@@ -71,10 +70,10 @@ public class UsernamePasswordFormFactory implements AuthenticatorFactory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getOptionalReferenceCategories() {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.PASSKEYS)
|
||||
public Set<String> getOptionalReferenceCategories(KeycloakSession session) {
|
||||
return WebAuthnConditionalUIAuthenticator.isPasskeysEnabled(session)
|
||||
? Collections.singleton(WebAuthnCredentialModel.TYPE_PASSWORDLESS)
|
||||
: AuthenticatorFactory.super.getOptionalReferenceCategories();
|
||||
: AuthenticatorFactory.super.getOptionalReferenceCategories(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -57,7 +57,12 @@ public class WebAuthnConditionalUIAuthenticator extends WebAuthnPasswordlessAuth
|
||||
}
|
||||
|
||||
public boolean isPasskeysEnabled() {
|
||||
return isPasskeysEnabled(session);
|
||||
}
|
||||
|
||||
static public boolean isPasskeysEnabled(KeycloakSession session) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.PASSKEYS) &&
|
||||
session.getContext().getRealm() != null &&
|
||||
Boolean.TRUE.equals(session.getContext().getRealm().getWebAuthnPolicyPasswordless().isPasskeysEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.Set;
|
||||
import org.keycloak.Config.Scope;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticatorFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.WebAuthnConditionalUIAuthenticator;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
@@ -72,9 +73,9 @@ public class OrganizationAuthenticatorFactory extends IdentityProviderAuthentica
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getOptionalReferenceCategories() {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.PASSKEYS)
|
||||
public Set<String> getOptionalReferenceCategories(KeycloakSession session) {
|
||||
return WebAuthnConditionalUIAuthenticator.isPasskeysEnabled(session)
|
||||
? Collections.singleton(WebAuthnCredentialModel.TYPE_PASSWORDLESS)
|
||||
: super.getOptionalReferenceCategories();
|
||||
: super.getOptionalReferenceCategories(session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ public class AccountCredentialResource {
|
||||
.map(exe -> (AuthenticatorFactory) session.getKeycloakSessionFactory()
|
||||
.getProviderFactory(Authenticator.class, exe.getAuthenticator()))
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(authFact -> Stream.concat(Stream.of(authFact.getReferenceCategory()), authFact.getOptionalReferenceCategories().stream()))
|
||||
.flatMap(authFact -> Stream.concat(Stream.of(authFact.getReferenceCategory()), authFact.getOptionalReferenceCategories(session).stream()))
|
||||
.filter(Objects::nonNull)
|
||||
).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ import org.openqa.selenium.firefox.FirefoxDriver;
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
@EnableFeature(value = Profile.Feature.PASSKEYS, skipRestart = true)
|
||||
@EnableFeature(value = Profile.Feature.PASSKEYS_CONDITIONAL_UI_AUTHENTICATOR, skipRestart = true)
|
||||
@IgnoreBrowserDriver(FirefoxDriver.class) // See https://github.com/keycloak/keycloak/issues/10368
|
||||
public class PasskeysConditionalUITest extends AbstractWebAuthnVirtualTest {
|
||||
|
||||
@@ -25,7 +25,6 @@ import static org.hamcrest.Matchers.nullValue;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.WebAuthnConstants;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.UserModel;
|
||||
@@ -34,7 +33,6 @@ import org.keycloak.models.utils.TimeBasedOTP;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.arquillian.annotation.IgnoreBrowserDriver;
|
||||
import org.keycloak.testsuite.auth.page.login.OneTimeCode;
|
||||
import org.keycloak.testsuite.pages.LoginConfigTotpPage;
|
||||
@@ -49,7 +47,6 @@ import org.openqa.selenium.firefox.FirefoxDriver;
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
@EnableFeature(value = Profile.Feature.PASSKEYS, skipRestart = true)
|
||||
@IgnoreBrowserDriver(FirefoxDriver.class) // See https://github.com/keycloak/keycloak/issues/10368
|
||||
public class PasskeysDefaultBrowserFlowTest extends AbstractWebAuthnVirtualTest {
|
||||
|
||||
|
||||
@@ -26,13 +26,11 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.AuthenticationManagementResource;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.models.IdentityProviderSyncMode;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.arquillian.annotation.IgnoreBrowserDriver;
|
||||
import org.keycloak.testsuite.broker.AbstractBrokerTest;
|
||||
import org.keycloak.testsuite.broker.AbstractInitializedBaseBrokerTest;
|
||||
@@ -59,7 +57,6 @@ import org.openqa.selenium.firefox.FirefoxDriver;
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
@EnableFeature(value = Profile.Feature.PASSKEYS, skipRestart = true)
|
||||
@IgnoreBrowserDriver(FirefoxDriver.class) // See https://github.com/keycloak/keycloak/issues/10368
|
||||
public class PasskeysKcOidcFirstBrokerLoginTest extends AbstractInitializedBaseBrokerTest {
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.WebAuthnConstants;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventType;
|
||||
@@ -42,20 +41,17 @@ import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.arquillian.annotation.IgnoreBrowserDriver;
|
||||
import org.keycloak.testsuite.util.WaitUtils;
|
||||
import org.keycloak.testsuite.webauthn.AbstractWebAuthnVirtualTest;
|
||||
import org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.firefox.FirefoxDriver;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
@EnableFeature(value = Profile.Feature.PASSKEYS, skipRestart = true)
|
||||
@IgnoreBrowserDriver(FirefoxDriver.class) // See https://github.com/keycloak/keycloak/issues/10368
|
||||
public class PasskeysOrganizationAuthenticationTest extends AbstractWebAuthnVirtualTest {
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import org.junit.Test;
|
||||
import org.keycloak.WebAuthnConstants;
|
||||
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventType;
|
||||
@@ -43,7 +42,6 @@ import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.arquillian.annotation.IgnoreBrowserDriver;
|
||||
import org.keycloak.testsuite.util.WaitUtils;
|
||||
import org.keycloak.testsuite.webauthn.AbstractWebAuthnVirtualTest;
|
||||
@@ -59,7 +57,6 @@ import static org.junit.Assert.assertEquals;
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
@EnableFeature(value = Profile.Feature.PASSKEYS, skipRestart = true)
|
||||
@IgnoreBrowserDriver(FirefoxDriver.class) // See https://github.com/keycloak/keycloak/issues/10368
|
||||
public class PasskeysUsernameFormTest extends AbstractWebAuthnVirtualTest {
|
||||
|
||||
|
||||
@@ -27,19 +27,16 @@ import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.WebAuthnConstants;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.arquillian.annotation.IgnoreBrowserDriver;
|
||||
import org.keycloak.testsuite.pages.SelectOrganizationPage;
|
||||
import org.keycloak.testsuite.util.WaitUtils;
|
||||
@@ -51,13 +48,11 @@ import org.openqa.selenium.firefox.FirefoxDriver;
|
||||
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
@EnableFeature(value = Profile.Feature.PASSKEYS, skipRestart = true)
|
||||
@IgnoreBrowserDriver(FirefoxDriver.class) // See https://github.com/keycloak/keycloak/issues/10368
|
||||
public class PasskeysUsernamePasswordFormTest extends AbstractWebAuthnVirtualTest {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user