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.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" ]
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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=")));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.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();
|
||||
|
|
|
@ -161,6 +161,12 @@
|
|||
<module>springboot-tests</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>webauthn</id>
|
||||
<modules>
|
||||
<module>webauthn</module>
|
||||
</modules>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
</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.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");
|
|
@ -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
|
|
@ -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();
|
|
@ -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;
|
||||
|
||||
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();
|
|
@ -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;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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",
|
||||
"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"]
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue