From faefeccbeeaf1f2ebcde2c787f26b0fb46060b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Barto=C5=A1?= Date: Tue, 12 Oct 2021 13:05:31 +0200 Subject: [PATCH] KEYCLOAK-19487 Test cases for managing 2FA authenticators in account console --- .../tests/other/base-ui/pom.xml | 10 + .../ui/account2/page/AbstractAccountPage.java | 0 .../account2/page/AbstractLoggedInPage.java | 0 .../ui/account2/page/ApplicationsPage.java | 0 .../ui/account2/page/DeviceActivityPage.java | 0 .../ui/account2/page/ForbiddenPage.java | 0 .../ui/account2/page/LinkedAccountsPage.java | 0 .../ui/account2/page/MyResourcesPage.java | 0 .../ui/account2/page/PageNotFound.java | 0 .../ui/account2/page/PersonalInfoPage.java | 0 .../ui/account2/page/SigningInPage.java | 0 .../ui/account2/page/WelcomeScreen.java | 0 .../AbstractFragmentWithMobileLayout.java | 0 .../page/fragment/AbstractHeader.java | 0 .../account2/page/fragment/ContentAlert.java | 0 .../page/fragment/ContinueCancelModal.java | 0 .../page/fragment/LoggedInPageHeader.java | 0 .../ui/account2/page/fragment/Sidebar.java | 0 .../page/fragment/WelcomeScreenHeader.java | 0 .../page/utils/SigningInPageUtils.java | 102 ++++++++ .../ui/account2/BaseAccountPageTest.java | 10 +- .../testsuite/ui/account2/SigningInTest.java | 209 ++++----------- .../tests/other/pom.xml | 3 +- .../tests/other/webauthn/pom.xml | 5 + .../webauthn/pages/WebAuthnErrorPage.java | 39 +++ .../webauthn/pages/WebAuthnRegisterPage.java | 23 +- .../AppInitiatedActionWebAuthnTest.java | 6 + .../account/AbstractWebAuthnAccountTest.java | 168 ++++++++++++ .../webauthn/account/WebAuthnErrorTest.java | 100 ++++++++ .../account/WebAuthnSigningInTest.java | 242 ++++++++++++++++++ .../base/login/webauthn-authenticate.ftl | 3 + .../theme/base/login/webauthn-register.ftl | 2 +- .../account/messages/messages_en.properties | 4 +- 33 files changed, 750 insertions(+), 176 deletions(-) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/AbstractAccountPage.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/AbstractLoggedInPage.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/ApplicationsPage.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/DeviceActivityPage.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/ForbiddenPage.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/LinkedAccountsPage.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/MyResourcesPage.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/PageNotFound.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/PersonalInfoPage.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/SigningInPage.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/WelcomeScreen.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/fragment/AbstractFragmentWithMobileLayout.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/fragment/AbstractHeader.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/fragment/ContentAlert.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/fragment/ContinueCancelModal.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/fragment/LoggedInPageHeader.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/fragment/Sidebar.java (100%) rename testsuite/integration-arquillian/tests/other/base-ui/src/{test => main}/java/org/keycloak/testsuite/ui/account2/page/fragment/WelcomeScreenHeader.java (100%) create mode 100644 testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/utils/SigningInPageUtils.java create mode 100644 testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/AbstractWebAuthnAccountTest.java create mode 100644 testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/WebAuthnErrorTest.java create mode 100644 testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/WebAuthnSigningInTest.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/pom.xml b/testsuite/integration-arquillian/tests/other/base-ui/pom.xml index 53d12b3a4d..e1e96544f9 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/pom.xml +++ b/testsuite/integration-arquillian/tests/other/base-ui/pom.xml @@ -96,6 +96,16 @@ arquillian-drone-appium-extension ${arquillian-drone.version} + + junit + junit + compile + + + org.hamcrest + hamcrest + compile + diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/AbstractAccountPage.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/AbstractAccountPage.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/AbstractAccountPage.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/AbstractAccountPage.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/AbstractLoggedInPage.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/AbstractLoggedInPage.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/AbstractLoggedInPage.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/AbstractLoggedInPage.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/ApplicationsPage.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/ApplicationsPage.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/ApplicationsPage.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/ApplicationsPage.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/DeviceActivityPage.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/DeviceActivityPage.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/DeviceActivityPage.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/DeviceActivityPage.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/ForbiddenPage.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/ForbiddenPage.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/ForbiddenPage.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/ForbiddenPage.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/LinkedAccountsPage.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/LinkedAccountsPage.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/LinkedAccountsPage.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/LinkedAccountsPage.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/MyResourcesPage.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/MyResourcesPage.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/MyResourcesPage.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/MyResourcesPage.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/PageNotFound.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/PageNotFound.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/PageNotFound.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/PageNotFound.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/PersonalInfoPage.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/PersonalInfoPage.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/PersonalInfoPage.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/PersonalInfoPage.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/SigningInPage.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/SigningInPage.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/SigningInPage.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/SigningInPage.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/WelcomeScreen.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/WelcomeScreen.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/WelcomeScreen.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/WelcomeScreen.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/AbstractFragmentWithMobileLayout.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/fragment/AbstractFragmentWithMobileLayout.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/AbstractFragmentWithMobileLayout.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/fragment/AbstractFragmentWithMobileLayout.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/AbstractHeader.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/fragment/AbstractHeader.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/AbstractHeader.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/fragment/AbstractHeader.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/ContentAlert.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/fragment/ContentAlert.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/ContentAlert.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/fragment/ContentAlert.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/ContinueCancelModal.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/fragment/ContinueCancelModal.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/ContinueCancelModal.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/fragment/ContinueCancelModal.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/LoggedInPageHeader.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/fragment/LoggedInPageHeader.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/LoggedInPageHeader.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/fragment/LoggedInPageHeader.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/Sidebar.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/fragment/Sidebar.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/Sidebar.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/fragment/Sidebar.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/WelcomeScreenHeader.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/fragment/WelcomeScreenHeader.java similarity index 100% rename from testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/page/fragment/WelcomeScreenHeader.java rename to testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/fragment/WelcomeScreenHeader.java diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/utils/SigningInPageUtils.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/utils/SigningInPageUtils.java new file mode 100644 index 0000000000..01d623ad55 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/utils/SigningInPageUtils.java @@ -0,0 +1,102 @@ +/* + * 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.ui.account2.page.utils; + +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.RequiredActionProviderRepresentation; +import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage; +import org.keycloak.testsuite.ui.account2.page.SigningInPage; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.keycloak.testsuite.util.UIUtils.refreshPageAndWaitForLoad; + +/** + * Helper class for SigningIn page + */ +public class SigningInPageUtils { + + public static void testModalDialog(AbstractLoggedInPage accountPage, Runnable triggerModal, Runnable onCancel) { + triggerModal.run(); + accountPage.modal().assertIsDisplayed(); + accountPage.modal().clickCancel(); + accountPage.modal().assertIsNotDisplayed(); + onCancel.run(); + triggerModal.run(); + accountPage.modal().clickConfirm(); + accountPage.modal().assertIsNotDisplayed(); + } + + public static void assertUserCredential(String expectedUserLabel, boolean removable, SigningInPage.UserCredential userCredential) { + assertThat(userCredential.getUserLabel(), is(expectedUserLabel)); + + // we expect the credential was created/edited no longer that 2 minutes ago (1 minute might not be enough in some corner cases) + LocalDateTime beforeNow = LocalDateTime.now().minusMinutes(2); + LocalDateTime now = LocalDateTime.now(); + // createdAt doesn't specify seconds so it should be something like 12:47:00 + LocalDateTime createdAt = userCredential.getCreatedAt(); + + assertThat("Creation time should be after time before update", createdAt.isAfter(beforeNow), is(true)); + assertThat("Creation time should be before now", createdAt.isBefore(now), is(true)); + + assertThat("Remove button visible", userCredential.isRemoveBtnDisplayed(), is(removable)); + assertThat("Update button visible", userCredential.isUpdateBtnDisplayed(), is(not(removable))); + } + + public static void testSetUpLink(RealmResource realmResource, SigningInPage.CredentialType credentialType, String requiredActionProviderId) { + assertThat("Set up link for \"" + credentialType.getType() + "\" is not visible", credentialType.isSetUpLinkVisible(), is(true)); + + RequiredActionProviderRepresentation requiredAction = new RequiredActionProviderRepresentation(); + requiredAction.setEnabled(false); + realmResource.flows().updateRequiredAction(requiredActionProviderId, requiredAction); + + refreshPageAndWaitForLoad(); + + assertThat("Set up link for \"" + credentialType.getType() + "\" is visible", credentialType.isSetUpLinkVisible(), is(false)); + assertThat("Title for \"" + credentialType.getType() + "\" is visible", credentialType.isTitleVisible(), is(false)); + assertThat("Set up link for \"" + credentialType.getType() + "\" is visible", credentialType.isNotSetUpLabelVisible(), is(false)); + } + + public static void testRemoveCredential(AbstractLoggedInPage accountPage, SigningInPage.UserCredential userCredential) { + int countBeforeRemove = userCredential.getCredentialType().getUserCredentialsCount(); + + testModalDialog(accountPage, userCredential::clickRemoveBtn, () -> { + assertThat(userCredential.isPresent(), is(true)); + assertThat(userCredential.getCredentialType().getUserCredentialsCount(), is(countBeforeRemove)); + }); + accountPage.alert().assertSuccess(); + + assertThat(userCredential.isPresent(), is(false)); + assertThat(userCredential.getCredentialType().getUserCredentialsCount(), is(countBeforeRemove - 1)); + } + + public static SigningInPage.UserCredential getNewestUserCredential(UserResource userResource, SigningInPage.CredentialType credentialType) { + List credentials = userResource.credentials(); + SigningInPage.UserCredential userCredential = + credentialType.getUserCredential(credentials.get(credentials.size() - 1).getId()); + + assertThat(userCredential.isPresent(), is(true)); + return userCredential; + } +} diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/BaseAccountPageTest.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/BaseAccountPageTest.java index 8c51894b80..99e9c2b06a 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/BaseAccountPageTest.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/BaseAccountPageTest.java @@ -19,6 +19,7 @@ package org.keycloak.testsuite.ui.account2; import org.junit.Test; import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage; +import org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils; import static org.junit.Assert.assertTrue; @@ -50,13 +51,6 @@ public abstract class BaseAccountPageTest extends AbstractAccountTest { } protected void testModalDialog(Runnable triggerModal, Runnable onCancel) { - triggerModal.run(); - getAccountPage().modal().assertIsDisplayed(); - getAccountPage().modal().clickCancel(); - getAccountPage().modal().assertIsNotDisplayed(); - onCancel.run(); - triggerModal.run(); - getAccountPage().modal().clickConfirm(); - getAccountPage().modal().assertIsNotDisplayed(); + SigningInPageUtils.testModalDialog(getAccountPage(), triggerModal, onCancel); } } diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/SigningInTest.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/SigningInTest.java index 0b0d3cdaca..279a53ca40 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/SigningInTest.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/SigningInTest.java @@ -20,51 +20,37 @@ package org.keycloak.testsuite.ui.account2; import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Test; -import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory; -import org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory; -import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory; -import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory; -import org.keycloak.common.Profile; import org.keycloak.models.credential.OTPCredentialModel; import org.keycloak.models.credential.PasswordCredentialModel; -import org.keycloak.models.credential.WebAuthnCredentialModel; import org.keycloak.models.utils.Base32; import org.keycloak.models.utils.TimeBasedOTP; -import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; -import org.keycloak.representations.idm.AuthenticationFlowRepresentation; -import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RequiredActionProviderRepresentation; -import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation; import org.keycloak.testsuite.admin.Users; -import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.auth.page.login.OTPSetup; import org.keycloak.testsuite.auth.page.login.UpdatePassword; import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage; import org.keycloak.testsuite.ui.account2.page.SigningInPage; +import org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils; import java.time.LocalDateTime; -import java.util.List; import static java.util.Collections.emptyList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertTrue; -import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; import static org.keycloak.models.UserModel.RequiredAction.CONFIGURE_TOTP; import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; +import static org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils.assertUserCredential; +import static org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils.testSetUpLink; import static org.keycloak.testsuite.util.UIUtils.refreshPageAndWaitForLoad; import static org.keycloak.testsuite.util.WaitUtils.pause; -import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad; /** * @author Vaclav Muzikar */ -@EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true) public class SigningInTest extends BaseAccountPageTest { public static final String PASSWORD_LABEL = "My Password"; - public static final String WEBAUTHN_FLOW_ID = "75e2390e-f296-49e6-acf8-6d21071d7e10"; @Page private SigningInPage signingInPage; @@ -75,12 +61,8 @@ public class SigningInTest extends BaseAccountPageTest { @Page private OTPSetup otpSetupPage; - - private SigningInPage.CredentialType passwordCredentialType; private SigningInPage.CredentialType otpCredentialType; - private SigningInPage.CredentialType webAuthnCredentialType; - private SigningInPage.CredentialType webAuthnPwdlessCredentialType; private TimeBasedOTP otpGenerator; @Override @@ -88,42 +70,6 @@ public class SigningInTest extends BaseAccountPageTest { return signingInPage; } - @Override - protected void afterAbstractKeycloakTestRealmImport() { - super.afterAbstractKeycloakTestRealmImport(); - - // configure WebAuthn - // we can't do this during the realm import because we'd need to specify all built-in flows as well - - AuthenticationFlowRepresentation flow = new AuthenticationFlowRepresentation(); - flow.setId(WEBAUTHN_FLOW_ID); - flow.setAlias("webauthn flow"); - flow.setProviderId("basic-flow"); - flow.setBuiltIn(false); - flow.setTopLevel(true); - testRealmResource().flows().createFlow(flow); - - AuthenticationExecutionRepresentation execution = new AuthenticationExecutionRepresentation(); - execution.setAuthenticator(WebAuthnAuthenticatorFactory.PROVIDER_ID); - execution.setPriority(10); - execution.setRequirement(REQUIRED.toString()); - execution.setParentFlow(WEBAUTHN_FLOW_ID); - testRealmResource().flows().addExecution(execution); - - execution.setAuthenticator(WebAuthnPasswordlessAuthenticatorFactory.PROVIDER_ID); - testRealmResource().flows().addExecution(execution); - - RequiredActionProviderSimpleRepresentation requiredAction = new RequiredActionProviderSimpleRepresentation(); - requiredAction.setProviderId(WebAuthnRegisterFactory.PROVIDER_ID); - requiredAction.setName("blahblah"); - testRealmResource().flows().registerRequiredAction(requiredAction); - - requiredAction.setProviderId(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID); - testRealmResource().flows().registerRequiredAction(requiredAction); - - // no need to actually configure the authentication, in Account Console tests we just verify the registration - } - @Override public void setDefaultPageUriParameters() { super.setDefaultPageUriParameters(); @@ -135,8 +81,6 @@ public class SigningInTest extends BaseAccountPageTest { public void beforeSigningInTest() { passwordCredentialType = signingInPage.getCredentialType(PasswordCredentialModel.TYPE); otpCredentialType = signingInPage.getCredentialType(OTPCredentialModel.TYPE); - webAuthnCredentialType = signingInPage.getCredentialType(WebAuthnCredentialModel.TYPE_TWOFACTOR); - webAuthnPwdlessCredentialType = signingInPage.getCredentialType(WebAuthnCredentialModel.TYPE_PASSWORDLESS); RealmRepresentation realm = testRealmResource().toRepresentation(); otpGenerator = new TimeBasedOTP(realm.getOtpPolicyAlgorithm(), realm.getOtpPolicyDigits(), realm.getOtpPolicyPeriod(), 0); @@ -146,25 +90,21 @@ public class SigningInTest extends BaseAccountPageTest { public void categoriesTest() { testContext.setTestRealmReps(emptyList()); // reimport realm after this test - assertEquals(3, signingInPage.getCategoriesCount()); - - assertEquals("Basic Authentication", signingInPage.getCategoryTitle("basic-authentication")); - assertEquals("Two-Factor Authentication", signingInPage.getCategoryTitle("two-factor")); - assertEquals("Passwordless", signingInPage.getCategoryTitle("passwordless")); - - // Delete WebAuthn flow ==> Passwordless category should disappear - testRealmResource().flows().deleteFlow(WEBAUTHN_FLOW_ID); - refreshPageAndWaitForLoad(); - assertEquals(2, signingInPage.getCategoriesCount()); + assertThat(signingInPage.getCategoriesCount(), is(1)); + assertThat(signingInPage.getCategoryTitle("basic-authentication"), is("Basic Authentication")); } @Test public void updatePasswordTest() { - SigningInPage.UserCredential passwordCred = - passwordCredentialType.getUserCredential(testUserResource().credentials().get(0).getId()); + SigningInPage.UserCredential passwordCred = passwordCredentialType.getUserCredential( + testUserResource() + .credentials() + .get(0) + .getId() + ); - assertFalse(passwordCredentialType.isSetUpLinkVisible()); - assertTrue(passwordCredentialType.isSetUp()); + assertThat(passwordCredentialType.isSetUpLinkVisible(), is(false)); + assertThat(passwordCredentialType.isSetUp(), is(true)); assertUserCredential(PASSWORD_LABEL, false, passwordCred); LocalDateTime previousCreatedAt = passwordCred.getCreatedAt(); @@ -178,95 +118,79 @@ public class SigningInTest extends BaseAccountPageTest { signingInPage.assertCurrent(); assertUserCredential(PASSWORD_LABEL, false, passwordCred); - assertNotEquals(previousCreatedAt, passwordCred.getCreatedAt()); + assertThat(passwordCred.getCreatedAt(), is(not(previousCreatedAt))); } @Test public void updatePasswordTestForUserWithoutPassword() { - // Remove password from the user through admin REST API - String passwordId = testUserResource().credentials().get(0).getId(); - testUserResource().removeCredential(passwordId); + // Remove password from the user through admin REST API + String passwordId = testUserResource().credentials().get(0).getId(); + testUserResource().removeCredential(passwordId); - // Refresh the page - refreshPageAndWaitForLoad(); + // Refresh the page + refreshPageAndWaitForLoad(); - // Test user doesn't have password set - assertTrue(passwordCredentialType.isSetUpLinkVisible()); - assertFalse(passwordCredentialType.isSetUp()); + // Test user doesn't have password set + assertThat(passwordCredentialType.isSetUpLinkVisible(), is(true)); + assertThat(passwordCredentialType.isSetUp(), is(false)); - // Set password - passwordCredentialType.clickSetUpLink(); - updatePasswordPage.assertCurrent(); - String originalPassword = Users.getPasswordOf(testUser); - updatePasswordPage.updatePasswords(originalPassword, originalPassword); - signingInPage.assertCurrent(); + // Set password + passwordCredentialType.clickSetUpLink(); + updatePasswordPage.assertCurrent(); + String originalPassword = Users.getPasswordOf(testUser); + updatePasswordPage.updatePasswords(originalPassword, originalPassword); + signingInPage.assertCurrent(); - // Credential set-up now - assertFalse(passwordCredentialType.isSetUpLinkVisible()); - assertTrue(passwordCredentialType.isSetUp()); - SigningInPage.UserCredential passwordCred = - passwordCredentialType.getUserCredential(testUserResource().credentials().get(0).getId()); - assertUserCredential(PASSWORD_LABEL, false, passwordCred); + // Credential set-up now + assertThat(passwordCredentialType.isSetUpLinkVisible(), is(false)); + assertThat(passwordCredentialType.isSetUp(), is(true)); + SigningInPage.UserCredential passwordCred = + passwordCredentialType.getUserCredential(testUserResource().credentials().get(0).getId()); + assertUserCredential(PASSWORD_LABEL, false, passwordCred); } @Test public void otpTest() { testContext.setTestRealmReps(emptyList()); - assertFalse(otpCredentialType.isSetUp()); + assertThat(otpCredentialType.isSetUp(), is(false)); otpCredentialType.clickSetUpLink(); otpSetupPage.cancel(); - signingInPage.assertCurrent(); - assertFalse(otpCredentialType.isSetUp()); - assertEquals("Authenticator Application", otpCredentialType.getTitle()); + signingInPage.assertCurrent(); + assertThat(otpCredentialType.isSetUp(), is(false)); + assertThat(otpCredentialType.getTitle(), is("Authenticator Application")); final String label1 = "OTP is secure"; final String label2 = "OTP is inconvenient"; SigningInPage.UserCredential otp1 = addOtpCredential(label1); - assertTrue(otpCredentialType.isSetUp()); - assertEquals(1, otpCredentialType.getUserCredentialsCount()); + assertThat(otpCredentialType.isSetUp(), is(true)); + assertThat(otpCredentialType.getUserCredentialsCount(), is(1)); assertUserCredential(label1, true, otp1); SigningInPage.UserCredential otp2 = addOtpCredential(label2); - assertEquals(2, otpCredentialType.getUserCredentialsCount()); + assertThat(otpCredentialType.getUserCredentialsCount(), is(2)); assertUserCredential(label2, true, otp2); - assertTrue("Set up link is not visible", otpCredentialType.isSetUpLinkVisible()); + assertThat("Set up link is not visible", otpCredentialType.isSetUpLinkVisible(), is(true)); RequiredActionProviderRepresentation requiredAction = new RequiredActionProviderRepresentation(); requiredAction.setEnabled(false); testRealmResource().flows().updateRequiredAction(CONFIGURE_TOTP.name(), requiredAction); refreshPageAndWaitForLoad(); - assertFalse("Set up link for \"otp\" is visible", otpCredentialType.isSetUpLinkVisible()); - assertFalse("Not set up link for \"otp\" is visible", otpCredentialType.isNotSetUpLabelVisible()); - assertTrue("Title for \"otp\" is not visible", otpCredentialType.isTitleVisible()); - assertEquals(2, otpCredentialType.getUserCredentialsCount()); + assertThat("Set up link for \"otp\" is visible", otpCredentialType.isSetUpLinkVisible(), is(false)); + assertThat("Not set up link for \"otp\" is visible", otpCredentialType.isNotSetUpLabelVisible(), is(false)); + assertThat("Title for \"otp\" is not visible", otpCredentialType.isTitleVisible(), is(true)); + assertThat(otpCredentialType.getUserCredentialsCount(), is(2)); testRemoveCredential(otp1); } @Test public void setUpLinksTest() { - testSetUpLink(otpCredentialType, CONFIGURE_TOTP.name()); - testSetUpLink(webAuthnCredentialType, WebAuthnRegisterFactory.PROVIDER_ID); - testSetUpLink(webAuthnPwdlessCredentialType, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID); - } - - private void testSetUpLink(SigningInPage.CredentialType credentialType, String requiredActionProviderId) { - assertTrue("Set up link for \"" + credentialType.getType() + "\" is not visible", credentialType.isSetUpLinkVisible()); - - RequiredActionProviderRepresentation requiredAction = new RequiredActionProviderRepresentation(); - requiredAction.setEnabled(false); - testRealmResource().flows().updateRequiredAction(requiredActionProviderId, requiredAction); - - refreshPageAndWaitForLoad(); - - assertFalse("Set up link for \"" + credentialType.getType() + "\" is visible", credentialType.isSetUpLinkVisible()); - assertFalse("Title for \"" + credentialType.getType() + "\" is visible", credentialType.isTitleVisible()); - assertFalse("Set up link for \"" + credentialType.getType() + "\" is visible", credentialType.isNotSetUpLabelVisible()); + testSetUpLink(testRealmResource(), otpCredentialType, CONFIGURE_TOTP.name()); } private SigningInPage.UserCredential addOtpCredential(String label) { @@ -285,39 +209,10 @@ public class SigningInTest extends BaseAccountPageTest { } private SigningInPage.UserCredential getNewestUserCredential(SigningInPage.CredentialType credentialType) { - List credentials = testUserResource().credentials(); - SigningInPage.UserCredential userCredential = - credentialType.getUserCredential(credentials.get(credentials.size() - 1).getId()); - assertTrue(userCredential.isPresent()); - return userCredential; + return SigningInPageUtils.getNewestUserCredential(testUserResource(), credentialType); } private void testRemoveCredential(SigningInPage.UserCredential userCredential) { - int countBeforeRemove = userCredential.getCredentialType().getUserCredentialsCount(); - - testModalDialog(userCredential::clickRemoveBtn, () -> { - assertTrue(userCredential.isPresent()); - assertEquals(countBeforeRemove, userCredential.getCredentialType().getUserCredentialsCount()); - }); - signingInPage.alert().assertSuccess(); - - assertFalse(userCredential.isPresent()); - assertEquals(countBeforeRemove - 1, userCredential.getCredentialType().getUserCredentialsCount()); - } - - private void assertUserCredential(String expectedUserLabel, boolean removable, SigningInPage.UserCredential userCredential) { - assertEquals(expectedUserLabel, userCredential.getUserLabel()); - - // we expect the credential was created/edited no longer that 2 minutes ago (1 minute might not be enough in some corner cases) - LocalDateTime beforeNow = LocalDateTime.now().minusMinutes(2); - LocalDateTime now = LocalDateTime.now(); - // createdAt doesn't specify seconds so it should be something like 12:47:00 - LocalDateTime createdAt = userCredential.getCreatedAt(); - - assertTrue("Creation time should be after time before update", createdAt.isAfter(beforeNow)); - assertTrue("Creation time should be before now", createdAt.isBefore(now)); - - assertEquals("Remove button visible", removable, userCredential.isRemoveBtnDisplayed()); - assertEquals("Update button visible", !removable, userCredential.isUpdateBtnDisplayed()); + SigningInPageUtils.testRemoveCredential(getAccountPage(), userCredential); } } diff --git a/testsuite/integration-arquillian/tests/other/pom.xml b/testsuite/integration-arquillian/tests/other/pom.xml index 5b38c3f4cf..f2dc82663c 100644 --- a/testsuite/integration-arquillian/tests/other/pom.xml +++ b/testsuite/integration-arquillian/tests/other/pom.xml @@ -32,7 +32,7 @@ pom Other Tests Modules - + Test modules that depend on the Base TestSuite. This POM contains common configuration for submodules. @@ -172,6 +172,7 @@ webauthn console + base-ui webauthn diff --git a/testsuite/integration-arquillian/tests/other/webauthn/pom.xml b/testsuite/integration-arquillian/tests/other/webauthn/pom.xml index d5c9f1814a..fef2fc273d 100644 --- a/testsuite/integration-arquillian/tests/other/webauthn/pom.xml +++ b/testsuite/integration-arquillian/tests/other/webauthn/pom.xml @@ -31,6 +31,11 @@ ${project.version} test-jar + + org.keycloak.testsuite + integration-arquillian-tests-base-ui + ${project.version} + org.jboss.arquillian.extension arquillian-drone-bom diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/pages/WebAuthnErrorPage.java b/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/pages/WebAuthnErrorPage.java index 7170b57ca4..27ab5bc993 100644 --- a/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/pages/WebAuthnErrorPage.java +++ b/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/pages/WebAuthnErrorPage.java @@ -2,12 +2,18 @@ package org.keycloak.testsuite.webauthn.pages; import org.junit.Assert; import org.keycloak.testsuite.pages.LanguageComboboxAwarePage; +import org.keycloak.testsuite.util.UIUtils; import org.keycloak.testsuite.util.WaitUtils; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + /** * @author Martin Bartos */ @@ -20,6 +26,12 @@ public class WebAuthnErrorPage extends LanguageComboboxAwarePage { @FindBy(id = "cancelWebAuthnAIA") private WebElement cancelRegistrationAIA; + @FindBy(className = "alert-error") + private WebElement errorMessage; + + @FindBy(id = "kc-webauthn-authenticator") + private List authenticators; + public void clickTryAgain() { WaitUtils.waitUntilElement(tryAgainButton).is().clickable(); tryAgainButton.click(); @@ -34,6 +46,33 @@ public class WebAuthnErrorPage extends LanguageComboboxAwarePage { } } + public String getError() { + try { + return UIUtils.getTextFromElement(errorMessage); + } catch (NoSuchElementException e) { + return null; + } + } + + public int getAuthenticatorsCount() { + try { + return authenticators.size(); + } catch (NoSuchElementException e) { + return 0; + } + } + + public List getAuthenticators() { + try { + return authenticators.stream() + .filter(Objects::nonNull) + .map(UIUtils::getTextFromElement) + .collect(Collectors.toList()); + } catch (NoSuchElementException e) { + return Collections.emptyList(); + } + } + @Override public boolean isCurrent() { try { diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/pages/WebAuthnRegisterPage.java b/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/pages/WebAuthnRegisterPage.java index 29fc90b3cc..18251ccc98 100644 --- a/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/pages/WebAuthnRegisterPage.java +++ b/testsuite/integration-arquillian/tests/other/webauthn/src/main/java/org/keycloak/testsuite/webauthn/pages/WebAuthnRegisterPage.java @@ -46,6 +46,9 @@ public class WebAuthnRegisterPage extends AbstractPage { @FindBy(id = "cancelWebAuthnAIA") private WebElement cancelAIAButton; + @FindBy(id = "kc-page-title") + private WebElement formTitle; + public void clickRegister() { WaitUtils.waitUntilElement(registerButton).is().clickable(); registerButton.click(); @@ -58,7 +61,7 @@ public class WebAuthnRegisterPage extends AbstractPage { } public void registerWebAuthnCredential(String authenticatorLabel) { - // label edit after registering authenicator by .create() + // label edit after registering authenticator by .create() WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(60)); Alert promptDialog = wait.until(ExpectedConditions.alertIsPresent()); @@ -68,9 +71,8 @@ public class WebAuthnRegisterPage extends AbstractPage { promptDialog.accept(); } - private boolean isAIA() { + public boolean isAIA() { try { - registerButton.getText(); cancelAIAButton.getText(); return true; } catch (NoSuchElementException e) { @@ -78,15 +80,22 @@ public class WebAuthnRegisterPage extends AbstractPage { } } - public boolean isCurrent() { + public String getFormTitle() { try { - registerButton.getText(); - return driver.getPageSource().contains("navigator.credentials.create"); + WaitUtils.waitUntilElement(formTitle).is().present(); + return formTitle.getText(); } catch (NoSuchElementException e) { - return false; + return null; } } + @Override + public boolean isCurrent() { + final String formTitle = getFormTitle(); + return formTitle != null && formTitle.equals("Security Key Registration") && + driver.getPageSource().contains("navigator.credentials.create"); + } + @Override public void open() { throw new UnsupportedOperationException(); diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/AppInitiatedActionWebAuthnTest.java b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/AppInitiatedActionWebAuthnTest.java index 4211c294b4..1502fbcd83 100644 --- a/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/AppInitiatedActionWebAuthnTest.java +++ b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/AppInitiatedActionWebAuthnTest.java @@ -45,6 +45,7 @@ import static org.keycloak.common.Profile.Feature.WEB_AUTHN; import static org.keycloak.models.AuthenticationExecutionModel.Requirement.ALTERNATIVE; import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED; import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE; +import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad; /** * @author Martin Bartos @@ -125,6 +126,8 @@ public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTe webAuthnRegisterPage.assertCurrent(); webAuthnRegisterPage.cancelAIA(); + waitForPageToLoad(); + assertKcActionStatus("cancelled"); } @@ -137,6 +140,9 @@ public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTe webAuthnRegisterPage.assertCurrent(); webAuthnRegisterPage.clickRegister(); webAuthnRegisterPage.registerWebAuthnCredential("authenticator1"); + + waitForPageToLoad(); + assertKcActionStatus("success"); } diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/AbstractWebAuthnAccountTest.java b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/AbstractWebAuthnAccountTest.java new file mode 100644 index 0000000000..f9066d0a3b --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/AbstractWebAuthnAccountTest.java @@ -0,0 +1,168 @@ +/* + * 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.account; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.After; +import org.junit.Before; +import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory; +import org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory; +import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory; +import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory; +import org.keycloak.common.Profile; +import org.keycloak.models.credential.WebAuthnCredentialModel; +import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation; +import org.keycloak.testsuite.AbstractAuthTest; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.page.AbstractPatternFlyAlert; +import org.keycloak.testsuite.ui.account2.page.SigningInPage; +import org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils; +import org.keycloak.testsuite.webauthn.AbstractWebAuthnVirtualTest; +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.pages.WebAuthnRegisterPage; +import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions; + +import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED; +import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad; + +@EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true) +public abstract class AbstractWebAuthnAccountTest extends AbstractAuthTest implements UseVirtualAuthenticators { + + @Page + protected SigningInPage signingInPage; + + @Page + protected WebAuthnRegisterPage webAuthnRegisterPage; + + private VirtualAuthenticatorManager webAuthnManager; + protected SigningInPage.CredentialType webAuthnCredentialType; + protected SigningInPage.CredentialType webAuthnPwdlessCredentialType; + + protected static final String WEBAUTHN_FLOW_ID = "75e2390e-f296-49e6-acf8-6d21071d7e10"; + + @Override + @Before + public void setUpVirtualAuthenticator() { + webAuthnManager = AbstractWebAuthnVirtualTest.createDefaultVirtualManager(driver, getDefaultOptions()); + } + + @Override + @After + public void removeVirtualAuthenticator() { + webAuthnManager.removeAuthenticator(); + } + + @Before + public void navigateBeforeTest() { + driver.manage().window().maximize(); + + webAuthnCredentialType = signingInPage.getCredentialType(WebAuthnCredentialModel.TYPE_TWOFACTOR); + webAuthnPwdlessCredentialType = signingInPage.getCredentialType(WebAuthnCredentialModel.TYPE_PASSWORDLESS); + + createTestUserWithAdminClient(false); + + signingInPage.navigateTo(); + loginToAccount(); + signingInPage.assertCurrent(); + } + + @Override + protected void afterAbstractKeycloakTestRealmImport() { + super.afterAbstractKeycloakTestRealmImport(); + + // configure WebAuthn + // we can't do this during the realm import because we'd need to specify all built-in flows as well + AuthenticationFlowRepresentation flow = new AuthenticationFlowRepresentation(); + flow.setId(WEBAUTHN_FLOW_ID); + flow.setAlias("webauthn flow"); + flow.setProviderId("basic-flow"); + flow.setBuiltIn(false); + flow.setTopLevel(true); + testRealmResource().flows().createFlow(flow); + + AuthenticationExecutionRepresentation execution = new AuthenticationExecutionRepresentation(); + execution.setAuthenticator(WebAuthnAuthenticatorFactory.PROVIDER_ID); + execution.setPriority(10); + execution.setRequirement(REQUIRED.toString()); + execution.setParentFlow(WEBAUTHN_FLOW_ID); + testRealmResource().flows().addExecution(execution); + + execution.setAuthenticator(WebAuthnPasswordlessAuthenticatorFactory.PROVIDER_ID); + testRealmResource().flows().addExecution(execution); + + RequiredActionProviderSimpleRepresentation requiredAction = new RequiredActionProviderSimpleRepresentation(); + requiredAction.setProviderId(WebAuthnRegisterFactory.PROVIDER_ID); + requiredAction.setName("blahblah"); + testRealmResource().flows().registerRequiredAction(requiredAction); + + requiredAction.setProviderId(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID); + testRealmResource().flows().registerRequiredAction(requiredAction); + } + + protected VirtualAuthenticatorManager getWebAuthnManager() { + return webAuthnManager; + } + + protected VirtualAuthenticatorOptions getDefaultOptions() { + return DefaultVirtualAuthOptions.DEFAULT.getOptions(); + } + + protected void loginToAccount() { + loginPage.assertCurrent(); + loginPage.form().login(testUser); + waitForPageToLoad(); + } + + protected void logout() { + signingInPage.navigateTo(); + signingInPage.assertCurrent(); + signingInPage.header().clickLogoutBtn(); + waitForPageToLoad(); + } + + protected SigningInPage.UserCredential addWebAuthnCredential(String label) { + return addWebAuthnCredential(label, false); + } + + protected SigningInPage.UserCredential addWebAuthnCredential(String label, boolean passwordless) { + SigningInPage.CredentialType credentialType = passwordless ? webAuthnPwdlessCredentialType : webAuthnCredentialType; + + AbstractPatternFlyAlert.waitUntilHidden(); + + credentialType.clickSetUpLink(); + webAuthnRegisterPage.assertCurrent(); + webAuthnRegisterPage.clickRegister(); + webAuthnRegisterPage.registerWebAuthnCredential(label); + waitForPageToLoad(); + signingInPage.assertCurrent(); + return getNewestUserCredential(credentialType); + } + + protected void testRemoveCredential(SigningInPage.UserCredential userCredential) { + AbstractPatternFlyAlert.waitUntilHidden(); + SigningInPageUtils.testRemoveCredential(signingInPage, userCredential); + } + + protected SigningInPage.UserCredential getNewestUserCredential(SigningInPage.CredentialType credentialType) { + return SigningInPageUtils.getNewestUserCredential(testUserResource(), credentialType); + } +} diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/WebAuthnErrorTest.java b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/WebAuthnErrorTest.java new file mode 100644 index 0000000000..777081243b --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/WebAuthnErrorTest.java @@ -0,0 +1,100 @@ +/* + * 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.account; + +import org.hamcrest.Matchers; +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Test; +import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory; +import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.updaters.RealmAttributeUpdater; +import org.keycloak.testsuite.util.FlowUtil; +import org.keycloak.testsuite.util.WaitUtils; +import org.keycloak.testsuite.webauthn.pages.WebAuthnErrorPage; +import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage; +import org.keycloak.testsuite.webauthn.updaters.WebAuthnRealmAttributeUpdater; + +import java.io.IOException; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +public class WebAuthnErrorTest extends AbstractWebAuthnAccountTest { + + @Page + protected WebAuthnLoginPage webAuthnLoginPage; + + @Page + protected WebAuthnErrorPage webAuthnErrorPage; + + @Test + public void errorPageWithPossibleAuthenticators() throws IOException { + final int timeoutSec = 3; + + addWebAuthnCredential("authenticator#1"); + addWebAuthnCredential("authenticator#2"); + + try (RealmAttributeUpdater u = new WebAuthnRealmAttributeUpdater(testRealmResource()) + .setWebAuthnPolicyCreateTimeout(timeoutSec) + .update()) { + + RealmRepresentation realm = testRealmResource().toRepresentation(); + assertThat(realm, notNullValue()); + assertThat(realm.getWebAuthnPolicyCreateTimeout(), is(timeoutSec)); + + final int webAuthnCount = webAuthnCredentialType.getUserCredentialsCount(); + assertThat(webAuthnCount, is(2)); + + getWebAuthnManager().getActualAuthenticator().getAuthenticator().removeAllCredentials(); + + setUpWebAuthnFlow("webAuthnFlow"); + logout(); + + signingInPage.navigateTo(); + loginToAccount(); + + webAuthnLoginPage.assertCurrent(); + webAuthnLoginPage.clickAuthenticate(); + + //Should fail after this time + WaitUtils.pause((timeoutSec + 1) * 1000); + + webAuthnErrorPage.assertCurrent(); + assertThat(webAuthnErrorPage.getError(), containsString("Failed to authenticate by the Security key.")); + assertThat(webAuthnErrorPage.getAuthenticatorsCount(), is(2)); + assertThat(webAuthnErrorPage.getAuthenticators(), Matchers.contains("authenticator#1", "authenticator#2")); + } + } + + private void setUpWebAuthnFlow(String newFlowAlias) { + testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias)); + testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session) + .selectFlow(newFlowAlias) + .inForms(forms -> forms + .clear() + .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, UsernamePasswordFormFactory.PROVIDER_ID) + .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, WebAuthnAuthenticatorFactory.PROVIDER_ID) + ) + .defineAsBrowserFlow() // Activate this new flow + ); + } +} diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/WebAuthnSigningInTest.java b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/WebAuthnSigningInTest.java new file mode 100644 index 0000000000..e6e8e6929e --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/account/WebAuthnSigningInTest.java @@ -0,0 +1,242 @@ +/* + * 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.account; + +import org.junit.Test; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory; +import org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory; +import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory; +import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory; +import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.RequiredActionProviderRepresentation; +import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation; +import org.keycloak.testsuite.ui.account2.page.SigningInPage; +import org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils; +import org.keycloak.testsuite.webauthn.authenticators.UseVirtualAuthenticators; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyList; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED; +import static org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils.assertUserCredential; +import static org.keycloak.testsuite.ui.account2.page.utils.SigningInPageUtils.testSetUpLink; +import static org.keycloak.testsuite.util.UIUtils.refreshPageAndWaitForLoad; +import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad; + +public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest implements UseVirtualAuthenticators { + + @Test + public void categoriesTest() { + testContext.setTestRealmReps(emptyList()); // reimport realm after this test + + assertThat(signingInPage.getCategoriesCount(), is(3)); + assertThat(signingInPage.getCategoryTitle("basic-authentication"), is("Basic Authentication")); + assertThat(signingInPage.getCategoryTitle("two-factor"), is("Two-Factor Authentication")); + assertThat(signingInPage.getCategoryTitle("passwordless"), is("Passwordless")); + + // Delete WebAuthn flow ==> Passwordless category should disappear + testRealmResource().flows().deleteFlow(WEBAUTHN_FLOW_ID); + refreshPageAndWaitForLoad(); + + assertThat(signingInPage.getCategoriesCount(), is(2)); + } + + @Test + public void twoFactorWebAuthnTest() { + testWebAuthn(false); + } + + @Test + public void passwordlessWebAuthnTest() { + testWebAuthn(true); + } + + @Test + public void testCreateWebAuthnSameUserLabel() { + final String SAME_LABEL = "key123"; + + // Do we really allow to have several authenticators with the same user label?? + + SigningInPage.UserCredential webAuthn = addWebAuthnCredential(SAME_LABEL, false); + assertThat(webAuthn, notNullValue()); + SigningInPage.UserCredential passwordless = addWebAuthnCredential(SAME_LABEL, true); + assertThat(passwordless, notNullValue()); + + assertThat(webAuthnCredentialType.getUserCredentialsCount(), is(1)); + webAuthn = webAuthnCredentialType.getUserCredential(webAuthn.getId()); + assertThat(webAuthn, notNullValue()); + assertThat(webAuthn.getUserLabel(), is(SAME_LABEL)); + + assertThat(webAuthnPwdlessCredentialType.getUserCredentialsCount(), is(1)); + passwordless = webAuthnPwdlessCredentialType.getUserCredential(passwordless.getId()); + assertThat(passwordless, notNullValue()); + assertThat(passwordless.getUserLabel(), is(SAME_LABEL)); + + SigningInPage.UserCredential webAuthn2 = addWebAuthnCredential(SAME_LABEL, false); + assertThat(webAuthn2, notNullValue()); + assertThat(webAuthn2.getUserLabel(), is(SAME_LABEL)); + + assertThat(webAuthnCredentialType.getUserCredentialsCount(), is(2)); + + SigningInPage.UserCredential passwordless2 = addWebAuthnCredential(SAME_LABEL, true); + assertThat(passwordless2, notNullValue()); + assertThat(passwordless2.getUserLabel(), is(SAME_LABEL)); + + assertThat(webAuthnPwdlessCredentialType.getUserCredentialsCount(), is(2)); + } + + @Test + public void testMultipleSecurityKeys() { + final String LABEL = "SecurityKey#"; + + List createdCredentials = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + SigningInPage.UserCredential key = addWebAuthnCredential(LABEL + i, i % 2 == 0); + assertThat(key, notNullValue()); + createdCredentials.add(key); + } + + final int webAuthnCount = webAuthnCredentialType.getUserCredentialsCount(); + assertThat(webAuthnCount, is(5)); + + final int passwordlessCount = webAuthnPwdlessCredentialType.getUserCredentialsCount(); + assertThat(passwordlessCount, is(5)); + + UserResource userResource = testRealmResource().users().get(testUser.getId()); + assertThat(userResource, notNullValue()); + + List list = userResource.credentials(); + assertThat(list, notNullValue()); + assertThat(list, not(empty())); + + final List credentialsLabels = list.stream() + .map(CredentialRepresentation::getUserLabel) + .collect(Collectors.toList()); + + assertThat(credentialsLabels, notNullValue()); + final List createdCredentialsLabels = createdCredentials.stream() + .map(SigningInPage.UserCredential::getUserLabel) + .collect(Collectors.toList()); + + assertThat(credentialsLabels.containsAll(createdCredentialsLabels), is(true)); + + final List credentials = createdCredentials.stream() + .filter(key -> key.getUserLabel().equals(LABEL + 0) + || key.getUserLabel().equals(LABEL + 1)) + .collect(Collectors.toList()); + + assertThat(credentials, hasSize(2)); + + testRemoveCredential(credentials.get(0)); + testRemoveCredential(credentials.get(1)); + + assertThat(webAuthnCredentialType.getUserCredentialsCount(), is(4)); + assertThat(webAuthnPwdlessCredentialType.getUserCredentialsCount(), is(4)); + } + + @Test + public void setUpLinksTest() { + testSetUpLink(testRealmResource(), webAuthnCredentialType, WebAuthnRegisterFactory.PROVIDER_ID); + testSetUpLink(testRealmResource(), webAuthnPwdlessCredentialType, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID); + } + + @Test + public void testCancelRegistration() { + cancelRegistration(false); + } + + @Test + public void testCancelPasswordlessRegistration() { + cancelRegistration(true); + } + + private void cancelRegistration(boolean passwordless) { + SigningInPage.CredentialType credentialType = passwordless ? webAuthnPwdlessCredentialType : webAuthnCredentialType; + + credentialType.clickSetUpLink(); + waitForPageToLoad(); + webAuthnRegisterPage.assertCurrent(); + assertThat(webAuthnRegisterPage.isAIA(), is(true)); + webAuthnRegisterPage.cancelAIA(); + + waitForPageToLoad(); + signingInPage.assertCurrent(); + } + + private void testWebAuthn(boolean passwordless) { + testContext.setTestRealmReps(emptyList()); + + SigningInPage.CredentialType credentialType; + final String expectedHelpText; + final String providerId; + + if (passwordless) { + credentialType = webAuthnPwdlessCredentialType; + expectedHelpText = "Use your security key for passwordless sign in."; + providerId = WebAuthnPasswordlessRegisterFactory.PROVIDER_ID; + } else { + credentialType = webAuthnCredentialType; + expectedHelpText = "Use your security key to sign in."; + providerId = WebAuthnRegisterFactory.PROVIDER_ID; + } + + assertThat(credentialType.isSetUp(), is(false)); + // no way to simulate registration cancellation + + assertThat("Set up link for \"" + credentialType.getType() + "\" is not visible", credentialType.isSetUpLinkVisible(), is(true)); + assertThat(credentialType.getTitle(), is("Security Key")); + assertThat(credentialType.getHelpText(), is(expectedHelpText)); + + final String label1 = "WebAuthn is convenient"; + final String label2 = "but not yet widely adopted"; + + SigningInPage.UserCredential webAuthn1 = addWebAuthnCredential(label1, passwordless); + assertThat(credentialType.isSetUp(), is(true)); + assertThat(credentialType.getUserCredentialsCount(), is(1)); + assertUserCredential(label1, true, webAuthn1); + + SigningInPage.UserCredential webAuthn2 = addWebAuthnCredential(label2, passwordless); + assertThat(credentialType.getUserCredentialsCount(), is(2)); + assertUserCredential(label2, true, webAuthn2); + + RequiredActionProviderRepresentation requiredAction = new RequiredActionProviderRepresentation(); + requiredAction.setEnabled(false); + testRealmResource().flows().updateRequiredAction(providerId, requiredAction); + + refreshPageAndWaitForLoad(); + + assertThat("Set up link for \"" + credentialType.getType() + "\" is visible", credentialType.isSetUpLinkVisible(), is(false)); + assertThat("Not set up link for \"" + credentialType.getType() + "\" is visible", credentialType.isNotSetUpLabelVisible(), is(false)); + assertThat("Title for \"" + credentialType.getType() + "\" is not visible", credentialType.isTitleVisible(), is(true)); + assertThat(credentialType.getUserCredentialsCount(), is(2)); + + testRemoveCredential(webAuthn1); + } +} diff --git a/themes/src/main/resources/theme/base/login/webauthn-authenticate.ftl b/themes/src/main/resources/theme/base/login/webauthn-authenticate.ftl index 79ac32804b..82eba8bc57 100644 --- a/themes/src/main/resources/theme/base/login/webauthn-authenticate.ftl +++ b/themes/src/main/resources/theme/base/login/webauthn-authenticate.ftl @@ -86,6 +86,9 @@ challenge: base64url.decode(challenge, { loose: true }) }; + let createTimeout = ${createTimeout}; + if (createTimeout !== 0) publicKey.timeout = createTimeout * 1000; + if (allowCredentials.length) { publicKey.allowCredentials = allowCredentials; } diff --git a/themes/src/main/resources/theme/base/login/webauthn-register.ftl b/themes/src/main/resources/theme/base/login/webauthn-register.ftl index 40f2be9880..5002ee72df 100644 --- a/themes/src/main/resources/theme/base/login/webauthn-register.ftl +++ b/themes/src/main/resources/theme/base/login/webauthn-register.ftl @@ -86,7 +86,7 @@ if (isAuthenticatorSelectionSpecified) publicKey.authenticatorSelection = authenticatorSelection; let createTimeout = ${createTimeout}; - if (createTimeout != 0) publicKey.timeout = createTimeout * 1000; + if (createTimeout !== 0) publicKey.timeout = createTimeout * 1000; let excludeCredentialIds = "${excludeCredentialIds}"; let excludeCredentials = getExcludeCredentials(excludeCredentialIds); diff --git a/themes/src/main/resources/theme/keycloak.v2/account/messages/messages_en.properties b/themes/src/main/resources/theme/keycloak.v2/account/messages/messages_en.properties index 7941439fa4..e59fe1680f 100644 --- a/themes/src/main/resources/theme/keycloak.v2/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/keycloak.v2/account/messages/messages_en.properties @@ -90,9 +90,9 @@ password=My Password otp-display-name=Authenticator Application otp-help-text=Enter a verification code from authenticator application. webauthn-display-name=Security Key -webauthn-help-text=Use your security key to log in. +webauthn-help-text=Use your security key to sign in. webauthn-passwordless-display-name=Security Key -webauthn-passwordless-help-text=Use your security key for passwordless log in. +webauthn-passwordless-help-text=Use your security key for passwordless sign in. basic-authentication=Basic Authentication invalidRequestMessage=Invalid Request