KEYCLOAK-19489 Verify WebAuthn settings in admin console
This commit is contained in:
parent
7aaa33739b
commit
5283db86c4
12 changed files with 1256 additions and 7 deletions
|
@ -77,6 +77,17 @@
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>2.2</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>test-jar</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
<pluginManagement>
|
<pluginManagement>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|
|
@ -41,16 +41,24 @@ public class MultivaluedStringProperty {
|
||||||
@FindBy(xpath = "//button[@data-ng-click='addValueToMultivalued(option.name)']")
|
@FindBy(xpath = "//button[@data-ng-click='addValueToMultivalued(option.name)']")
|
||||||
private WebElement plusButton;
|
private WebElement plusButton;
|
||||||
|
|
||||||
|
protected List<WebElement> getMinusButtons() {
|
||||||
|
return minusButtons;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected WebElement getPlusButton() {
|
||||||
|
return plusButton;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isPresent() {
|
public boolean isPresent() {
|
||||||
try {
|
try {
|
||||||
return plusButton.isDisplayed() && items != null && !items.isEmpty();
|
return getPlusButton().isDisplayed() && getItems() != null && !getItems().isEmpty();
|
||||||
} catch (NoSuchElementException e) {
|
} catch (NoSuchElementException e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clickAddItem() {
|
public void clickAddItem() {
|
||||||
plusButton.click();
|
getPlusButton().click();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<WebElement> getItems() {
|
public List<WebElement> getItems() {
|
||||||
|
@ -71,7 +79,10 @@ public class MultivaluedStringProperty {
|
||||||
clickAddItem();
|
clickAddItem();
|
||||||
|
|
||||||
final List<WebElement> items = getItems();
|
final List<WebElement> 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);
|
setTextInputValue(webElement, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,11 +91,12 @@ public class MultivaluedStringProperty {
|
||||||
if (index == getItems().size() - 1) {
|
if (index == getItems().size() - 1) {
|
||||||
editItem(index, "");
|
editItem(index, "");
|
||||||
} else {
|
} else {
|
||||||
minusButtons.get(index).click();
|
getMinusButtons().get(index).click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateIndex(int index) {
|
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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,6 +164,7 @@
|
||||||
<profile>
|
<profile>
|
||||||
<id>webauthn</id>
|
<id>webauthn</id>
|
||||||
<modules>
|
<modules>
|
||||||
|
<module>console</module>
|
||||||
<module>webauthn</module>
|
<module>webauthn</module>
|
||||||
</modules>
|
</modules>
|
||||||
</profile>
|
</profile>
|
||||||
|
|
|
@ -20,6 +20,17 @@
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak.testsuite</groupId>
|
||||||
|
<artifactId>integration-arquillian-tests-console</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak.testsuite</groupId>
|
||||||
|
<artifactId>integration-arquillian-tests-console</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<type>test-jar</type>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jboss.arquillian.extension</groupId>
|
<groupId>org.jboss.arquillian.extension</groupId>
|
||||||
<artifactId>arquillian-drone-bom</artifactId>
|
<artifactId>arquillian-drone-bom</artifactId>
|
||||||
|
|
|
@ -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 <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||||
|
*/
|
||||||
|
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> T checkElement(Supplier<T> 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> T getRequirementOrNull(Supplier<T> supplier) {
|
||||||
|
try {
|
||||||
|
return supplier.get();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MultivaluedAcceptableAaguid extends MultivaluedStringProperty {
|
||||||
|
|
||||||
|
@FindBy(className = "webauthn-acceptable-aaguid")
|
||||||
|
private List<WebElement> aaguids;
|
||||||
|
|
||||||
|
@FindBy(id = "newAcceptableAaguid")
|
||||||
|
private WebElement newAaguid;
|
||||||
|
|
||||||
|
@FindBy(xpath = "//button[@data-ng-click='deleteAcceptableAaguid($index)']")
|
||||||
|
private List<WebElement> minusButtons;
|
||||||
|
|
||||||
|
@FindBy(xpath = "//button[@data-ng-click='newAcceptableAaguid.length > 0 && addAcceptableAaguid()']")
|
||||||
|
private WebElement plusButton;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<WebElement> getItems() {
|
||||||
|
return checkElement(() -> aaguids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addItem(String item) {
|
||||||
|
setTextInputValue(checkElement(() -> newAaguid), item);
|
||||||
|
clickAddItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<WebElement> getMinusButtons() {
|
||||||
|
return minusButtons;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected WebElement getPlusButton() {
|
||||||
|
return plusButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||||
|
*/
|
||||||
|
public class WebAuthnPolicyPasswordlessPage extends WebAuthnPolicyPage {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUriFragment() {
|
||||||
|
return super.getAuthenticationUriFragment() + "/webauthn-policy-passwordless";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||||
|
*/
|
||||||
|
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<String> algorithms,
|
||||||
|
String attestationPreference,
|
||||||
|
String authenticatorAttachment,
|
||||||
|
String requireResidentKey,
|
||||||
|
String rpId,
|
||||||
|
String userVerification,
|
||||||
|
List<String> 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 <T> void checkAndSet(T value, Consumer<T> 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<String> 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<String> 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<String> 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<String> getAcceptableAaguid(WebAuthnPolicyPage.MultivaluedAcceptableAaguid acceptableAaguid) {
|
||||||
|
return acceptableAaguid.getItems()
|
||||||
|
.stream()
|
||||||
|
.map(UIUtils::getTextInputValue)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||||
|
*/
|
||||||
|
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<String> 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())));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||||
|
*/
|
||||||
|
public class WebAuthnPolicySettingsTest extends AbstractWebAuthnPolicySettingsTest {
|
||||||
|
|
||||||
|
@Page
|
||||||
|
WebAuthnPolicyPage webAuthnPolicyPage;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected WebAuthnPolicyPage getPolicyPage() {
|
||||||
|
return webAuthnPolicyPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AbstractWebAuthnRealmUpdater<WebAuthnRealmAttributeUpdater> 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<String> 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())));
|
||||||
|
}
|
||||||
|
}
|
|
@ -144,7 +144,7 @@
|
||||||
<label for="type" class="col-md-2 control-label">{{:: 'webauthn-acceptable-aaguids' | translate}}</label>
|
<label for="type" class="col-md-2 control-label">{{:: 'webauthn-acceptable-aaguids' | translate}}</label>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<div class="input-group" ng-repeat="(i, acceptableAaguid) in realm.webAuthnPolicyPasswordlessAcceptableAaguids track by $index">
|
<div class="input-group" ng-repeat="(i, acceptableAaguid) in realm.webAuthnPolicyPasswordlessAcceptableAaguids track by $index">
|
||||||
<input class="form-control" ng-model="realm.webAuthnPolicyPasswordlessAcceptableAaguids[i]">
|
<input class="form-control webauthn-acceptable-aaguid" ng-model="realm.webAuthnPolicyPasswordlessAcceptableAaguids[i]">
|
||||||
<div class="input-group-btn">
|
<div class="input-group-btn">
|
||||||
<button class="btn btn-default" type="button" data-ng-click="deleteAcceptableAaguid($index)">
|
<button class="btn btn-default" type="button" data-ng-click="deleteAcceptableAaguid($index)">
|
||||||
<span class="fa fa-minus"></span>
|
<span class="fa fa-minus"></span>
|
||||||
|
|
|
@ -126,7 +126,7 @@
|
||||||
<label for="type" class="col-md-2 control-label">{{:: 'webauthn-acceptable-aaguids' | translate}}</label>
|
<label for="type" class="col-md-2 control-label">{{:: 'webauthn-acceptable-aaguids' | translate}}</label>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<div class="input-group" ng-repeat="(i, acceptableAaguid) in realm.webAuthnPolicyAcceptableAaguids track by $index">
|
<div class="input-group" ng-repeat="(i, acceptableAaguid) in realm.webAuthnPolicyAcceptableAaguids track by $index">
|
||||||
<input id="type" class="form-control" ng-model="realm.webAuthnPolicyAcceptableAaguids[i]">
|
<input id="type" class="form-control webauthn-acceptable-aaguid" ng-model="realm.webAuthnPolicyAcceptableAaguids[i]">
|
||||||
<div class="input-group-btn">
|
<div class="input-group-btn">
|
||||||
<button class="btn btn-default" type="button" data-ng-click="deleteAcceptableAaguid($index)">
|
<button class="btn btn-default" type="button" data-ng-click="deleteAcceptableAaguid($index)">
|
||||||
<span class="fa fa-minus"></span>
|
<span class="fa fa-minus"></span>
|
||||||
|
|
Loading…
Reference in a new issue