diff --git a/testsuite/integration-arquillian/tests/other/console/pom.xml b/testsuite/integration-arquillian/tests/other/console/pom.xml
index 8343e722ca..45068356db 100644
--- a/testsuite/integration-arquillian/tests/other/console/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/console/pom.xml
@@ -77,6 +77,17 @@
+
+ maven-jar-plugin
+ 2.2
+
+
+
+ test-jar
+
+
+
+
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/idp/mappers/MultivaluedStringProperty.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/idp/mappers/MultivaluedStringProperty.java
index d524c75ca4..3d37097417 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/idp/mappers/MultivaluedStringProperty.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/idp/mappers/MultivaluedStringProperty.java
@@ -41,16 +41,24 @@ public class MultivaluedStringProperty {
@FindBy(xpath = "//button[@data-ng-click='addValueToMultivalued(option.name)']")
private WebElement plusButton;
+ protected List getMinusButtons() {
+ return minusButtons;
+ }
+
+ protected WebElement getPlusButton() {
+ return plusButton;
+ }
+
public boolean isPresent() {
try {
- return plusButton.isDisplayed() && items != null && !items.isEmpty();
+ return getPlusButton().isDisplayed() && getItems() != null && !getItems().isEmpty();
} catch (NoSuchElementException e) {
return false;
}
}
public void clickAddItem() {
- plusButton.click();
+ getPlusButton().click();
}
public List getItems() {
@@ -71,7 +79,10 @@ public class MultivaluedStringProperty {
clickAddItem();
final List items = getItems();
- WebElement webElement = items.get(items.size() - 1);
+ final int index = items.size() - 1;
+
+ validateIndex(index);
+ WebElement webElement = items.get(index);
setTextInputValue(webElement, item);
}
@@ -80,11 +91,12 @@ public class MultivaluedStringProperty {
if (index == getItems().size() - 1) {
editItem(index, "");
} else {
- minusButtons.get(index).click();
+ getMinusButtons().get(index).click();
}
}
private void validateIndex(int index) {
- if (index >= getItems().size()) throw new AssertionError("Input with index: " + index + " does not exist.");
+ if (index < 0 || index >= getItems().size())
+ throw new AssertionError("Input with index: " + index + " does not exist.");
}
}
diff --git a/testsuite/integration-arquillian/tests/other/pom.xml b/testsuite/integration-arquillian/tests/other/pom.xml
index 0cf1792f7d..2e011b125e 100644
--- a/testsuite/integration-arquillian/tests/other/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/pom.xml
@@ -164,6 +164,7 @@
webauthn
+ console
webauthn
diff --git a/testsuite/integration-arquillian/tests/other/webauthn/pom.xml b/testsuite/integration-arquillian/tests/other/webauthn/pom.xml
index 139af1cbaa..d5c9f1814a 100644
--- a/testsuite/integration-arquillian/tests/other/webauthn/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/webauthn/pom.xml
@@ -20,6 +20,17 @@
+
+ org.keycloak.testsuite
+ integration-arquillian-tests-console
+ ${project.version}
+
+
+ org.keycloak.testsuite
+ integration-arquillian-tests-console
+ ${project.version}
+ test-jar
+
org.jboss.arquillian.extension
arquillian-drone-bom
diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/pages/WebAuthnPolicyPage.java b/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/pages/WebAuthnPolicyPage.java
new file mode 100644
index 0000000000..725c5f75ca
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/pages/WebAuthnPolicyPage.java
@@ -0,0 +1,297 @@
+/*
+ * 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.pages;
+
+import com.webauthn4j.data.AttestationConveyancePreference;
+import com.webauthn4j.data.AuthenticatorAttachment;
+import com.webauthn4j.data.UserVerificationRequirement;
+import org.jboss.arquillian.graphene.elements.GrapheneSelect;
+import org.jboss.arquillian.graphene.page.Page;
+import org.keycloak.testsuite.console.page.authentication.Authentication;
+import org.keycloak.testsuite.console.page.fragment.OnOffSwitch;
+import org.keycloak.testsuite.console.page.idp.mappers.MultivaluedStringProperty;
+import org.keycloak.testsuite.webauthn.utils.PropertyRequirement;
+import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.ui.ISelect;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.keycloak.testsuite.util.UIUtils.getTextInputValue;
+import static org.keycloak.testsuite.util.UIUtils.setTextInputValue;
+import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
+
+/**
+ * Helper class for WebAuthnPolicy Page
+ *
+ * @author Martin Bartos
+ */
+public class WebAuthnPolicyPage extends Authentication {
+
+ @FindBy(id = "name")
+ private WebElement rpEntityName;
+
+ @FindBy(xpath = "//select[@id='sigalg']")
+ private GrapheneSelect signatureAlgorithms;
+
+ @FindBy(id = "rpid")
+ private WebElement rpEntityId;
+
+ @FindBy(id = "attpref")
+ private GrapheneSelect attestationConveyancePreference;
+
+ @FindBy(id = "authnatt")
+ private GrapheneSelect authenticatorAttachment;
+
+ @FindBy(id = "reqresident")
+ private GrapheneSelect requireResidentKey;
+
+ @FindBy(id = "usrverify")
+ private GrapheneSelect userVerification;
+
+ @FindBy(id = "timeout")
+ private WebElement timeout;
+
+ @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='avoidsame']]")
+ private OnOffSwitch avoidSameAuthenticatorRegister;
+
+ @Page
+ private MultivaluedAcceptableAaguid acceptableAaguid;
+
+ @FindBy(xpath = "//button[text()='Save']")
+ private WebElement saveButton;
+
+ @FindBy(xpath = "//button[text()='Cancel']")
+ private WebElement cancelButton;
+
+ @Override
+ public String getUriFragment() {
+ return getAuthenticationUriFragment() + "/webauthn-policy";
+ }
+
+ public String getAuthenticationUriFragment() {
+ return super.getUriFragment();
+ }
+
+ /* Relaying Party Entity Name */
+
+ public String getRpEntityName() {
+ waitUntilElement(checkElement(() -> rpEntityName)).is().present();
+ return getTextInputValue(rpEntityName);
+ }
+
+ public void setRpEntityName(String entityName) {
+ waitUntilElement(checkElement(() -> rpEntityName)).is().present();
+ setTextInputValue(rpEntityName, entityName);
+ }
+
+ /* Signature Algorithms */
+
+ public ISelect getSignatureAlgorithms() {
+ GrapheneSelect select = checkElement(() -> signatureAlgorithms);
+ select.setIsMulti(true);
+ return select;
+ }
+
+ /* Relaying Party Entity ID */
+
+ public String getRpEntityId() {
+ waitUntilElement(checkElement(() -> rpEntityId)).is().present();
+ return getTextInputValue(rpEntityId);
+ }
+
+ public void setRpEntityId(String id) {
+ waitUntilElement(checkElement(() -> rpEntityId)).is().present();
+ setTextInputValue(rpEntityId, id);
+ }
+
+ /* Attestation Conveyance Preference */
+
+ public int getAttestationConveyancePreferenceItemsCount() {
+ return checkElement(() -> attestationConveyancePreference.getOptions().size());
+ }
+
+ public AttestationConveyancePreference getAttestationConveyancePreference() {
+ return getRequirementOrNull(() ->
+ AttestationConveyancePreference.create(checkElement(() -> attestationConveyancePreference
+ .getFirstSelectedOption()
+ .getText()))
+ );
+ }
+
+ public void setAttestationConveyancePreference(AttestationConveyancePreference attestation) {
+ checkElement(() -> attestationConveyancePreference)
+ .selectByValue(attestation.getValue());
+ }
+
+ /* Authenticator Attachment */
+
+ public int getAuthenticatorAttachmentItemsCount() {
+ return checkElement(() -> authenticatorAttachment.getOptions().size());
+ }
+
+ public AuthenticatorAttachment getAuthenticatorAttachment() {
+ return getRequirementOrNull(() ->
+ AuthenticatorAttachment.create(checkElement(() -> authenticatorAttachment
+ .getFirstSelectedOption()
+ .getText()))
+ );
+ }
+
+ public void setAuthenticatorAttachment(AuthenticatorAttachment attachment) {
+ checkElement(() -> authenticatorAttachment).selectByValue(attachment.getValue());
+ }
+
+ /* Require Resident Key */
+ // If returns null, the requirement for resident key is not set up
+ public PropertyRequirement requireResidentKey() {
+ final int size = checkElement(() -> requireResidentKey).getAllSelectedOptions().size();
+ if (size == 0) return null;
+
+ final String value = requireResidentKey.getFirstSelectedOption().getText();
+ return PropertyRequirement.fromValue(value);
+ }
+
+ // If parameter state is null, the requirement is considered as not set up
+ public void requireResidentKey(PropertyRequirement requiresProperty) {
+ if (requiresProperty == null) return;
+ GrapheneSelect select = checkElement(() -> requireResidentKey);
+ select.selectByVisibleText(requiresProperty.getValue());
+ }
+
+ /* User Verification Requirement */
+
+ public int getUserVerificationItemsCount() {
+ return checkElement(() -> userVerification).getOptions().size();
+ }
+
+ public UserVerificationRequirement getUserVerification() {
+ return getRequirementOrNull(() ->
+ UserVerificationRequirement.create(checkElement(() -> userVerification
+ .getFirstSelectedOption()
+ .getText()))
+ );
+ }
+
+ public void setUserVerification(UserVerificationRequirement verification) {
+ checkElement(() -> userVerification).selectByValue(verification.getValue());
+ }
+
+ /* Timeout */
+ public int getTimeout() {
+ final String value = getTextInputValue(checkElement(() -> timeout));
+ return checkElement(() -> value == null || value.isEmpty() ? 0 : Integer.parseInt(value));
+ }
+
+ public void setTimeout(Integer time) {
+ waitUntilElement(checkElement(() -> timeout)).is().present();
+ setTextInputValue(timeout, time == null ? "0" : String.valueOf(time));
+ }
+
+ /* Avoid Same Authenticator Registration */
+ public boolean avoidSameAuthenticatorRegistration() {
+ return checkElement(() -> avoidSameAuthenticatorRegister.isOn());
+ }
+
+ public void avoidSameAuthenticatorRegister(boolean state) {
+ if (avoidSameAuthenticatorRegistration() != state) {
+ checkElement(() -> avoidSameAuthenticatorRegister).setOn(state);
+ }
+ }
+
+ public MultivaluedAcceptableAaguid getAcceptableAaguid() {
+ return acceptableAaguid;
+ }
+
+ /* Buttons */
+ public void clickSaveButton() {
+ waitUntilElement(checkElement(() -> saveButton)).is().clickable();
+ saveButton.click();
+ }
+
+ public void clickCancelButton() {
+ waitUntilElement(checkElement(() -> cancelButton)).is().clickable();
+ cancelButton.click();
+ }
+
+ public boolean isSaveButtonEnabled() {
+ waitUntilElement(checkElement(() -> saveButton)).is().present();
+ return saveButton.isEnabled();
+ }
+
+ public boolean isCancelButtonEnabled() {
+ waitUntilElement(checkElement(() -> cancelButton)).is().present();
+ return cancelButton.isEnabled();
+ }
+
+ private T checkElement(Supplier supplier) {
+ try {
+ return supplier.get();
+ } catch (NoSuchElementException e) {
+ throw new RuntimeException("Cannot find required element in WebAuthn Policy");
+ } catch (NumberFormatException e) {
+ throw new RuntimeException("Cannot convert element value to number in WebAuthn Policy");
+ }
+ }
+
+ private T getRequirementOrNull(Supplier supplier) {
+ try {
+ return supplier.get();
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ public class MultivaluedAcceptableAaguid extends MultivaluedStringProperty {
+
+ @FindBy(className = "webauthn-acceptable-aaguid")
+ private List aaguids;
+
+ @FindBy(id = "newAcceptableAaguid")
+ private WebElement newAaguid;
+
+ @FindBy(xpath = "//button[@data-ng-click='deleteAcceptableAaguid($index)']")
+ private List minusButtons;
+
+ @FindBy(xpath = "//button[@data-ng-click='newAcceptableAaguid.length > 0 && addAcceptableAaguid()']")
+ private WebElement plusButton;
+
+ @Override
+ public List getItems() {
+ return checkElement(() -> aaguids);
+ }
+
+ @Override
+ public void addItem(String item) {
+ setTextInputValue(checkElement(() -> newAaguid), item);
+ clickAddItem();
+ }
+
+ @Override
+ protected List getMinusButtons() {
+ return minusButtons;
+ }
+
+ @Override
+ protected WebElement getPlusButton() {
+ return plusButton;
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/pages/WebAuthnPolicyPasswordlessPage.java b/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/pages/WebAuthnPolicyPasswordlessPage.java
new file mode 100644
index 0000000000..2cc54f487f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/pages/WebAuthnPolicyPasswordlessPage.java
@@ -0,0 +1,31 @@
+/*
+ * 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.pages;
+
+/**
+ * Helper class for WebAuthnPolicy Passwordless Page
+ *
+ * @author Martin Bartos
+ */
+public class WebAuthnPolicyPasswordlessPage extends WebAuthnPolicyPage {
+
+ @Override
+ public String getUriFragment() {
+ return super.getAuthenticationUriFragment() + "/webauthn-policy-passwordless";
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/utils/PropertyRequirement.java b/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/utils/PropertyRequirement.java
new file mode 100644
index 0000000000..7d0c488ba1
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/utils/PropertyRequirement.java
@@ -0,0 +1,28 @@
+package org.keycloak.testsuite.webauthn.utils;
+
+import org.keycloak.WebAuthnConstants;
+
+import java.util.Arrays;
+
+public enum PropertyRequirement {
+ NOT_SPECIFIED(WebAuthnConstants.OPTION_NOT_SPECIFIED),
+ YES("Yes"),
+ NO("No");
+
+ private final String value;
+
+ PropertyRequirement(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public static PropertyRequirement fromValue(String value) {
+ return Arrays.stream(PropertyRequirement.values())
+ .filter(f -> f.getValue().equals(value))
+ .findFirst()
+ .orElse(NOT_SPECIFIED);
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/admin/AbstractWebAuthnPolicySettingsTest.java b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/admin/AbstractWebAuthnPolicySettingsTest.java
new file mode 100644
index 0000000000..f5f45fe90e
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/admin/AbstractWebAuthnPolicySettingsTest.java
@@ -0,0 +1,416 @@
+/*
+ * 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.admin;
+
+import com.webauthn4j.data.AttestationConveyancePreference;
+import com.webauthn4j.data.AuthenticatorAttachment;
+import com.webauthn4j.data.UserVerificationRequirement;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.keycloak.models.Constants;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.console.AbstractConsoleTest;
+import org.keycloak.testsuite.util.UIUtils;
+import org.keycloak.testsuite.webauthn.pages.WebAuthnPolicyPage;
+import org.keycloak.testsuite.webauthn.updaters.AbstractWebAuthnRealmUpdater;
+import org.keycloak.testsuite.webauthn.utils.PropertyRequirement;
+import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.ui.ISelect;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+import static org.keycloak.testsuite.util.WaitUtils.pause;
+import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
+import static org.keycloak.testsuite.webauthn.utils.PropertyRequirement.NO;
+import static org.keycloak.testsuite.webauthn.utils.PropertyRequirement.YES;
+
+/**
+ * @author Martin Bartos
+ */
+public abstract class AbstractWebAuthnPolicySettingsTest extends AbstractConsoleTest {
+
+ protected static final String ALL_ZERO_AAGUID = "00000000-0000-0000-0000-000000000000";
+ protected static final String ALL_ONE_AAGUID = "11111111-1111-1111-1111-111111111111";
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+ @Before
+ public void navigateToPolicy() {
+ getPolicyPage().navigateTo();
+ waitForPageToLoad();
+ getPolicyPage().assertCurrent();
+ }
+
+ protected abstract WebAuthnPolicyPage getPolicyPage();
+
+ protected abstract AbstractWebAuthnRealmUpdater getWebAuthnRealmUpdater();
+
+ protected AbstractWebAuthnRealmUpdater updateWebAuthnPolicy(
+ String rpName,
+ List algorithms,
+ String attestationPreference,
+ String authenticatorAttachment,
+ String requireResidentKey,
+ String rpId,
+ String userVerification,
+ List acceptableAaguids) {
+
+ AbstractWebAuthnRealmUpdater updater = getWebAuthnRealmUpdater().setWebAuthnPolicyRpEntityName(rpName);
+
+ checkAndSet(algorithms, updater::setWebAuthnPolicySignatureAlgorithms);
+ checkAndSet(attestationPreference, updater::setWebAuthnPolicyAttestationConveyancePreference);
+ checkAndSet(authenticatorAttachment, updater::setWebAuthnPolicyAuthenticatorAttachment);
+ checkAndSet(requireResidentKey, updater::setWebAuthnPolicyRequireResidentKey);
+ checkAndSet(rpId, updater::setWebAuthnPolicyRpId);
+ checkAndSet(userVerification, updater::setWebAuthnPolicyUserVerificationRequirement);
+ checkAndSet(acceptableAaguids, updater::setWebAuthnPolicyAcceptableAaguids);
+
+ return (AbstractWebAuthnRealmUpdater) updater.update();
+ }
+
+ private void checkAndSet(T value, Consumer consumer) {
+ if (value != null) {
+ consumer.accept(value);
+ }
+ }
+
+ protected void checkRpEntityValues() {
+ String rpEntityName = getPolicyPage().getRpEntityName();
+ assertThat(rpEntityName, notNullValue());
+ assertThat(rpEntityName, is(Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME));
+
+ getPolicyPage().setRpEntityName("newEntityName");
+ getPolicyPage().clickSaveButton();
+
+ rpEntityName = getPolicyPage().getRpEntityName();
+ assertThat(rpEntityName, notNullValue());
+ assertThat(rpEntityName, is("newEntityName"));
+
+ getPolicyPage().setRpEntityName("");
+ getPolicyPage().clickSaveButton();
+
+ rpEntityName = getPolicyPage().getRpEntityName();
+ assertThat(rpEntityName, notNullValue());
+ assertThat(rpEntityName, is(Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME));
+
+ String rpEntityId = getPolicyPage().getRpEntityId();
+ assertThat(rpEntityId, notNullValue());
+ assertThat(rpEntityId, is(""));
+
+ getPolicyPage().setRpEntityId("rpId123");
+ getPolicyPage().clickSaveButton();
+
+ rpEntityId = getPolicyPage().getRpEntityId();
+ assertThat(rpEntityId, notNullValue());
+ assertThat(rpEntityId, is("rpId123"));
+ }
+
+ protected void checkWrongSignatureAlgorithm() throws IOException {
+ try (AbstractWebAuthnRealmUpdater rau = (AbstractWebAuthnRealmUpdater) getWebAuthnRealmUpdater()
+ .setWebAuthnPolicySignatureAlgorithms(Collections.singletonList("something-bad"))
+ .update()) {
+
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ final List signatureAlgorithms = realm.getWebAuthnPolicySignatureAlgorithms();
+ assertThat(signatureAlgorithms, notNullValue());
+ assertThat(signatureAlgorithms.size(), is(1));
+
+ getPolicyPage().navigateTo();
+ waitForPageToLoad();
+
+ ISelect selectedAlg = getPolicyPage().getSignatureAlgorithms();
+ assertThat(selectedAlg, notNullValue());
+
+ try {
+ // should throw an exception
+ selectedAlg.getFirstSelectedOption();
+ } catch (NoSuchElementException e) {
+ assertThat(e.getMessage(), containsString("No options are selected"));
+ }
+ }
+ }
+
+ protected void checkSignatureAlgorithms() {
+ getPolicyPage().assertCurrent();
+
+ final ISelect algorithms = getPolicyPage().getSignatureAlgorithms();
+ assertThat(algorithms, notNullValue());
+
+ algorithms.selectByValue("ES256");
+ algorithms.selectByValue("ES384");
+ algorithms.selectByValue("RS1");
+
+ final List selectedAlgs = algorithms.getAllSelectedOptions()
+ .stream()
+ .map(WebElement::getText)
+ .collect(Collectors.toList());
+
+ assertThat(selectedAlgs, notNullValue());
+ assertThat(selectedAlgs, hasSize(3));
+
+ try {
+ algorithms.selectByValue("something-bad");
+ } catch (NoSuchElementException e) {
+ assertThat(e.getMessage(), containsString("Cannot locate option with value: something-bad"));
+ }
+
+ assertThat(getPolicyPage().isSaveButtonEnabled(), is(true));
+ assertThat(getPolicyPage().isCancelButtonEnabled(), is(true));
+
+ getPolicyPage().clickSaveButton();
+
+ assertThat(getPolicyPage().isSaveButtonEnabled(), is(false));
+ assertThat(getPolicyPage().isCancelButtonEnabled(), is(false));
+ }
+
+ public void checkAttestationConveyancePreference() {
+ // default not specified
+ AttestationConveyancePreference attestation = getPolicyPage().getAttestationConveyancePreference();
+ assertThat(attestation, nullValue());
+
+ // Direct
+ getPolicyPage().setAttestationConveyancePreference(AttestationConveyancePreference.DIRECT);
+ getPolicyPage().clickSaveButton();
+
+ attestation = getPolicyPage().getAttestationConveyancePreference();
+ assertThat(attestation, notNullValue());
+ assertThat(attestation, is(AttestationConveyancePreference.DIRECT));
+
+ // Indirect
+ getPolicyPage().setAttestationConveyancePreference(AttestationConveyancePreference.INDIRECT);
+ getPolicyPage().clickSaveButton();
+
+ attestation = getPolicyPage().getAttestationConveyancePreference();
+ assertThat(attestation, notNullValue());
+ assertThat(attestation, is(AttestationConveyancePreference.INDIRECT));
+
+ // None
+ getPolicyPage().setAttestationConveyancePreference(AttestationConveyancePreference.NONE);
+ getPolicyPage().clickSaveButton();
+
+ attestation = getPolicyPage().getAttestationConveyancePreference();
+ assertThat(attestation, notNullValue());
+ assertThat(attestation, is(AttestationConveyancePreference.NONE));
+
+ try {
+ getPolicyPage().setAttestationConveyancePreference(AttestationConveyancePreference.ENTERPRISE);
+ Assert.fail("We don't support 'Enterprise' mode at this moment");
+ } catch (NoSuchElementException e) {
+ }
+ }
+
+ protected void checkAuthenticatorAttachment() {
+ AuthenticatorAttachment attachment = getPolicyPage().getAuthenticatorAttachment();
+ assertThat(attachment, nullValue());
+
+ // Cross-platform
+ getPolicyPage().setAuthenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM);
+ getPolicyPage().clickSaveButton();
+
+ attachment = getPolicyPage().getAuthenticatorAttachment();
+ assertThat(attachment, notNullValue());
+ assertThat(attachment, is(AuthenticatorAttachment.CROSS_PLATFORM));
+
+ // Platform
+ getPolicyPage().setAuthenticatorAttachment(AuthenticatorAttachment.PLATFORM);
+ getPolicyPage().clickSaveButton();
+
+ attachment = getPolicyPage().getAuthenticatorAttachment();
+ assertThat(attachment, notNullValue());
+ assertThat(attachment, is(AuthenticatorAttachment.PLATFORM));
+ }
+
+ protected void checkResidentKey() {
+ PropertyRequirement requireResidentKey = getPolicyPage().requireResidentKey();
+ assertThat(requireResidentKey, notNullValue());
+ assertThat(requireResidentKey, is(PropertyRequirement.NOT_SPECIFIED));
+
+ getPolicyPage().requireResidentKey(YES);
+ getPolicyPage().clickSaveButton();
+
+ // Yes
+ requireResidentKey = getPolicyPage().requireResidentKey();
+ assertThat(requireResidentKey, notNullValue());
+ assertThat(requireResidentKey, is(YES));
+
+ getPolicyPage().requireResidentKey(NO);
+ getPolicyPage().clickSaveButton();
+
+ // Null
+ getPolicyPage().requireResidentKey(null);
+ assertThat(getPolicyPage().isSaveButtonEnabled(), is(false));
+
+ // Not specified
+ getPolicyPage().requireResidentKey(PropertyRequirement.NOT_SPECIFIED);
+ assertThat(getPolicyPage().isSaveButtonEnabled(), is(true));
+ getPolicyPage().clickSaveButton();
+
+ // No
+ getPolicyPage().requireResidentKey(NO);
+ getPolicyPage().clickSaveButton();
+
+ requireResidentKey = getPolicyPage().requireResidentKey();
+ assertThat(requireResidentKey, notNullValue());
+ assertThat(requireResidentKey, is(NO));
+ }
+
+ protected void checkUserVerification() {
+ UserVerificationRequirement userVerification = getPolicyPage().getUserVerification();
+ assertThat(userVerification, nullValue());
+
+ // Preferred
+ getPolicyPage().setUserVerification(UserVerificationRequirement.PREFERRED);
+ getPolicyPage().clickSaveButton();
+
+ userVerification = getPolicyPage().getUserVerification();
+ assertThat(userVerification, notNullValue());
+ assertThat(userVerification, is(UserVerificationRequirement.PREFERRED));
+
+ // Required
+ getPolicyPage().setUserVerification(UserVerificationRequirement.REQUIRED);
+ getPolicyPage().clickSaveButton();
+
+ userVerification = getPolicyPage().getUserVerification();
+ assertThat(userVerification, notNullValue());
+ assertThat(userVerification, is(UserVerificationRequirement.REQUIRED));
+
+ // Discouraged
+ getPolicyPage().setUserVerification(UserVerificationRequirement.DISCOURAGED);
+ getPolicyPage().clickSaveButton();
+
+ userVerification = getPolicyPage().getUserVerification();
+ assertThat(userVerification, notNullValue());
+ assertThat(userVerification, is(UserVerificationRequirement.DISCOURAGED));
+ }
+
+ protected void checkTimeout() {
+ int timeout = getPolicyPage().getTimeout();
+ assertThat(timeout, is(0));
+
+ getPolicyPage().setTimeout(10);
+ getPolicyPage().clickSaveButton();
+
+ timeout = getPolicyPage().getTimeout();
+ assertThat(timeout, is(10));
+
+ getPolicyPage().setTimeout(-10);
+ getPolicyPage().clickSaveButton();
+ assertAlertDanger();
+
+ timeout = getPolicyPage().getTimeout();
+ assertThat(timeout, is(-10));
+
+ getPolicyPage().navigateTo();
+ waitForPageToLoad();
+
+ timeout = getPolicyPage().getTimeout();
+ assertThat(timeout, is(10));
+
+ getPolicyPage().setTimeout(1000000);
+ getPolicyPage().clickSaveButton();
+ assertAlertDanger();
+
+ getPolicyPage().setTimeout(500);
+ getPolicyPage().clickSaveButton();
+
+ timeout = getPolicyPage().getTimeout();
+ assertThat(timeout, is(500));
+ }
+
+ protected void checkAvoidSameAuthenticatorRegistration() {
+ boolean avoidSameAuthenticatorRegistration = getPolicyPage().avoidSameAuthenticatorRegistration();
+ assertThat(avoidSameAuthenticatorRegistration, is(false));
+
+ getPolicyPage().avoidSameAuthenticatorRegister(true);
+ assertThat(getPolicyPage().isSaveButtonEnabled(), is(true));
+ getPolicyPage().clickSaveButton();
+
+ avoidSameAuthenticatorRegistration = getPolicyPage().avoidSameAuthenticatorRegistration();
+ assertThat(avoidSameAuthenticatorRegistration, is(true));
+
+ getPolicyPage().avoidSameAuthenticatorRegister(false);
+ getPolicyPage().clickSaveButton();
+
+ avoidSameAuthenticatorRegistration = getPolicyPage().avoidSameAuthenticatorRegistration();
+ assertThat(avoidSameAuthenticatorRegistration, is(false));
+ }
+
+ protected void checkAcceptableAaguid() {
+ WebAuthnPolicyPage.MultivaluedAcceptableAaguid acceptableAaguid = getPolicyPage().getAcceptableAaguid();
+ assertThat(acceptableAaguid, notNullValue());
+
+ List items = getAcceptableAaguid(getPolicyPage().getAcceptableAaguid());
+ assertThat(items, notNullValue());
+
+ acceptableAaguid.addItem(ALL_ONE_AAGUID);
+ getPolicyPage().clickSaveButton();
+
+ items = getAcceptableAaguid(getPolicyPage().getAcceptableAaguid());
+
+ assertThat(items, notNullValue());
+ assertThat(items.isEmpty(), is(false));
+ assertThat(items.contains(ALL_ONE_AAGUID), is(true));
+
+ final String YUBIKEY_5_AAGUID = "cb69481e-8ff7-4039-93ec-0a2729a154a8";
+ final String YUBICO_AAGUID = "f8a011f3-8c0a-4d15-8006-17111f9edc7d";
+
+ acceptableAaguid.addItem(YUBIKEY_5_AAGUID);
+ acceptableAaguid.addItem(YUBICO_AAGUID);
+ items = getAcceptableAaguid(getPolicyPage().getAcceptableAaguid());
+
+ assertThat(items, notNullValue());
+ assertThat(items, hasSize(3));
+
+ getPolicyPage().clickSaveButton();
+ acceptableAaguid.removeItem(0);
+ items = getAcceptableAaguid(getPolicyPage().getAcceptableAaguid());
+
+ assertThat(items, notNullValue());
+ assertThat(items, hasSize(2));
+ assertThat(items.contains(YUBICO_AAGUID), is(true));
+ assertThat(items.contains(YUBIKEY_5_AAGUID), is(true));
+ assertThat(items.contains(ALL_ONE_AAGUID), is(false));
+
+ assertThat(getPolicyPage().isSaveButtonEnabled(), is(true));
+ getPolicyPage().clickSaveButton();
+ pause(100);
+ }
+
+ protected List getAcceptableAaguid(WebAuthnPolicyPage.MultivaluedAcceptableAaguid acceptableAaguid) {
+ return acceptableAaguid.getItems()
+ .stream()
+ .map(UIUtils::getTextInputValue)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/admin/WebAuthnPolicyPasswordlessSettingsTest.java b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/admin/WebAuthnPolicyPasswordlessSettingsTest.java
new file mode 100644
index 0000000000..267a684bb6
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/admin/WebAuthnPolicyPasswordlessSettingsTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.admin;
+
+import com.webauthn4j.data.AttestationConveyancePreference;
+import com.webauthn4j.data.AuthenticatorAttachment;
+import com.webauthn4j.data.UserVerificationRequirement;
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Test;
+import org.keycloak.models.Constants;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
+import org.keycloak.testsuite.webauthn.pages.WebAuthnPolicyPage;
+import org.keycloak.testsuite.webauthn.pages.WebAuthnPolicyPasswordlessPage;
+import org.keycloak.testsuite.webauthn.updaters.AbstractWebAuthnRealmUpdater;
+import org.keycloak.testsuite.webauthn.updaters.PasswordLessRealmAttributeUpdater;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import static com.webauthn4j.data.AttestationConveyancePreference.DIRECT;
+import static com.webauthn4j.data.AuthenticatorAttachment.PLATFORM;
+import static com.webauthn4j.data.UserVerificationRequirement.PREFERRED;
+import static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.hasSize;
+import static org.keycloak.models.Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
+
+/**
+ * @author Martin Bartos
+ */
+public class WebAuthnPolicyPasswordlessSettingsTest extends AbstractWebAuthnPolicySettingsTest {
+
+ @Page
+ WebAuthnPolicyPasswordlessPage webAuthnPolicyPasswordlessPage;
+
+ @Override
+ protected WebAuthnPolicyPage getPolicyPage() {
+ return webAuthnPolicyPasswordlessPage;
+ }
+
+ @Override
+ protected AbstractWebAuthnRealmUpdater getWebAuthnRealmUpdater() {
+ return new PasswordLessRealmAttributeUpdater(testRealmResource());
+ }
+
+ @Test
+ public void policySettingsWithExternalProperties() throws IOException {
+ try (RealmAttributeUpdater rau = updateWebAuthnPolicy(
+ "rpNamePasswordless",
+ Collections.singletonList("RS256"),
+ DIRECT.getValue(),
+ PLATFORM.getValue(),
+ "Yes",
+ "1234",
+ PREFERRED.getValue(),
+ Collections.singletonList(ALL_ZERO_AAGUID))
+ ) {
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyPasswordlessSignatureAlgorithms(), hasItems("RS256"));
+ assertThat(realm.getWebAuthnPolicyPasswordlessAttestationConveyancePreference(), is(DIRECT.getValue()));
+ assertThat(realm.getWebAuthnPolicyPasswordlessAuthenticatorAttachment(), is(PLATFORM.getValue()));
+ assertThat(realm.getWebAuthnPolicyPasswordlessRequireResidentKey(), is("Yes"));
+ assertThat(realm.getWebAuthnPolicyPasswordlessRpId(), is("1234"));
+ assertThat(realm.getWebAuthnPolicyPasswordlessUserVerificationRequirement(), is(PREFERRED.getValue()));
+ assertThat(realm.getWebAuthnPolicyPasswordlessAcceptableAaguids(), hasItems(ALL_ZERO_AAGUID));
+ }
+ }
+
+ @Test
+ public void wrongSignatureAlgorithm() throws IOException {
+ checkWrongSignatureAlgorithm();
+ }
+
+ @Test
+ public void algorithmsValuesSetUpInAdminConsole() {
+ checkSignatureAlgorithms();
+
+ final RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ final List realmSignatureAlgs = realm.getWebAuthnPolicyPasswordlessSignatureAlgorithms();
+ assertThat(realmSignatureAlgs, notNullValue());
+ assertThat(realmSignatureAlgs, hasSize(3));
+ assertThat(realmSignatureAlgs, contains("ES256", "ES384", "RS1"));
+ }
+
+ @Test
+ public void rpValuesSetUpInAdminConsole() {
+ checkRpEntityValues();
+
+ final RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyPasswordlessRpEntityName(), is(Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME));
+ assertThat(realm.getWebAuthnPolicyPasswordlessRpId(), is("rpId123"));
+ }
+
+ @Test
+ public void attestationConveyancePreferenceSettings() {
+ checkAttestationConveyancePreference();
+
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyPasswordlessAttestationConveyancePreference(), is(AttestationConveyancePreference.NONE.getValue()));
+
+ realm.setWebAuthnPolicyPasswordlessAttestationConveyancePreference(null);
+ testRealmResource().update(realm);
+
+ realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyPasswordlessAttestationConveyancePreference(), is(DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED));
+ }
+
+ @Test
+ public void authenticatorAttachmentSettings() {
+ checkAuthenticatorAttachment();
+
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyPasswordlessAuthenticatorAttachment(), is(AuthenticatorAttachment.PLATFORM.getValue()));
+
+ realm.setWebAuthnPolicyPasswordlessAuthenticatorAttachment(null);
+ testRealmResource().update(realm);
+
+ realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyPasswordlessAuthenticatorAttachment(), is(DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED));
+ }
+
+ @Test
+ public void requireResidentKeySettings() {
+ checkResidentKey();
+
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyPasswordlessRequireResidentKey(), is("No"));
+
+ realm.setWebAuthnPolicyPasswordlessRequireResidentKey(null);
+ testRealmResource().update(realm);
+
+ realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyPasswordlessRequireResidentKey(), is(DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED));
+ }
+
+ @Test
+ public void userVerificationRequirementSettings() {
+ checkUserVerification();
+
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyPasswordlessUserVerificationRequirement(), is(UserVerificationRequirement.DISCOURAGED.getValue()));
+
+ realm.setWebAuthnPolicyPasswordlessUserVerificationRequirement(null);
+ testRealmResource().update(realm);
+
+ realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyPasswordlessUserVerificationRequirement(), is(DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED));
+ }
+
+ @Test
+ public void timeoutSettings() {
+ checkTimeout();
+
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyPasswordlessCreateTimeout(), is(500));
+ }
+
+ @Test
+ public void avoidSameAuthenticatorRegistrationSettings() {
+ checkAvoidSameAuthenticatorRegistration();
+
+ final RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+ assertThat(realm.isWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister(), is(false));
+ }
+
+ @Test
+ public void acceptableAaguidSettings() {
+ checkAcceptableAaguid();
+
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+ assertThat(realm.getWebAuthnPolicyPasswordlessAcceptableAaguids(), is(getAcceptableAaguid(getPolicyPage().getAcceptableAaguid())));
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/admin/WebAuthnPolicySettingsTest.java b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/admin/WebAuthnPolicySettingsTest.java
new file mode 100644
index 0000000000..6a1e66dc6d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/admin/WebAuthnPolicySettingsTest.java
@@ -0,0 +1,223 @@
+/*
+ * 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.admin;
+
+import com.webauthn4j.data.AttestationConveyancePreference;
+import com.webauthn4j.data.AuthenticatorAttachment;
+import com.webauthn4j.data.UserVerificationRequirement;
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Test;
+import org.keycloak.models.Constants;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
+import org.keycloak.testsuite.webauthn.pages.WebAuthnPolicyPage;
+import org.keycloak.testsuite.webauthn.updaters.AbstractWebAuthnRealmUpdater;
+import org.keycloak.testsuite.webauthn.updaters.WebAuthnRealmAttributeUpdater;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import static com.webauthn4j.data.AttestationConveyancePreference.INDIRECT;
+import static com.webauthn4j.data.AuthenticatorAttachment.CROSS_PLATFORM;
+import static com.webauthn4j.data.UserVerificationRequirement.PREFERRED;
+import static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.hasSize;
+import static org.keycloak.models.Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
+
+/**
+ * @author Martin Bartos
+ */
+public class WebAuthnPolicySettingsTest extends AbstractWebAuthnPolicySettingsTest {
+
+ @Page
+ WebAuthnPolicyPage webAuthnPolicyPage;
+
+ @Override
+ protected WebAuthnPolicyPage getPolicyPage() {
+ return webAuthnPolicyPage;
+ }
+
+ @Override
+ protected AbstractWebAuthnRealmUpdater getWebAuthnRealmUpdater() {
+ return new WebAuthnRealmAttributeUpdater(testRealmResource());
+ }
+
+ @Test
+ public void policySettingsWithExternalProperties() throws IOException {
+ try (RealmAttributeUpdater rau = updateWebAuthnPolicy(
+ "rpName",
+ Collections.singletonList("ES256"),
+ INDIRECT.getValue(),
+ CROSS_PLATFORM.getValue(),
+ "No",
+ null,
+ PREFERRED.getValue(),
+ Collections.singletonList(ALL_ZERO_AAGUID))
+ ) {
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicySignatureAlgorithms(), hasItems("ES256"));
+ assertThat(realm.getWebAuthnPolicyAttestationConveyancePreference(), is(INDIRECT.getValue()));
+ assertThat(realm.getWebAuthnPolicyAuthenticatorAttachment(), is(CROSS_PLATFORM.getValue()));
+ assertThat(realm.getWebAuthnPolicyRequireResidentKey(), is("No"));
+ assertThat(realm.getWebAuthnPolicyRpId(), is(""));
+ assertThat(realm.getWebAuthnPolicyUserVerificationRequirement(), is(PREFERRED.getValue()));
+ assertThat(realm.getWebAuthnPolicyAcceptableAaguids(), hasItems(ALL_ZERO_AAGUID));
+ }
+ }
+
+ @Test
+ public void wrongSignatureAlgorithm() throws IOException {
+ checkWrongSignatureAlgorithm();
+ }
+
+ @Test
+ public void algorithmsValuesSetUpInAdminConsole() {
+ checkSignatureAlgorithms();
+
+ final RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ final List realmSignatureAlgs = realm.getWebAuthnPolicySignatureAlgorithms();
+ assertThat(realmSignatureAlgs, notNullValue());
+ assertThat(realmSignatureAlgs, hasSize(3));
+ assertThat(realmSignatureAlgs, contains("ES256", "ES384", "RS1"));
+ }
+
+ @Test
+ public void rpValuesSetUpInAdminConsole() {
+ checkRpEntityValues();
+
+ final RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyRpEntityName(), is(Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME));
+ assertThat(realm.getWebAuthnPolicyRpId(), is("rpId123"));
+ }
+
+ @Test
+ public void attestationConveyancePreferenceSettings() {
+ checkAttestationConveyancePreference();
+
+ // Realm
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyAttestationConveyancePreference(), is(AttestationConveyancePreference.NONE.getValue()));
+
+ realm.setWebAuthnPolicyAttestationConveyancePreference(null);
+ testRealmResource().update(realm);
+
+ realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyAttestationConveyancePreference(), is(DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED));
+ }
+
+ @Test
+ public void authenticatorAttachmentSettings() {
+ checkAuthenticatorAttachment();
+
+ // Realm
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyAuthenticatorAttachment(), is(AuthenticatorAttachment.PLATFORM.getValue()));
+
+ realm.setWebAuthnPolicyAuthenticatorAttachment(null);
+ testRealmResource().update(realm);
+
+ realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyAuthenticatorAttachment(), is(DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED));
+ }
+
+ @Test
+ public void requireResidentKeySettings() {
+ checkResidentKey();
+
+ // Realm
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyRequireResidentKey(), is("No"));
+
+ realm.setWebAuthnPolicyRequireResidentKey(null);
+ testRealmResource().update(realm);
+
+ realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyRequireResidentKey(), is(DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED));
+ }
+
+ @Test
+ public void userVerificationRequirementSettings() {
+ checkUserVerification();
+
+ // Realm
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyUserVerificationRequirement(), is(UserVerificationRequirement.DISCOURAGED.getValue()));
+
+ realm.setWebAuthnPolicyUserVerificationRequirement(null);
+ testRealmResource().update(realm);
+
+ realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyUserVerificationRequirement(), is(DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED));
+ }
+
+ @Test
+ public void timeoutSettings() {
+ checkTimeout();
+
+ // Realm
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+
+ assertThat(realm.getWebAuthnPolicyCreateTimeout(), is(500));
+ }
+
+ @Test
+ public void avoidSameAuthenticatorRegistrationSettings() {
+ checkAvoidSameAuthenticatorRegistration();
+
+ final RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+ assertThat(realm.isWebAuthnPolicyAvoidSameAuthenticatorRegister(), is(false));
+ }
+
+ @Test
+ public void acceptableAaguidSettings() {
+ checkAcceptableAaguid();
+
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertThat(realm, notNullValue());
+ assertThat(realm.getWebAuthnPolicyAcceptableAaguids(), is(getAcceptableAaguid(getPolicyPage().getAcceptableAaguid())));
+ }
+}
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/webauthn-policy-passwordless.html b/themes/src/main/resources/theme/base/admin/resources/partials/webauthn-policy-passwordless.html
index dbb1e0e83b..b15dfec6e4 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/webauthn-policy-passwordless.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/webauthn-policy-passwordless.html
@@ -144,7 +144,7 @@