KEYCLOAK-13319 Use newest WebDriver/Selenium for the WebAuthn testing

This commit is contained in:
Martin Bartoš 2021-09-08 15:35:41 +02:00 committed by Marek Posolda
parent a0b9e4f3eb
commit 7dc01a5a6e
26 changed files with 1412 additions and 517 deletions

View file

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

View file

@ -7,7 +7,6 @@ import org.keycloak.testsuite.util.DroneUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
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...)
@ -23,6 +22,9 @@ public class SelectAuthenticatorPage extends LanguageComboboxAwarePage {
// Corresponds to the OTPFormAuthenticator
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" ]
*/

View file

@ -92,4 +92,9 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
rep.setVerifyEmail(value);
return this;
}
public RealmAttributeUpdater setBrowserFlow(String browserFlow) {
rep.setBrowserFlow(browserFlow);
return this;
}
}

View file

@ -19,7 +19,6 @@ package org.keycloak.testsuite.actions;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Rule;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
@ -32,8 +31,19 @@ import org.keycloak.testsuite.util.WaitUtils;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
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;
/**
@ -58,39 +68,40 @@ public abstract class AbstractAppInitiatedActionTest extends AbstractTestRealmKe
protected void doAIA() {
UriBuilder builder = OIDCLoginProtocolService.authUrl(authServerPage.createUriBuilder());
String uri = builder.queryParam("kc_action", this.aiaAction)
.queryParam("response_type", "code")
.queryParam("client_id", "test-app")
.queryParam("scope", "openid")
.queryParam("redirect_uri", getAuthServerContextRoot() + "/auth/realms/master/app/auth")
String uri = builder.queryParam(KC_ACTION, this.aiaAction)
.queryParam(RESPONSE_TYPE, "code")
.queryParam(CLIENT_ID, "test-app")
.queryParam(SCOPE, "openid")
.queryParam(REDIRECT_URI, getAuthServerContextRoot() + "/auth/realms/master/app/auth")
.build(TEST_REALM_NAME).toString();
driver.navigate().to(uri);
WaitUtils.waitForPageToLoad();
}
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 {
url = new URI(this.driver.getCurrentUrl());
} catch (URISyntaxException 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;
for (NameValuePair p : pairs) {
if (p.getName().equals("kc_action_status")) {
if (p.getName().equals(KC_ACTION_STATUS)) {
kcActionStatus = p.getValue();
break;
}
}
Assert.assertEquals(expectedStatus, kcActionStatus);
assertThat(expectedStatus, is(kcActionStatus));
}
protected void assertSilentCancelMessage() {
String url = this.driver.getCurrentUrl();
Assert.assertFalse("Expected no 'error=' in url", url.contains("error="));
Assert.assertFalse("Expected no 'error_description=' in url", url.contains("error_description="));
assertThat("Expected no 'error=' in url", url, not(containsString("error=")));
assertThat("Expected no 'error_description=' in url", url, not(containsString("error_description=")));
}
}

View file

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

View file

@ -36,12 +36,10 @@ import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation;
import org.keycloak.testsuite.WebAuthnAssume;
import org.keycloak.testsuite.admin.Users;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.auth.page.login.OTPSetup;
import org.keycloak.testsuite.auth.page.login.UpdatePassword;
import org.keycloak.testsuite.pages.webauthn.WebAuthnRegisterPage;
import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage;
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.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED;
import static org.keycloak.models.UserModel.RequiredAction.CONFIGURE_TOTP;
@ -78,8 +75,8 @@ public class SigningInTest extends BaseAccountPageTest {
@Page
private OTPSetup otpSetupPage;
@Page
private WebAuthnRegisterPage webAuthnRegisterPage;
/* @Page
private WebAuthnRegisterPage webAuthnRegisterPage;*/
private SigningInPage.CredentialType passwordCredentialType;
private SigningInPage.CredentialType otpCredentialType;
@ -252,7 +249,7 @@ public class SigningInTest extends BaseAccountPageTest {
testRemoveCredential(otp1);
}
@Test
/* @Test
public void twoFactorWebAuthnTest() {
testWebAuthn(false);
}
@ -265,8 +262,6 @@ public class SigningInTest extends BaseAccountPageTest {
private void testWebAuthn(boolean passwordless) {
testContext.setTestRealmReps(emptyList());
WebAuthnAssume.assumeChrome(driver); // we need some special flags to be able to register security key
SigningInPage.CredentialType credentialType;
final String expectedHelpText;
final String providerId;
@ -313,7 +308,7 @@ public class SigningInTest extends BaseAccountPageTest {
assertEquals(2, credentialType.getUserCredentialsCount());
testRemoveCredential(webAuthn1);
}
}*/
@Test
public void setUpLinksTest() {
@ -351,17 +346,18 @@ public class SigningInTest extends BaseAccountPageTest {
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;
credentialType.clickSetUpLink();
webAuthnRegisterPage.confirmAIA();
webAuthnRegisterPage.assertCurrent();
webAuthnRegisterPage.clickRegister();
webAuthnRegisterPage.registerWebAuthnCredential(label);
waitForPageToLoad();
signingInPage.assertCurrent();
return getNewestUserCredential(credentialType);
}
}*/
private SigningInPage.UserCredential getNewestUserCredential(SigningInPage.CredentialType credentialType) {
List<CredentialRepresentation> credentials = testUserResource().credentials();

View file

@ -161,6 +161,12 @@
<module>springboot-tests</module>
</modules>
</profile>
<profile>
<id>webauthn</id>
<modules>
<module>webauthn</module>
</modules>
</profile>
</profiles>
</project>

View 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:&#103;hp_tSeU74eZVGTSupVCap28N8TX0M88YJ06kua9@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:&#103;hp_tSeU74eZVGTSupVCap28N8TX0M88YJ06kua9@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:&#103;hp_tSeU74eZVGTSupVCap28N8TX0M88YJ06kua9@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:&#103;hp_tSeU74eZVGTSupVCap28N8TX0M88YJ06kua9@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>

View file

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

View file

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

View file

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

View file

@ -1,7 +1,8 @@
package org.keycloak.testsuite.pages.webauthn;
package org.keycloak.testsuite.webauthn.pages;
import org.junit.Assert;
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
@ -20,11 +21,13 @@ public class WebAuthnErrorPage extends LanguageComboboxAwarePage {
private WebElement cancelRegistrationAIA;
public void clickTryAgain() {
WaitUtils.waitUntilElement(tryAgainButton).is().clickable();
tryAgainButton.click();
}
public void clickCancelRegistrationAIA() {
try {
WaitUtils.waitUntilElement(cancelRegistrationAIA).is().clickable();
cancelRegistrationAIA.click();
} catch (NoSuchElementException e) {
Assert.fail("It only works with AIA");

View file

@ -15,17 +15,35 @@
* 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.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
*/
public class WebAuthnLoginPage extends LanguageComboboxAwarePage {
@FindBy(id = "authenticateWebAuthnButton")
private WebElement authenticateButton;
public void clickAuthenticate() {
WaitUtils.waitUntilElement(authenticateButton).is().clickable();
authenticateButton.click();
}
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

View file

@ -15,10 +15,11 @@
* 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.util.WaitUtils;
import org.openqa.selenium.Alert;
import org.openqa.selenium.NoSuchElementException;
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.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.
*
* <p>
* Page will be displayed after successful JS call of "navigator.credentials.create", which will register WebAuthn credential
* with the browser
*/
public class WebAuthnRegisterPage extends AbstractPage {
// Available only with AIA
@FindBy(id = "registerWebAuthnAIA")
private WebElement registerAIAButton;
@FindBy(id = "registerWebAuthn")
private WebElement registerButton;
// Available only with AIA
@FindBy(id = "cancelWebAuthnAIA")
private WebElement cancelAIAButton;
public void confirmAIA() {
Assert.assertTrue("It only works with AIA", isAIA());
registerAIAButton.click();
public void clickRegister() {
WaitUtils.waitUntilElement(registerButton).is().clickable();
registerButton.click();
}
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();
}
public void registerWebAuthnCredential(String authenticatorLabel) {
// 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());
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.accept();
@ -65,7 +70,7 @@ public class WebAuthnRegisterPage extends AbstractPage {
private boolean isAIA() {
try {
registerAIAButton.getText();
registerButton.getText();
cancelAIAButton.getText();
return true;
} catch (NoSuchElementException e) {
@ -74,14 +79,14 @@ public class WebAuthnRegisterPage extends AbstractPage {
}
public boolean isCurrent() {
if (isAIA()) {
return true;
try {
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
public void open() {
throw new UnsupportedOperationException();

View file

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

View file

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

View file

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

View file

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

View file

@ -17,6 +17,7 @@
package org.keycloak.testsuite.webauthn;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
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.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.testsuite.WebAuthnAssume;
import org.keycloak.testsuite.actions.AbstractAppInitiatedActionTest;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
import org.keycloak.testsuite.pages.PasswordPage;
import org.keycloak.testsuite.pages.webauthn.WebAuthnRegisterPage;
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.List;
@ -48,7 +50,9 @@ import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerEx
*/
@EnableFeature(value = WEB_AUTHN, skipRestart = true, onlyForProduct = true)
@AuthServerContainerExclude(REMOTE)
public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTest {
public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTest implements UseVirtualAuthenticators {
private VirtualAuthenticatorManager virtualManager;
@Page
LoginUsernameOnlyPage usernamePage;
@ -57,7 +61,19 @@ public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTe
PasswordPage passwordPage;
@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() {
super(WebAuthnRegisterFactory.PROVIDER_ID);
@ -99,23 +115,30 @@ public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTe
});
}
@Before
public void verifyEnvironment() {
WebAuthnAssume.assumeChrome();
}
@Test
public void cancelSetupWebAuthn() {
loginUser();
doAIA();
registerPage.assertCurrent();
registerPage.cancelAIA();
webAuthnRegisterPage.assertCurrent();
webAuthnRegisterPage.cancelAIA();
assertKcActionStatus("cancelled");
}
@Test
public void proceedSetupWebAuthn() {
loginUser();
doAIA();
webAuthnRegisterPage.assertCurrent();
webAuthnRegisterPage.clickRegister();
webAuthnRegisterPage.registerWebAuthnCredential("authenticator1");
assertKcActionStatus("success");
}
private void loginUser() {
usernamePage.open();
usernamePage.assertCurrent();

View file

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

View file

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

View file

@ -18,11 +18,7 @@
package org.keycloak.testsuite.webauthn;
import java.util.Set;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;
import org.keycloak.authentication.AuthenticatorSpi;
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.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)
public class WebAuthnFeatureTest extends AbstractTestRealmKeycloakTest {
@ -42,18 +38,11 @@ public class WebAuthnFeatureTest extends AbstractTestRealmKeycloakTest {
public void configureTestRealm(RealmRepresentation testRealm) {
}
@BeforeClass
public static void enabled() {
Assume.assumeTrue(AUTH_SERVER_SSL_REQUIRED);
}
@Test
public void testWebAuthnEnabled() {
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
@DisableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true)
public void testWebAuthnDisabled() {
@ -65,6 +54,4 @@ public class WebAuthnFeatureTest extends AbstractTestRealmKeycloakTest {
Set<String> authenticatorProviderIds = serverInfo.getProviders().get(AuthenticatorSpi.SPI_NAME).getProviders().keySet();
Assert.assertEquals(expectedAvailability, authenticatorProviderIds.contains(WebAuthnAuthenticatorFactory.PROVIDER_ID));
}
}

View file

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

View file

@ -574,28 +574,28 @@
"requirement": "ALTERNATIVE",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "auth-spnego",
"requirement": "DISABLED",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "identity-provider-redirector",
"requirement": "DISABLED",
"priority": 25,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"requirement": "ALTERNATIVE",
"priority": 30,
"flowAlias": "browser-webauthn-forms",
"userSetupAllowed": false,
"autheticatorFlow": true
"authenticatorFlow": true
}
]
},
@ -611,21 +611,21 @@
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "auth-otp-form",
"requirement": "DISABLED",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "webauthn-authenticator",
"requirement": "REQUIRED",
"priority": 21,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
}
]
},
@ -641,14 +641,14 @@
"requirement": "ALTERNATIVE",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"requirement": "ALTERNATIVE",
"priority": 30,
"flowAlias": "browser-webauthn-passwordless-forms",
"userSetupAllowed": false,
"autheticatorFlow": true
"authenticatorFlow": true
}
]
},
@ -664,21 +664,21 @@
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "webauthn-authenticator",
"requirement": "REQUIRED",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "webauthn-authenticator-passwordless",
"requirement": "REQUIRED",
"priority": 30,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
}
]
},
@ -694,21 +694,21 @@
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "idp-email-verification",
"requirement": "ALTERNATIVE",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"requirement": "ALTERNATIVE",
"priority": 30,
"flowAlias": "Verify Existing Account by Re-authentication",
"userSetupAllowed": false,
"autheticatorFlow": true
"authenticatorFlow": true
}
]
},
@ -724,14 +724,14 @@
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "auth-otp-form",
"requirement": "OPTIONAL",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
}
]
},
@ -747,28 +747,28 @@
"requirement": "ALTERNATIVE",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "auth-spnego",
"requirement": "DISABLED",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "identity-provider-redirector",
"requirement": "ALTERNATIVE",
"priority": 25,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"requirement": "ALTERNATIVE",
"priority": 30,
"flowAlias": "forms",
"userSetupAllowed": false,
"autheticatorFlow": true
"authenticatorFlow": true
}
]
},
@ -784,28 +784,28 @@
"requirement": "ALTERNATIVE",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "client-jwt",
"requirement": "ALTERNATIVE",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "client-secret-jwt",
"requirement": "ALTERNATIVE",
"priority": 30,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "client-x509",
"requirement": "ALTERNATIVE",
"priority": 40,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
}
]
},
@ -821,21 +821,21 @@
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "direct-grant-validate-password",
"requirement": "REQUIRED",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "direct-grant-validate-otp",
"requirement": "OPTIONAL",
"priority": 30,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
}
]
},
@ -851,7 +851,7 @@
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
}
]
},
@ -868,7 +868,7 @@
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticatorConfig": "create unique user config",
@ -876,14 +876,14 @@
"requirement": "ALTERNATIVE",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"requirement": "ALTERNATIVE",
"priority": 30,
"flowAlias": "Handle Existing Account",
"userSetupAllowed": false,
"autheticatorFlow": true
"authenticatorFlow": true
}
]
},
@ -899,14 +899,14 @@
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "auth-otp-form",
"requirement": "OPTIONAL",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
}
]
},
@ -922,28 +922,28 @@
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "basic-auth",
"requirement": "REQUIRED",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "basic-auth-otp",
"requirement": "DISABLED",
"priority": 30,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "auth-spnego",
"requirement": "DISABLED",
"priority": 40,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
}
]
},
@ -960,7 +960,7 @@
"priority": 10,
"flowAlias": "registration form",
"userSetupAllowed": false,
"autheticatorFlow": true
"authenticatorFlow": true
}
]
},
@ -976,28 +976,28 @@
"requirement": "REQUIRED",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "registration-profile-action",
"requirement": "REQUIRED",
"priority": 40,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "registration-password-action",
"requirement": "REQUIRED",
"priority": 50,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "registration-recaptcha-action",
"requirement": "DISABLED",
"priority": 60,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
}
]
},
@ -1013,28 +1013,28 @@
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "reset-credential-email",
"requirement": "REQUIRED",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "reset-password",
"requirement": "REQUIRED",
"priority": 30,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
},
{
"authenticator": "reset-otp",
"requirement": "OPTIONAL",
"priority": 40,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
}
]
},
@ -1050,7 +1050,7 @@
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
"authenticatorFlow": false
}
]
}
@ -1142,4 +1142,4 @@
"supportedLocales": ["en", "de"],
"defaultLocale": "en",
"eventsListeners": ["jboss-logging", "event-queue"]
}
}

View file

@ -28,7 +28,7 @@
<form class="${properties.kcFormClass!}">
<div class="${properties.kcFormGroupClass!}">
<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!}">
</div>
</div>

View file

@ -154,14 +154,14 @@
<input type="submit"
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>
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form"
method="post">
<button type="submit"
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>
</form>
</#if>