KEYCLOAK-19486 Verify the WebAuthn registration functionality

This commit is contained in:
Martin Bartoš 2021-11-23 10:22:24 +01:00 committed by Marek Posolda
parent 3a2bf0c04b
commit 8e8fab857e
29 changed files with 1349 additions and 38 deletions

View file

@ -513,6 +513,8 @@ mvn -f testsuite/integration-arquillian/tests/other/pom.xml clean test \
-Dbrowser=chrome -Pwebauthn
```
**Note:** You can also execute those tests with `chromeHeadless` browser in order to not open a new window.
#### Troubleshooting
If you try to run WebAuthn tests with Chrome browser and you see error like:

View file

@ -24,7 +24,9 @@ public class TestClassProvider {
"/org/jboss/arquillian",
"/org/jboss/shrinkwrap",
"/org/jboss/jandex",
"/org/openqa/selenium"
"/org/openqa/selenium",
"/com/webauthn4j",
"/com/fasterxml/jackson/dataformat/cbor"
};
private Undertow server;

View file

@ -33,7 +33,7 @@
<properties>
<keycloak.theme.dir>${auth.server.home}/themes</keycloak.theme.dir>
<supportedBrowsers>firefox|chrome|internetExplorer|safari</supportedBrowsers>
<supportedBrowsers>firefox|chrome|internetExplorer|safari|chromeHeadless</supportedBrowsers>
</properties>
<build>

View file

@ -42,7 +42,13 @@ public enum DefaultVirtualAuthOptions {
YUBIKEY_4(DefaultVirtualAuthOptions::getYubiKeyGeneralOptions),
YUBIKEY_5_USB(DefaultVirtualAuthOptions::getYubiKeyGeneralOptions),
YUBIKEY_5_NFC(() -> getYubiKeyGeneralOptions().setTransport(NFC));
YUBIKEY_5_NFC(() -> getYubiKeyGeneralOptions().setTransport(NFC)),
TOUCH_ID(() -> DEFAULT.getOptions()
.setTransport(INTERNAL)
.setHasUserVerification(true)
.setIsUserVerified(true)
);
private final Supplier<VirtualAuthenticatorOptions> options;
@ -50,7 +56,7 @@ public enum DefaultVirtualAuthOptions {
this.options = options;
}
public VirtualAuthenticatorOptions getOptions() {
public final VirtualAuthenticatorOptions getOptions() {
return options.get();
}

View file

@ -55,11 +55,12 @@ public class KcVirtualAuthenticator {
private final boolean hasUserVerification;
private final boolean isUserConsenting;
private final boolean isUserVerified;
private final Map<String, Object> map;
private Options(VirtualAuthenticatorOptions options) {
this.options = options;
final Map<String, Object> map = options.toMap();
this.map = options.toMap();
this.protocol = protocolFromMap(map);
this.transport = transportFromMap(map);
this.hasResidentKey = (Boolean) map.get("hasResidentKey");
@ -96,6 +97,10 @@ public class KcVirtualAuthenticator {
return options;
}
public Map<String, Object> asMap() {
return map;
}
private static VirtualAuthenticatorOptions.Protocol protocolFromMap(Map<String, Object> map) {
return Arrays.stream(VirtualAuthenticatorOptions.Protocol.values())
.filter(f -> f.id.equals(map.get("protocol")))

View file

@ -17,11 +17,15 @@
package org.keycloak.testsuite.webauthn.authenticators;
import org.apache.http.util.Args;
import org.hamcrest.CoreMatchers;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.virtualauthenticator.HasVirtualAuthenticator;
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
import java.io.Closeable;
import java.util.Optional;
import static org.hamcrest.MatcherAssert.assertThat;
/**
@ -31,7 +35,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
*/
public class VirtualAuthenticatorManager {
private final HasVirtualAuthenticator driver;
private KcVirtualAuthenticator actualAuthenticator;
private KcVirtualAuthenticator currentAuthenticator;
public VirtualAuthenticatorManager(WebDriver driver) {
assertThat("Driver must support Virtual Authenticators", driver, CoreMatchers.instanceOf(HasVirtualAuthenticator.class));
@ -39,18 +43,20 @@ public class VirtualAuthenticatorManager {
}
public KcVirtualAuthenticator useAuthenticator(VirtualAuthenticatorOptions options) {
this.actualAuthenticator = new KcVirtualAuthenticator(driver.addVirtualAuthenticator(options), options);
return actualAuthenticator;
if (options == null) return null;
this.currentAuthenticator = new KcVirtualAuthenticator(driver.addVirtualAuthenticator(options), options);
return currentAuthenticator;
}
public KcVirtualAuthenticator getActualAuthenticator() {
return actualAuthenticator;
public KcVirtualAuthenticator getCurrent() {
return currentAuthenticator;
}
public void removeAuthenticator() {
if (actualAuthenticator != null) {
driver.removeVirtualAuthenticator(actualAuthenticator.getAuthenticator());
this.actualAuthenticator = null;
if (currentAuthenticator != null) {
driver.removeVirtualAuthenticator(currentAuthenticator.getAuthenticator());
this.currentAuthenticator = null;
}
}
}

View file

@ -22,6 +22,9 @@ import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import java.util.List;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public abstract class AbstractWebAuthnRealmUpdater<T extends AbstractWebAuthnRealmUpdater> extends RealmAttributeUpdater {
public AbstractWebAuthnRealmUpdater(RealmResource resource) {
@ -47,4 +50,10 @@ public abstract class AbstractWebAuthnRealmUpdater<T extends AbstractWebAuthnRea
public abstract T setWebAuthnPolicyUserVerificationRequirement(String webAuthnPolicyUserVerificationRequirement);
public abstract T setWebAuthnPolicyAcceptableAaguids(List<String> webAuthnPolicyAcceptableAaguids);
@Override
@SuppressWarnings("unchecked")
public AbstractWebAuthnRealmUpdater<T> update() {
return (AbstractWebAuthnRealmUpdater<T>) super.update();
}
}

View file

@ -21,6 +21,9 @@ import org.keycloak.admin.client.resource.RealmResource;
import java.util.List;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class PasswordLessRealmAttributeUpdater extends AbstractWebAuthnRealmUpdater<PasswordLessRealmAttributeUpdater> {
public PasswordLessRealmAttributeUpdater(RealmResource resource) {
super(resource);

View file

@ -21,6 +21,9 @@ import org.keycloak.admin.client.resource.RealmResource;
import java.util.List;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class WebAuthnRealmAttributeUpdater extends AbstractWebAuthnRealmUpdater<WebAuthnRealmAttributeUpdater> {
public WebAuthnRealmAttributeUpdater(RealmResource resource) {
super(resource);

View file

@ -0,0 +1,82 @@
/*
* 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.utils;
import com.webauthn4j.converter.util.CborConverter;
import com.webauthn4j.converter.util.ObjectConverter;
import com.webauthn4j.data.attestation.authenticator.COSEKey;
import org.keycloak.common.util.Base64Url;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserCredentialManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
import java.io.Serializable;
import static org.keycloak.models.credential.WebAuthnCredentialModel.createFromCredentialModel;
/**
* Helper class for WebAuthn data wrapping
*
* @author Martin Bartos <mabartos@redhat.com>
*/
public class WebAuthnDataWrapper implements Serializable {
private static final ObjectConverter converter = new ObjectConverter();
private final KeycloakSession session;
private final String username;
private final String credentialType;
private WebAuthnCredentialData webAuthnData = null;
public WebAuthnDataWrapper(KeycloakSession session, String username, String credentialType) {
this.session = session;
this.username = username;
this.credentialType = credentialType;
init();
}
private void init() {
final UserModel user = session.users().getUserByUsername(session.getContext().getRealm(), username);
if (user == null) return;
final UserCredentialManager userCredentialManager = session.userCredentialManager();
if (userCredentialManager == null) return;
final CredentialModel credential = userCredentialManager
.getStoredCredentialsByTypeStream(session.getContext().getRealm(), user, credentialType)
.findFirst()
.orElse(null);
if (credential == null) return;
this.webAuthnData = createFromCredentialModel(credential).getWebAuthnCredentialData();
}
public COSEKey getKey() {
if (webAuthnData != null) {
CborConverter cborConverter = converter.getCborConverter();
return cborConverter.readValue(Base64Url.decode(webAuthnData.getCredentialPublicKey()), COSEKey.class);
}
return null;
}
public WebAuthnCredentialData getWebAuthnData() {
return webAuthnData;
}
}

View file

@ -0,0 +1,82 @@
/*
* 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.utils;
import org.keycloak.representations.idm.RealmRepresentation;
import java.util.List;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* Helper class for retrieving WebAuthn data
*
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class WebAuthnRealmData {
private final RealmRepresentation realm;
private final boolean isPasswordless;
public WebAuthnRealmData(RealmRepresentation realm, boolean isPasswordless) {
assertThat("RealmRepresentation must not be NULL", realm, notNullValue());
this.realm = realm;
this.isPasswordless = isPasswordless;
}
public String getRpEntityName() {
return isPasswordless ? realm.getWebAuthnPolicyPasswordlessRpEntityName() : realm.getWebAuthnPolicyRpEntityName();
}
public List<String> getSignatureAlgorithms() {
return isPasswordless ? realm.getWebAuthnPolicyPasswordlessSignatureAlgorithms() : realm.getWebAuthnPolicySignatureAlgorithms();
}
public String getRpId() {
return isPasswordless ? realm.getWebAuthnPolicyPasswordlessRpId() : realm.getWebAuthnPolicyRpId();
}
public String getAttestationConveyancePreference() {
return isPasswordless ? realm.getWebAuthnPolicyPasswordlessAttestationConveyancePreference() : realm.getWebAuthnPolicyAttestationConveyancePreference();
}
public String getAuthenticatorAttachment() {
return isPasswordless ? realm.getWebAuthnPolicyPasswordlessAuthenticatorAttachment() : realm.getWebAuthnPolicyAuthenticatorAttachment();
}
public String getRequireResidentKey() {
return isPasswordless ? realm.getWebAuthnPolicyPasswordlessRequireResidentKey() : realm.getWebAuthnPolicyRequireResidentKey();
}
public String getUserVerificationRequirement() {
return isPasswordless ? realm.getWebAuthnPolicyPasswordlessUserVerificationRequirement() : realm.getWebAuthnPolicyUserVerificationRequirement();
}
public Integer getCreateTimeout() {
return isPasswordless ? realm.getWebAuthnPolicyPasswordlessCreateTimeout() : realm.getWebAuthnPolicyCreateTimeout();
}
public Boolean isAvoidSameAuthenticatorRegister() {
return isPasswordless ? realm.isWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister() : realm.isWebAuthnPolicyAvoidSameAuthenticatorRegister();
}
public List<String> getAcceptableAaguids() {
return isPasswordless ? realm.getWebAuthnPolicyPasswordlessAcceptableAaguids() : realm.getWebAuthnPolicyAcceptableAaguids();
}
}

View file

@ -20,11 +20,15 @@ package org.keycloak.testsuite.webauthn;
import org.junit.After;
import org.junit.Before;
import org.keycloak.common.Profile;
import org.keycloak.models.credential.WebAuthnCredentialModel;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions;
import org.keycloak.testsuite.webauthn.authenticators.UseVirtualAuthenticators;
import org.keycloak.testsuite.webauthn.authenticators.VirtualAuthenticatorManager;
import org.keycloak.testsuite.webauthn.updaters.AbstractWebAuthnRealmUpdater;
import org.keycloak.testsuite.webauthn.updaters.PasswordLessRealmAttributeUpdater;
import org.keycloak.testsuite.webauthn.updaters.WebAuthnRealmAttributeUpdater;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
@ -36,18 +40,22 @@ import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
@EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true)
public abstract class AbstractWebAuthnVirtualTest extends AbstractTestRealmKeycloakTest implements UseVirtualAuthenticators {
private VirtualAuthenticatorManager virtualAuthenticatorsManager;
protected static final String ALL_ZERO_AAGUID = "00000000-0000-0000-0000-000000000000";
protected static final String ALL_ONE_AAGUID = "11111111-1111-1111-1111-111111111111";
private VirtualAuthenticatorManager virtualAuthenticatorManager;
@Before
@Override
public void setUpVirtualAuthenticator() {
this.virtualAuthenticatorsManager = createDefaultVirtualManager(driver, getDefaultAuthenticatorOptions());
this.virtualAuthenticatorManager = createDefaultVirtualManager(driver, getDefaultAuthenticatorOptions());
clearEventQueue();
}
@After
@Override
public void removeVirtualAuthenticator() {
virtualAuthenticatorsManager.removeAuthenticator();
virtualAuthenticatorManager.removeAuthenticator();
clearEventQueue();
}
@ -55,12 +63,24 @@ public abstract class AbstractWebAuthnVirtualTest extends AbstractTestRealmKeycl
return DefaultVirtualAuthOptions.DEFAULT.getOptions();
}
public VirtualAuthenticatorManager getDefaultVirtualAuthManager() {
return virtualAuthenticatorsManager;
public VirtualAuthenticatorManager getVirtualAuthManager() {
return virtualAuthenticatorManager;
}
public void setDefaultVirtualAuthManager(VirtualAuthenticatorManager manager) {
this.virtualAuthenticatorsManager = manager;
public void setVirtualAuthManager(VirtualAuthenticatorManager manager) {
this.virtualAuthenticatorManager = manager;
}
public AbstractWebAuthnRealmUpdater getWebAuthnRealmUpdater() {
return isPasswordless() ? new PasswordLessRealmAttributeUpdater(testRealm()) : new WebAuthnRealmAttributeUpdater(testRealm());
}
public String getCredentialType() {
return isPasswordless() ? WebAuthnCredentialModel.TYPE_PASSWORDLESS : WebAuthnCredentialModel.TYPE_TWOFACTOR;
}
public boolean isPasswordless() {
return false;
}
protected void clearEventQueue() {

View file

@ -27,10 +27,15 @@ import org.keycloak.testsuite.webauthn.authenticators.VirtualAuthenticatorManage
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
import java.io.Closeable;
import java.io.IOException;
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.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions.DEFAULT_NFC;
import static org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions.DEFAULT_USB;
/**
* Test class for VirtualAuthenticatorManager
@ -44,7 +49,7 @@ public class VirtualAuthenticatorsManagerTest extends AbstractWebAuthnVirtualTes
WebDriver driver2;
@Test
public void testAddVirtualAuthenticator() {
public void addVirtualAuthenticator() {
final VirtualAuthenticatorManager manager = new VirtualAuthenticatorManager(driver);
assertThat(manager, notNullValue());
@ -52,23 +57,23 @@ public class VirtualAuthenticatorsManagerTest extends AbstractWebAuthnVirtualTes
assertAuthenticatorOptions(authenticator);
manager.removeAuthenticator();
assertThat(manager.getActualAuthenticator(), Matchers.nullValue());
assertThat(manager.getCurrent(), Matchers.nullValue());
authenticator = useDefaultTestingAuthenticator(manager);
assertAuthenticatorOptions(authenticator);
manager.removeAuthenticator();
assertThat(manager.getActualAuthenticator(), Matchers.nullValue());
assertThat(manager.getCurrent(), Matchers.nullValue());
}
@Test
public void testOverrideUsedAuthenticator() {
public void overrideUsedAuthenticator() {
final VirtualAuthenticatorManager manager = new VirtualAuthenticatorManager(driver);
assertThat(manager, notNullValue());
KcVirtualAuthenticator defaultTesting = useDefaultTestingAuthenticator(manager);
assertAuthenticatorOptions(defaultTesting);
assertThat(manager.getActualAuthenticator(), is(defaultTesting));
assertThat(manager.getCurrent(), is(defaultTesting));
VirtualAuthenticatorOptions defaultBleOptions = DefaultVirtualAuthOptions.DEFAULT_BLE.getOptions();
assertThat(defaultBleOptions, notNullValue());
@ -77,22 +82,40 @@ public class VirtualAuthenticatorsManagerTest extends AbstractWebAuthnVirtualTes
assertThat(defaultBLE, notNullValue());
assertAuthenticatorOptions(defaultTesting);
assertThat(manager.getActualAuthenticator(), is(defaultBLE));
assertThat(manager.getActualAuthenticator().getOptions().clone(), is(defaultBleOptions));
assertThat(manager.getCurrent(), is(defaultBLE));
assertThat(manager.getCurrent().getOptions().clone(), is(defaultBleOptions));
}
@Test
public void testDifferentDriver() {
public void differentDriver() {
final VirtualAuthenticatorManager manager = new VirtualAuthenticatorManager(driver);
assertThat(manager, notNullValue());
KcVirtualAuthenticator authenticator = useDefaultTestingAuthenticator(manager);
assertThat(authenticator, notNullValue());
assertThat(manager.getActualAuthenticator(), notNullValue());
assertThat(manager.getCurrent(), notNullValue());
final VirtualAuthenticatorManager manager2 = new VirtualAuthenticatorManager(driver2);
assertThat(manager2, notNullValue());
assertThat(manager2.getActualAuthenticator(), nullValue());
assertThat(manager2.getCurrent(), nullValue());
}
@Test
public void singleResponsibleAuthOptions() {
VirtualAuthenticatorOptions options = DefaultVirtualAuthOptions.DEFAULT_BLE.getOptions();
options.setTransport(VirtualAuthenticatorOptions.Transport.NFC);
final VirtualAuthenticatorManager manager = new VirtualAuthenticatorManager(driver);
assertThat(manager, notNullValue());
manager.useAuthenticator(options);
assertThat(manager.getCurrent().getOptions().getTransport(), is(VirtualAuthenticatorOptions.Transport.NFC));
options = DefaultVirtualAuthOptions.DEFAULT_BLE.getOptions();
manager.useAuthenticator(options);
assertThat(manager.getCurrent().getOptions().getTransport(), is(VirtualAuthenticatorOptions.Transport.BLE));
}
@Override
@ -104,7 +127,7 @@ public class VirtualAuthenticatorsManagerTest extends AbstractWebAuthnVirtualTes
KcVirtualAuthenticator authenticator = manager.useAuthenticator(defaultTestingAuthenticatorOptions());
assertThat(authenticator, notNullValue());
assertThat(manager.getActualAuthenticator(), is(authenticator));
assertThat(manager.getCurrent(), is(authenticator));
return authenticator;
}

View file

@ -57,6 +57,7 @@ import org.keycloak.testsuite.webauthn.updaters.WebAuthnRealmAttributeUpdater;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -99,15 +100,21 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
@Page
protected SelectAuthenticatorPage selectAuthenticatorPage;
private static final String ALL_ZERO_AAGUID = "00000000-0000-0000-0000-000000000000";
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realmRepresentation = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/webauthn/testrealm-webauthn.json"), RealmRepresentation.class);
List<String> acceptableAaguids = new ArrayList<>();
acceptableAaguids.add("00000000-0000-0000-0000-000000000000");
acceptableAaguids.add("6d44ba9b-f6ec-2e49-b930-0c8fe920cb73");
realmRepresentation.setWebAuthnPolicyAcceptableAaguids(acceptableAaguids);
testRealms.add(realmRepresentation);
}

View file

@ -64,7 +64,7 @@ public class WebAuthnErrorTest extends AbstractWebAuthnAccountTest {
final int webAuthnCount = webAuthnCredentialType.getUserCredentialsCount();
assertThat(webAuthnCount, is(2));
getWebAuthnManager().getActualAuthenticator().getAuthenticator().removeAllCredentials();
getWebAuthnManager().getCurrent().getAuthenticator().removeAllCredentials();
setUpWebAuthnFlow("webAuthnFlow");
logout();

View file

@ -0,0 +1,185 @@
/*
* 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.registration;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Rule;
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
import org.keycloak.common.util.SecretGenerator;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.AbstractAdminTest;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.webauthn.AbstractWebAuthnVirtualTest;
import org.keycloak.testsuite.webauthn.pages.WebAuthnErrorPage;
import org.keycloak.testsuite.webauthn.pages.WebAuthnRegisterPage;
import org.openqa.selenium.virtualauthenticator.Credential;
import javax.ws.rs.core.Response;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.List;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
@AuthServerContainerExclude(REMOTE)
public abstract class AbstractWebAuthnRegisterTest extends AbstractWebAuthnVirtualTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@Page
protected LoginPage loginPage;
@Page
protected RegisterPage registerPage;
@Page
protected WebAuthnRegisterPage webAuthnRegisterPage;
@Page
protected WebAuthnErrorPage webAuthnErrorPage;
@Page
protected AppPage appPage;
protected static final String USERNAME = "registerUserWebAuthnSuccess";
protected static final String PASSWORD = "password";
protected static final String EMAIL = "registerUserWebAuthnSuccess@email";
protected final static String base64EncodedPK =
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8_zMDQDYAxlU-Q"
+ "hk1Dwkf0v18GZca1DMF3SaJ9HPdmShRANCAASNYX5lyVCOZLzFZzrIKmeZ2jwU"
+ "RmgsJYxGP__fWN_S-j5sN4tT15XEpN_7QZnt14YvI6uvAgO0uJEboFaZlOEB";
protected final static PKCS8EncodedKeySpec privateKey = new PKCS8EncodedKeySpec(Base64.getUrlDecoder().decode(base64EncodedPK));
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realmRepresentation = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/webauthn/testrealm-webauthn.json"), RealmRepresentation.class);
if (isPasswordless()) {
makePasswordlessRequiredActionDefault(realmRepresentation);
}
testRealms.add(realmRepresentation);
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Override
protected void postAfterAbstractKeycloak() {
List<UserRepresentation> defaultUser = testRealm().users().search(USERNAME, true);
if (defaultUser != null && !defaultUser.isEmpty()) {
Response response = testRealm().users().delete(defaultUser.get(0).getId());
assertThat(response, notNullValue());
assertThat(response.getStatus(), is(204));
}
}
protected void registerDefaultWebAuthnUser(boolean promptLabel) {
if (promptLabel) {
registerDefaultWebAuthnUser();
} else {
registerDefaultWebAuthnUser(null);
}
}
protected void registerDefaultWebAuthnUser(String authenticatorLabel) {
registerWebAuthnUser(USERNAME, PASSWORD, EMAIL, authenticatorLabel);
}
protected void registerDefaultWebAuthnUser() {
registerDefaultWebAuthnUser(SecretGenerator.getInstance().randomString(24));
}
protected void registerWebAuthnUser(String username, String password, String email, String authenticatorLabel) {
loginPage.open();
loginPage.clickRegister();
waitForPageToLoad();
registerPage.assertCurrent();
registerPage.register("firstName", "lastName", email, username, password, password);
// User was registered. Now he needs to register WebAuthn credential
waitForPageToLoad();
webAuthnRegisterPage.assertCurrent();
webAuthnRegisterPage.clickRegister();
if (authenticatorLabel != null) {
webAuthnRegisterPage.registerWebAuthnCredential(authenticatorLabel);
}
}
protected String displayErrorMessageIfPresent() {
if (webAuthnErrorPage.isCurrent()) {
final String msg = webAuthnErrorPage.getError();
log.info("Error message from Error Page: " + msg);
return msg;
}
return null;
}
protected Credential getDefaultResidentKeyCredential() {
byte[] credentialId = {1, 2, 3, 4};
byte[] userHandle = {1};
return Credential.createResidentCredential(credentialId, "localhost", privateKey, userHandle, 0);
}
protected Credential getDefaultNonResidentKeyCredential() {
byte[] credentialId = {1, 2, 3, 4};
return Credential.createNonResidentCredential(credentialId, "localhost", privateKey, 0);
}
protected static void makePasswordlessRequiredActionDefault(RealmRepresentation realm) {
RequiredActionProviderRepresentation webAuthnProvider = realm.getRequiredActions()
.stream()
.filter(f -> f.getProviderId().equals(WebAuthnRegisterFactory.PROVIDER_ID))
.findFirst()
.orElse(null);
assertThat(webAuthnProvider, notNullValue());
webAuthnProvider.setEnabled(false);
RequiredActionProviderRepresentation webAuthnPasswordlessProvider = realm.getRequiredActions()
.stream()
.filter(f -> f.getProviderId().equals(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID))
.findFirst()
.orElse(null);
assertThat(webAuthnPasswordlessProvider, notNullValue());
webAuthnPasswordlessProvider.setEnabled(true);
webAuthnPasswordlessProvider.setDefaultAction(true);
}
}

View file

@ -0,0 +1,115 @@
/*
* 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.registration;
import com.webauthn4j.data.AttestationConveyancePreference;
import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
import org.keycloak.testsuite.webauthn.updaters.AbstractWebAuthnRealmUpdater;
import org.keycloak.testsuite.webauthn.utils.WebAuthnDataWrapper;
import org.keycloak.testsuite.webauthn.utils.WebAuthnRealmData;
import org.openqa.selenium.virtualauthenticator.Credential;
import java.io.IOException;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.keycloak.models.Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
import static org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions.DEFAULT;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class AttestationConveyanceRegisterTest extends AbstractWebAuthnRegisterTest {
@Test
public void attestationDefaultValue() {
WebAuthnRealmData realmData = new WebAuthnRealmData(testRealm().toRepresentation(), isPasswordless());
assertThat(realmData.getAttestationConveyancePreference(), is(DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED));
registerDefaultWebAuthnUser();
displayErrorMessageIfPresent();
final String credentialType = getCredentialType();
getTestingClient().server(TEST_REALM_NAME).run(session -> {
final WebAuthnDataWrapper dataWrapper = new WebAuthnDataWrapper(session, USERNAME, credentialType);
assertThat(dataWrapper, notNullValue());
final WebAuthnCredentialData data = dataWrapper.getWebAuthnData();
assertThat(data, notNullValue());
assertThat(data.getAttestationStatementFormat(), is(AttestationConveyancePreference.NONE.getValue()));
});
}
@Ignore("invalid cert path")
@Test
public void attestationConveyancePreferenceNone() {
assertAttestationConveyance(true, AttestationConveyancePreference.NONE);
}
@Ignore("invalid cert path")
@Test
public void attestationConveyancePreferenceIndirect() {
assertAttestationConveyance(true, AttestationConveyancePreference.INDIRECT);
}
@Ignore("invalid cert path")
@Test
public void attestationConveyancePreferenceDirect() {
getVirtualAuthManager().useAuthenticator(DEFAULT.getOptions().setHasResidentKey(true).setIsUserConsenting(true).setHasUserVerification(true));
assertAttestationConveyance(true, AttestationConveyancePreference.DIRECT);
}
protected void assertAttestationConveyance(boolean shouldSuccess, AttestationConveyancePreference attestation) {
Credential credential = getDefaultResidentKeyCredential();
getVirtualAuthManager().useAuthenticator(getDefaultAuthenticatorOptions().setHasResidentKey(true));
getVirtualAuthManager().getCurrent().getAuthenticator().addCredential(credential);
try (AbstractWebAuthnRealmUpdater updater = getWebAuthnRealmUpdater()
.setWebAuthnPolicyAttestationConveyancePreference(attestation.getValue())
.update()) {
WebAuthnRealmData realmData = new WebAuthnRealmData(testRealm().toRepresentation(), isPasswordless());
assertThat(realmData.getAttestationConveyancePreference(), is(attestation.getValue()));
registerDefaultWebAuthnUser(shouldSuccess);
displayErrorMessageIfPresent();
final boolean isErrorCurrent = webAuthnErrorPage.isCurrent();
assertThat(isErrorCurrent, is(!shouldSuccess));
final String credentialType = getCredentialType();
getTestingClient().server(TEST_REALM_NAME).run(session -> {
final WebAuthnDataWrapper dataWrapper = new WebAuthnDataWrapper(session, USERNAME, credentialType);
assertThat(dataWrapper, notNullValue());
final WebAuthnCredentialData data = dataWrapper.getWebAuthnData();
assertThat(data, notNullValue());
assertThat(data.getAttestationStatementFormat(), is(attestation.getValue()));
});
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,99 @@
/*
* 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.registration;
import com.webauthn4j.data.AuthenticatorAttachment;
import com.webauthn4j.data.UserVerificationRequirement;
import org.junit.Test;
import org.keycloak.testsuite.webauthn.utils.WebAuthnRealmData;
import java.io.Closeable;
import java.io.IOException;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions.DEFAULT_BLE;
import static org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions.DEFAULT_INTERNAL;
import static org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions.DEFAULT_USB;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class AuthAttachmentRegisterTest extends AbstractWebAuthnRegisterTest {
@Test
public void authenticatorAttachmentCrossPlatform() {
getVirtualAuthManager().useAuthenticator(DEFAULT_USB.getOptions());
assertAuthenticatorAttachment(true, AuthenticatorAttachment.CROSS_PLATFORM);
}
@Test
public void authenticatorAttachmentCrossPlatformInternal() {
getVirtualAuthManager().useAuthenticator(DEFAULT_INTERNAL.getOptions());
assertAuthenticatorAttachment(true, AuthenticatorAttachment.CROSS_PLATFORM);
}
@Test
public void authenticatorAttachmentPlatform() throws IOException {
try (Closeable u = getWebAuthnRealmUpdater()
.setWebAuthnPolicyAuthenticatorAttachment(AuthenticatorAttachment.PLATFORM.getValue())
.setWebAuthnPolicyUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED.getValue())
.update()) {
// It shouldn't be possible to register the authenticator
getVirtualAuthManager().useAuthenticator(DEFAULT_BLE.getOptions());
WebAuthnRealmData realmData = new WebAuthnRealmData(testRealm().toRepresentation(), isPasswordless());
assertThat(realmData.getAuthenticatorAttachment(), is(AuthenticatorAttachment.PLATFORM.getValue()));
assertThat(realmData.getUserVerificationRequirement(), is(UserVerificationRequirement.DISCOURAGED.getValue()));
registerDefaultWebAuthnUser(false);
webAuthnRegisterPage.assertCurrent();
webAuthnRegisterPage.clickRegister();
webAuthnErrorPage.assertCurrent();
assertThat(webAuthnErrorPage.getError(), containsString("A request is already pending."));
}
}
@Test
public void authenticatorAttachmentPlatformInternal() {
getVirtualAuthManager().useAuthenticator(DEFAULT_INTERNAL.getOptions());
assertAuthenticatorAttachment(true, AuthenticatorAttachment.PLATFORM);
}
private void assertAuthenticatorAttachment(boolean shouldSuccess, AuthenticatorAttachment attachment) {
try (Closeable u = getWebAuthnRealmUpdater()
.setWebAuthnPolicyAuthenticatorAttachment(attachment.getValue())
.update()) {
WebAuthnRealmData realmData = new WebAuthnRealmData(testRealm().toRepresentation(), isPasswordless());
assertThat(realmData.getAuthenticatorAttachment(), is(attachment.getValue()));
registerDefaultWebAuthnUser(shouldSuccess);
displayErrorMessageIfPresent();
assertThat(webAuthnErrorPage.isCurrent(), is(!shouldSuccess));
} catch (IOException e) {
throw new RuntimeException(e.getCause());
}
}
}

View file

@ -0,0 +1,115 @@
/*
* 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.registration;
import com.beust.jcommander.internal.Lists;
import com.webauthn4j.data.attestation.authenticator.COSEKey;
import com.webauthn4j.data.attestation.statement.COSEAlgorithmIdentifier;
import org.junit.Test;
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
import org.keycloak.testsuite.webauthn.utils.WebAuthnDataWrapper;
import org.keycloak.testsuite.webauthn.utils.WebAuthnRealmData;
import java.io.Closeable;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.keycloak.crypto.Algorithm.ES256;
import static org.keycloak.crypto.Algorithm.ES512;
import static org.keycloak.crypto.Algorithm.RS256;
import static org.keycloak.crypto.Algorithm.RS512;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class PubKeySignRegisterTest extends AbstractWebAuthnRegisterTest {
@Test
public void publicKeySignaturesWrong() {
assertPublicKeyAlgorithms(false, null, Lists.newArrayList(RS512, ES512));
}
@Test
public void publicKeySignaturesAlternatives() {
assertPublicKeyAlgorithms(true, COSEAlgorithmIdentifier.ES256, Lists.newArrayList(ES256, ES512));
}
@Test
public void publicKeySignaturesCorrect() {
assertPublicKeyAlgorithms(true, COSEAlgorithmIdentifier.ES256, Collections.singletonList(ES256));
}
@Test
public void publicKeySignaturesRSA() {
assertPublicKeyAlgorithms(false, null, Lists.newArrayList(RS256, ES512));
}
@Test
public void publicKeySignaturesEmpty() {
assertPublicKeyAlgorithms(true, COSEAlgorithmIdentifier.ES256, Collections.emptyList());
}
@Test
public void publicKeySignaturesNonExisting() {
assertPublicKeyAlgorithms(true, COSEAlgorithmIdentifier.ES256, Collections.singletonList("RSSSS2048"));
}
private void assertPublicKeyAlgorithms(boolean shouldSuccess, COSEAlgorithmIdentifier selectedAlgorithm, List<String> algorithms) {
assertThat(algorithms, notNullValue());
try (Closeable u = getWebAuthnRealmUpdater()
.setWebAuthnPolicySignatureAlgorithms(algorithms)
.update()) {
if (!algorithms.isEmpty()) {
WebAuthnRealmData realmData = new WebAuthnRealmData(testRealm().toRepresentation(), isPasswordless());
assertThat(realmData.getSignatureAlgorithms(), is(algorithms));
}
registerDefaultWebAuthnUser(shouldSuccess);
assertThat(webAuthnErrorPage.isCurrent(), is(!shouldSuccess));
if (!shouldSuccess) {
assertThat(webAuthnErrorPage.getError(), containsString("The operation either timed out or was not allowed"));
return;
}
final String credentialType = getCredentialType();
getTestingClient().server(TEST_REALM_NAME).run(session -> {
final WebAuthnDataWrapper dataWrapper = new WebAuthnDataWrapper(session, USERNAME, credentialType);
assertThat(dataWrapper, notNullValue());
final WebAuthnCredentialData data = dataWrapper.getWebAuthnData();
assertThat(data, notNullValue());
final COSEKey pubKey = dataWrapper.getKey();
assertThat(pubKey, notNullValue());
assertThat(pubKey.getAlgorithm(), notNullValue());
assertThat(pubKey.getAlgorithm().getValue(), is(selectedAlgorithm.getValue()));
assertThat(pubKey.hasPublicKey(), is(true));
});
} catch (IOException e) {
throw new RuntimeException(e.getCause());
}
}
}

View file

@ -0,0 +1,87 @@
/*
* 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.registration;
import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.testsuite.webauthn.utils.PropertyRequirement;
import org.keycloak.testsuite.webauthn.utils.WebAuthnRealmData;
import org.openqa.selenium.virtualauthenticator.Credential;
import java.io.Closeable;
import java.io.IOException;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class ResidentKeyRegisterTest extends AbstractWebAuthnRegisterTest{
@Test
public void residentKeyNotRequiredNoRK() {
assertResidentKey(true, PropertyRequirement.NO, false);
}
@Test
public void residentKeyNotRequiredPresent() {
assertResidentKey(true, PropertyRequirement.NO, true);
}
@Ignore("Not working")
@Test
public void residentKeyRequiredCorrect() {
assertResidentKey(true, PropertyRequirement.YES, true);
}
@Test
public void residentKeyRequiredWrong() {
assertResidentKey(false, PropertyRequirement.YES, false);
}
private void assertResidentKey(boolean shouldSuccess, PropertyRequirement requirement, boolean hasResidentKey) {
Credential credential;
getVirtualAuthManager().useAuthenticator(getDefaultAuthenticatorOptions().setHasResidentKey(hasResidentKey));
if (hasResidentKey) {
credential = getDefaultResidentKeyCredential();
} else {
credential = getDefaultNonResidentKeyCredential();
}
getVirtualAuthManager().getCurrent().getAuthenticator().addCredential(credential);
try (Closeable u = getWebAuthnRealmUpdater()
.setWebAuthnPolicyRequireResidentKey(requirement.getValue())
.update()) {
WebAuthnRealmData realmData = new WebAuthnRealmData(testRealm().toRepresentation(), isPasswordless());
assertThat(realmData.getRequireResidentKey(), containsString(requirement.getValue()));
registerDefaultWebAuthnUser(shouldSuccess);
displayErrorMessageIfPresent();
assertThat(webAuthnErrorPage.isCurrent(), is(!shouldSuccess));
} catch (IOException e) {
throw new RuntimeException(e.getCause());
}
}
}

View file

@ -0,0 +1,119 @@
/*
* 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.registration;
import com.webauthn4j.data.UserVerificationRequirement;
import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.testsuite.webauthn.utils.WebAuthnRealmData;
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
import java.io.Closeable;
import java.io.IOException;
import java.util.function.Consumer;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class UserVerificationRegisterTest extends AbstractWebAuthnRegisterTest {
@Test
public void discouragedAny() {
assertUserVerification(true, UserVerificationRequirement.DISCOURAGED,
auth -> auth.setHasUserVerification(true).setIsUserVerified(true));
}
@Test
public void discouraged() {
assertUserVerification(true, UserVerificationRequirement.DISCOURAGED,
auth -> auth.setHasUserVerification(true).setIsUserVerified(false));
}
@Test
public void discouragedNoVerification() {
assertUserVerification(true, UserVerificationRequirement.DISCOURAGED,
auth -> auth.setHasUserVerification(false));
}
@Test
public void preferredNoVerification() {
assertUserVerification(true, UserVerificationRequirement.PREFERRED,
auth -> auth.setHasUserVerification(false));
}
@Test
public void preferredVerificationWrong() {
assertUserVerification(true, UserVerificationRequirement.PREFERRED,
auth -> auth.setHasUserVerification(true).setIsUserVerified(false));
}
@Test
public void preferredVerificationCorrect() {
assertUserVerification(true, UserVerificationRequirement.PREFERRED,
auth -> auth.setHasUserVerification(true).setIsUserVerified(true));
}
@Test
public void requiredWrong() {
assertUserVerification(false, UserVerificationRequirement.REQUIRED,
auth -> auth.setHasUserVerification(true).setIsUserVerified(false));
}
@Test
public void requiredWrongNoVerification() {
assertUserVerification(false, UserVerificationRequirement.REQUIRED,
auth -> auth.setHasUserVerification(false));
}
@Ignore("Not working")
@Test
public void required() {
assertUserVerification(true, UserVerificationRequirement.REQUIRED,
auth -> auth.setHasUserVerification(true).setIsUserVerified(true));
}
private void assertUserVerification(boolean shouldSuccess,
UserVerificationRequirement requirement,
Consumer<VirtualAuthenticatorOptions> authenticator) {
VirtualAuthenticatorOptions options = getDefaultAuthenticatorOptions();
authenticator.accept(options);
getVirtualAuthManager().useAuthenticator(options);
WaitUtils.pause(200);
try (Closeable u = getWebAuthnRealmUpdater()
.setWebAuthnPolicyUserVerificationRequirement(requirement.getValue())
.update()) {
WebAuthnRealmData realmData = new WebAuthnRealmData(testRealm().toRepresentation(), isPasswordless());
assertThat(realmData.getUserVerificationRequirement(), containsString(requirement.getValue()));
registerDefaultWebAuthnUser(shouldSuccess);
displayErrorMessageIfPresent();
assertThat(webAuthnErrorPage.isCurrent(), is(!shouldSuccess));
} catch (IOException e) {
throw new RuntimeException(e.getCause());
}
}
}

View file

@ -0,0 +1,159 @@
/*
* 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.registration;
import com.webauthn4j.data.AttestationConveyancePreference;
import com.webauthn4j.data.attestation.authenticator.COSEKey;
import com.webauthn4j.data.attestation.statement.COSEAlgorithmIdentifier;
import com.webauthn4j.data.attestation.statement.COSEKeyType;
import org.hamcrest.Matchers;
import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.WebAuthnConstants;
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.testsuite.webauthn.utils.WebAuthnDataWrapper;
import org.keycloak.testsuite.webauthn.utils.WebAuthnRealmData;
import java.io.Closeable;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.anyOf;
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.keycloak.testsuite.util.WaitUtils.pause;
import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class WebAuthnOtherSettingsTest extends AbstractWebAuthnRegisterTest {
@Test
public void defaultValues() {
registerDefaultWebAuthnUser("webauthn");
WaitUtils.waitForPageToLoad();
appPage.assertCurrent();
String userId = events.expectRegister(USERNAME, EMAIL).assertEvent().getUserId();
events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
.user(userId)
.detail(Details.CUSTOM_REQUIRED_ACTION, isPasswordless()
? WebAuthnPasswordlessRegisterFactory.PROVIDER_ID
: WebAuthnRegisterFactory.PROVIDER_ID)
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, "webauthn")
.detail(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, ALL_ZERO_AAGUID)
.assertEvent();
final String credentialType = getCredentialType();
getTestingClient().server(TEST_REALM_NAME).run(session -> {
final WebAuthnDataWrapper dataWrapper = new WebAuthnDataWrapper(session, USERNAME, credentialType);
assertThat(dataWrapper, notNullValue());
final WebAuthnCredentialData data = dataWrapper.getWebAuthnData();
assertThat(data, notNullValue());
assertThat(data.getCredentialId(), notNullValue());
assertThat(data.getAaguid(), is(ALL_ZERO_AAGUID));
assertThat(data.getAttestationStatement(), nullValue());
assertThat(data.getCredentialPublicKey(), notNullValue());
assertThat(data.getCounter(), is(1L));
assertThat(data.getAttestationStatementFormat(), is(AttestationConveyancePreference.NONE.getValue()));
final COSEKey pubKey = dataWrapper.getKey();
assertThat(pubKey, notNullValue());
assertThat(pubKey.getAlgorithm(), notNullValue());
assertThat(pubKey.getAlgorithm().getValue(), is(COSEAlgorithmIdentifier.ES256.getValue()));
assertThat(pubKey.getKeyType(), is(COSEKeyType.EC2));
assertThat(pubKey.hasPublicKey(), is(true));
});
}
@Ignore("Individually it works, otherwise not")
@Test
public void timeout() throws IOException {
final Integer TIMEOUT = 3; //seconds
getVirtualAuthManager().removeAuthenticator();
try (Closeable u = getWebAuthnRealmUpdater().setWebAuthnPolicyCreateTimeout(TIMEOUT).update()) {
WebAuthnRealmData realmData = new WebAuthnRealmData(testRealm().toRepresentation(), isPasswordless());
assertThat(realmData.getCreateTimeout(), is(TIMEOUT));
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
registerPage.register("firstName", "lastName", EMAIL, USERNAME, PASSWORD, PASSWORD);
// User was registered. Now he needs to register WebAuthn credential
webAuthnRegisterPage.assertCurrent();
webAuthnRegisterPage.clickRegister();
pause((TIMEOUT + 2) * 1000);
webAuthnErrorPage.assertCurrent();
assertThat(webAuthnErrorPage.getError(), containsString("The operation either timed out or was not allowed"));
webAuthnErrorPage.clickTryAgain();
waitForPageToLoad();
webAuthnRegisterPage.assertCurrent();
webAuthnRegisterPage.clickRegister();
assertThat(webAuthnErrorPage.isCurrent(), is(false));
}
}
@Test
public void acceptableAaguidsShouldBeEmptyOrNullByDefault() {
WebAuthnRealmData realmData = new WebAuthnRealmData(testRealm().toRepresentation(), isPasswordless());
assertThat(realmData.getAcceptableAaguids(), anyOf(nullValue(), Matchers.empty()));
}
@Test
public void excludeCredentials() throws IOException {
List<String> acceptableAaguids = Collections.singletonList(ALL_ONE_AAGUID);
try (Closeable u = getWebAuthnRealmUpdater()
.setWebAuthnPolicyAcceptableAaguids(acceptableAaguids)
.update()) {
WebAuthnRealmData realmData = new WebAuthnRealmData(testRealm().toRepresentation(), isPasswordless());
assertThat(realmData.getAcceptableAaguids(), Matchers.contains(ALL_ONE_AAGUID));
registerDefaultWebAuthnUser();
webAuthnErrorPage.assertCurrent();
assertThat(webAuthnErrorPage.getError(), allOf(containsString("not acceptable aaguid"), containsString(ALL_ZERO_AAGUID)));
}
}
}

View file

@ -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.registration.passwordless;
import org.keycloak.testsuite.webauthn.registration.AttestationConveyanceRegisterTest;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class PwdLessAttestationRegTest extends AttestationConveyanceRegisterTest {
@Override
public boolean isPasswordless() {
return true;
}
}

View file

@ -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.registration.passwordless;
import org.keycloak.testsuite.webauthn.registration.AuthAttachmentRegisterTest;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class PwdLessAuthAttachmentRegTest extends AuthAttachmentRegisterTest {
@Override
public boolean isPasswordless() {
return true;
}
}

View file

@ -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.registration.passwordless;
import org.keycloak.testsuite.webauthn.registration.WebAuthnOtherSettingsTest;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class PwdLessOtherSettingsTest extends WebAuthnOtherSettingsTest {
@Override
public boolean isPasswordless() {
return true;
}
}

View file

@ -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.registration.passwordless;
import org.keycloak.testsuite.webauthn.registration.PubKeySignRegisterTest;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class PwdLessPubKeySignRegTest extends PubKeySignRegisterTest {
@Override
public boolean isPasswordless() {
return true;
}
}

View file

@ -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.registration.passwordless;
import org.keycloak.testsuite.webauthn.registration.ResidentKeyRegisterTest;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class PwdLessResidentKeyRegTest extends ResidentKeyRegisterTest {
@Override
public boolean isPasswordless() {
return true;
}
}

View file

@ -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.registration.passwordless;
import org.keycloak.testsuite.webauthn.registration.UserVerificationRegisterTest;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class PwdLessUserVerRegTest extends UserVerificationRegisterTest {
@Override
public boolean isPasswordless() {
return true;
}
}

View file

@ -22,10 +22,6 @@
"webAuthnPolicyRpEntityName": "keycloak-webauthn-2FA",
"webAuthnPolicyCreateTimeout": 60,
"webAuthnPolicyAvoidSameAuthenticatorRegister": true,
"webAuthnPolicyAcceptableAaguids": [
"00000000-0000-0000-0000-000000000000",
"6d44ba9b-f6ec-2e49-b930-0c8fe920cb73"
],
"smtpServer": {
"from": "auto@keycloak.org",
"host": "localhost",