KEYCLOAK-13319 Use newest WebDriver/Selenium for the WebAuthn testing
This commit is contained in:
parent
a0b9e4f3eb
commit
7dc01a5a6e
26 changed files with 1412 additions and 517 deletions
|
@ -1,35 +0,0 @@
|
||||||
package org.keycloak.testsuite;
|
|
||||||
|
|
||||||
import org.junit.Assume;
|
|
||||||
import org.openqa.selenium.Capabilities;
|
|
||||||
import org.openqa.selenium.WebDriver;
|
|
||||||
import org.openqa.selenium.remote.BrowserType;
|
|
||||||
import org.openqa.selenium.remote.RemoteWebDriver;
|
|
||||||
|
|
||||||
import static org.keycloak.testsuite.util.DroneUtils.getCurrentDriver;
|
|
||||||
|
|
||||||
public class WebAuthnAssume {
|
|
||||||
|
|
||||||
public static final String CHROME_NAME = BrowserType.CHROME;
|
|
||||||
public static final int CHROME_MIN_VERSION = 68;
|
|
||||||
public static final int CHROME_MAX_VERSION = 80;
|
|
||||||
|
|
||||||
public static void assumeChrome() {
|
|
||||||
assumeChrome(getCurrentDriver());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void assumeChrome(WebDriver driver) {
|
|
||||||
Assume.assumeNotNull(driver);
|
|
||||||
String chromeArguments = System.getProperty("chromeArguments");
|
|
||||||
Assume.assumeNotNull(chromeArguments);
|
|
||||||
Assume.assumeTrue(chromeArguments.contains("--enable-web-authentication-testing-api"));
|
|
||||||
Assume.assumeTrue("Browser must be Chrome (RemoteWebDriver)!", driver instanceof RemoteWebDriver);
|
|
||||||
Capabilities cap = ((RemoteWebDriver) driver).getCapabilities();
|
|
||||||
String browserName = cap.getBrowserName().toLowerCase();
|
|
||||||
int version = Integer.parseInt(cap.getVersion().substring(0, cap.getVersion().indexOf(".")));
|
|
||||||
|
|
||||||
Assume.assumeTrue("Browser must be Chrome !", browserName.equals(CHROME_NAME));
|
|
||||||
Assume.assumeTrue("Version of Chrome must be higher than or equal to " + CHROME_MIN_VERSION, version >= CHROME_MIN_VERSION);
|
|
||||||
Assume.assumeTrue("Version of Chrome must be lower than or equal to " + CHROME_MAX_VERSION, version <= CHROME_MAX_VERSION);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,7 +7,6 @@ import org.keycloak.testsuite.util.DroneUtils;
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.NoSuchElementException;
|
import org.openqa.selenium.NoSuchElementException;
|
||||||
import org.openqa.selenium.WebElement;
|
import org.openqa.selenium.WebElement;
|
||||||
import org.openqa.selenium.support.FindBy;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login page with the list of authentication mechanisms, which are available to the user (Password, OTP, WebAuthn...)
|
* Login page with the list of authentication mechanisms, which are available to the user (Password, OTP, WebAuthn...)
|
||||||
|
@ -23,6 +22,9 @@ public class SelectAuthenticatorPage extends LanguageComboboxAwarePage {
|
||||||
// Corresponds to the OTPFormAuthenticator
|
// Corresponds to the OTPFormAuthenticator
|
||||||
public static final String AUTHENTICATOR_APPLICATION = "Authenticator Application";
|
public static final String AUTHENTICATOR_APPLICATION = "Authenticator Application";
|
||||||
|
|
||||||
|
// Corresponds to the WebAuthn authenticators
|
||||||
|
public static final String SECURITY_KEY = "Security Key";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of names like for example [ "Password", "Authenticator Application", "Security Key" ]
|
* Return list of names like for example [ "Password", "Authenticator Application", "Security Key" ]
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -92,4 +92,9 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
|
||||||
rep.setVerifyEmail(value);
|
rep.setVerifyEmail(value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RealmAttributeUpdater setBrowserFlow(String browserFlow) {
|
||||||
|
rep.setBrowserFlow(browserFlow);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ package org.keycloak.testsuite.actions;
|
||||||
import org.apache.http.NameValuePair;
|
import org.apache.http.NameValuePair;
|
||||||
import org.apache.http.client.utils.URLEncodedUtils;
|
import org.apache.http.client.utils.URLEncodedUtils;
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Assert;
|
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
|
@ -32,8 +31,19 @@ import org.keycloak.testsuite.util.WaitUtils;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.containsString;
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
import static org.hamcrest.CoreMatchers.not;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.keycloak.OAuth2Constants.REDIRECT_URI;
|
||||||
|
import static org.keycloak.OAuth2Constants.RESPONSE_TYPE;
|
||||||
|
import static org.keycloak.OAuth2Constants.SCOPE;
|
||||||
|
import static org.keycloak.models.Constants.CLIENT_ID;
|
||||||
|
import static org.keycloak.models.Constants.KC_ACTION;
|
||||||
|
import static org.keycloak.models.Constants.KC_ACTION_STATUS;
|
||||||
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
|
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -58,39 +68,40 @@ public abstract class AbstractAppInitiatedActionTest extends AbstractTestRealmKe
|
||||||
|
|
||||||
protected void doAIA() {
|
protected void doAIA() {
|
||||||
UriBuilder builder = OIDCLoginProtocolService.authUrl(authServerPage.createUriBuilder());
|
UriBuilder builder = OIDCLoginProtocolService.authUrl(authServerPage.createUriBuilder());
|
||||||
String uri = builder.queryParam("kc_action", this.aiaAction)
|
String uri = builder.queryParam(KC_ACTION, this.aiaAction)
|
||||||
.queryParam("response_type", "code")
|
.queryParam(RESPONSE_TYPE, "code")
|
||||||
.queryParam("client_id", "test-app")
|
.queryParam(CLIENT_ID, "test-app")
|
||||||
.queryParam("scope", "openid")
|
.queryParam(SCOPE, "openid")
|
||||||
.queryParam("redirect_uri", getAuthServerContextRoot() + "/auth/realms/master/app/auth")
|
.queryParam(REDIRECT_URI, getAuthServerContextRoot() + "/auth/realms/master/app/auth")
|
||||||
.build(TEST_REALM_NAME).toString();
|
.build(TEST_REALM_NAME).toString();
|
||||||
driver.navigate().to(uri);
|
driver.navigate().to(uri);
|
||||||
WaitUtils.waitForPageToLoad();
|
WaitUtils.waitForPageToLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void assertKcActionStatus(String expectedStatus) {
|
protected void assertKcActionStatus(String expectedStatus) {
|
||||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
assertThat(appPage.getRequestType(),is(RequestType.AUTH_RESPONSE));
|
||||||
|
|
||||||
URI url = null;
|
final URI url;
|
||||||
try {
|
try {
|
||||||
url = new URI(this.driver.getCurrentUrl());
|
url = new URI(this.driver.getCurrentUrl());
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
List<NameValuePair> pairs = URLEncodedUtils.parse(url, "UTF-8");
|
|
||||||
|
List<NameValuePair> pairs = URLEncodedUtils.parse(url, StandardCharsets.UTF_8);
|
||||||
String kcActionStatus = null;
|
String kcActionStatus = null;
|
||||||
for (NameValuePair p : pairs) {
|
for (NameValuePair p : pairs) {
|
||||||
if (p.getName().equals("kc_action_status")) {
|
if (p.getName().equals(KC_ACTION_STATUS)) {
|
||||||
kcActionStatus = p.getValue();
|
kcActionStatus = p.getValue();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Assert.assertEquals(expectedStatus, kcActionStatus);
|
assertThat(expectedStatus, is(kcActionStatus));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void assertSilentCancelMessage() {
|
protected void assertSilentCancelMessage() {
|
||||||
String url = this.driver.getCurrentUrl();
|
String url = this.driver.getCurrentUrl();
|
||||||
Assert.assertFalse("Expected no 'error=' in url", url.contains("error="));
|
assertThat("Expected no 'error=' in url", url, not(containsString("error=")));
|
||||||
Assert.assertFalse("Expected no 'error_description=' in url", url.contains("error_description="));
|
assertThat("Expected no 'error_description=' in url", url, not(containsString("error_description=")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,358 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.webauthn;
|
|
||||||
|
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
|
||||||
import org.junit.Assert;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Rule;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.keycloak.WebAuthnConstants;
|
|
||||||
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
|
||||||
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
|
|
||||||
import org.keycloak.common.Profile;
|
|
||||||
import org.keycloak.common.util.SecretGenerator;
|
|
||||||
import org.keycloak.events.Details;
|
|
||||||
import org.keycloak.events.EventType;
|
|
||||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
|
||||||
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
|
|
||||||
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.testsuite.AssertEvents;
|
|
||||||
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
|
||||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
|
||||||
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;
|
|
||||||
import org.keycloak.testsuite.pages.webauthn.WebAuthnRegisterPage;
|
|
||||||
import org.keycloak.util.JsonSerialization;
|
|
||||||
import org.keycloak.testsuite.WebAuthnAssume;
|
|
||||||
import org.keycloak.testsuite.pages.AppPage;
|
|
||||||
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.junit.Assume;
|
|
||||||
import org.junit.BeforeClass;
|
|
||||||
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SSL_REQUIRED;
|
|
||||||
|
|
||||||
@EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true)
|
|
||||||
public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest {
|
|
||||||
|
|
||||||
@Rule
|
|
||||||
public AssertEvents events = new AssertEvents(this);
|
|
||||||
|
|
||||||
@Page
|
|
||||||
protected AppPage appPage;
|
|
||||||
|
|
||||||
@Page
|
|
||||||
protected LoginPage loginPage;
|
|
||||||
|
|
||||||
@Page
|
|
||||||
protected WebAuthnLoginPage webAuthnLoginPage;
|
|
||||||
|
|
||||||
@Page
|
|
||||||
protected RegisterPage registerPage;
|
|
||||||
|
|
||||||
@Page
|
|
||||||
protected WebAuthnRegisterPage webAuthnRegisterPage;
|
|
||||||
|
|
||||||
private static final String ALL_ZERO_AAGUID = "00000000-0000-0000-0000-000000000000";
|
|
||||||
|
|
||||||
private List<String> signatureAlgorithms;
|
|
||||||
private String attestationConveyancePreference;
|
|
||||||
private String authenticatorAttachment;
|
|
||||||
private String requireResidentKey;
|
|
||||||
private String rpEntityName;
|
|
||||||
private String userVerificationRequirement;
|
|
||||||
private String rpId;
|
|
||||||
private int createTimeout;
|
|
||||||
private boolean avoidSameAuthenticatorRegister;
|
|
||||||
private List<String> acceptableAaguids;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeClass
|
|
||||||
public static void enabled() {
|
|
||||||
Assume.assumeTrue(AUTH_SERVER_SSL_REQUIRED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void verifyEnvironment() {
|
|
||||||
WebAuthnAssume.assumeChrome(driver);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
|
||||||
RealmRepresentation realmRepresentation = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/webauthn/testrealm-webauthn.json"), RealmRepresentation.class);
|
|
||||||
testRealms.add(realmRepresentation);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void registerUserSuccess() {
|
|
||||||
String username = "registerUserSuccess";
|
|
||||||
String password = "password";
|
|
||||||
String email = "registerUserSuccess@email";
|
|
||||||
|
|
||||||
try {
|
|
||||||
RealmRepresentation rep = backupWebAuthnRealmSettings();
|
|
||||||
rep.setWebAuthnPolicySignatureAlgorithms(Arrays.asList("ES256"));
|
|
||||||
rep.setWebAuthnPolicyAttestationConveyancePreference("none");
|
|
||||||
rep.setWebAuthnPolicyAuthenticatorAttachment("cross-platform");
|
|
||||||
rep.setWebAuthnPolicyRequireResidentKey("No");
|
|
||||||
rep.setWebAuthnPolicyRpId(null);
|
|
||||||
rep.setWebAuthnPolicyUserVerificationRequirement("preferred");
|
|
||||||
rep.setWebAuthnPolicyAcceptableAaguids(Arrays.asList(ALL_ZERO_AAGUID));
|
|
||||||
testRealm().update(rep);
|
|
||||||
|
|
||||||
loginPage.open();
|
|
||||||
loginPage.clickRegister();
|
|
||||||
registerPage.assertCurrent();
|
|
||||||
|
|
||||||
String authenticatorLabel = SecretGenerator.getInstance().randomString(24);
|
|
||||||
registerPage.register("firstName", "lastName", email, username, password, password);
|
|
||||||
|
|
||||||
// User was registered. Now he needs to register WebAuthn credential
|
|
||||||
webAuthnRegisterPage.registerWebAuthnCredential(authenticatorLabel);
|
|
||||||
|
|
||||||
appPage.assertCurrent();
|
|
||||||
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
|
||||||
appPage.openAccount();
|
|
||||||
|
|
||||||
// confirm that registration is successfully completed
|
|
||||||
String userId = events.expectRegister(username, email).assertEvent().getUserId();
|
|
||||||
// confirm registration event
|
|
||||||
EventRepresentation eventRep = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
|
|
||||||
.user(userId)
|
|
||||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
|
||||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel)
|
|
||||||
.assertEvent();
|
|
||||||
String regPubKeyCredentialId = eventRep.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
|
|
||||||
//String regPubKeyCredentialAaguid = eventRep.getDetails().get("public_key_credential_aaguid");
|
|
||||||
//String regPubKeyCredentialLabel = eventRep.getDetails().get("public_key_credential_label");
|
|
||||||
|
|
||||||
// confirm login event
|
|
||||||
String sessionId = events.expectLogin()
|
|
||||||
.user(userId)
|
|
||||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
|
||||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel)
|
|
||||||
.assertEvent().getSessionId();
|
|
||||||
// confirm user registered
|
|
||||||
assertUserRegistered(userId, username.toLowerCase(), email.toLowerCase());
|
|
||||||
assertRegisteredCredentials(userId, ALL_ZERO_AAGUID, "none");
|
|
||||||
|
|
||||||
// logout by user
|
|
||||||
appPage.logout();
|
|
||||||
// confirm logout event
|
|
||||||
events.expectLogout(sessionId)
|
|
||||||
.user(userId)
|
|
||||||
.assertEvent();
|
|
||||||
|
|
||||||
// login by user
|
|
||||||
loginPage.open();
|
|
||||||
loginPage.login(username, password);
|
|
||||||
|
|
||||||
// User is authenticated by Chrome WebAuthN testing API
|
|
||||||
|
|
||||||
appPage.assertCurrent();
|
|
||||||
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
|
||||||
appPage.openAccount();
|
|
||||||
// confirm login event
|
|
||||||
sessionId = events.expectLogin()
|
|
||||||
.user(userId)
|
|
||||||
.detail(WebAuthnConstants.PUBKEY_CRED_ID_ATTR, regPubKeyCredentialId)
|
|
||||||
// .detail("web_authn_authenticator_user_verification_checked", Boolean.FALSE.toString())
|
|
||||||
.assertEvent().getSessionId();
|
|
||||||
|
|
||||||
// logout by user
|
|
||||||
appPage.logout();
|
|
||||||
// confirm logout event
|
|
||||||
events.expectLogout(sessionId)
|
|
||||||
.user(userId)
|
|
||||||
.assertEvent();
|
|
||||||
} finally {
|
|
||||||
restoreWebAuthnRealmSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testWebAuthnTwoFactorAndWebAuthnPasswordlessTogether() {
|
|
||||||
|
|
||||||
// Change binding to browser-webauthn-passwordless. This is flow, which contains both "webauthn" and "webauthn-passwordless" authenticator
|
|
||||||
RealmRepresentation realmRep = testRealm().toRepresentation();
|
|
||||||
realmRep.setBrowserFlow("browser-webauthn-passwordless");
|
|
||||||
testRealm().update(realmRep);
|
|
||||||
|
|
||||||
//WaitUtils.pause(10000000);
|
|
||||||
|
|
||||||
try {
|
|
||||||
String userId = ApiUtil.findUserByUsername(testRealm(), "test-user@localhost").getId();
|
|
||||||
|
|
||||||
// Login as test-user@localhost with password
|
|
||||||
loginPage.open();
|
|
||||||
loginPage.login("test-user@localhost", "password");
|
|
||||||
|
|
||||||
// Register first requiredAction is needed. Use label "Label1"
|
|
||||||
webAuthnRegisterPage.registerWebAuthnCredential("label1");
|
|
||||||
|
|
||||||
// Register second requiredAction is needed. Use label "Label2". This will be for passwordless WebAuthn credential
|
|
||||||
webAuthnRegisterPage.registerWebAuthnCredential("label2");
|
|
||||||
|
|
||||||
appPage.assertCurrent();
|
|
||||||
|
|
||||||
// Assert user is logged and WebAuthn credentials were registered
|
|
||||||
EventRepresentation eventRep = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
|
|
||||||
.user(userId)
|
|
||||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
|
||||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, "label1")
|
|
||||||
.assertEvent();
|
|
||||||
String regPubKeyCredentialId1 = eventRep.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
|
|
||||||
|
|
||||||
eventRep = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
|
|
||||||
.user(userId)
|
|
||||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID)
|
|
||||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, "label2")
|
|
||||||
.assertEvent();
|
|
||||||
String regPubKeyCredentialId2 = eventRep.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
|
|
||||||
|
|
||||||
String sessionId = events.expectLogin()
|
|
||||||
.user(userId)
|
|
||||||
.assertEvent().getSessionId();
|
|
||||||
|
|
||||||
// Logout
|
|
||||||
appPage.logout();
|
|
||||||
events.expectLogout(sessionId)
|
|
||||||
.user(userId)
|
|
||||||
.assertEvent();
|
|
||||||
|
|
||||||
// Assert user has 2 webauthn credentials. One of type "webauthn" and the other of type "webauthn-passwordless".
|
|
||||||
List<CredentialRepresentation> rep = testRealm().users().get(userId).credentials();
|
|
||||||
|
|
||||||
CredentialRepresentation webAuthnCredential1 = rep.stream()
|
|
||||||
.filter(credential -> WebAuthnCredentialModel.TYPE_TWOFACTOR.equals(credential.getType()))
|
|
||||||
.findFirst().orElse(null);
|
|
||||||
|
|
||||||
Assert.assertNotNull(webAuthnCredential1);
|
|
||||||
Assert.assertEquals("label1", webAuthnCredential1.getUserLabel());
|
|
||||||
|
|
||||||
CredentialRepresentation webAuthnCredential2 = rep.stream()
|
|
||||||
.filter(credential -> WebAuthnCredentialModel.TYPE_PASSWORDLESS.equals(credential.getType()))
|
|
||||||
.findFirst().orElse(null);
|
|
||||||
|
|
||||||
Assert.assertNotNull(webAuthnCredential2);
|
|
||||||
Assert.assertEquals("label2", webAuthnCredential2.getUserLabel());
|
|
||||||
|
|
||||||
// Assert user needs to authenticate first with "webauthn" during login
|
|
||||||
loginPage.open();
|
|
||||||
loginPage.login("test-user@localhost", "password");
|
|
||||||
|
|
||||||
// User is authenticated by Chrome WebAuthN testing API
|
|
||||||
|
|
||||||
// Assert user logged now
|
|
||||||
appPage.assertCurrent();
|
|
||||||
events.expectLogin()
|
|
||||||
.user(userId)
|
|
||||||
.assertEvent();
|
|
||||||
|
|
||||||
// Remove webauthn credentials from the user
|
|
||||||
testRealm().users().get(userId).removeCredential(webAuthnCredential1.getId());
|
|
||||||
testRealm().users().get(userId).removeCredential(webAuthnCredential2.getId());
|
|
||||||
} finally {
|
|
||||||
// Revert binding to browser-webauthn
|
|
||||||
realmRep.setBrowserFlow("browser-webauthn");
|
|
||||||
testRealm().update(realmRep);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertUserRegistered(String userId, String username, String email) {
|
|
||||||
UserRepresentation user = getUser(userId);
|
|
||||||
Assert.assertNotNull(user);
|
|
||||||
Assert.assertNotNull(user.getCreatedTimestamp());
|
|
||||||
// test that timestamp is current with 60s tollerance
|
|
||||||
Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 60000);
|
|
||||||
// test user info is set from form
|
|
||||||
assertEquals(username.toLowerCase(), user.getUsername());
|
|
||||||
assertEquals(email.toLowerCase(), user.getEmail());
|
|
||||||
assertEquals("firstName", user.getFirstName());
|
|
||||||
assertEquals("lastName", user.getLastName());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertRegisteredCredentials(String userId, String aaguid, String attestationStatementFormat) {
|
|
||||||
List<CredentialRepresentation> credentials = getCredentials(userId);
|
|
||||||
credentials.stream().forEach(i -> {
|
|
||||||
if (WebAuthnCredentialModel.TYPE_TWOFACTOR.equals(i.getType())) {
|
|
||||||
try {
|
|
||||||
WebAuthnCredentialData data = JsonSerialization.readValue(i.getCredentialData(), WebAuthnCredentialData.class);
|
|
||||||
assertEquals(aaguid, data.getAaguid());
|
|
||||||
assertEquals(attestationStatementFormat, data.getAttestationStatementFormat());
|
|
||||||
} catch (IOException e) {
|
|
||||||
Assert.fail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected UserRepresentation getUser(String userId) {
|
|
||||||
return testRealm().users().get(userId).toRepresentation();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected List<CredentialRepresentation> getCredentials(String userId) {
|
|
||||||
return testRealm().users().get(userId).credentials();
|
|
||||||
}
|
|
||||||
|
|
||||||
private RealmRepresentation backupWebAuthnRealmSettings() {
|
|
||||||
RealmRepresentation rep = testRealm().toRepresentation();
|
|
||||||
signatureAlgorithms = rep.getWebAuthnPolicySignatureAlgorithms();
|
|
||||||
attestationConveyancePreference = rep.getWebAuthnPolicyAttestationConveyancePreference();
|
|
||||||
authenticatorAttachment = rep.getWebAuthnPolicyAuthenticatorAttachment();
|
|
||||||
requireResidentKey = rep.getWebAuthnPolicyRequireResidentKey();
|
|
||||||
rpEntityName = rep.getWebAuthnPolicyRpEntityName();
|
|
||||||
userVerificationRequirement = rep.getWebAuthnPolicyUserVerificationRequirement();
|
|
||||||
rpId = rep.getWebAuthnPolicyRpId();
|
|
||||||
createTimeout = rep.getWebAuthnPolicyCreateTimeout();
|
|
||||||
avoidSameAuthenticatorRegister = rep.isWebAuthnPolicyAvoidSameAuthenticatorRegister();
|
|
||||||
acceptableAaguids = rep.getWebAuthnPolicyAcceptableAaguids();
|
|
||||||
return rep;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void restoreWebAuthnRealmSettings() {
|
|
||||||
RealmRepresentation rep = testRealm().toRepresentation();
|
|
||||||
rep.setWebAuthnPolicySignatureAlgorithms(signatureAlgorithms);
|
|
||||||
rep.setWebAuthnPolicyAttestationConveyancePreference(attestationConveyancePreference);
|
|
||||||
rep.setWebAuthnPolicyAuthenticatorAttachment(authenticatorAttachment);
|
|
||||||
rep.setWebAuthnPolicyRequireResidentKey(requireResidentKey);
|
|
||||||
rep.setWebAuthnPolicyRpEntityName(rpEntityName);
|
|
||||||
rep.setWebAuthnPolicyUserVerificationRequirement(userVerificationRequirement);
|
|
||||||
rep.setWebAuthnPolicyRpId(rpId);
|
|
||||||
rep.setWebAuthnPolicyCreateTimeout(createTimeout);
|
|
||||||
rep.setWebAuthnPolicyAvoidSameAuthenticatorRegister(avoidSameAuthenticatorRegister);
|
|
||||||
rep.setWebAuthnPolicyAcceptableAaguids(acceptableAaguids);
|
|
||||||
testRealm().update(rep);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -36,12 +36,10 @@ import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||||
import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation;
|
import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation;
|
||||||
import org.keycloak.testsuite.WebAuthnAssume;
|
|
||||||
import org.keycloak.testsuite.admin.Users;
|
import org.keycloak.testsuite.admin.Users;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
import org.keycloak.testsuite.auth.page.login.OTPSetup;
|
import org.keycloak.testsuite.auth.page.login.OTPSetup;
|
||||||
import org.keycloak.testsuite.auth.page.login.UpdatePassword;
|
import org.keycloak.testsuite.auth.page.login.UpdatePassword;
|
||||||
import org.keycloak.testsuite.pages.webauthn.WebAuthnRegisterPage;
|
|
||||||
import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage;
|
import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage;
|
||||||
import org.keycloak.testsuite.ui.account2.page.SigningInPage;
|
import org.keycloak.testsuite.ui.account2.page.SigningInPage;
|
||||||
|
|
||||||
|
@ -52,7 +50,6 @@ import static java.util.Collections.emptyList;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotEquals;
|
import static org.junit.Assert.assertNotEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED;
|
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED;
|
||||||
import static org.keycloak.models.UserModel.RequiredAction.CONFIGURE_TOTP;
|
import static org.keycloak.models.UserModel.RequiredAction.CONFIGURE_TOTP;
|
||||||
|
@ -78,8 +75,8 @@ public class SigningInTest extends BaseAccountPageTest {
|
||||||
@Page
|
@Page
|
||||||
private OTPSetup otpSetupPage;
|
private OTPSetup otpSetupPage;
|
||||||
|
|
||||||
@Page
|
/* @Page
|
||||||
private WebAuthnRegisterPage webAuthnRegisterPage;
|
private WebAuthnRegisterPage webAuthnRegisterPage;*/
|
||||||
|
|
||||||
private SigningInPage.CredentialType passwordCredentialType;
|
private SigningInPage.CredentialType passwordCredentialType;
|
||||||
private SigningInPage.CredentialType otpCredentialType;
|
private SigningInPage.CredentialType otpCredentialType;
|
||||||
|
@ -252,7 +249,7 @@ public class SigningInTest extends BaseAccountPageTest {
|
||||||
testRemoveCredential(otp1);
|
testRemoveCredential(otp1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
/* @Test
|
||||||
public void twoFactorWebAuthnTest() {
|
public void twoFactorWebAuthnTest() {
|
||||||
testWebAuthn(false);
|
testWebAuthn(false);
|
||||||
}
|
}
|
||||||
|
@ -265,8 +262,6 @@ public class SigningInTest extends BaseAccountPageTest {
|
||||||
private void testWebAuthn(boolean passwordless) {
|
private void testWebAuthn(boolean passwordless) {
|
||||||
testContext.setTestRealmReps(emptyList());
|
testContext.setTestRealmReps(emptyList());
|
||||||
|
|
||||||
WebAuthnAssume.assumeChrome(driver); // we need some special flags to be able to register security key
|
|
||||||
|
|
||||||
SigningInPage.CredentialType credentialType;
|
SigningInPage.CredentialType credentialType;
|
||||||
final String expectedHelpText;
|
final String expectedHelpText;
|
||||||
final String providerId;
|
final String providerId;
|
||||||
|
@ -313,7 +308,7 @@ public class SigningInTest extends BaseAccountPageTest {
|
||||||
assertEquals(2, credentialType.getUserCredentialsCount());
|
assertEquals(2, credentialType.getUserCredentialsCount());
|
||||||
|
|
||||||
testRemoveCredential(webAuthn1);
|
testRemoveCredential(webAuthn1);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void setUpLinksTest() {
|
public void setUpLinksTest() {
|
||||||
|
@ -351,17 +346,18 @@ public class SigningInTest extends BaseAccountPageTest {
|
||||||
return getNewestUserCredential(otpCredentialType);
|
return getNewestUserCredential(otpCredentialType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private SigningInPage.UserCredential addWebAuthnCredential(String label, boolean passwordless) {
|
/*private SigningInPage.UserCredential addWebAuthnCredential(String label, boolean passwordless) {
|
||||||
SigningInPage.CredentialType credentialType = passwordless ? webAuthnPwdlessCredentialType : webAuthnCredentialType;
|
SigningInPage.CredentialType credentialType = passwordless ? webAuthnPwdlessCredentialType : webAuthnCredentialType;
|
||||||
|
|
||||||
credentialType.clickSetUpLink();
|
credentialType.clickSetUpLink();
|
||||||
webAuthnRegisterPage.confirmAIA();
|
webAuthnRegisterPage.assertCurrent();
|
||||||
|
webAuthnRegisterPage.clickRegister();
|
||||||
webAuthnRegisterPage.registerWebAuthnCredential(label);
|
webAuthnRegisterPage.registerWebAuthnCredential(label);
|
||||||
waitForPageToLoad();
|
waitForPageToLoad();
|
||||||
signingInPage.assertCurrent();
|
signingInPage.assertCurrent();
|
||||||
|
|
||||||
return getNewestUserCredential(credentialType);
|
return getNewestUserCredential(credentialType);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
private SigningInPage.UserCredential getNewestUserCredential(SigningInPage.CredentialType credentialType) {
|
private SigningInPage.UserCredential getNewestUserCredential(SigningInPage.CredentialType credentialType) {
|
||||||
List<CredentialRepresentation> credentials = testUserResource().credentials();
|
List<CredentialRepresentation> credentials = testUserResource().credentials();
|
||||||
|
|
|
@ -161,6 +161,12 @@
|
||||||
<module>springboot-tests</module>
|
<module>springboot-tests</module>
|
||||||
</modules>
|
</modules>
|
||||||
</profile>
|
</profile>
|
||||||
|
<profile>
|
||||||
|
<id>webauthn</id>
|
||||||
|
<modules>
|
||||||
|
<module>webauthn</module>
|
||||||
|
</modules>
|
||||||
|
</profile>
|
||||||
</profiles>
|
</profiles>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
104
testsuite/integration-arquillian/tests/other/webauthn/pom.xml
Normal file
104
testsuite/integration-arquillian/tests/other/webauthn/pom.xml
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<parent>
|
||||||
|
<groupId>org.keycloak.testsuite</groupId>
|
||||||
|
<artifactId>integration-arquillian-tests-other</artifactId>
|
||||||
|
<version>16.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>integration-arquillian-tests-webauthn</artifactId>
|
||||||
|
|
||||||
|
<name>WebAuthn tests</name>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<selenium.version>4.0.0-rc-3</selenium.version>
|
||||||
|
<selenium.server.version>4.0.0-alpha-2</selenium.server.version>
|
||||||
|
<arquillian.drone.version>${selenium.version}</arquillian.drone.version>
|
||||||
|
<graphene.webdriver.version>2.4.0.Alpha-2</graphene.webdriver.version>
|
||||||
|
<htmlunit.driver.version>${selenium.version}</htmlunit.driver.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>github-selenium-bom</id>
|
||||||
|
<url>https://keycloak-packages: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@maven.pkg.github.com/mabartos/selenium-bom</url>
|
||||||
|
<releases>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
</releases>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>github-arquillian</id>
|
||||||
|
<url>https://keycloak-packages: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@maven.pkg.github.com/mabartos/arquillian-extension-drone</url>
|
||||||
|
<releases>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</releases>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>github-htmlUnit-driver</id>
|
||||||
|
<url>https://keycloak-packages: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@maven.pkg.github.com/mabartos/htmlunit-driver</url>
|
||||||
|
<releases>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</releases>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>github-arquillian-graphene</id>
|
||||||
|
<url>https://keycloak-packages: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@maven.pkg.github.com/mabartos/arquillian-graphene</url>
|
||||||
|
<releases>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</releases>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.arquillian.extension</groupId>
|
||||||
|
<artifactId>arquillian-drone-bom</artifactId>
|
||||||
|
<version>${arquillian.drone.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.arquillian.extension</groupId>
|
||||||
|
<artifactId>arquillian-drone-webdriver</artifactId>
|
||||||
|
<version>${arquillian.drone.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.arquillian.selenium</groupId>
|
||||||
|
<artifactId>selenium-bom</artifactId>
|
||||||
|
<version>${selenium.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.seleniumhq.selenium</groupId>
|
||||||
|
<artifactId>htmlunit-driver</artifactId>
|
||||||
|
<version>${htmlunit.driver.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.arquillian.graphene</groupId>
|
||||||
|
<artifactId>graphene-webdriver-impl</artifactId>
|
||||||
|
<version>${graphene.webdriver.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hamcrest</groupId>
|
||||||
|
<artifactId>hamcrest-all</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.webauthn.authenticators;
|
||||||
|
|
||||||
|
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Options for various authenticators
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||||
|
*/
|
||||||
|
public class DefaultVirtualAuthOptions {
|
||||||
|
|
||||||
|
public static VirtualAuthenticatorOptions DEFAULT = getDefault();
|
||||||
|
|
||||||
|
// Default authenticators with different Transport type
|
||||||
|
public static VirtualAuthenticatorOptions DEFAULT_BTE = getDefault().setTransport(VirtualAuthenticatorOptions.Transport.BLE);
|
||||||
|
public static VirtualAuthenticatorOptions DEFAULT_NFC = getDefault().setTransport(VirtualAuthenticatorOptions.Transport.NFC);
|
||||||
|
public static VirtualAuthenticatorOptions DEFAULT_USB = getDefault().setTransport(VirtualAuthenticatorOptions.Transport.USB);
|
||||||
|
public static VirtualAuthenticatorOptions DEFAULT_INTERNAL = getDefault().setTransport(VirtualAuthenticatorOptions.Transport.INTERNAL);
|
||||||
|
|
||||||
|
// Default authenticators with different Protocol type
|
||||||
|
public static VirtualAuthenticatorOptions DEFAULT_CTAP_2 = getDefault().setProtocol(VirtualAuthenticatorOptions.Protocol.CTAP2);
|
||||||
|
public static VirtualAuthenticatorOptions DEFAULT_U2F = getDefault().setProtocol(VirtualAuthenticatorOptions.Protocol.U2F);
|
||||||
|
|
||||||
|
// YubiKey authenticators
|
||||||
|
public static VirtualAuthenticatorOptions YUBIKEY_4 = getYubikeyGeneral();
|
||||||
|
public static VirtualAuthenticatorOptions YUBIKEY_5_USB = getYubikeyGeneral();
|
||||||
|
public static VirtualAuthenticatorOptions YUBIKEY_5_NFC = getYubikeyGeneral().setTransport(VirtualAuthenticatorOptions.Transport.NFC);
|
||||||
|
|
||||||
|
private static VirtualAuthenticatorOptions getYubikeyGeneral() {
|
||||||
|
return new VirtualAuthenticatorOptions()
|
||||||
|
.setTransport(VirtualAuthenticatorOptions.Transport.USB)
|
||||||
|
.setProtocol(VirtualAuthenticatorOptions.Protocol.U2F)
|
||||||
|
.setHasUserVerification(true)
|
||||||
|
.setIsUserConsenting(true)
|
||||||
|
.setIsUserVerified(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VirtualAuthenticatorOptions getDefault() {
|
||||||
|
return new VirtualAuthenticatorOptions();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.webauthn.authenticators;
|
||||||
|
|
||||||
|
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticator;
|
||||||
|
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keycloak Virtual Authenticator
|
||||||
|
* <p>
|
||||||
|
* Used as wrapper for VirtualAuthenticator and its options*
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||||
|
*/
|
||||||
|
public class KcVirtualAuthenticator {
|
||||||
|
private final VirtualAuthenticator authenticator;
|
||||||
|
private final Options options;
|
||||||
|
|
||||||
|
public KcVirtualAuthenticator(VirtualAuthenticator authenticator, VirtualAuthenticatorOptions options) {
|
||||||
|
this.authenticator = authenticator;
|
||||||
|
this.options = new Options(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public VirtualAuthenticator getAuthenticator() {
|
||||||
|
return authenticator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Options getOptions() {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Options {
|
||||||
|
private final VirtualAuthenticatorOptions options;
|
||||||
|
private final VirtualAuthenticatorOptions.Protocol protocol;
|
||||||
|
private final VirtualAuthenticatorOptions.Transport transport;
|
||||||
|
private final boolean hasResidentKey;
|
||||||
|
private final boolean hasUserVerification;
|
||||||
|
private final boolean isUserConsenting;
|
||||||
|
private final boolean isUserVerified;
|
||||||
|
|
||||||
|
private Options(VirtualAuthenticatorOptions options) {
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
|
final Map<String, Object> map = options.toMap();
|
||||||
|
this.protocol = protocolFromMap(map);
|
||||||
|
this.transport = transportFromMap(map);
|
||||||
|
this.hasResidentKey = (Boolean) map.get("hasResidentKey");
|
||||||
|
this.hasUserVerification = (Boolean) map.get("hasUserVerification");
|
||||||
|
this.isUserConsenting = (Boolean) map.get("isUserConsenting");
|
||||||
|
this.isUserVerified = (Boolean) map.get("isUserVerified");
|
||||||
|
}
|
||||||
|
|
||||||
|
public VirtualAuthenticatorOptions.Protocol getProtocol() {
|
||||||
|
return protocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VirtualAuthenticatorOptions.Transport getTransport() {
|
||||||
|
return transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasResidentKey() {
|
||||||
|
return hasResidentKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasUserVerification() {
|
||||||
|
return hasUserVerification;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUserConsenting() {
|
||||||
|
return isUserConsenting;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUserVerified() {
|
||||||
|
return isUserVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VirtualAuthenticatorOptions clone() {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VirtualAuthenticatorOptions.Protocol protocolFromMap(Map<String, Object> map) {
|
||||||
|
return Arrays.stream(VirtualAuthenticatorOptions.Protocol.values())
|
||||||
|
.filter(f -> f.id.equals(map.get("protocol")))
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VirtualAuthenticatorOptions.Transport transportFromMap(Map<String, Object> map) {
|
||||||
|
return Arrays.stream(VirtualAuthenticatorOptions.Transport.values())
|
||||||
|
.filter(f -> f.id.equals(map.get("transport")))
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.webauthn.authenticators;
|
||||||
|
|
||||||
|
import org.hamcrest.CoreMatchers;
|
||||||
|
import org.openqa.selenium.WebDriver;
|
||||||
|
import org.openqa.selenium.virtualauthenticator.HasVirtualAuthenticator;
|
||||||
|
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager for Virtual Authenticators
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||||
|
*/
|
||||||
|
public class VirtualAuthenticatorManager {
|
||||||
|
private final HasVirtualAuthenticator driver;
|
||||||
|
private KcVirtualAuthenticator actualAuthenticator;
|
||||||
|
|
||||||
|
public VirtualAuthenticatorManager(WebDriver driver) {
|
||||||
|
assertThat("Driver must support Virtual Authenticators", driver, CoreMatchers.instanceOf(HasVirtualAuthenticator.class));
|
||||||
|
this.driver = (HasVirtualAuthenticator) driver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KcVirtualAuthenticator useAuthenticator(VirtualAuthenticatorOptions options) {
|
||||||
|
this.actualAuthenticator = new KcVirtualAuthenticator(driver.addVirtualAuthenticator(options), options);
|
||||||
|
return actualAuthenticator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KcVirtualAuthenticator getActualAuthenticator() {
|
||||||
|
return actualAuthenticator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeAuthenticator() {
|
||||||
|
if (actualAuthenticator != null) {
|
||||||
|
driver.removeVirtualAuthenticator(actualAuthenticator.getAuthenticator());
|
||||||
|
this.actualAuthenticator = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
package org.keycloak.testsuite.pages.webauthn;
|
package org.keycloak.testsuite.webauthn.pages;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
||||||
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.NoSuchElementException;
|
import org.openqa.selenium.NoSuchElementException;
|
||||||
import org.openqa.selenium.WebElement;
|
import org.openqa.selenium.WebElement;
|
||||||
|
@ -20,11 +21,13 @@ public class WebAuthnErrorPage extends LanguageComboboxAwarePage {
|
||||||
private WebElement cancelRegistrationAIA;
|
private WebElement cancelRegistrationAIA;
|
||||||
|
|
||||||
public void clickTryAgain() {
|
public void clickTryAgain() {
|
||||||
|
WaitUtils.waitUntilElement(tryAgainButton).is().clickable();
|
||||||
tryAgainButton.click();
|
tryAgainButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clickCancelRegistrationAIA() {
|
public void clickCancelRegistrationAIA() {
|
||||||
try {
|
try {
|
||||||
|
WaitUtils.waitUntilElement(cancelRegistrationAIA).is().clickable();
|
||||||
cancelRegistrationAIA.click();
|
cancelRegistrationAIA.click();
|
||||||
} catch (NoSuchElementException e) {
|
} catch (NoSuchElementException e) {
|
||||||
Assert.fail("It only works with AIA");
|
Assert.fail("It only works with AIA");
|
|
@ -15,17 +15,35 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.testsuite.pages.webauthn;
|
package org.keycloak.testsuite.webauthn.pages;
|
||||||
|
|
||||||
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
||||||
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
|
import org.openqa.selenium.WebElement;
|
||||||
|
import org.openqa.selenium.support.FindBy;
|
||||||
|
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page shown during WebAuthn login. Page is useful with Chrome testing API
|
* Page shown during WebAuthn login. Page is useful with Chrome testing API
|
||||||
*/
|
*/
|
||||||
public class WebAuthnLoginPage extends LanguageComboboxAwarePage {
|
public class WebAuthnLoginPage extends LanguageComboboxAwarePage {
|
||||||
|
|
||||||
|
@FindBy(id = "authenticateWebAuthnButton")
|
||||||
|
private WebElement authenticateButton;
|
||||||
|
|
||||||
|
public void clickAuthenticate() {
|
||||||
|
WaitUtils.waitUntilElement(authenticateButton).is().clickable();
|
||||||
|
authenticateButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isCurrent() {
|
public boolean isCurrent() {
|
||||||
return driver.getPageSource().contains("navigator.credentials.get");
|
try {
|
||||||
|
authenticateButton.getText();
|
||||||
|
return driver.getPageSource().contains("navigator.credentials.get");
|
||||||
|
} catch (NoSuchElementException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
|
@ -15,10 +15,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.testsuite.pages.webauthn;
|
package org.keycloak.testsuite.webauthn.pages;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.hamcrest.CoreMatchers;
|
||||||
import org.keycloak.testsuite.pages.AbstractPage;
|
import org.keycloak.testsuite.pages.AbstractPage;
|
||||||
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
import org.openqa.selenium.Alert;
|
import org.openqa.selenium.Alert;
|
||||||
import org.openqa.selenium.NoSuchElementException;
|
import org.openqa.selenium.NoSuchElementException;
|
||||||
import org.openqa.selenium.WebElement;
|
import org.openqa.selenium.WebElement;
|
||||||
|
@ -26,38 +27,42 @@ import org.openqa.selenium.support.FindBy;
|
||||||
import org.openqa.selenium.support.ui.ExpectedConditions;
|
import org.openqa.selenium.support.ui.ExpectedConditions;
|
||||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebAuthnRegisterPage, which is displayed when WebAuthnRegister required action is triggered. It is useful with Chrome testing API.
|
* WebAuthnRegisterPage, which is displayed when WebAuthnRegister required action is triggered. It is useful with Chrome testing API.
|
||||||
*
|
* <p>
|
||||||
* Page will be displayed after successful JS call of "navigator.credentials.create", which will register WebAuthn credential
|
* Page will be displayed after successful JS call of "navigator.credentials.create", which will register WebAuthn credential
|
||||||
* with the browser
|
* with the browser
|
||||||
*/
|
*/
|
||||||
public class WebAuthnRegisterPage extends AbstractPage {
|
public class WebAuthnRegisterPage extends AbstractPage {
|
||||||
|
|
||||||
// Available only with AIA
|
@FindBy(id = "registerWebAuthn")
|
||||||
@FindBy(id = "registerWebAuthnAIA")
|
private WebElement registerButton;
|
||||||
private WebElement registerAIAButton;
|
|
||||||
|
|
||||||
// Available only with AIA
|
// Available only with AIA
|
||||||
@FindBy(id = "cancelWebAuthnAIA")
|
@FindBy(id = "cancelWebAuthnAIA")
|
||||||
private WebElement cancelAIAButton;
|
private WebElement cancelAIAButton;
|
||||||
|
|
||||||
public void confirmAIA() {
|
public void clickRegister() {
|
||||||
Assert.assertTrue("It only works with AIA", isAIA());
|
WaitUtils.waitUntilElement(registerButton).is().clickable();
|
||||||
registerAIAButton.click();
|
registerButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelAIA() {
|
public void cancelAIA() {
|
||||||
Assert.assertTrue("It only works with AIA", isAIA());
|
assertThat("It only works with AIA", isAIA(), CoreMatchers.is(true));
|
||||||
|
WaitUtils.waitUntilElement(cancelAIAButton).is().clickable();
|
||||||
cancelAIAButton.click();
|
cancelAIAButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void registerWebAuthnCredential(String authenticatorLabel) {
|
public void registerWebAuthnCredential(String authenticatorLabel) {
|
||||||
// label edit after registering authenicator by .create()
|
// label edit after registering authenicator by .create()
|
||||||
WebDriverWait wait = new WebDriverWait(driver, 60);
|
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(60));
|
||||||
Alert promptDialog = wait.until(ExpectedConditions.alertIsPresent());
|
Alert promptDialog = wait.until(ExpectedConditions.alertIsPresent());
|
||||||
|
|
||||||
Assert.assertEquals("Please input your registered authenticator's label", promptDialog.getText());
|
assertThat(promptDialog.getText(), CoreMatchers.is("Please input your registered authenticator's label"));
|
||||||
|
|
||||||
promptDialog.sendKeys(authenticatorLabel);
|
promptDialog.sendKeys(authenticatorLabel);
|
||||||
promptDialog.accept();
|
promptDialog.accept();
|
||||||
|
@ -65,7 +70,7 @@ public class WebAuthnRegisterPage extends AbstractPage {
|
||||||
|
|
||||||
private boolean isAIA() {
|
private boolean isAIA() {
|
||||||
try {
|
try {
|
||||||
registerAIAButton.getText();
|
registerButton.getText();
|
||||||
cancelAIAButton.getText();
|
cancelAIAButton.getText();
|
||||||
return true;
|
return true;
|
||||||
} catch (NoSuchElementException e) {
|
} catch (NoSuchElementException e) {
|
||||||
|
@ -74,14 +79,14 @@ public class WebAuthnRegisterPage extends AbstractPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isCurrent() {
|
public boolean isCurrent() {
|
||||||
if (isAIA()) {
|
try {
|
||||||
return true;
|
registerButton.getText();
|
||||||
|
return driver.getPageSource().contains("navigator.credentials.create");
|
||||||
|
} catch (NoSuchElementException e) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
// Cant verify the page in case that prompt is shown. Prompt is shown immediately when WebAuthnRegisterPage is displayed
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void open() {
|
public void open() {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.webauthn.updaters;
|
||||||
|
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public abstract class AbstractWebAuthnRealmUpdater<T extends AbstractWebAuthnRealmUpdater> extends RealmAttributeUpdater {
|
||||||
|
|
||||||
|
public AbstractWebAuthnRealmUpdater(RealmResource resource) {
|
||||||
|
super(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract T setWebAuthnPolicyRpEntityName(String webAuthnPolicyRpEntityName);
|
||||||
|
|
||||||
|
public abstract T setWebAuthnPolicyCreateTimeout(Integer webAuthnPolicyCreateTimeout);
|
||||||
|
|
||||||
|
public abstract T setWebAuthnPolicyAvoidSameAuthenticatorRegister(Boolean webAuthnPolicyAvoidSameAuthenticatorRegister);
|
||||||
|
|
||||||
|
public abstract T setWebAuthnPolicySignatureAlgorithms(List<String> webAuthnPolicySignatureAlgorithms);
|
||||||
|
|
||||||
|
public abstract T setWebAuthnPolicyAttestationConveyancePreference(String webAuthnPolicyAttestationConveyancePreference);
|
||||||
|
|
||||||
|
public abstract T setWebAuthnPolicyAuthenticatorAttachment(String webAuthnPolicyAuthenticatorAttachment);
|
||||||
|
|
||||||
|
public abstract T setWebAuthnPolicyRequireResidentKey(String webAuthnPolicyRequireResidentKey);
|
||||||
|
|
||||||
|
public abstract T setWebAuthnPolicyRpId(String webAuthnPolicyRpId);
|
||||||
|
|
||||||
|
public abstract T setWebAuthnPolicyUserVerificationRequirement(String webAuthnPolicyUserVerificationRequirement);
|
||||||
|
|
||||||
|
public abstract T setWebAuthnPolicyAcceptableAaguids(List<String> webAuthnPolicyAcceptableAaguids);
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.webauthn.updaters;
|
||||||
|
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PasswordLessRealmAttributeUpdater extends AbstractWebAuthnRealmUpdater<PasswordLessRealmAttributeUpdater> {
|
||||||
|
public PasswordLessRealmAttributeUpdater(RealmResource resource) {
|
||||||
|
super(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PasswordLessRealmAttributeUpdater setWebAuthnPolicyRpEntityName(String webAuthnPolicyRpEntityName) {
|
||||||
|
rep.setWebAuthnPolicyPasswordlessRpEntityName(webAuthnPolicyRpEntityName);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PasswordLessRealmAttributeUpdater setWebAuthnPolicyCreateTimeout(Integer webAuthnPolicyCreateTimeout) {
|
||||||
|
rep.setWebAuthnPolicyPasswordlessCreateTimeout(webAuthnPolicyCreateTimeout);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PasswordLessRealmAttributeUpdater setWebAuthnPolicyAvoidSameAuthenticatorRegister(Boolean webAuthnPolicyAvoidSameAuthenticatorRegister) {
|
||||||
|
rep.setWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister(webAuthnPolicyAvoidSameAuthenticatorRegister);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PasswordLessRealmAttributeUpdater setWebAuthnPolicySignatureAlgorithms(List<String> webAuthnPolicySignatureAlgorithms) {
|
||||||
|
rep.setWebAuthnPolicyPasswordlessSignatureAlgorithms(webAuthnPolicySignatureAlgorithms);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PasswordLessRealmAttributeUpdater setWebAuthnPolicyAttestationConveyancePreference(String webAuthnPolicyAttestationConveyancePreference) {
|
||||||
|
rep.setWebAuthnPolicyPasswordlessAttestationConveyancePreference(webAuthnPolicyAttestationConveyancePreference);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PasswordLessRealmAttributeUpdater setWebAuthnPolicyAuthenticatorAttachment(String webAuthnPolicyAuthenticatorAttachment) {
|
||||||
|
rep.setWebAuthnPolicyPasswordlessAuthenticatorAttachment(webAuthnPolicyAuthenticatorAttachment);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PasswordLessRealmAttributeUpdater setWebAuthnPolicyRequireResidentKey(String webAuthnPolicyRequireResidentKey) {
|
||||||
|
rep.setWebAuthnPolicyPasswordlessRequireResidentKey(webAuthnPolicyRequireResidentKey);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PasswordLessRealmAttributeUpdater setWebAuthnPolicyRpId(String webAuthnPolicyRpId) {
|
||||||
|
rep.setWebAuthnPolicyPasswordlessRpId(webAuthnPolicyRpId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PasswordLessRealmAttributeUpdater setWebAuthnPolicyUserVerificationRequirement(String webAuthnPolicyUserVerificationRequirement) {
|
||||||
|
rep.setWebAuthnPolicyPasswordlessUserVerificationRequirement(webAuthnPolicyUserVerificationRequirement);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PasswordLessRealmAttributeUpdater setWebAuthnPolicyAcceptableAaguids(List<String> webAuthnPolicyAcceptableAaguids) {
|
||||||
|
rep.setWebAuthnPolicyPasswordlessAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.webauthn.updaters;
|
||||||
|
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class WebAuthnRealmAttributeUpdater extends AbstractWebAuthnRealmUpdater<WebAuthnRealmAttributeUpdater> {
|
||||||
|
public WebAuthnRealmAttributeUpdater(RealmResource resource) {
|
||||||
|
super(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebAuthnRealmAttributeUpdater setWebAuthnPolicyRpEntityName(String webAuthnPolicyRpEntityName) {
|
||||||
|
rep.setWebAuthnPolicyRpEntityName(webAuthnPolicyRpEntityName);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebAuthnRealmAttributeUpdater setWebAuthnPolicyCreateTimeout(Integer webAuthnPolicyCreateTimeout) {
|
||||||
|
rep.setWebAuthnPolicyCreateTimeout(webAuthnPolicyCreateTimeout);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebAuthnRealmAttributeUpdater setWebAuthnPolicyAvoidSameAuthenticatorRegister(Boolean webAuthnPolicyAvoidSameAuthenticatorRegister) {
|
||||||
|
rep.setWebAuthnPolicyAvoidSameAuthenticatorRegister(webAuthnPolicyAvoidSameAuthenticatorRegister);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebAuthnRealmAttributeUpdater setWebAuthnPolicySignatureAlgorithms(List<String> webAuthnPolicySignatureAlgorithms) {
|
||||||
|
rep.setWebAuthnPolicySignatureAlgorithms(webAuthnPolicySignatureAlgorithms);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebAuthnRealmAttributeUpdater setWebAuthnPolicyAttestationConveyancePreference(String webAuthnPolicyAttestationConveyancePreference) {
|
||||||
|
rep.setWebAuthnPolicyAttestationConveyancePreference(webAuthnPolicyAttestationConveyancePreference);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebAuthnRealmAttributeUpdater setWebAuthnPolicyAuthenticatorAttachment(String webAuthnPolicyAuthenticatorAttachment) {
|
||||||
|
rep.setWebAuthnPolicyAuthenticatorAttachment(webAuthnPolicyAuthenticatorAttachment);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebAuthnRealmAttributeUpdater setWebAuthnPolicyRequireResidentKey(String webAuthnPolicyRequireResidentKey) {
|
||||||
|
rep.setWebAuthnPolicyRequireResidentKey(webAuthnPolicyRequireResidentKey);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebAuthnRealmAttributeUpdater setWebAuthnPolicyRpId(String webAuthnPolicyRpId) {
|
||||||
|
rep.setWebAuthnPolicyRpId(webAuthnPolicyRpId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebAuthnRealmAttributeUpdater setWebAuthnPolicyUserVerificationRequirement(String webAuthnPolicyUserVerificationRequirement) {
|
||||||
|
rep.setWebAuthnPolicyUserVerificationRequirement(webAuthnPolicyUserVerificationRequirement);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebAuthnRealmAttributeUpdater setWebAuthnPolicyAcceptableAaguids(List<String> webAuthnPolicyAcceptableAaguids) {
|
||||||
|
rep.setWebAuthnPolicyAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.webauthn;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
import org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions;
|
||||||
|
import org.keycloak.testsuite.webauthn.authenticators.VirtualAuthenticatorManager;
|
||||||
|
import org.openqa.selenium.WebDriver;
|
||||||
|
import org.openqa.selenium.virtualauthenticator.HasVirtualAuthenticator;
|
||||||
|
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.instanceOf;
|
||||||
|
import static org.junit.Assume.assumeThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class for WebAuthn tests which use Virtual Authenticators
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||||
|
*/
|
||||||
|
@EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true)
|
||||||
|
public abstract class AbstractWebAuthnVirtualTest extends AbstractTestRealmKeycloakTest implements UseVirtualAuthenticators {
|
||||||
|
|
||||||
|
private VirtualAuthenticatorManager virtualAuthenticatorsManager;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
@Override
|
||||||
|
public void setUpVirtualAuthenticator() {
|
||||||
|
assumeThat("Driver must support Virtual Authenticators", driver, instanceOf(HasVirtualAuthenticator.class));
|
||||||
|
this.virtualAuthenticatorsManager = createDefaultVirtualManager(driver, getDefaultAuthenticatorOptions());
|
||||||
|
clearEventQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void removeVirtualAuthenticator() {
|
||||||
|
virtualAuthenticatorsManager.removeAuthenticator();
|
||||||
|
clearEventQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public VirtualAuthenticatorOptions getDefaultAuthenticatorOptions() {
|
||||||
|
return DefaultVirtualAuthOptions.DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VirtualAuthenticatorManager getDefaultVirtualAuthManager() {
|
||||||
|
return virtualAuthenticatorsManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getDefaultVirtualAuthManager(VirtualAuthenticatorManager manager) {
|
||||||
|
this.virtualAuthenticatorsManager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void clearEventQueue() {
|
||||||
|
getTestingClient().testing().clearEventQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VirtualAuthenticatorManager createDefaultVirtualManager(WebDriver webDriver, VirtualAuthenticatorOptions options) {
|
||||||
|
VirtualAuthenticatorManager manager = new VirtualAuthenticatorManager(webDriver);
|
||||||
|
manager.useAuthenticator(options);
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@
|
||||||
package org.keycloak.testsuite.webauthn;
|
package org.keycloak.testsuite.webauthn;
|
||||||
|
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory;
|
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory;
|
||||||
|
@ -26,14 +27,15 @@ import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||||
import org.keycloak.testsuite.WebAuthnAssume;
|
|
||||||
import org.keycloak.testsuite.actions.AbstractAppInitiatedActionTest;
|
import org.keycloak.testsuite.actions.AbstractAppInitiatedActionTest;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
|
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
|
||||||
import org.keycloak.testsuite.pages.PasswordPage;
|
import org.keycloak.testsuite.pages.PasswordPage;
|
||||||
import org.keycloak.testsuite.pages.webauthn.WebAuthnRegisterPage;
|
|
||||||
import org.keycloak.testsuite.util.FlowUtil;
|
import org.keycloak.testsuite.util.FlowUtil;
|
||||||
|
import org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions;
|
||||||
|
import org.keycloak.testsuite.webauthn.authenticators.VirtualAuthenticatorManager;
|
||||||
|
import org.keycloak.testsuite.webauthn.pages.WebAuthnRegisterPage;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -48,7 +50,9 @@ import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerEx
|
||||||
*/
|
*/
|
||||||
@EnableFeature(value = WEB_AUTHN, skipRestart = true, onlyForProduct = true)
|
@EnableFeature(value = WEB_AUTHN, skipRestart = true, onlyForProduct = true)
|
||||||
@AuthServerContainerExclude(REMOTE)
|
@AuthServerContainerExclude(REMOTE)
|
||||||
public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTest {
|
public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTest implements UseVirtualAuthenticators {
|
||||||
|
|
||||||
|
private VirtualAuthenticatorManager virtualManager;
|
||||||
|
|
||||||
@Page
|
@Page
|
||||||
LoginUsernameOnlyPage usernamePage;
|
LoginUsernameOnlyPage usernamePage;
|
||||||
|
@ -57,7 +61,19 @@ public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTe
|
||||||
PasswordPage passwordPage;
|
PasswordPage passwordPage;
|
||||||
|
|
||||||
@Page
|
@Page
|
||||||
WebAuthnRegisterPage registerPage;
|
WebAuthnRegisterPage webAuthnRegisterPage;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
@Override
|
||||||
|
public void setUpVirtualAuthenticator() {
|
||||||
|
virtualManager = AbstractWebAuthnVirtualTest.createDefaultVirtualManager(driver, DefaultVirtualAuthOptions.DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
@Override
|
||||||
|
public void removeVirtualAuthenticator() {
|
||||||
|
virtualManager.removeAuthenticator();
|
||||||
|
}
|
||||||
|
|
||||||
public AppInitiatedActionWebAuthnTest() {
|
public AppInitiatedActionWebAuthnTest() {
|
||||||
super(WebAuthnRegisterFactory.PROVIDER_ID);
|
super(WebAuthnRegisterFactory.PROVIDER_ID);
|
||||||
|
@ -99,23 +115,30 @@ public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTe
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
|
||||||
public void verifyEnvironment() {
|
|
||||||
WebAuthnAssume.assumeChrome();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void cancelSetupWebAuthn() {
|
public void cancelSetupWebAuthn() {
|
||||||
loginUser();
|
loginUser();
|
||||||
|
|
||||||
doAIA();
|
doAIA();
|
||||||
|
|
||||||
registerPage.assertCurrent();
|
webAuthnRegisterPage.assertCurrent();
|
||||||
registerPage.cancelAIA();
|
webAuthnRegisterPage.cancelAIA();
|
||||||
|
|
||||||
assertKcActionStatus("cancelled");
|
assertKcActionStatus("cancelled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void proceedSetupWebAuthn() {
|
||||||
|
loginUser();
|
||||||
|
|
||||||
|
doAIA();
|
||||||
|
|
||||||
|
webAuthnRegisterPage.assertCurrent();
|
||||||
|
webAuthnRegisterPage.clickRegister();
|
||||||
|
webAuthnRegisterPage.registerWebAuthnCredential("authenticator1");
|
||||||
|
assertKcActionStatus("success");
|
||||||
|
}
|
||||||
|
|
||||||
private void loginUser() {
|
private void loginUser() {
|
||||||
usernamePage.open();
|
usernamePage.open();
|
||||||
usernamePage.assertCurrent();
|
usernamePage.assertCurrent();
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.webauthn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for test classes which use Virtual Authenticators
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||||
|
*/
|
||||||
|
public interface UseVirtualAuthenticators {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up Virtual Authenticator in @Before method for each test method
|
||||||
|
*/
|
||||||
|
void setUpVirtualAuthenticator();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove Virtual Authenticator in @After method for each test method
|
||||||
|
*/
|
||||||
|
void removeVirtualAuthenticator();
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.webauthn;
|
||||||
|
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
|
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.testsuite.util.SecondBrowser;
|
||||||
|
import org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions;
|
||||||
|
import org.keycloak.testsuite.webauthn.authenticators.KcVirtualAuthenticator;
|
||||||
|
import org.keycloak.testsuite.webauthn.authenticators.VirtualAuthenticatorManager;
|
||||||
|
import org.openqa.selenium.WebDriver;
|
||||||
|
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||||
|
import static org.hamcrest.CoreMatchers.nullValue;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test class for VirtualAuthenticatorManager
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||||
|
*/
|
||||||
|
public class VirtualAuthenticatorsManagerTest extends AbstractWebAuthnVirtualTest {
|
||||||
|
|
||||||
|
@Drone
|
||||||
|
@SecondBrowser
|
||||||
|
WebDriver driver2;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddVirtualAuthenticator() {
|
||||||
|
final VirtualAuthenticatorManager manager = new VirtualAuthenticatorManager(driver);
|
||||||
|
assertThat(manager, notNullValue());
|
||||||
|
|
||||||
|
KcVirtualAuthenticator authenticator = useDefaultTestingAuthenticator(manager);
|
||||||
|
assertAuthenticatorOptions(authenticator);
|
||||||
|
|
||||||
|
manager.removeAuthenticator();
|
||||||
|
assertThat(manager.getActualAuthenticator(), Matchers.nullValue());
|
||||||
|
|
||||||
|
authenticator = useDefaultTestingAuthenticator(manager);
|
||||||
|
assertAuthenticatorOptions(authenticator);
|
||||||
|
|
||||||
|
manager.removeAuthenticator();
|
||||||
|
assertThat(manager.getActualAuthenticator(), Matchers.nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOverrideUsedAuthenticator() {
|
||||||
|
final VirtualAuthenticatorManager manager = new VirtualAuthenticatorManager(driver);
|
||||||
|
assertThat(manager, notNullValue());
|
||||||
|
|
||||||
|
KcVirtualAuthenticator defaultTesting = useDefaultTestingAuthenticator(manager);
|
||||||
|
assertAuthenticatorOptions(defaultTesting);
|
||||||
|
assertThat(manager.getActualAuthenticator(), is(defaultTesting));
|
||||||
|
|
||||||
|
VirtualAuthenticatorOptions defaultBteOptions = DefaultVirtualAuthOptions.DEFAULT_BTE;
|
||||||
|
assertThat(defaultBteOptions, notNullValue());
|
||||||
|
|
||||||
|
KcVirtualAuthenticator defaultBTE = manager.useAuthenticator(defaultBteOptions);
|
||||||
|
assertThat(defaultBTE, notNullValue());
|
||||||
|
assertAuthenticatorOptions(defaultTesting);
|
||||||
|
|
||||||
|
assertThat(manager.getActualAuthenticator(), is(defaultBTE));
|
||||||
|
assertThat(manager.getActualAuthenticator().getOptions().clone(), is(defaultBteOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDifferentDriver() {
|
||||||
|
final VirtualAuthenticatorManager manager = new VirtualAuthenticatorManager(driver);
|
||||||
|
assertThat(manager, notNullValue());
|
||||||
|
|
||||||
|
KcVirtualAuthenticator authenticator = useDefaultTestingAuthenticator(manager);
|
||||||
|
assertThat(authenticator, notNullValue());
|
||||||
|
assertThat(manager.getActualAuthenticator(), notNullValue());
|
||||||
|
|
||||||
|
final VirtualAuthenticatorManager manager2 = new VirtualAuthenticatorManager(driver2);
|
||||||
|
assertThat(manager2, notNullValue());
|
||||||
|
assertThat(manager2.getActualAuthenticator(), nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KcVirtualAuthenticator useDefaultTestingAuthenticator(VirtualAuthenticatorManager manager) {
|
||||||
|
KcVirtualAuthenticator authenticator = manager.useAuthenticator(defaultTestingAuthenticatorOptions());
|
||||||
|
assertThat(authenticator, notNullValue());
|
||||||
|
|
||||||
|
assertThat(manager.getActualAuthenticator(), is(authenticator));
|
||||||
|
|
||||||
|
return authenticator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertAuthenticatorOptions(KcVirtualAuthenticator authenticator) {
|
||||||
|
KcVirtualAuthenticator.Options options = authenticator.getOptions();
|
||||||
|
assertThat(options, notNullValue());
|
||||||
|
assertThat(options.getProtocol(), is(VirtualAuthenticatorOptions.Protocol.CTAP2));
|
||||||
|
assertThat(options.getTransport(), is(VirtualAuthenticatorOptions.Transport.BLE));
|
||||||
|
assertThat(options.hasUserVerification(), is(true));
|
||||||
|
assertThat(options.isUserConsenting(), is(false));
|
||||||
|
assertThat(options.hasResidentKey(), is(true));
|
||||||
|
assertThat(options.isUserVerified(), is(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VirtualAuthenticatorOptions defaultTestingAuthenticatorOptions() {
|
||||||
|
return new VirtualAuthenticatorOptions()
|
||||||
|
.setProtocol(VirtualAuthenticatorOptions.Protocol.CTAP2)
|
||||||
|
.setTransport(VirtualAuthenticatorOptions.Transport.BLE)
|
||||||
|
.setHasUserVerification(true)
|
||||||
|
.setIsUserConsenting(false)
|
||||||
|
.setHasResidentKey(true)
|
||||||
|
.setIsUserVerified(true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,11 +18,7 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.webauthn;
|
package org.keycloak.testsuite.webauthn;
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Assume;
|
|
||||||
import org.junit.BeforeClass;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.authentication.AuthenticatorSpi;
|
import org.keycloak.authentication.AuthenticatorSpi;
|
||||||
import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory;
|
import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory;
|
||||||
|
@ -33,7 +29,7 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
|
||||||
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SSL_REQUIRED;
|
import java.util.Set;
|
||||||
|
|
||||||
@EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true)
|
@EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true)
|
||||||
public class WebAuthnFeatureTest extends AbstractTestRealmKeycloakTest {
|
public class WebAuthnFeatureTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
@ -42,18 +38,11 @@ public class WebAuthnFeatureTest extends AbstractTestRealmKeycloakTest {
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeClass
|
|
||||||
public static void enabled() {
|
|
||||||
Assume.assumeTrue(AUTH_SERVER_SSL_REQUIRED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testWebAuthnEnabled() {
|
public void testWebAuthnEnabled() {
|
||||||
testWebAuthnAvailability(true);
|
testWebAuthnAvailability(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This class should not use "WebAuthnAssume". Otherwise this test won't re-enable the WebAuthn feature and will later fail when executed with
|
|
||||||
// the "product" profile
|
|
||||||
@Test
|
@Test
|
||||||
@DisableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true)
|
@DisableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true)
|
||||||
public void testWebAuthnDisabled() {
|
public void testWebAuthnDisabled() {
|
||||||
|
@ -65,6 +54,4 @@ public class WebAuthnFeatureTest extends AbstractTestRealmKeycloakTest {
|
||||||
Set<String> authenticatorProviderIds = serverInfo.getProviders().get(AuthenticatorSpi.SPI_NAME).getProviders().keySet();
|
Set<String> authenticatorProviderIds = serverInfo.getProviders().get(AuthenticatorSpi.SPI_NAME).getProviders().keySet();
|
||||||
Assert.assertEquals(expectedAvailability, authenticatorProviderIds.contains(WebAuthnAuthenticatorFactory.PROVIDER_ID));
|
Assert.assertEquals(expectedAvailability, authenticatorProviderIds.contains(WebAuthnAuthenticatorFactory.PROVIDER_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,429 @@
|
||||||
|
/*
|
||||||
|
* 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.webauthn;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.WebAuthnConstants;
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
|
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory;
|
||||||
|
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory;
|
||||||
|
import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory;
|
||||||
|
import org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory;
|
||||||
|
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
|
||||||
|
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||||
|
import org.keycloak.common.util.SecretGenerator;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||||
|
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
|
||||||
|
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.testsuite.AssertEvents;
|
||||||
|
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||||
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
|
import org.keycloak.testsuite.pages.AppPage;
|
||||||
|
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
||||||
|
import org.keycloak.testsuite.pages.ErrorPage;
|
||||||
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
|
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
|
||||||
|
import org.keycloak.testsuite.pages.PasswordPage;
|
||||||
|
import org.keycloak.testsuite.pages.RegisterPage;
|
||||||
|
import org.keycloak.testsuite.pages.SelectAuthenticatorPage;
|
||||||
|
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||||
|
import org.keycloak.testsuite.util.FlowUtil;
|
||||||
|
import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage;
|
||||||
|
import org.keycloak.testsuite.webauthn.pages.WebAuthnRegisterPage;
|
||||||
|
import org.keycloak.testsuite.webauthn.updaters.WebAuthnRealmAttributeUpdater;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.hasItem;
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.ALTERNATIVE;
|
||||||
|
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED;
|
||||||
|
|
||||||
|
public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected AppPage appPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected WebAuthnLoginPage webAuthnLoginPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected RegisterPage registerPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected ErrorPage errorPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected WebAuthnRegisterPage webAuthnRegisterPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected LoginUsernameOnlyPage loginUsernamePage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected PasswordPage passwordPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected SelectAuthenticatorPage selectAuthenticatorPage;
|
||||||
|
|
||||||
|
private static final String ALL_ZERO_AAGUID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
|
RealmRepresentation realmRepresentation = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/webauthn/testrealm-webauthn.json"), RealmRepresentation.class);
|
||||||
|
testRealms.add(realmRepresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerUserSuccess() throws IOException {
|
||||||
|
String username = "registerUserSuccess";
|
||||||
|
String password = "password";
|
||||||
|
String email = "registerUserSuccess@email";
|
||||||
|
String userId = null;
|
||||||
|
|
||||||
|
try (RealmAttributeUpdater rau = updateRealmWithDefaultWebAuthnSettings(testRealm()).update()) {
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.clickRegister();
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
|
||||||
|
String authenticatorLabel = SecretGenerator.getInstance().randomString(24);
|
||||||
|
registerPage.register("firstName", "lastName", email, username, password, password);
|
||||||
|
|
||||||
|
// User was registered. Now he needs to register WebAuthn credential
|
||||||
|
webAuthnRegisterPage.assertCurrent();
|
||||||
|
webAuthnRegisterPage.clickRegister();
|
||||||
|
webAuthnRegisterPage.registerWebAuthnCredential(authenticatorLabel);
|
||||||
|
|
||||||
|
appPage.assertCurrent();
|
||||||
|
assertThat(appPage.getRequestType(), is(RequestType.AUTH_RESPONSE));
|
||||||
|
appPage.openAccount();
|
||||||
|
|
||||||
|
// confirm that registration is successfully completed
|
||||||
|
userId = events.expectRegister(username, email).assertEvent().getUserId();
|
||||||
|
// confirm registration event
|
||||||
|
EventRepresentation eventRep = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
|
||||||
|
.user(userId)
|
||||||
|
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||||
|
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel)
|
||||||
|
.detail(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, ALL_ZERO_AAGUID)
|
||||||
|
.assertEvent();
|
||||||
|
String regPubKeyCredentialId = eventRep.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
|
||||||
|
|
||||||
|
// confirm login event
|
||||||
|
String sessionId = events.expectLogin()
|
||||||
|
.user(userId)
|
||||||
|
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||||
|
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel)
|
||||||
|
.assertEvent().getSessionId();
|
||||||
|
// confirm user registered
|
||||||
|
assertUserRegistered(userId, username.toLowerCase(), email.toLowerCase());
|
||||||
|
assertRegisteredCredentials(userId, ALL_ZERO_AAGUID, "none");
|
||||||
|
|
||||||
|
events.clear();
|
||||||
|
|
||||||
|
// logout by user
|
||||||
|
appPage.logout();
|
||||||
|
// confirm logout event
|
||||||
|
events.expectLogout(sessionId)
|
||||||
|
.user(userId)
|
||||||
|
.assertEvent();
|
||||||
|
|
||||||
|
// login by user
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login(username, password);
|
||||||
|
|
||||||
|
webAuthnLoginPage.assertCurrent();
|
||||||
|
webAuthnLoginPage.clickAuthenticate();
|
||||||
|
|
||||||
|
appPage.assertCurrent();
|
||||||
|
assertThat(appPage.getRequestType(), is(RequestType.AUTH_RESPONSE));
|
||||||
|
appPage.openAccount();
|
||||||
|
|
||||||
|
// confirm login event
|
||||||
|
sessionId = events.expectLogin()
|
||||||
|
.user(userId)
|
||||||
|
.detail(WebAuthnConstants.PUBKEY_CRED_ID_ATTR, regPubKeyCredentialId)
|
||||||
|
.detail("web_authn_authenticator_user_verification_checked", Boolean.FALSE.toString())
|
||||||
|
.assertEvent().getSessionId();
|
||||||
|
|
||||||
|
events.clear();
|
||||||
|
// logout by user
|
||||||
|
appPage.logout();
|
||||||
|
// confirm logout event
|
||||||
|
events.expectLogout(sessionId)
|
||||||
|
.user(userId)
|
||||||
|
.assertEvent();
|
||||||
|
} finally {
|
||||||
|
removeFirstCredentialForUser(userId, WebAuthnCredentialModel.TYPE_TWOFACTOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWebAuthnPasswordlessAlternativeWithWebAuthnAndPassword() throws IOException {
|
||||||
|
String userId = null;
|
||||||
|
|
||||||
|
final String WEBAUTHN_LABEL = "webauthn";
|
||||||
|
final String PASSWORDLESS_LABEL = "passwordless";
|
||||||
|
|
||||||
|
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm())
|
||||||
|
.setBrowserFlow(webAuthnTogetherPasswordlessFlow())
|
||||||
|
.update()) {
|
||||||
|
|
||||||
|
UserRepresentation user = ApiUtil.findUserByUsername(testRealm(), "test-user@localhost");
|
||||||
|
assertThat(user, notNullValue());
|
||||||
|
user.getRequiredActions().add(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
|
||||||
|
|
||||||
|
UserResource userResource = testRealm().users().get(user.getId());
|
||||||
|
assertThat(userResource, notNullValue());
|
||||||
|
userResource.update(user);
|
||||||
|
|
||||||
|
user = userResource.toRepresentation();
|
||||||
|
assertThat(user, notNullValue());
|
||||||
|
assertThat(user.getRequiredActions(), hasItem(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID));
|
||||||
|
|
||||||
|
userId = user.getId();
|
||||||
|
|
||||||
|
loginUsernamePage.open();
|
||||||
|
loginUsernamePage.login("test-user@localhost");
|
||||||
|
|
||||||
|
passwordPage.assertCurrent();
|
||||||
|
passwordPage.login("password");
|
||||||
|
|
||||||
|
events.clear();
|
||||||
|
|
||||||
|
webAuthnRegisterPage.assertCurrent();
|
||||||
|
webAuthnRegisterPage.clickRegister();
|
||||||
|
webAuthnRegisterPage.registerWebAuthnCredential(PASSWORDLESS_LABEL);
|
||||||
|
|
||||||
|
webAuthnRegisterPage.assertCurrent();
|
||||||
|
webAuthnRegisterPage.clickRegister();
|
||||||
|
webAuthnRegisterPage.registerWebAuthnCredential(WEBAUTHN_LABEL);
|
||||||
|
|
||||||
|
appPage.assertCurrent();
|
||||||
|
|
||||||
|
events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
|
||||||
|
.user(userId)
|
||||||
|
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID)
|
||||||
|
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, PASSWORDLESS_LABEL)
|
||||||
|
.assertEvent();
|
||||||
|
|
||||||
|
events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
|
||||||
|
.user(userId)
|
||||||
|
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||||
|
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, WEBAUTHN_LABEL)
|
||||||
|
.assertEvent();
|
||||||
|
|
||||||
|
final String sessionID = events.expectLogin()
|
||||||
|
.user(userId)
|
||||||
|
.assertEvent()
|
||||||
|
.getSessionId();
|
||||||
|
|
||||||
|
events.clear();
|
||||||
|
|
||||||
|
appPage.logout();
|
||||||
|
|
||||||
|
events.expectLogout(sessionID)
|
||||||
|
.user(userId)
|
||||||
|
.assertEvent();
|
||||||
|
|
||||||
|
// Password + WebAuthn security key
|
||||||
|
loginUsernamePage.open();
|
||||||
|
loginUsernamePage.assertCurrent();
|
||||||
|
loginUsernamePage.login("test-user@localhost");
|
||||||
|
|
||||||
|
passwordPage.assertCurrent();
|
||||||
|
passwordPage.login("password");
|
||||||
|
|
||||||
|
webAuthnLoginPage.assertCurrent();
|
||||||
|
webAuthnLoginPage.clickAuthenticate();
|
||||||
|
|
||||||
|
appPage.assertCurrent();
|
||||||
|
appPage.logout();
|
||||||
|
|
||||||
|
// Only passwordless login
|
||||||
|
loginUsernamePage.open();
|
||||||
|
loginUsernamePage.login("test-user@localhost");
|
||||||
|
|
||||||
|
passwordPage.assertCurrent();
|
||||||
|
passwordPage.assertTryAnotherWayLinkAvailability(true);
|
||||||
|
passwordPage.clickTryAnotherWayLink();
|
||||||
|
|
||||||
|
selectAuthenticatorPage.assertCurrent();
|
||||||
|
assertThat(selectAuthenticatorPage.getLoginMethodHelpText(SelectAuthenticatorPage.SECURITY_KEY),
|
||||||
|
is("Use your security key for passwordless sign in."));
|
||||||
|
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.SECURITY_KEY);
|
||||||
|
|
||||||
|
webAuthnLoginPage.assertCurrent();
|
||||||
|
webAuthnLoginPage.clickAuthenticate();
|
||||||
|
|
||||||
|
appPage.assertCurrent();
|
||||||
|
appPage.logout();
|
||||||
|
} finally {
|
||||||
|
removeFirstCredentialForUser(userId, WebAuthnCredentialModel.TYPE_TWOFACTOR, WEBAUTHN_LABEL);
|
||||||
|
removeFirstCredentialForUser(userId, WebAuthnCredentialModel.TYPE_PASSWORDLESS, PASSWORDLESS_LABEL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWebAuthnTwoFactorAndWebAuthnPasswordlessTogether() throws IOException {
|
||||||
|
// Change binding to browser-webauthn-passwordless. This is flow, which contains both "webauthn" and "webauthn-passwordless" authenticator
|
||||||
|
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm()).setBrowserFlow("browser-webauthn-passwordless").update()) {
|
||||||
|
// Login as test-user@localhost with password
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
|
||||||
|
errorPage.assertCurrent();
|
||||||
|
|
||||||
|
// User is not allowed to register passwordless authenticator in this flow
|
||||||
|
assertThat(events.poll().getError(), is("invalid_user_credentials"));
|
||||||
|
assertThat(errorPage.getError(), is("Cannot login, credential setup required."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertUserRegistered(String userId, String username, String email) {
|
||||||
|
UserRepresentation user = getUser(userId);
|
||||||
|
assertThat(user, notNullValue());
|
||||||
|
assertThat(user.getCreatedTimestamp(), notNullValue());
|
||||||
|
|
||||||
|
// test that timestamp is current with 60s tollerance
|
||||||
|
assertThat((System.currentTimeMillis() - user.getCreatedTimestamp()) < 60000, is(true));
|
||||||
|
|
||||||
|
// test user info is set from form
|
||||||
|
assertThat(user.getUsername(), is(username.toLowerCase()));
|
||||||
|
assertThat(user.getEmail(), is(email.toLowerCase()));
|
||||||
|
assertThat(user.getFirstName(), is("firstName"));
|
||||||
|
assertThat(user.getLastName(), is("lastName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertRegisteredCredentials(String userId, String aaguid, String attestationStatementFormat) {
|
||||||
|
List<CredentialRepresentation> credentials = getCredentials(userId);
|
||||||
|
credentials.forEach(i -> {
|
||||||
|
if (WebAuthnCredentialModel.TYPE_TWOFACTOR.equals(i.getType())) {
|
||||||
|
try {
|
||||||
|
WebAuthnCredentialData data = JsonSerialization.readValue(i.getCredentialData(), WebAuthnCredentialData.class);
|
||||||
|
assertThat(data.getAaguid(), is(aaguid));
|
||||||
|
assertThat(data.getAttestationStatementFormat(), is(attestationStatementFormat));
|
||||||
|
} catch (IOException e) {
|
||||||
|
Assert.fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected UserRepresentation getUser(String userId) {
|
||||||
|
return testRealm().users().get(userId).toRepresentation();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<CredentialRepresentation> getCredentials(String userId) {
|
||||||
|
return testRealm().users().get(userId).credentials();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WebAuthnRealmAttributeUpdater updateRealmWithDefaultWebAuthnSettings(RealmResource resource) {
|
||||||
|
return new WebAuthnRealmAttributeUpdater(resource)
|
||||||
|
.setWebAuthnPolicySignatureAlgorithms(Collections.singletonList("ES256"))
|
||||||
|
.setWebAuthnPolicyAttestationConveyancePreference("none")
|
||||||
|
.setWebAuthnPolicyAuthenticatorAttachment("cross-platform")
|
||||||
|
.setWebAuthnPolicyRequireResidentKey("No")
|
||||||
|
.setWebAuthnPolicyRpId(null)
|
||||||
|
.setWebAuthnPolicyUserVerificationRequirement("preferred")
|
||||||
|
.setWebAuthnPolicyAcceptableAaguids(Collections.singletonList(ALL_ZERO_AAGUID));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This flow contains:
|
||||||
|
* <p>
|
||||||
|
* UsernameForm REQUIRED
|
||||||
|
* Subflow REQUIRED
|
||||||
|
* ** WebAuthnPasswordlessAuthenticator ALTERNATIVE
|
||||||
|
* ** sub-subflow ALTERNATIVE
|
||||||
|
* **** PasswordForm ALTERNATIVE
|
||||||
|
* **** WebAuthnAuthenticator ALTERNATIVE
|
||||||
|
*
|
||||||
|
* @return flow alias
|
||||||
|
*/
|
||||||
|
private String webAuthnTogetherPasswordlessFlow() {
|
||||||
|
final String newFlowAlias = "browser-together-webauthn-flow";
|
||||||
|
testingClient.server(TEST_REALM_NAME).run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
|
||||||
|
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||||
|
FlowUtil.inCurrentRealm(session)
|
||||||
|
.selectFlow(newFlowAlias)
|
||||||
|
.inForms(forms -> forms
|
||||||
|
.clear()
|
||||||
|
.addAuthenticatorExecution(REQUIRED, UsernameFormFactory.PROVIDER_ID)
|
||||||
|
.addSubFlowExecution(REQUIRED, subFlow -> subFlow
|
||||||
|
.addAuthenticatorExecution(ALTERNATIVE, WebAuthnPasswordlessAuthenticatorFactory.PROVIDER_ID)
|
||||||
|
.addSubFlowExecution(ALTERNATIVE, passwordFlow -> passwordFlow
|
||||||
|
.addAuthenticatorExecution(REQUIRED, PasswordFormFactory.PROVIDER_ID)
|
||||||
|
.addAuthenticatorExecution(REQUIRED, WebAuthnAuthenticatorFactory.PROVIDER_ID))
|
||||||
|
))
|
||||||
|
.defineAsBrowserFlow();
|
||||||
|
});
|
||||||
|
return newFlowAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeFirstCredentialForUser(String userId, String credentialType) {
|
||||||
|
removeFirstCredentialForUser(userId, credentialType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove first occurring credential from user with specific credentialType
|
||||||
|
*
|
||||||
|
* @param userId userId
|
||||||
|
* @param credentialType type of credential
|
||||||
|
* @param assertUserLabel user label of credential
|
||||||
|
*/
|
||||||
|
private void removeFirstCredentialForUser(String userId, String credentialType, String assertUserLabel) {
|
||||||
|
if (userId == null || credentialType == null) return;
|
||||||
|
|
||||||
|
final UserResource userResource = testRealm().users().get(userId);
|
||||||
|
|
||||||
|
final CredentialRepresentation credentialRep = userResource.credentials()
|
||||||
|
.stream()
|
||||||
|
.filter(credential -> credentialType.equals(credential.getType()))
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
|
||||||
|
assertThat(credentialRep, notNullValue());
|
||||||
|
if (assertUserLabel != null) {
|
||||||
|
assertThat(credentialRep.getUserLabel(), is(assertUserLabel));
|
||||||
|
}
|
||||||
|
userResource.removeCredential(credentialRep.getId());
|
||||||
|
}
|
||||||
|
}
|
|
@ -574,28 +574,28 @@
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "auth-spnego",
|
"authenticator": "auth-spnego",
|
||||||
"requirement": "DISABLED",
|
"requirement": "DISABLED",
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "identity-provider-redirector",
|
"authenticator": "identity-provider-redirector",
|
||||||
"requirement": "DISABLED",
|
"requirement": "DISABLED",
|
||||||
"priority": 25,
|
"priority": 25,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 30,
|
"priority": 30,
|
||||||
"flowAlias": "browser-webauthn-forms",
|
"flowAlias": "browser-webauthn-forms",
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": true
|
"authenticatorFlow": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -611,21 +611,21 @@
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "auth-otp-form",
|
"authenticator": "auth-otp-form",
|
||||||
"requirement": "DISABLED",
|
"requirement": "DISABLED",
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "webauthn-authenticator",
|
"authenticator": "webauthn-authenticator",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 21,
|
"priority": 21,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -641,14 +641,14 @@
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 30,
|
"priority": 30,
|
||||||
"flowAlias": "browser-webauthn-passwordless-forms",
|
"flowAlias": "browser-webauthn-passwordless-forms",
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": true
|
"authenticatorFlow": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -664,21 +664,21 @@
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "webauthn-authenticator",
|
"authenticator": "webauthn-authenticator",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "webauthn-authenticator-passwordless",
|
"authenticator": "webauthn-authenticator-passwordless",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 30,
|
"priority": 30,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -694,21 +694,21 @@
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "idp-email-verification",
|
"authenticator": "idp-email-verification",
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 30,
|
"priority": 30,
|
||||||
"flowAlias": "Verify Existing Account by Re-authentication",
|
"flowAlias": "Verify Existing Account by Re-authentication",
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": true
|
"authenticatorFlow": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -724,14 +724,14 @@
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "auth-otp-form",
|
"authenticator": "auth-otp-form",
|
||||||
"requirement": "OPTIONAL",
|
"requirement": "OPTIONAL",
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -747,28 +747,28 @@
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "auth-spnego",
|
"authenticator": "auth-spnego",
|
||||||
"requirement": "DISABLED",
|
"requirement": "DISABLED",
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "identity-provider-redirector",
|
"authenticator": "identity-provider-redirector",
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 25,
|
"priority": 25,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 30,
|
"priority": 30,
|
||||||
"flowAlias": "forms",
|
"flowAlias": "forms",
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": true
|
"authenticatorFlow": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -784,28 +784,28 @@
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "client-jwt",
|
"authenticator": "client-jwt",
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "client-secret-jwt",
|
"authenticator": "client-secret-jwt",
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 30,
|
"priority": 30,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "client-x509",
|
"authenticator": "client-x509",
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 40,
|
"priority": 40,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -821,21 +821,21 @@
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "direct-grant-validate-password",
|
"authenticator": "direct-grant-validate-password",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "direct-grant-validate-otp",
|
"authenticator": "direct-grant-validate-otp",
|
||||||
"requirement": "OPTIONAL",
|
"requirement": "OPTIONAL",
|
||||||
"priority": 30,
|
"priority": 30,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -851,7 +851,7 @@
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -868,7 +868,7 @@
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticatorConfig": "create unique user config",
|
"authenticatorConfig": "create unique user config",
|
||||||
|
@ -876,14 +876,14 @@
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 30,
|
"priority": 30,
|
||||||
"flowAlias": "Handle Existing Account",
|
"flowAlias": "Handle Existing Account",
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": true
|
"authenticatorFlow": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -899,14 +899,14 @@
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "auth-otp-form",
|
"authenticator": "auth-otp-form",
|
||||||
"requirement": "OPTIONAL",
|
"requirement": "OPTIONAL",
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -922,28 +922,28 @@
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "basic-auth",
|
"authenticator": "basic-auth",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "basic-auth-otp",
|
"authenticator": "basic-auth-otp",
|
||||||
"requirement": "DISABLED",
|
"requirement": "DISABLED",
|
||||||
"priority": 30,
|
"priority": 30,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "auth-spnego",
|
"authenticator": "auth-spnego",
|
||||||
"requirement": "DISABLED",
|
"requirement": "DISABLED",
|
||||||
"priority": 40,
|
"priority": 40,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -960,7 +960,7 @@
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"flowAlias": "registration form",
|
"flowAlias": "registration form",
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": true
|
"authenticatorFlow": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -976,28 +976,28 @@
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "registration-profile-action",
|
"authenticator": "registration-profile-action",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 40,
|
"priority": 40,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "registration-password-action",
|
"authenticator": "registration-password-action",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 50,
|
"priority": 50,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "registration-recaptcha-action",
|
"authenticator": "registration-recaptcha-action",
|
||||||
"requirement": "DISABLED",
|
"requirement": "DISABLED",
|
||||||
"priority": 60,
|
"priority": 60,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1013,28 +1013,28 @@
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "reset-credential-email",
|
"authenticator": "reset-credential-email",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "reset-password",
|
"authenticator": "reset-password",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 30,
|
"priority": 30,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authenticator": "reset-otp",
|
"authenticator": "reset-otp",
|
||||||
"requirement": "OPTIONAL",
|
"requirement": "OPTIONAL",
|
||||||
"priority": 40,
|
"priority": 40,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1050,7 +1050,7 @@
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": false
|
"authenticatorFlow": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1142,4 +1142,4 @@
|
||||||
"supportedLocales": ["en", "de"],
|
"supportedLocales": ["en", "de"],
|
||||||
"defaultLocale": "en",
|
"defaultLocale": "en",
|
||||||
"eventsListeners": ["jboss-logging", "event-queue"]
|
"eventsListeners": ["jboss-logging", "event-queue"]
|
||||||
}
|
}
|
|
@ -28,7 +28,7 @@
|
||||||
<form class="${properties.kcFormClass!}">
|
<form class="${properties.kcFormClass!}">
|
||||||
<div class="${properties.kcFormGroupClass!}">
|
<div class="${properties.kcFormGroupClass!}">
|
||||||
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
|
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
|
||||||
<input type="button" onclick="webAuthnAuthenticate()" value="${kcSanitize(msg("webauthn-doAuthenticate"))}"
|
<input id="authenticateWebAuthnButton" type="button" onclick="webAuthnAuthenticate()" value="${kcSanitize(msg("webauthn-doAuthenticate"))}"
|
||||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}">
|
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -154,14 +154,14 @@
|
||||||
|
|
||||||
<input type="submit"
|
<input type="submit"
|
||||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
|
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
|
||||||
id="registerWebAuthnAIA" value="${msg("doRegister")}" onclick="registerSecurityKey()"/>
|
id="registerWebAuthn" value="${msg("doRegister")}" onclick="registerSecurityKey()"/>
|
||||||
|
|
||||||
<#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
|
<#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
|
||||||
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form"
|
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form"
|
||||||
method="post">
|
method="post">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
|
class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
|
||||||
id="cancelWebAuthnAIA" name="cancel-aia" value="true"/>${msg("doCancel")}
|
id="cancelWebAuthnAIA" name="cancel-aia" value="true">${msg("doCancel")}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</#if>
|
</#if>
|
||||||
|
|
Loading…
Reference in a new issue