KEYCLOAK-19487 Test cases for managing 2FA authenticators in account console
This commit is contained in:
parent
38174212f9
commit
faefeccbee
33 changed files with 750 additions and 176 deletions
|
@ -96,6 +96,16 @@
|
|||
<artifactId>arquillian-drone-appium-extension</artifactId>
|
||||
<version>${arquillian-drone.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
|
|
|
@ -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<CredentialRepresentation> credentials = userResource.credentials();
|
||||
SigningInPage.UserCredential userCredential =
|
||||
credentialType.getUserCredential(credentials.get(credentials.size() - 1).getId());
|
||||
|
||||
assertThat(userCredential.isPresent(), is(true));
|
||||
return userCredential;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <vmuzikar@redhat.com>
|
||||
*/
|
||||
@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<CredentialRepresentation> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
<packaging>pom</packaging>
|
||||
|
||||
<name>Other Tests Modules</name>
|
||||
|
||||
|
||||
<description>Test modules that depend on the Base TestSuite.
|
||||
This POM contains common configuration for submodules.</description>
|
||||
|
||||
|
@ -172,6 +172,7 @@
|
|||
<id>webauthn</id>
|
||||
<modules>
|
||||
<module>console</module>
|
||||
<module>base-ui</module>
|
||||
<module>webauthn</module>
|
||||
</modules>
|
||||
</profile>
|
||||
|
|
|
@ -31,6 +31,11 @@
|
|||
<version>${project.version}</version>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak.testsuite</groupId>
|
||||
<artifactId>integration-arquillian-tests-base-ui</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.arquillian.extension</groupId>
|
||||
<artifactId>arquillian-drone-bom</artifactId>
|
||||
|
|
|
@ -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 <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
|
@ -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<WebElement> 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<String> 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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<SigningInPage.UserCredential> 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<CredentialRepresentation> list = userResource.credentials();
|
||||
assertThat(list, notNullValue());
|
||||
assertThat(list, not(empty()));
|
||||
|
||||
final List<String> credentialsLabels = list.stream()
|
||||
.map(CredentialRepresentation::getUserLabel)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
assertThat(credentialsLabels, notNullValue());
|
||||
final List<String> createdCredentialsLabels = createdCredentials.stream()
|
||||
.map(SigningInPage.UserCredential::getUserLabel)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
assertThat(credentialsLabels.containsAll(createdCredentialsLabels), is(true));
|
||||
|
||||
final List<SigningInPage.UserCredential> 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue