mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-16 12:05:49 -06:00
[Test Framework] Migrate initial WebAuthn setup + WebAuthnRegisterAndLoginTest. (#44016)
Signed-off-by: Lukas Hanusovsky <lhanusov@redhat.com>
This commit is contained in:
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@@ -929,12 +929,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
matrix:
|
||||
browser:
|
||||
- chrome
|
||||
- firefox
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
@@ -943,13 +937,16 @@ jobs:
|
||||
uses: ./.github/actions/integration-test-setup
|
||||
|
||||
- uses: ./.github/actions/install-chrome
|
||||
if: matrix.browser == 'chrome'
|
||||
|
||||
- name: Run WebAuthn IT
|
||||
run: |
|
||||
TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh webauthn`
|
||||
echo "Tests: $TESTS"
|
||||
./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=$TESTS -Dbrowser=${{ matrix.browser }} "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" "-Dwebdriver.gecko.driver=$GECKOWEBDRIVER/geckodriver" -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh
|
||||
./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Dtest=$TESTS -Dbrowser=chrome "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh
|
||||
|
||||
- name: Run new WebAuthn IT
|
||||
run: |
|
||||
./mvnw test -f tests/webauthn/pom.xml
|
||||
|
||||
- uses: ./.github/actions/upload-flaky-tests
|
||||
name: Upload flaky tests
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.keycloak.testframework.events;
|
||||
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
|
||||
@@ -37,6 +38,16 @@ public class EventAssertion {
|
||||
return this;
|
||||
}
|
||||
|
||||
public EventAssertion hasSessionId() {
|
||||
MatcherAssert.assertThat(event.getSessionId(), EventMatchers.isSessionId());
|
||||
return this;
|
||||
}
|
||||
|
||||
public EventAssertion isCodeId() {
|
||||
MatcherAssert.assertThat(event.getDetails().get(Details.CODE_ID), EventMatchers.isCodeId());
|
||||
return this;
|
||||
}
|
||||
|
||||
public EventAssertion clientId(String clientId) {
|
||||
Assertions.assertEquals(clientId, event.getClientId());
|
||||
return this;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.keycloak.testframework.realm;
|
||||
|
||||
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
|
||||
|
||||
public class AuthenticationExecutionExportConfigBuilder {
|
||||
|
||||
private final AuthenticationExecutionExportRepresentation rep;
|
||||
|
||||
private AuthenticationExecutionExportConfigBuilder(AuthenticationExecutionExportRepresentation rep) {
|
||||
this.rep = rep;
|
||||
}
|
||||
|
||||
public static AuthenticationExecutionExportConfigBuilder create() {
|
||||
AuthenticationExecutionExportRepresentation rep = new AuthenticationExecutionExportRepresentation();
|
||||
return new AuthenticationExecutionExportConfigBuilder(rep);
|
||||
}
|
||||
|
||||
public static AuthenticationExecutionExportConfigBuilder update(AuthenticationExecutionExportRepresentation rep) {
|
||||
return new AuthenticationExecutionExportConfigBuilder(rep);
|
||||
}
|
||||
|
||||
public AuthenticationExecutionExportConfigBuilder authenticator(String authenticator) {
|
||||
rep.setAuthenticator(authenticator);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthenticationExecutionExportConfigBuilder flowAlias(String flowAlias) {
|
||||
rep.setFlowAlias(flowAlias);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthenticationExecutionExportConfigBuilder requirement(String requirement) {
|
||||
rep.setRequirement(requirement);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthenticationExecutionExportConfigBuilder priority(Integer priority) {
|
||||
rep.setPriority(priority);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthenticationExecutionExportConfigBuilder userSetupAllowed(boolean allowed) {
|
||||
rep.setUserSetupAllowed(allowed);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthenticationExecutionExportConfigBuilder authenticatorFlow(boolean enabled) {
|
||||
rep.setAuthenticatorFlow(enabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthenticationExecutionExportRepresentation build() {
|
||||
return rep;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.keycloak.testframework.realm;
|
||||
|
||||
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
|
||||
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
|
||||
|
||||
public class AuthenticationFlowConfigBuilder {
|
||||
|
||||
private final AuthenticationFlowRepresentation rep;
|
||||
|
||||
private AuthenticationFlowConfigBuilder(AuthenticationFlowRepresentation rep) {
|
||||
this.rep = rep;
|
||||
}
|
||||
|
||||
public static AuthenticationFlowConfigBuilder create() {
|
||||
AuthenticationFlowRepresentation rep = new AuthenticationFlowRepresentation();
|
||||
return new AuthenticationFlowConfigBuilder(rep);
|
||||
}
|
||||
|
||||
public static AuthenticationFlowConfigBuilder update(AuthenticationFlowRepresentation rep) {
|
||||
return new AuthenticationFlowConfigBuilder(rep);
|
||||
}
|
||||
|
||||
public AuthenticationFlowConfigBuilder alias(String alias) {
|
||||
rep.setAlias(alias);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthenticationFlowConfigBuilder description(String description) {
|
||||
rep.setDescription(description);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthenticationFlowConfigBuilder providerId(String providerId) {
|
||||
rep.setProviderId(providerId);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthenticationFlowConfigBuilder topLevel(boolean enabled) {
|
||||
rep.setTopLevel(enabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthenticationFlowConfigBuilder builtIn(boolean enabled) {
|
||||
rep.setBuiltIn(enabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthenticationExecutionExportConfigBuilder addAuthenticationExecutionWithAuthenticator(String authenticator, String requirement, Integer priority, boolean userSetupAllowed) {
|
||||
AuthenticationExecutionExportRepresentation exec = new AuthenticationExecutionExportRepresentation();
|
||||
rep.setAuthenticationExecutions(Collections.combine(rep.getAuthenticationExecutions(), exec));
|
||||
|
||||
return AuthenticationExecutionExportConfigBuilder.update(exec).authenticator(authenticator).requirement(requirement)
|
||||
.priority(priority).userSetupAllowed(userSetupAllowed).authenticatorFlow(false);
|
||||
}
|
||||
|
||||
public AuthenticationExecutionExportConfigBuilder addAuthenticationExecutionWithAliasFlow(String flowAlias, String requirement, Integer priority, boolean userSetupAllowed) {
|
||||
AuthenticationExecutionExportRepresentation exec = new AuthenticationExecutionExportRepresentation();
|
||||
rep.setAuthenticationExecutions(Collections.combine(rep.getAuthenticationExecutions(), exec));
|
||||
|
||||
return AuthenticationExecutionExportConfigBuilder.update(exec).flowAlias(flowAlias).requirement(requirement)
|
||||
.priority(priority).userSetupAllowed(userSetupAllowed).authenticatorFlow(true);
|
||||
}
|
||||
|
||||
public AuthenticationFlowRepresentation build() {
|
||||
return rep;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.ClientProfileRepresentation;
|
||||
@@ -13,6 +14,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.RolesRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
@@ -88,11 +90,22 @@ public class RealmConfigBuilder {
|
||||
return RoleConfigBuilder.update(role).name(roleName);
|
||||
}
|
||||
|
||||
public AuthenticationFlowConfigBuilder addAuthenticationFlow(String alias, String description, String providerId, boolean topLevel, boolean builtIn) {
|
||||
AuthenticationFlowRepresentation flow = new AuthenticationFlowRepresentation();
|
||||
rep.setAuthenticationFlows(Collections.combine(rep.getAuthenticationFlows(), flow));
|
||||
return AuthenticationFlowConfigBuilder.update(flow).alias(alias).description(description).providerId(providerId).topLevel(topLevel).builtIn(builtIn);
|
||||
}
|
||||
|
||||
public RealmConfigBuilder registrationEmailAsUsername(boolean registrationEmailAsUsername) {
|
||||
rep.setRegistrationEmailAsUsername(registrationEmailAsUsername);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder registrationAllowed(boolean allowed) {
|
||||
rep.setRegistrationAllowed(allowed);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder editUsernameAllowed(boolean allowed) {
|
||||
rep.setEditUsernameAllowed(allowed);
|
||||
return this;
|
||||
@@ -267,6 +280,106 @@ public class RealmConfigBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder browserFlow(String browserFlow) {
|
||||
rep.setBrowserFlow(browserFlow);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder requiredAction(RequiredActionProviderRepresentation requiredAction) {
|
||||
rep.setRequiredActions(Collections.combine(rep.getRequiredActions(), requiredAction));
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicySignatureAlgorithms(List<String> algorithms) {
|
||||
rep.setWebAuthnPolicySignatureAlgorithms(algorithms);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyAttestationConveyancePreference(String preference) {
|
||||
rep.setWebAuthnPolicyAttestationConveyancePreference(preference);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyAuthenticatorAttachment(String attachment) {
|
||||
rep.setWebAuthnPolicyAuthenticatorAttachment(attachment);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyRequireResidentKey(String residentKey) {
|
||||
rep.setWebAuthnPolicyRequireResidentKey(residentKey);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyUserVerificationRequirement(String requirement) {
|
||||
rep.setWebAuthnPolicyUserVerificationRequirement(requirement);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyRpEntityName(String entityName) {
|
||||
rep.setWebAuthnPolicyRpEntityName(entityName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyRpId(String rpId) {
|
||||
rep.setWebAuthnPolicyRpId(rpId);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyCreateTimeout(Integer timeout) {
|
||||
rep.setWebAuthnPolicyCreateTimeout(timeout);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyAvoidSameAuthenticatorRegister(Boolean register) {
|
||||
rep.setWebAuthnPolicyAvoidSameAuthenticatorRegister(register);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyPasswordlessSignatureAlgorithms(List<String> algorithms) {
|
||||
rep.setWebAuthnPolicySignatureAlgorithms(algorithms);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyPasswordlessAttestationConveyancePreference(String preference) {
|
||||
rep.setWebAuthnPolicyPasswordlessAttestationConveyancePreference(preference);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyPasswordlessAuthenticatorAttachment(String attachment) {
|
||||
rep.setWebAuthnPolicyPasswordlessAuthenticatorAttachment(attachment);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyPasswordlessRequireResidentKey(String residentKey) {
|
||||
rep.setWebAuthnPolicyPasswordlessRequireResidentKey(residentKey);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyPasswordlessUserVerificationRequirement(String requirement) {
|
||||
rep.setWebAuthnPolicyPasswordlessUserVerificationRequirement(requirement);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyPasswordlessRpEntityName(String entityName) {
|
||||
rep.setWebAuthnPolicyPasswordlessRpEntityName(entityName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyPasswordlessCreateTimeout(Integer timeout) {
|
||||
rep.setWebAuthnPolicyPasswordlessCreateTimeout(timeout);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister(Boolean register) {
|
||||
rep.setWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister(register);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmConfigBuilder webAuthnPolicyAcceptableAaguids(List<String> aaguids) {
|
||||
rep.setWebAuthnPolicyAcceptableAaguids(aaguids);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Best practice is to use other convenience methods when configuring a realm, but while the framework is under
|
||||
* active development there may not be a way to perform all updates required. In these cases this method allows
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.keycloak.testframework.ui.page;
|
||||
|
||||
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
|
||||
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
public class LoginUsernamePage extends AbstractLoginPage {
|
||||
|
||||
@FindBy(id = "username")
|
||||
private WebElement usernameInput;
|
||||
|
||||
@FindBy(css = "[type=submit]")
|
||||
private WebElement submitButton;
|
||||
|
||||
public LoginUsernamePage(ManagedWebDriver driver) {
|
||||
super(driver);
|
||||
}
|
||||
|
||||
public void fillLoginWithUsernameOnly(String username) {
|
||||
usernameInput.sendKeys(username);
|
||||
}
|
||||
|
||||
public void submit() {
|
||||
submitButton.click();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpectedPageId() {
|
||||
return "login-login-username";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2022 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.testframework.ui.page;
|
||||
|
||||
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
|
||||
|
||||
import org.openqa.selenium.Keys;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class LogoutConfirmPage extends AbstractLoginPage {
|
||||
|
||||
@FindBy(css = "input[type=\"submit\"]")
|
||||
private WebElement confirmLogoutButton;
|
||||
|
||||
public LogoutConfirmPage(ManagedWebDriver driver) {
|
||||
super(driver);
|
||||
}
|
||||
|
||||
public void confirmLogout() {
|
||||
confirmLogoutButton.sendKeys(Keys.ENTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpectedPageId() {
|
||||
return "login-logout-confirm";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.keycloak.testframework.ui.page;
|
||||
|
||||
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
|
||||
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
/**
|
||||
* login page for PasswordForm. It contains only password, but not username
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class PasswordPage extends AbstractLoginPage {
|
||||
|
||||
@FindBy(id = "password")
|
||||
private WebElement passwordInput;
|
||||
|
||||
@FindBy(id = "input-error-password")
|
||||
private WebElement passwordError;
|
||||
|
||||
@FindBy(name = "login")
|
||||
private WebElement submitButton;
|
||||
|
||||
@FindBy(css = "div[class^='pf-v5-c-alert'], div[class^='alert-error']")
|
||||
private WebElement loginErrorMessage;
|
||||
|
||||
@FindBy(linkText = "Forgot Password?")
|
||||
private WebElement resetPasswordLink;
|
||||
|
||||
@FindBy(id = "try-another-way")
|
||||
private WebElement tryAnotherWayLink;
|
||||
|
||||
public PasswordPage(ManagedWebDriver driver) {
|
||||
super(driver);
|
||||
}
|
||||
|
||||
public void fillPassword(String password) {
|
||||
passwordInput.clear();
|
||||
passwordInput.sendKeys(password);
|
||||
}
|
||||
|
||||
public void submit() {
|
||||
submitButton.click();
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return passwordInput.getAttribute("value");
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
try {
|
||||
return loginErrorMessage.getText();
|
||||
} catch (NoSuchElementException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void clickTryAnotherWayLink() {
|
||||
tryAnotherWayLink.click();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpectedPageId() {
|
||||
return "login-login-password";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright 2016 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.testframework.ui.page;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
|
||||
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.Keys;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class RegisterPage extends AbstractLoginPage {
|
||||
|
||||
@FindBy(name = "firstName")
|
||||
private WebElement firstNameInput;
|
||||
|
||||
@FindBy(name = "lastName")
|
||||
private WebElement lastNameInput;
|
||||
|
||||
@FindBy(name = "email")
|
||||
private WebElement emailInput;
|
||||
|
||||
@FindBy(name = "username")
|
||||
private WebElement usernameInput;
|
||||
|
||||
@FindBy(name = "password")
|
||||
private WebElement passwordInput;
|
||||
|
||||
@FindBy(name = "password-confirm")
|
||||
private WebElement passwordConfirmInput;
|
||||
|
||||
@FindBy(name = "department")
|
||||
private WebElement departmentInput;
|
||||
|
||||
@FindBy(name = "termsAccepted")
|
||||
private WebElement termsAcceptedInput;
|
||||
|
||||
@FindBy(css = "input[type=\"submit\"]")
|
||||
private WebElement submitButton;
|
||||
|
||||
public RegisterPage(ManagedWebDriver driver) {
|
||||
super(driver);
|
||||
}
|
||||
|
||||
public void register(String firstName, String lastName, String email, String username, String password) {
|
||||
register(firstName, lastName, email, username, password, password, null, null, null);
|
||||
}
|
||||
|
||||
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm) {
|
||||
register(firstName, lastName, email, username, password, passwordConfirm, null, null, null);
|
||||
}
|
||||
|
||||
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm, String department, Boolean termsAccepted, Map<String, String> attributes) {
|
||||
firstNameInput.clear();
|
||||
if (firstName != null) {
|
||||
firstNameInput.sendKeys(firstName);
|
||||
}
|
||||
|
||||
lastNameInput.clear();
|
||||
if (lastName != null) {
|
||||
lastNameInput.sendKeys(lastName);
|
||||
}
|
||||
|
||||
if (email != null) {
|
||||
if (isEmailPresent()) {
|
||||
emailInput.clear();
|
||||
emailInput.sendKeys(email);
|
||||
}
|
||||
}
|
||||
|
||||
usernameInput.clear();
|
||||
if (username != null) {
|
||||
usernameInput.sendKeys(username);
|
||||
}
|
||||
|
||||
passwordInput.clear();
|
||||
if (password != null) {
|
||||
passwordInput.sendKeys(password);
|
||||
}
|
||||
|
||||
passwordConfirmInput.clear();
|
||||
if (passwordConfirm != null) {
|
||||
passwordConfirmInput.sendKeys(passwordConfirm);
|
||||
}
|
||||
|
||||
|
||||
if (department != null) {
|
||||
if(isDepartmentPresent()) {
|
||||
departmentInput.clear();
|
||||
departmentInput.sendKeys(department);
|
||||
}
|
||||
}
|
||||
|
||||
if (termsAccepted != null && termsAccepted) {
|
||||
termsAcceptedInput.click();
|
||||
}
|
||||
|
||||
if (attributes != null) {
|
||||
for (Entry<String, String> attribute : attributes.entrySet()) {
|
||||
driver.findElement(By.name(Constants.USER_ATTRIBUTES_PREFIX + attribute.getKey())).sendKeys(attribute.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
submitButton.sendKeys(Keys.ENTER);
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstNameInput.getAttribute("value");
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastNameInput.getAttribute("value");
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return emailInput.getAttribute("value");
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return usernameInput.getAttribute("value");
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return passwordInput.getAttribute("value");
|
||||
}
|
||||
|
||||
public boolean isDepartmentPresent() {
|
||||
try {
|
||||
return driver.findElement(By.name("department")).isDisplayed();
|
||||
} catch (NoSuchElementException nse) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmailPresent() {
|
||||
try {
|
||||
return driver.findElement(By.name("email")).isDisplayed();
|
||||
} catch (NoSuchElementException nse) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpectedPageId() {
|
||||
return "login-register";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.keycloak.testframework.ui.page;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
|
||||
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebElement;
|
||||
|
||||
/**
|
||||
* Login page with the list of authentication mechanisms, which are available to the user (Password, OTP, WebAuthn...)
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
* @author <a href="mailto:pzaoral@redhat.com">Peter Zaoral</a>
|
||||
*/
|
||||
public class SelectAuthenticatorPage extends AbstractLoginPage {
|
||||
|
||||
// Corresponds to the PasswordForm
|
||||
public static final String PASSWORD = "Password";
|
||||
|
||||
// Corresponds to the WebAuthn authenticators
|
||||
public static final String SECURITY_KEY = "Passkey";
|
||||
|
||||
public SelectAuthenticatorPage(ManagedWebDriver driver) {
|
||||
super(driver);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Selects the chosen login method (For example "Password") by click on it.
|
||||
*
|
||||
* @param loginMethodName name as displayed. For example "Password" or "Authenticator Application"
|
||||
*
|
||||
*/
|
||||
public void selectLoginMethod(String loginMethodName) {
|
||||
getLoginMethodRowByName(loginMethodName).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return help text corresponding to the named login method
|
||||
*
|
||||
* @param loginMethodName name as displayed. For example "Password" or "Authenticator Application"
|
||||
* @return
|
||||
*/
|
||||
public String getLoginMethodHelpText(String loginMethodName) {
|
||||
return getLoginMethodRowByName(loginMethodName).findElement(By.className("select-auth-box-desc")).getText();
|
||||
}
|
||||
|
||||
|
||||
private List<WebElement> getLoginMethodsRows() {
|
||||
return driver.driver().findElements(By.className("select-auth-box-parent"));
|
||||
}
|
||||
|
||||
private String getLoginMethodNameFromRow(WebElement loginMethodRow) {
|
||||
return loginMethodRow.findElement(By.className("select-auth-box-headline")).getText();
|
||||
}
|
||||
|
||||
private WebElement getLoginMethodRowByName(String loginMethodName) {
|
||||
return getLoginMethodsRows().stream()
|
||||
.filter(loginMethodRow -> loginMethodName.equals(getLoginMethodNameFromRow(loginMethodRow)))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("Login method '" + loginMethodName + "' not found in the available authentication mechanisms"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpectedPageId() {
|
||||
return "login-select-authenticator";
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public class WaitUtils {
|
||||
}
|
||||
|
||||
private WebDriverWait createDefaultWait() {
|
||||
return new WebDriverWait(managed.driver(), Duration.ofSeconds(10), Duration.ofMillis(50));
|
||||
return new WebDriverWait(managed.driver(), Duration.ofSeconds(5), Duration.ofMillis(50));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -431,6 +431,8 @@ public class LoginPageTest {
|
||||
String nonExistingUrl = oauth.loginForm().build().split("protocol")[0] + "incorrect-path";
|
||||
driver.open(nonExistingUrl);
|
||||
|
||||
errorPage.assertCurrent();
|
||||
|
||||
assertThat(driver.page().getPageSource(), containsString(realmLocalizationMessageValue));
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
<module>custom-providers</module>
|
||||
<module>custom-scripts</module>
|
||||
<module>clustering</module>
|
||||
<module>webauthn</module>
|
||||
</modules>
|
||||
|
||||
<profiles>
|
||||
|
||||
106
tests/webauthn/pom.xml
Normal file
106
tests/webauthn/pom.xml
Normal file
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0"?>
|
||||
<!--
|
||||
~ Copyright 2016 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.
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-tests-parent</artifactId>
|
||||
<groupId>org.keycloak.tests</groupId>
|
||||
<version>999.0.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-tests-webauthn</artifactId>
|
||||
<name>New Keycloak WebAuthn Testsuite</name>
|
||||
<packaging>jar</packaging>
|
||||
<description>New Keycloak WebAuthn Testsuite</description>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak.testframework</groupId>
|
||||
<artifactId>keycloak-test-framework-bom</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>import</scope>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak.testframework</groupId>
|
||||
<artifactId>keycloak-test-framework-core</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak.testframework</groupId>
|
||||
<artifactId>keycloak-test-framework-junit5-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak.testframework</groupId>
|
||||
<artifactId>keycloak-test-framework-ui</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak.testframework</groupId>
|
||||
<artifactId>keycloak-test-framework-oauth</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak.testframework</groupId>
|
||||
<artifactId>keycloak-test-framework-remote</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak.tests</groupId>
|
||||
<artifactId>keycloak-tests-utils</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak.tests</groupId>
|
||||
<artifactId>keycloak-tests-utils-shared</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.platform</groupId>
|
||||
<artifactId>junit-platform-suite</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logmanager</groupId>
|
||||
<artifactId>jboss-logmanager</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<java.util.concurrent.ForkJoinPool.common.threadFactory>io.quarkus.bootstrap.forkjoin.QuarkusForkJoinWorkerThreadFactory</java.util.concurrent.ForkJoinPool.common.threadFactory>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.tests.webauthn.authenticators;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
|
||||
|
||||
import static org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions.Protocol.U2F;
|
||||
import static org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions.Transport.BLE;
|
||||
import static org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions.Transport.INTERNAL;
|
||||
import static org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions.Transport.NFC;
|
||||
import static org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions.Transport.USB;
|
||||
|
||||
/**
|
||||
* Default Options for various authenticators
|
||||
*
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
public enum DefaultVirtualAuthOptions {
|
||||
DEFAULT(VirtualAuthenticatorOptions::new),
|
||||
|
||||
DEFAULT_BLE(() -> DEFAULT.getOptions().setTransport(BLE)),
|
||||
DEFAULT_NFC(() -> DEFAULT.getOptions().setTransport(NFC)),
|
||||
DEFAULT_USB(() -> DEFAULT.getOptions().setTransport(USB)),
|
||||
DEFAULT_INTERNAL(() -> DEFAULT.getOptions().setTransport(INTERNAL)),
|
||||
DEFAULT_RESIDENT_KEY(() -> DEFAULT.getOptions()
|
||||
.setHasResidentKey(true)
|
||||
.setHasUserVerification(true)
|
||||
.setIsUserVerified(true)
|
||||
.setIsUserConsenting(true)),
|
||||
PASSKEYS(() -> DEFAULT_RESIDENT_KEY.getOptions().setTransport(INTERNAL)),
|
||||
|
||||
YUBIKEY_4(DefaultVirtualAuthOptions::getYubiKeyGeneralOptions),
|
||||
YUBIKEY_5_USB(DefaultVirtualAuthOptions::getYubiKeyGeneralOptions),
|
||||
YUBIKEY_5_NFC(() -> getYubiKeyGeneralOptions().setTransport(NFC)),
|
||||
|
||||
TOUCH_ID(() -> DEFAULT.getOptions()
|
||||
.setTransport(INTERNAL)
|
||||
.setHasUserVerification(true)
|
||||
.setIsUserVerified(true)
|
||||
);
|
||||
|
||||
private final Supplier<VirtualAuthenticatorOptions> options;
|
||||
|
||||
DefaultVirtualAuthOptions(Supplier<VirtualAuthenticatorOptions> options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public final VirtualAuthenticatorOptions getOptions() {
|
||||
return options.get();
|
||||
}
|
||||
|
||||
private static VirtualAuthenticatorOptions getYubiKeyGeneralOptions() {
|
||||
return new VirtualAuthenticatorOptions()
|
||||
.setTransport(USB)
|
||||
.setProtocol(U2F)
|
||||
.setHasUserVerification(true)
|
||||
.setIsUserConsenting(true)
|
||||
.setIsUserVerified(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.tests.webauthn.authenticators;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticator;
|
||||
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
|
||||
|
||||
/**
|
||||
* Keycloak Virtual Authenticator
|
||||
* <p>
|
||||
* Used as wrapper for VirtualAuthenticator and its options*
|
||||
*
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
public class KcVirtualAuthenticator {
|
||||
private final VirtualAuthenticator authenticator;
|
||||
private final Options options;
|
||||
|
||||
public KcVirtualAuthenticator(VirtualAuthenticator authenticator, VirtualAuthenticatorOptions options) {
|
||||
this.authenticator = authenticator;
|
||||
this.options = new Options(options);
|
||||
}
|
||||
|
||||
public VirtualAuthenticator getAuthenticator() {
|
||||
return authenticator;
|
||||
}
|
||||
|
||||
public Options getOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
public static final class Options {
|
||||
private final VirtualAuthenticatorOptions options;
|
||||
private final VirtualAuthenticatorOptions.Protocol protocol;
|
||||
private final VirtualAuthenticatorOptions.Transport transport;
|
||||
private final boolean hasResidentKey;
|
||||
private final boolean hasUserVerification;
|
||||
private final boolean isUserConsenting;
|
||||
private final boolean isUserVerified;
|
||||
private final Map<String, Object> map;
|
||||
|
||||
private Options(VirtualAuthenticatorOptions options) {
|
||||
this.options = options;
|
||||
|
||||
this.map = options.toMap();
|
||||
this.protocol = protocolFromMap(map);
|
||||
this.transport = transportFromMap(map);
|
||||
this.hasResidentKey = (Boolean) map.get("hasResidentKey");
|
||||
this.hasUserVerification = (Boolean) map.get("hasUserVerification");
|
||||
this.isUserConsenting = (Boolean) map.get("isUserConsenting");
|
||||
this.isUserVerified = (Boolean) map.get("isUserVerified");
|
||||
}
|
||||
|
||||
public VirtualAuthenticatorOptions.Protocol getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
public VirtualAuthenticatorOptions.Transport getTransport() {
|
||||
return transport;
|
||||
}
|
||||
|
||||
public boolean hasResidentKey() {
|
||||
return hasResidentKey;
|
||||
}
|
||||
|
||||
public boolean hasUserVerification() {
|
||||
return hasUserVerification;
|
||||
}
|
||||
|
||||
public boolean isUserConsenting() {
|
||||
return isUserConsenting;
|
||||
}
|
||||
|
||||
public boolean isUserVerified() {
|
||||
return isUserVerified;
|
||||
}
|
||||
|
||||
public VirtualAuthenticatorOptions clone() {
|
||||
return options;
|
||||
}
|
||||
|
||||
public Map<String, Object> asMap() {
|
||||
return map;
|
||||
}
|
||||
|
||||
private static VirtualAuthenticatorOptions.Protocol protocolFromMap(Map<String, Object> map) {
|
||||
return Arrays.stream(VirtualAuthenticatorOptions.Protocol.values())
|
||||
.filter(f -> f.id.equals(map.get("protocol")))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
private static VirtualAuthenticatorOptions.Transport transportFromMap(Map<String, Object> map) {
|
||||
return Arrays.stream(VirtualAuthenticatorOptions.Transport.values())
|
||||
.filter(f -> f.id.equals(map.get("transport")))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.tests.webauthn.authenticators;
|
||||
|
||||
|
||||
/**
|
||||
* Interface for test classes which use Virtual Authenticators
|
||||
*
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
public interface UseVirtualAuthenticators {
|
||||
|
||||
/**
|
||||
* Set up Virtual Authenticator in @Before method for each test method
|
||||
*/
|
||||
void setUpVirtualAuthenticator();
|
||||
|
||||
/**
|
||||
* Remove Virtual Authenticator in @After method for each test method
|
||||
*/
|
||||
void removeVirtualAuthenticator();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.tests.webauthn.authenticators;
|
||||
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.virtualauthenticator.HasVirtualAuthenticator;
|
||||
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
/**
|
||||
* Manager for Virtual Authenticators
|
||||
*
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
public class VirtualAuthenticatorManager {
|
||||
private final HasVirtualAuthenticator driver;
|
||||
private KcVirtualAuthenticator currentAuthenticator;
|
||||
|
||||
public VirtualAuthenticatorManager(WebDriver driver) {
|
||||
assertThat("Driver must support Virtual Authenticators", driver, CoreMatchers.instanceOf(HasVirtualAuthenticator.class));
|
||||
this.driver = (HasVirtualAuthenticator) driver;
|
||||
}
|
||||
|
||||
public KcVirtualAuthenticator useAuthenticator(VirtualAuthenticatorOptions options) {
|
||||
if (options == null) return null;
|
||||
|
||||
removeAuthenticator();
|
||||
this.currentAuthenticator = new KcVirtualAuthenticator(driver.addVirtualAuthenticator(options), options);
|
||||
return currentAuthenticator;
|
||||
}
|
||||
|
||||
public KcVirtualAuthenticator getCurrent() {
|
||||
return currentAuthenticator;
|
||||
}
|
||||
|
||||
public void removeAuthenticator() {
|
||||
if (currentAuthenticator != null) {
|
||||
currentAuthenticator.getAuthenticator().removeAllCredentials();
|
||||
driver.removeVirtualAuthenticator(currentAuthenticator.getAuthenticator());
|
||||
this.currentAuthenticator = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.keycloak.tests.webauthn.page;
|
||||
|
||||
import org.keycloak.testframework.ui.page.AbstractLoginPage;
|
||||
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
public class WebAuthnErrorPage extends AbstractLoginPage {
|
||||
|
||||
@FindBy(id = "kc-try-again")
|
||||
private WebElement tryAgainButton;
|
||||
|
||||
// Available only with AIA
|
||||
@FindBy(id = "cancelWebAuthnAIA")
|
||||
private WebElement cancelRegistrationAIA;
|
||||
|
||||
@FindBy(css = "div[class^='pf-v5-c-alert'], div[class^='alert-error']")
|
||||
private WebElement errorMessage;
|
||||
|
||||
public WebAuthnErrorPage(ManagedWebDriver driver) {
|
||||
super(driver);
|
||||
}
|
||||
|
||||
public void clickTryAgain() {
|
||||
tryAgainButton.click();
|
||||
}
|
||||
|
||||
public void clickCancelRegistrationAIA() {
|
||||
try {
|
||||
cancelRegistrationAIA.click();
|
||||
} catch (NoSuchElementException e) {
|
||||
Assertions.fail("It only works with AIA");
|
||||
}
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
try {
|
||||
return errorMessage.getText();
|
||||
} catch (NoSuchElementException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpectedPageId() {
|
||||
return "login-webauthn-error";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2019 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.tests.webauthn.page;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.testframework.ui.page.AbstractLoginPage;
|
||||
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
|
||||
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
/**
|
||||
* Page shown during WebAuthn login. Page is useful with Chrome testing API
|
||||
*/
|
||||
public class WebAuthnLoginPage extends AbstractLoginPage {
|
||||
|
||||
@FindBy(id = "authenticateWebAuthnButton")
|
||||
private WebElement authenticateButton;
|
||||
|
||||
@FindBy(xpath = "//div[contains(@id,'kc-webauthn-authenticator-label-')]")
|
||||
private List<WebElement> authenticatorsLabels;
|
||||
|
||||
@FindBy(xpath = "//div[contains(@id,'kc-webauthn-authenticator-item-')]")
|
||||
private List<WebElement> authenticators;
|
||||
|
||||
public WebAuthnLoginPage(ManagedWebDriver driver) {
|
||||
super(driver);
|
||||
}
|
||||
|
||||
public void clickAuthenticate() {
|
||||
authenticateButton.click();
|
||||
}
|
||||
|
||||
public List<WebAuthnAuthenticatorItem> getItems() {
|
||||
try {
|
||||
List<WebAuthnAuthenticatorItem> items = new ArrayList<>();
|
||||
for (int i = 0; i < authenticators.size(); i++) {
|
||||
WebElement auth = authenticators.get(i);
|
||||
final String nameId = "kc-webauthn-authenticator-label-" + i;
|
||||
String name = auth.findElement(By.id(nameId)).isDisplayed() ?
|
||||
auth.findElement(By.id(nameId)).getText() : null;
|
||||
final String createdAtId = "kc-webauthn-authenticator-created-" + i;
|
||||
String createdAt = auth.findElement(By.id(createdAtId)).isDisplayed() ?
|
||||
auth.findElement(By.id(createdAtId)).getText() : null;
|
||||
final String createdAtLabelId = "kc-webauthn-authenticator-createdlabel-" + i;
|
||||
String createdAtLabel = auth.findElement(By.id(createdAtLabelId)).isDisplayed() ?
|
||||
auth.findElement(By.id(createdAtLabelId)).getText() : null;
|
||||
final String transportId = "kc-webauthn-authenticator-transport-" + i;
|
||||
String transport = auth.findElement(By.id(transportId)).isDisplayed() ?
|
||||
auth.findElement(By.id(transportId)).getText() : null;
|
||||
items.add(new WebAuthnAuthenticatorItem(name, createdAt, createdAtLabel, transport));
|
||||
}
|
||||
return items;
|
||||
} catch (NoSuchElementException e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
try {
|
||||
return authenticators.size();
|
||||
} catch (NoSuchElementException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getLabels() {
|
||||
try {
|
||||
return getItems().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(WebAuthnAuthenticatorItem::getName)
|
||||
.collect(Collectors.toList());
|
||||
} catch (NoSuchElementException e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpectedPageId() {
|
||||
return "login-webauthn-authenticate";
|
||||
}
|
||||
|
||||
public static class WebAuthnAuthenticatorItem {
|
||||
private final String name;
|
||||
private final String createdAt;
|
||||
private final String createdAtLabel;
|
||||
private final String transport;
|
||||
|
||||
public WebAuthnAuthenticatorItem(String name, String createdAt, String createdAtLabel, String transport) {
|
||||
this.name = name;
|
||||
this.createdAt = createdAt;
|
||||
this.createdAtLabel = createdAtLabel;
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getCreatedDate() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public String getCreatedLabel() {
|
||||
return createdAtLabel;
|
||||
}
|
||||
|
||||
public String getTransport() {
|
||||
return transport;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2019 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.tests.webauthn.page;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.keycloak.testframework.ui.page.AbstractLoginPage;
|
||||
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
|
||||
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.openqa.selenium.Alert;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.TimeoutException;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
import org.openqa.selenium.support.ui.ExpectedConditions;
|
||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
/**
|
||||
* WebAuthnRegisterPage, which is displayed when WebAuthnRegister required action is triggered. It is useful with Chrome testing API.
|
||||
* <p>
|
||||
* Page will be displayed after successful JS call of "navigator.credentials.create", which will register WebAuthn credential
|
||||
* with the browser
|
||||
*/
|
||||
public class WebAuthnRegisterPage extends AbstractLoginPage {
|
||||
|
||||
public static final long ALERT_CHECK_TIMEOUT = 3; //seconds
|
||||
public static final long ALERT_DEFAULT_TIMEOUT = 60; //seconds
|
||||
|
||||
@FindBy(id = "registerWebAuthn")
|
||||
private WebElement registerButton;
|
||||
|
||||
// Available only with AIA
|
||||
@FindBy(id = "cancelWebAuthnAIA")
|
||||
private WebElement cancelAIAButton;
|
||||
|
||||
@FindBy(id = "kc-page-title")
|
||||
private WebElement formTitle;
|
||||
|
||||
public WebAuthnRegisterPage(ManagedWebDriver driver) {
|
||||
super(driver);
|
||||
}
|
||||
|
||||
public void clickRegister() {
|
||||
registerButton.click();
|
||||
}
|
||||
|
||||
public void cancelAIA() {
|
||||
assertThat("It only works with AIA", isAIA(), CoreMatchers.is(true));
|
||||
cancelAIAButton.click();
|
||||
}
|
||||
|
||||
public void registerWebAuthnCredential(String authenticatorLabel) {
|
||||
if (!isRegisterAlertPresent(ALERT_DEFAULT_TIMEOUT)) {
|
||||
throw new TimeoutException("Cannot register Passkey due to missing prompt for registration");
|
||||
}
|
||||
|
||||
Alert promptDialog = driver.driver().switchTo().alert();
|
||||
promptDialog.sendKeys(authenticatorLabel);
|
||||
promptDialog.accept();
|
||||
}
|
||||
|
||||
public boolean isRegisterAlertPresent() {
|
||||
return isRegisterAlertPresent(ALERT_CHECK_TIMEOUT);
|
||||
}
|
||||
|
||||
public boolean isRegisterAlertPresent(long seconds) {
|
||||
try {
|
||||
// label edit after registering authenticator by .create()
|
||||
WebDriverWait wait = new WebDriverWait(driver.driver(), Duration.ofSeconds(seconds));
|
||||
Alert promptDialog = wait.until(ExpectedConditions.alertIsPresent());
|
||||
assertThat(promptDialog.getText(), CoreMatchers.is("Please input your registered passkey's label"));
|
||||
return true;
|
||||
} catch (TimeoutException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean isAIA() {
|
||||
try {
|
||||
cancelAIAButton.getText();
|
||||
return true;
|
||||
} catch (NoSuchElementException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpectedPageId() {
|
||||
return "login-webauthn-register";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
/*
|
||||
* 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.tests.webauthn;
|
||||
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
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.common.util.SecretGenerator;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
|
||||
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectEvents;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.events.Events;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
import org.keycloak.testframework.oauth.TestApp;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectTestApp;
|
||||
import org.keycloak.testframework.realm.AuthenticationFlowConfigBuilder;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.RealmConfig;
|
||||
import org.keycloak.testframework.realm.RealmConfigBuilder;
|
||||
import org.keycloak.testframework.ui.annotations.InjectPage;
|
||||
import org.keycloak.testframework.ui.annotations.InjectWebDriver;
|
||||
import org.keycloak.testframework.ui.page.ErrorPage;
|
||||
import org.keycloak.testframework.ui.page.InfoPage;
|
||||
import org.keycloak.testframework.ui.page.LoginPage;
|
||||
import org.keycloak.testframework.ui.page.LoginUsernamePage;
|
||||
import org.keycloak.testframework.ui.page.LogoutConfirmPage;
|
||||
import org.keycloak.testframework.ui.page.RegisterPage;
|
||||
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
|
||||
import org.keycloak.tests.utils.admin.AdminApiUtil;
|
||||
import org.keycloak.tests.webauthn.authenticators.DefaultVirtualAuthOptions;
|
||||
import org.keycloak.tests.webauthn.authenticators.KcVirtualAuthenticator;
|
||||
import org.keycloak.tests.webauthn.authenticators.UseVirtualAuthenticators;
|
||||
import org.keycloak.tests.webauthn.authenticators.VirtualAuthenticatorManager;
|
||||
import org.keycloak.tests.webauthn.page.WebAuthnErrorPage;
|
||||
import org.keycloak.tests.webauthn.page.WebAuthnLoginPage;
|
||||
import org.keycloak.tests.webauthn.page.WebAuthnRegisterPage;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.firefox.FirefoxDriver;
|
||||
import org.openqa.selenium.virtualauthenticator.Credential;
|
||||
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
/**
|
||||
* Abstract class for WebAuthn tests which use Virtual Authenticators
|
||||
*
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
@KeycloakIntegrationTest
|
||||
public abstract class AbstractWebAuthnVirtualTest implements UseVirtualAuthenticators {
|
||||
|
||||
@InjectRealm(ref = "webauthn", config = WebAuthnRealmConfig.class)
|
||||
ManagedRealm managedRealm;
|
||||
|
||||
@InjectEvents(realmRef = "webauthn")
|
||||
Events events;
|
||||
|
||||
@InjectOAuthClient(realmRef = "webauthn")
|
||||
OAuthClient oAuthClient;
|
||||
|
||||
@InjectTestApp
|
||||
TestApp testApp;
|
||||
|
||||
@InjectWebDriver
|
||||
ManagedWebDriver driver;
|
||||
|
||||
@InjectPage
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@InjectPage
|
||||
protected LoginUsernamePage loginUsernamePage;
|
||||
|
||||
@InjectPage
|
||||
protected ErrorPage errorPage;
|
||||
|
||||
@InjectPage
|
||||
protected RegisterPage registerPage;
|
||||
|
||||
@InjectPage
|
||||
protected WebAuthnRegisterPage webAuthnRegisterPage;
|
||||
|
||||
@InjectPage
|
||||
protected WebAuthnErrorPage webAuthnErrorPage;
|
||||
|
||||
@InjectPage
|
||||
protected WebAuthnLoginPage webAuthnLoginPage;
|
||||
|
||||
@InjectPage
|
||||
protected LogoutConfirmPage logoutConfirmPage;
|
||||
|
||||
@InjectPage
|
||||
protected InfoPage infoPage;
|
||||
|
||||
protected static final Logger LOGGER = Logger.getLogger(AbstractWebAuthnVirtualTest.class);
|
||||
protected static final String ALL_ZERO_AAGUID = "00000000-0000-0000-0000-000000000000";
|
||||
protected static final String ALL_ONE_AAGUID = "11111111-1111-1111-1111-111111111111";
|
||||
protected static final String USERNAME = "UserWebAuthn";
|
||||
protected static final String PASSWORD = generatePassword();
|
||||
protected static final String EMAIL = "UserWebAuthn@email";
|
||||
|
||||
protected final static String base64EncodedPK =
|
||||
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8_zMDQDYAxlU-Q"
|
||||
+ "hk1Dwkf0v18GZca1DMF3SaJ9HPdmShRANCAASNYX5lyVCOZLzFZzrIKmeZ2jwU"
|
||||
+ "RmgsJYxGP__fWN_S-j5sN4tT15XEpN_7QZnt14YvI6uvAgO0uJEboFaZlOEB";
|
||||
|
||||
protected final static PKCS8EncodedKeySpec privateKey = new PKCS8EncodedKeySpec(Base64.getUrlDecoder().decode(base64EncodedPK));
|
||||
|
||||
private VirtualAuthenticatorManager virtualAuthenticatorManager;
|
||||
|
||||
@BeforeEach
|
||||
public void initWebAuthnTestRealm() {
|
||||
RealmRepresentation realmRep = managedRealm.admin().toRepresentation();
|
||||
if (isPasswordless()) {
|
||||
makePasswordlessRequiredActionDefault(realmRep);
|
||||
switchExecutionInBrowserFormToPasswordless(realmRep);
|
||||
}
|
||||
managedRealm.updateWithCleanup(r -> r.update(realmRep));
|
||||
|
||||
setUpVirtualAuthenticator();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanup() {
|
||||
removeVirtualAuthenticator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUpVirtualAuthenticator() {
|
||||
this.virtualAuthenticatorManager = createDefaultVirtualManager(driver.driver(), getDefaultAuthenticatorOptions());
|
||||
events.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeVirtualAuthenticator() {
|
||||
virtualAuthenticatorManager.removeAuthenticator();
|
||||
events.clear();
|
||||
}
|
||||
|
||||
public UserResource userResource() {
|
||||
return AdminApiUtil.findUserByUsernameId(managedRealm.admin(), USERNAME);
|
||||
}
|
||||
|
||||
public VirtualAuthenticatorOptions getDefaultAuthenticatorOptions() {
|
||||
return DefaultVirtualAuthOptions.DEFAULT.getOptions();
|
||||
}
|
||||
|
||||
public VirtualAuthenticatorManager getVirtualAuthManager() {
|
||||
return virtualAuthenticatorManager;
|
||||
}
|
||||
|
||||
public void setVirtualAuthManager(VirtualAuthenticatorManager manager) {
|
||||
this.virtualAuthenticatorManager = manager;
|
||||
}
|
||||
|
||||
public String getCredentialType() {
|
||||
return isPasswordless() ? WebAuthnCredentialModel.TYPE_PASSWORDLESS : WebAuthnCredentialModel.TYPE_TWOFACTOR;
|
||||
}
|
||||
|
||||
public boolean isPasswordless() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static VirtualAuthenticatorManager createDefaultVirtualManager(WebDriver webDriver, VirtualAuthenticatorOptions options) {
|
||||
VirtualAuthenticatorManager manager = new VirtualAuthenticatorManager(webDriver);
|
||||
manager.useAuthenticator(options);
|
||||
return manager;
|
||||
}
|
||||
|
||||
// Registration
|
||||
|
||||
protected void registerDefaultUser() {
|
||||
registerDefaultUser(true);
|
||||
}
|
||||
|
||||
protected void registerDefaultUser(boolean shouldSuccess) {
|
||||
registerDefaultUser(SecretGenerator.getInstance().randomString(24), shouldSuccess);
|
||||
}
|
||||
|
||||
protected void registerDefaultUser(String authenticatorLabel) {
|
||||
registerDefaultUser(authenticatorLabel, true);
|
||||
}
|
||||
|
||||
private void registerDefaultUser(String authenticatorLabel, boolean shouldSuccess) {
|
||||
registerUser(USERNAME, PASSWORD, EMAIL, authenticatorLabel, shouldSuccess);
|
||||
}
|
||||
|
||||
protected void registerUser(String username, String password, String email, String authenticatorLabel, boolean shouldSuccess) {
|
||||
oAuthClient.openRegistrationForm();
|
||||
|
||||
registerPage.assertCurrent();
|
||||
registerPage.register("firstName", "lastName", email, username, password, password);
|
||||
|
||||
// User was registered. Now he needs to register WebAuthn credential
|
||||
webAuthnRegisterPage.assertCurrent();
|
||||
webAuthnRegisterPage.clickRegister();
|
||||
|
||||
if (shouldSuccess) {
|
||||
events.clear();
|
||||
tryRegisterAuthenticator(authenticatorLabel);
|
||||
}
|
||||
}
|
||||
|
||||
private void tryRegisterAuthenticator(String authenticatorLabel) {
|
||||
tryRegisterAuthenticator(authenticatorLabel, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for registering Passkey
|
||||
* Sometimes, it's not possible to register the key, when the Resident Key is required
|
||||
* It seems it's related to Virtual authenticators provided by Selenium framework
|
||||
* Manual testing with Google Chrome authenticators works as expected
|
||||
*/
|
||||
private void tryRegisterAuthenticator(String authenticatorLabel, int numberOfAllowedRetries) {
|
||||
final boolean hasResidentKey = Optional.ofNullable(getVirtualAuthManager())
|
||||
.map(VirtualAuthenticatorManager::getCurrent)
|
||||
.map(KcVirtualAuthenticator::getOptions)
|
||||
.map(KcVirtualAuthenticator.Options::hasResidentKey)
|
||||
.orElse(false);
|
||||
|
||||
if (hasResidentKey && !webAuthnRegisterPage.isRegisterAlertPresent()) {
|
||||
for (int i = 0; i < numberOfAllowedRetries; i++) {
|
||||
events.clear();
|
||||
webAuthnErrorPage.clickTryAgain();
|
||||
webAuthnRegisterPage.assertCurrent();
|
||||
webAuthnRegisterPage.clickRegister();
|
||||
|
||||
if (webAuthnRegisterPage.isRegisterAlertPresent()) {
|
||||
webAuthnRegisterPage.registerWebAuthnCredential(authenticatorLabel);
|
||||
return;
|
||||
} else {
|
||||
webAuthnRegisterPage.assertCurrent();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
webAuthnRegisterPage.registerWebAuthnCredential(authenticatorLabel);
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication
|
||||
|
||||
protected void authenticateDefaultUser() {
|
||||
authenticateDefaultUser(true);
|
||||
}
|
||||
|
||||
protected void authenticateDefaultUser(boolean shouldSuccess) {
|
||||
authenticateUser("test-user@localhost", "password", shouldSuccess);
|
||||
}
|
||||
|
||||
protected void authenticateUser(String username, String password, boolean shouldSuccess) {
|
||||
oAuthClient.openLoginForm();
|
||||
loginPage.assertCurrent();
|
||||
loginPage.fillLogin(username, password);
|
||||
loginPage.submit();
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
if (shouldSuccess) {
|
||||
Assertions.assertNotNull(oAuthClient.parseLoginResponse().getCode());
|
||||
} else {
|
||||
displayErrorMessageIfPresent();
|
||||
}
|
||||
}
|
||||
|
||||
protected String displayErrorMessageIfPresent() {
|
||||
if (webAuthnErrorPage.getExpectedPageId().equals(driver.page().getCurrentPageId())) {
|
||||
final String msg = webAuthnErrorPage.getError();
|
||||
LOGGER.info("Error message from Error Page: " + msg);
|
||||
return msg;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected Credential getDefaultResidentKeyCredential() {
|
||||
byte[] credentialId = {1, 2, 3, 4};
|
||||
byte[] userHandle = {1};
|
||||
return Credential.createResidentCredential(credentialId, "localhost", privateKey, userHandle, 0);
|
||||
}
|
||||
|
||||
protected Credential getDefaultNonResidentKeyCredential() {
|
||||
byte[] credentialId = {1, 2, 3, 4};
|
||||
return Credential.createNonResidentCredential(credentialId, "localhost", privateKey, 0);
|
||||
}
|
||||
|
||||
protected static void makePasswordlessRequiredActionDefault(RealmRepresentation realm) {
|
||||
RequiredActionProviderRepresentation webAuthnProvider = realm.getRequiredActions()
|
||||
.stream()
|
||||
.filter(f -> f.getProviderId().equals(WebAuthnRegisterFactory.PROVIDER_ID))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
assertThat(webAuthnProvider, notNullValue());
|
||||
|
||||
webAuthnProvider.setEnabled(false);
|
||||
|
||||
RequiredActionProviderRepresentation webAuthnPasswordlessProvider = realm.getRequiredActions()
|
||||
.stream()
|
||||
.filter(f -> f.getProviderId().equals(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
assertThat(webAuthnPasswordlessProvider, notNullValue());
|
||||
|
||||
webAuthnPasswordlessProvider.setEnabled(true);
|
||||
webAuthnPasswordlessProvider.setDefaultAction(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the flow "browser-webauthn-forms" to use the passed authenticator as required.
|
||||
* @param realm The realm representation
|
||||
* @param providerId The provider Id to set as required
|
||||
*/
|
||||
protected void switchExecutionInBrowserFormToProvider(RealmRepresentation realm, String providerId) {
|
||||
List<AuthenticationFlowRepresentation> flows = realm.getAuthenticationFlows();
|
||||
assertThat(flows, notNullValue());
|
||||
|
||||
AuthenticationFlowRepresentation browserForm = flows.stream()
|
||||
.filter(f -> f.getAlias().equals("browser-webauthn-forms"))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
assertThat("Cannot find 'browser-webauthn-forms' flow", browserForm, notNullValue());
|
||||
|
||||
flows.removeIf(f -> f.getAlias().equals(browserForm.getAlias()));
|
||||
|
||||
// set just one authenticator with the passkeys conditional UI
|
||||
AuthenticationExecutionExportRepresentation passkeysConditionalUI = new AuthenticationExecutionExportRepresentation();
|
||||
passkeysConditionalUI.setAuthenticator(providerId);
|
||||
passkeysConditionalUI.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED.name());
|
||||
passkeysConditionalUI.setPriority(10);
|
||||
passkeysConditionalUI.setAuthenticatorFlow(false);
|
||||
passkeysConditionalUI.setUserSetupAllowed(false);
|
||||
|
||||
browserForm.setAuthenticationExecutions(List.of(passkeysConditionalUI));
|
||||
flows.add(browserForm);
|
||||
|
||||
realm.setAuthenticationFlows(flows);
|
||||
}
|
||||
|
||||
// Switch WebAuthn authenticator with Passwordless authenticator in browser flow
|
||||
protected void switchExecutionInBrowserFormToPasswordless(RealmRepresentation realm) {
|
||||
List<AuthenticationFlowRepresentation> flows = realm.getAuthenticationFlows();
|
||||
assertThat(flows, notNullValue());
|
||||
|
||||
AuthenticationFlowRepresentation browserForm = flows.stream()
|
||||
.filter(f -> f.getAlias().equals("browser-webauthn-forms"))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
assertThat("Cannot find 'browser-webauthn-forms' flow", browserForm, notNullValue());
|
||||
|
||||
flows.removeIf(f -> f.getAlias().equals(browserForm.getAlias()));
|
||||
|
||||
List<AuthenticationExecutionExportRepresentation> browserFormExecutions = browserForm.getAuthenticationExecutions();
|
||||
assertThat("Flow 'browser-webauthn-forms' doesn't have any executions", browserForm, notNullValue());
|
||||
|
||||
AuthenticationExecutionExportRepresentation webAuthn = browserFormExecutions.stream()
|
||||
.filter(f -> WebAuthnAuthenticatorFactory.PROVIDER_ID.equals(f.getAuthenticator()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
assertThat("Cannot find WebAuthn execution in Browser flow", webAuthn, notNullValue());
|
||||
|
||||
browserFormExecutions.removeIf(f -> webAuthn.getAuthenticator().equals(f.getAuthenticator()));
|
||||
webAuthn.setAuthenticator(WebAuthnPasswordlessAuthenticatorFactory.PROVIDER_ID);
|
||||
browserFormExecutions.add(webAuthn);
|
||||
browserForm.setAuthenticationExecutions(browserFormExecutions);
|
||||
flows.add(browserForm);
|
||||
|
||||
realm.setAuthenticationFlows(flows);
|
||||
}
|
||||
|
||||
protected void logout() {
|
||||
try {
|
||||
oAuthClient.openLogoutForm();
|
||||
logoutConfirmPage.assertCurrent();
|
||||
logoutConfirmPage.confirmLogout();
|
||||
infoPage.assertCurrent();
|
||||
Assertions.assertEquals("You are logged out", infoPage.getInfo());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Cannot logout user", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected String getExpectedMessageByDriver(Map<Class<? extends WebDriver>, String> values) {
|
||||
if (values == null || values.isEmpty()) return "";
|
||||
|
||||
return values.entrySet()
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(f -> f.getKey().isAssignableFrom(driver.getClass()))
|
||||
.findFirst()
|
||||
.map(Map.Entry::getValue)
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
protected String getExpectedMessageByDriver(String firefoxMessage, String chromeMessage) {
|
||||
final Map<Class<? extends WebDriver>, String> map = new HashMap<>();
|
||||
map.put(FirefoxDriver.class, firefoxMessage);
|
||||
map.put(ChromeDriver.class, chromeMessage);
|
||||
|
||||
return getExpectedMessageByDriver(map);
|
||||
}
|
||||
|
||||
protected void checkWebAuthnConfiguration(String residentKey, String userVerification) {
|
||||
RealmRepresentation realmRep = managedRealm.admin().toRepresentation();
|
||||
assertThat(realmRep, notNullValue());
|
||||
if(!isPasswordless()) {
|
||||
assertThat(realmRep.getWebAuthnPolicyRpEntityName(), is("localhost"));
|
||||
assertThat(realmRep.getWebAuthnPolicyRequireResidentKey(), is(residentKey));
|
||||
assertThat(realmRep.getWebAuthnPolicyUserVerificationRequirement(), is(userVerification));
|
||||
} else {
|
||||
assertThat(realmRep.getWebAuthnPolicyPasswordlessRpEntityName(), is("localhost"));
|
||||
assertThat(realmRep.getWebAuthnPolicyPasswordlessRequireResidentKey(), is(residentKey));
|
||||
assertThat(realmRep.getWebAuthnPolicyPasswordlessUserVerificationRequirement(), is(userVerification));
|
||||
}
|
||||
}
|
||||
|
||||
protected static String generatePassword() {
|
||||
return SecretGenerator.getInstance().randomString(64);
|
||||
}
|
||||
|
||||
public static class WebAuthnRealmConfig implements RealmConfig {
|
||||
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder builder) {
|
||||
builder.name("webauthn").registrationAllowed(true);
|
||||
|
||||
AuthenticationFlowConfigBuilder flowBuilder1 = builder
|
||||
.addAuthenticationFlow("browser-webauthn", "browser based authentication", "basic-flow", true, false);
|
||||
flowBuilder1.addAuthenticationExecutionWithAuthenticator("auth-cookie", "ALTERNATIVE", 10, false);
|
||||
flowBuilder1.addAuthenticationExecutionWithAuthenticator("auth-spnego", "DISABLED", 20, false);
|
||||
flowBuilder1.addAuthenticationExecutionWithAuthenticator("identity-provider-redirector", "DISABLED", 25, false);
|
||||
flowBuilder1.addAuthenticationExecutionWithAliasFlow("browser-webauthn-organization", "ALTERNATIVE", 26, false);
|
||||
flowBuilder1.addAuthenticationExecutionWithAliasFlow("browser-webauthn-forms","ALTERNATIVE", 30, false);
|
||||
|
||||
builder.addAuthenticationFlow("browser-webauthn-organization", "", "basic-flow", false, true)
|
||||
.addAuthenticationExecutionWithAliasFlow("browser-webauthn-conditional-organization", "CONDITIONAL", 10, false);
|
||||
|
||||
AuthenticationFlowConfigBuilder flowBuilder2 = builder.addAuthenticationFlow("browser-webauthn-conditional-organization", "Flow to determine if the organization identity-first login is to be used", "basic-flow", false, true);
|
||||
flowBuilder2.addAuthenticationExecutionWithAuthenticator("conditional-user-configured", "REQUIRED", 10, false);
|
||||
flowBuilder2.addAuthenticationExecutionWithAuthenticator("organization", "ALTERNATIVE" , 20, false);
|
||||
|
||||
AuthenticationFlowConfigBuilder flowBuilder3 = builder.addAuthenticationFlow("browser-webauthn-forms", "Username, password, otp and other auth forms.", "basic-flow", false,false);
|
||||
flowBuilder3.addAuthenticationExecutionWithAuthenticator("auth-username-password-form", "REQUIRED", 10, false);
|
||||
flowBuilder3.addAuthenticationExecutionWithAuthenticator("auth-otp-form", "DISABLED" , 20, false);
|
||||
flowBuilder3.addAuthenticationExecutionWithAuthenticator("webauthn-authenticator", "REQUIRED", 21, false);
|
||||
|
||||
AuthenticationFlowConfigBuilder flowBuilder4 = builder.addAuthenticationFlow("browser-webauthn-passwordless", "browser based authentication", "basic-flow", true, false);
|
||||
flowBuilder4.addAuthenticationExecutionWithAuthenticator("auth-cookie", "ALTERNATIVE", 10, false);
|
||||
flowBuilder4.addAuthenticationExecutionWithAliasFlow("browser-webauthn-passwordless-forms", "ALTERNATIVE", 30, false);
|
||||
|
||||
AuthenticationFlowConfigBuilder flowBuilder5 = builder.addAuthenticationFlow("browser-webauthn-passwordless-forms", "Username, password, otp and other auth forms.", "basic-flow", false, false);
|
||||
flowBuilder5.addAuthenticationExecutionWithAuthenticator("auth-username-password-form", "REQUIRED", 10, false);
|
||||
flowBuilder5.addAuthenticationExecutionWithAuthenticator("webauthn-authenticator", "REQUIRED", 20, false);
|
||||
flowBuilder5.addAuthenticationExecutionWithAuthenticator("webauthn-authenticator-passwordless", "REQUIRED", 30, false);
|
||||
|
||||
RequiredActionProviderRepresentation actionRep1 = new RequiredActionProviderRepresentation();
|
||||
actionRep1.setAlias("webauthn-register");
|
||||
actionRep1.setName("Webauthn Register");
|
||||
actionRep1.setProviderId("webauthn-register");
|
||||
actionRep1.setEnabled(true);
|
||||
actionRep1.setDefaultAction(true);
|
||||
actionRep1.setPriority(51);
|
||||
actionRep1.setConfig(Collections.emptyMap());
|
||||
|
||||
builder.requiredAction(actionRep1);
|
||||
|
||||
RequiredActionProviderRepresentation actionRep2 = new RequiredActionProviderRepresentation();
|
||||
actionRep2.setAlias("webauthn-register-passwordless");
|
||||
actionRep2.setName("Webauthn Register Passwordless");
|
||||
actionRep2.setProviderId("webauthn-register-passwordless");
|
||||
actionRep2.setEnabled(true);
|
||||
actionRep2.setDefaultAction(false);
|
||||
actionRep2.setPriority(52);
|
||||
actionRep2.setConfig(Collections.emptyMap());
|
||||
|
||||
builder.requiredAction(actionRep2);
|
||||
|
||||
builder.webAuthnPolicySignatureAlgorithms(List.of("ES256", "RS256", "RS1"))
|
||||
.webAuthnPolicyAttestationConveyancePreference("not specified")
|
||||
.webAuthnPolicyAuthenticatorAttachment("not specified")
|
||||
.webAuthnPolicyRequireResidentKey("not specified")
|
||||
.webAuthnPolicyUserVerificationRequirement("not specified")
|
||||
.webAuthnPolicyRpEntityName("keycloak-webauthn-2FA")
|
||||
.webAuthnPolicyCreateTimeout(60)
|
||||
.webAuthnPolicyAvoidSameAuthenticatorRegister(true);
|
||||
|
||||
builder.webAuthnPolicyPasswordlessSignatureAlgorithms(List.of("ES256", "RS256", "RS1"))
|
||||
.webAuthnPolicyPasswordlessAttestationConveyancePreference("not specified")
|
||||
.webAuthnPolicyPasswordlessAuthenticatorAttachment("not specified")
|
||||
.webAuthnPolicyPasswordlessRequireResidentKey("not specified")
|
||||
.webAuthnPolicyPasswordlessUserVerificationRequirement("not specified")
|
||||
.webAuthnPolicyPasswordlessRpEntityName("keycloak-webauthn-passwordless-2FA")
|
||||
.webAuthnPolicyPasswordlessCreateTimeout(60)
|
||||
.webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister(true);
|
||||
|
||||
builder.browserFlow("browser-webauthn");
|
||||
|
||||
builder.addUser(USERNAME).password(PASSWORD).name("WebAuthn", "User")
|
||||
.email("webauthn-user@localhost").emailVerified(true);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
/*
|
||||
* Copyright 2019 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.tests.webauthn;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.WebAuthnConstants;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory;
|
||||
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.util.SecretGenerator;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.events.EventAssertion;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
import org.keycloak.testframework.ui.annotations.InjectPage;
|
||||
import org.keycloak.testframework.ui.page.ErrorPage;
|
||||
import org.keycloak.testframework.ui.page.PasswordPage;
|
||||
import org.keycloak.testframework.ui.page.SelectAuthenticatorPage;
|
||||
import org.keycloak.tests.utils.admin.AdminApiUtil;
|
||||
import org.keycloak.testsuite.util.FlowUtil;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.ALTERNATIVE;
|
||||
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.hasItem;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
@KeycloakIntegrationTest
|
||||
public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
|
||||
|
||||
@InjectRunOnServer(realmRef = "webauthn")
|
||||
RunOnServerClient runOnServer;
|
||||
|
||||
@InjectPage
|
||||
ErrorPage errorPage;
|
||||
|
||||
@InjectPage
|
||||
PasswordPage passwordPage;
|
||||
|
||||
@InjectPage
|
||||
SelectAuthenticatorPage selectAuthenticatorPage;
|
||||
|
||||
@BeforeEach
|
||||
public void customizeWebAuthnTestRealm() {
|
||||
List<String> acceptableAaguids = new ArrayList<>();
|
||||
acceptableAaguids.add("00000000-0000-0000-0000-000000000000");
|
||||
acceptableAaguids.add("6d44ba9b-f6ec-2e49-b930-0c8fe920cb73");
|
||||
|
||||
managedRealm.updateWithCleanup(r -> r.webAuthnPolicyAcceptableAaguids(acceptableAaguids));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerUserSuccess() {
|
||||
String username = "registerUserSuccess";
|
||||
String email = "registerUserSuccess@email";
|
||||
String password = generatePassword();
|
||||
String userId;
|
||||
|
||||
updateRealmWithDefaultWebAuthnSettings();
|
||||
|
||||
oAuthClient.openRegistrationForm();
|
||||
registerPage.assertCurrent();
|
||||
|
||||
String authenticatorLabel = SecretGenerator.getInstance().randomString(24);
|
||||
registerPage.register("firstName", "lastName", email, username, password);
|
||||
|
||||
// User was registered. Now he needs to register WebAuthn credential
|
||||
webAuthnRegisterPage.assertCurrent();
|
||||
webAuthnRegisterPage.clickRegister();
|
||||
webAuthnRegisterPage.registerWebAuthnCredential(authenticatorLabel);
|
||||
|
||||
Assertions.assertNotNull(oAuthClient.parseLoginResponse().getCode());
|
||||
|
||||
// confirm that registration is successfully completed
|
||||
userId = AdminApiUtil.findUserByUsername(managedRealm.admin(), username).getId();
|
||||
|
||||
EventAssertion.assertSuccess(events.poll()).type(EventType.REGISTER).sessionId(null)
|
||||
.userId(userId)
|
||||
.clientId(Objects.requireNonNull(AdminApiUtil.findClientByClientId(managedRealm.admin(), "test-app")).toRepresentation().getClientId())
|
||||
.details(Details.USERNAME, username)
|
||||
.details(Details.EMAIL, email)
|
||||
.details(Details.REGISTER_METHOD, "form")
|
||||
.details(Details.REDIRECT_URI, testApp.getRedirectionUri());
|
||||
|
||||
|
||||
EventRepresentation event = events.poll();
|
||||
|
||||
EventAssertion.assertSuccess(event).type(EventType.CUSTOM_REQUIRED_ACTION).sessionId(null).userId(userId).isCodeId()
|
||||
.details(Details.REDIRECT_URI, testApp.getRedirectionUri())
|
||||
.details(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.details(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel)
|
||||
.details(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, ALL_ZERO_AAGUID);
|
||||
|
||||
String regPubKeyCredentialId1 = event.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
|
||||
|
||||
EventRepresentation event2 = events.poll();
|
||||
|
||||
EventAssertion.assertSuccess(event2).type(EventType.UPDATE_CREDENTIAL).sessionId(null).userId(userId).isCodeId()
|
||||
.details(Details.REDIRECT_URI, testApp.getRedirectionUri())
|
||||
.details(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.details(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel)
|
||||
.details(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, ALL_ZERO_AAGUID);
|
||||
|
||||
String regPubKeyCredentialId2 = event2.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
|
||||
|
||||
assertThat(regPubKeyCredentialId1, equalTo(regPubKeyCredentialId2));
|
||||
|
||||
// confirm login event
|
||||
EventAssertion.assertSuccess(events.poll()).type(EventType.LOGIN).hasSessionId().userId(userId).isCodeId()
|
||||
.details(Details.REDIRECT_URI, testApp.getRedirectionUri())
|
||||
.details(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.details(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel);
|
||||
|
||||
// confirm user registered
|
||||
assertUserRegistered(userId, username.toLowerCase(), email.toLowerCase());
|
||||
assertRegisteredCredentials(userId, ALL_ZERO_AAGUID, "none");
|
||||
|
||||
events.clear();
|
||||
|
||||
// logout by user
|
||||
logout();
|
||||
|
||||
// confirm logout event
|
||||
EventAssertion.assertSuccess(events.poll()).type(EventType.LOGOUT).hasSessionId().userId(userId)
|
||||
.clientId(Objects.requireNonNull(AdminApiUtil.findClientByClientId(managedRealm.admin(), "account")).toRepresentation().getClientId());
|
||||
|
||||
// login by user
|
||||
oAuthClient.openLoginForm();
|
||||
loginPage.fillLogin(username, password);
|
||||
loginPage.submit();
|
||||
|
||||
assertThat(webAuthnLoginPage.getCount(), is(1));
|
||||
assertThat(webAuthnLoginPage.getLabels(), Matchers.contains(authenticatorLabel));
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
Assertions.assertNotNull(oAuthClient.parseLoginResponse().getCode());
|
||||
|
||||
// confirm login event
|
||||
EventAssertion.assertSuccess(events.poll()).type(EventType.LOGIN).hasSessionId().userId(userId).isCodeId()
|
||||
.details(Details.REDIRECT_URI, testApp.getRedirectionUri())
|
||||
.details(WebAuthnConstants.PUBKEY_CRED_ID_ATTR, regPubKeyCredentialId2)
|
||||
.details(WebAuthnConstants.USER_VERIFICATION_CHECKED, Boolean.FALSE.toString());
|
||||
|
||||
events.clear();
|
||||
// logout by user
|
||||
logout();
|
||||
|
||||
// confirm logout event
|
||||
EventAssertion.assertSuccess(events.poll()).type(EventType.LOGOUT).hasSessionId().userId(userId)
|
||||
.clientId(Objects.requireNonNull(AdminApiUtil.findClientByClientId(managedRealm.admin(), "account")).toRepresentation().getClientId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void webAuthnPasswordlessAlternativeWithWebAuthnAndPassword() {
|
||||
String userId;
|
||||
|
||||
final String WEBAUTHN_LABEL = "webauthn";
|
||||
final String PASSWORDLESS_LABEL = "passwordless";
|
||||
|
||||
managedRealm.updateWithCleanup(r -> r.browserFlow(webAuthnTogetherPasswordlessFlow()));
|
||||
final UserRepresentation cleanupUser = AdminApiUtil.findUserByUsername(managedRealm.admin(), USERNAME);
|
||||
managedRealm.cleanup().add(r -> r.users().get(cleanupUser.getId()).update(cleanupUser));
|
||||
|
||||
UserRepresentation user = AdminApiUtil.findUserByUsername(managedRealm.admin(), USERNAME);
|
||||
|
||||
assertThat(user, notNullValue());
|
||||
user.getRequiredActions().add(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
|
||||
|
||||
UserResource userResource = managedRealm.admin().users().get(user.getId());
|
||||
assertThat(userResource, notNullValue());
|
||||
userResource.update(user);
|
||||
|
||||
user = userResource.toRepresentation();
|
||||
assertThat(user, notNullValue());
|
||||
assertThat(user.getRequiredActions(), hasItem(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID));
|
||||
|
||||
userId = user.getId();
|
||||
|
||||
oAuthClient.openLoginForm();
|
||||
loginUsernamePage.assertCurrent();
|
||||
loginUsernamePage.fillLoginWithUsernameOnly(USERNAME);
|
||||
loginUsernamePage.submit();
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.fillPassword(PASSWORD);
|
||||
passwordPage.submit();
|
||||
|
||||
events.clear();
|
||||
|
||||
webAuthnRegisterPage.assertCurrent();
|
||||
webAuthnRegisterPage.clickRegister();
|
||||
webAuthnRegisterPage.registerWebAuthnCredential(WEBAUTHN_LABEL);
|
||||
|
||||
webAuthnRegisterPage.assertCurrent();
|
||||
|
||||
EventAssertion.assertSuccess(events.poll()).type(EventType.CUSTOM_REQUIRED_ACTION).sessionId(null).userId(userId).isCodeId()
|
||||
.details(Details.REDIRECT_URI, testApp.getRedirectionUri())
|
||||
.details(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.details(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, WEBAUTHN_LABEL);
|
||||
|
||||
EventAssertion.assertSuccess(events.poll()).type(EventType.UPDATE_CREDENTIAL).sessionId(null).userId(userId).isCodeId()
|
||||
.details(Details.REDIRECT_URI, testApp.getRedirectionUri())
|
||||
.details(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.details(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, WEBAUTHN_LABEL);
|
||||
|
||||
webAuthnRegisterPage.clickRegister();
|
||||
webAuthnRegisterPage.registerWebAuthnCredential(PASSWORDLESS_LABEL);
|
||||
|
||||
Assertions.assertNotNull(oAuthClient.parseLoginResponse().getCode());
|
||||
|
||||
EventAssertion.assertSuccess(events.poll()).type(EventType.CUSTOM_REQUIRED_ACTION).sessionId(null).userId(userId).isCodeId()
|
||||
.details(Details.REDIRECT_URI, testApp.getRedirectionUri())
|
||||
.details(Details.CUSTOM_REQUIRED_ACTION, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID)
|
||||
.details(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, PASSWORDLESS_LABEL);
|
||||
|
||||
EventAssertion.assertSuccess(events.poll()).type(EventType.UPDATE_CREDENTIAL).sessionId(null).userId(userId).isCodeId()
|
||||
.details(Details.REDIRECT_URI, testApp.getRedirectionUri())
|
||||
.details(Details.CUSTOM_REQUIRED_ACTION, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID)
|
||||
.details(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, PASSWORDLESS_LABEL);
|
||||
|
||||
EventAssertion.assertSuccess(events.poll()).type(EventType.LOGIN).hasSessionId().userId(userId).isCodeId()
|
||||
.details(Details.REDIRECT_URI, testApp.getRedirectionUri())
|
||||
.details(Details.CUSTOM_REQUIRED_ACTION, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
|
||||
|
||||
events.clear();
|
||||
|
||||
logout();
|
||||
|
||||
EventAssertion.assertSuccess(events.poll()).type(EventType.LOGOUT).hasSessionId().userId(userId)
|
||||
.clientId(Objects.requireNonNull(AdminApiUtil.findClientByClientId(managedRealm.admin(), "account")).toRepresentation().getClientId());
|
||||
|
||||
// Password + WebAuthn Passkey
|
||||
oAuthClient.openLoginForm();
|
||||
loginUsernamePage.assertCurrent();
|
||||
loginUsernamePage.fillLoginWithUsernameOnly(USERNAME);
|
||||
loginUsernamePage.submit();
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.fillPassword(PASSWORD);
|
||||
passwordPage.submit();
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
|
||||
assertThat(webAuthnLoginPage.getCount(), is(1));
|
||||
assertThat(webAuthnLoginPage.getLabels(), Matchers.contains(WEBAUTHN_LABEL));
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
Assertions.assertNotNull(oAuthClient.parseLoginResponse().getCode());
|
||||
logout();
|
||||
|
||||
// Only passwordless login
|
||||
oAuthClient.openLoginForm();
|
||||
loginUsernamePage.assertCurrent();
|
||||
loginUsernamePage.fillLoginWithUsernameOnly(USERNAME);
|
||||
loginUsernamePage.submit();
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.clickTryAnotherWayLink();
|
||||
|
||||
selectAuthenticatorPage.assertCurrent();
|
||||
assertThat(selectAuthenticatorPage.getLoginMethodHelpText(SelectAuthenticatorPage.SECURITY_KEY),
|
||||
is("Use your Passkey for passwordless sign in."));
|
||||
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.SECURITY_KEY);
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
assertThat(webAuthnLoginPage.getCount(), is(0));
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
Assertions.assertNotNull(oAuthClient.parseLoginResponse().getCode());
|
||||
logout();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void webAuthnPasswordlessShouldFailIfUserIsDeletedInBetween() {
|
||||
final String WEBAUTHN_LABEL = "webauthn";
|
||||
final String PASSWORDLESS_LABEL = "passwordless";
|
||||
|
||||
managedRealm.updateWithCleanup(r -> r.browserFlow(webAuthnTogetherPasswordlessFlow()));
|
||||
|
||||
String username = "webauthn-tester@localhost";
|
||||
String password = generatePassword();
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername(username);
|
||||
user.setEnabled(true);
|
||||
user.setFirstName("WebAuthN");
|
||||
user.setLastName("Tester");
|
||||
|
||||
String userId = AdminApiUtil.createUserAndResetPasswordWithAdminClient(managedRealm.admin(), user, password, false);
|
||||
|
||||
user = AdminApiUtil.findUserByUsername(managedRealm.admin(), username);
|
||||
|
||||
assertThat(user, notNullValue());
|
||||
user.getRequiredActions().add(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
|
||||
|
||||
UserResource userResource = managedRealm.admin().users().get(user.getId());
|
||||
assertThat(userResource, notNullValue());
|
||||
userResource.update(user);
|
||||
|
||||
user = userResource.toRepresentation();
|
||||
assertThat(user, notNullValue());
|
||||
assertThat(user.getRequiredActions(), hasItem(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID));
|
||||
|
||||
oAuthClient.openLoginForm();
|
||||
loginUsernamePage.assertCurrent();
|
||||
loginUsernamePage.fillLoginWithUsernameOnly(username);
|
||||
loginUsernamePage.submit();
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.fillPassword(password);
|
||||
passwordPage.submit();
|
||||
|
||||
events.clear();
|
||||
|
||||
webAuthnRegisterPage.assertCurrent();
|
||||
webAuthnRegisterPage.clickRegister();
|
||||
webAuthnRegisterPage.registerWebAuthnCredential(WEBAUTHN_LABEL);
|
||||
|
||||
webAuthnRegisterPage.assertCurrent();
|
||||
|
||||
EventAssertion.assertSuccess(events.poll()).type(EventType.CUSTOM_REQUIRED_ACTION).sessionId(null).userId(userId).isCodeId()
|
||||
.details(Details.REDIRECT_URI, testApp.getRedirectionUri())
|
||||
.details(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.details(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, WEBAUTHN_LABEL);
|
||||
|
||||
EventAssertion.assertSuccess(events.poll()).type(EventType.UPDATE_CREDENTIAL).sessionId(null).userId(userId).isCodeId()
|
||||
.details(Details.REDIRECT_URI, testApp.getRedirectionUri())
|
||||
.details(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.details(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, WEBAUTHN_LABEL);
|
||||
|
||||
webAuthnRegisterPage.clickRegister();
|
||||
webAuthnRegisterPage.registerWebAuthnCredential(PASSWORDLESS_LABEL);
|
||||
|
||||
Assertions.assertNotNull(oAuthClient.parseLoginResponse().getCode());
|
||||
|
||||
logout();
|
||||
|
||||
// Password + WebAuthn Passkey
|
||||
oAuthClient.openLoginForm();
|
||||
loginUsernamePage.assertCurrent();
|
||||
loginUsernamePage.fillLoginWithUsernameOnly(username);
|
||||
loginUsernamePage.submit();
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.fillPassword(password);
|
||||
passwordPage.submit();
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
|
||||
assertThat(webAuthnLoginPage.getCount(), is(1));
|
||||
assertThat(webAuthnLoginPage.getLabels(), Matchers.contains(WEBAUTHN_LABEL));
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
Assertions.assertNotNull(oAuthClient.parseLoginResponse().getCode());
|
||||
logout();
|
||||
|
||||
// Only passwordless login
|
||||
oAuthClient.openLoginForm();
|
||||
loginUsernamePage.assertCurrent();
|
||||
loginUsernamePage.fillLoginWithUsernameOnly(username);
|
||||
loginUsernamePage.submit();
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.clickTryAnotherWayLink();
|
||||
|
||||
selectAuthenticatorPage.assertCurrent();
|
||||
assertThat(selectAuthenticatorPage.getLoginMethodHelpText(SelectAuthenticatorPage.SECURITY_KEY),
|
||||
is("Use your Passkey for passwordless sign in."));
|
||||
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.SECURITY_KEY);
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
assertThat(webAuthnLoginPage.getCount(), is(0));
|
||||
|
||||
// remove testuser before user authenticates via webauthn
|
||||
try (Response resp = managedRealm.admin().users().delete(userId)) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
webAuthnErrorPage.assertCurrent();
|
||||
assertThat(webAuthnErrorPage.getError(), is("Unknown user authenticated by the Passkey."));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void webAuthnTwoFactorAndWebAuthnPasswordlessTogether() {
|
||||
// Change binding to browser-webauthn-passwordless. This is flow, which contains both "webauthn" and "webauthn-passwordless" authenticator
|
||||
managedRealm.updateWithCleanup(r -> r.browserFlow("browser-webauthn-passwordless"));
|
||||
// Login as webauthn-user with password
|
||||
oAuthClient.openLoginForm();
|
||||
loginPage.fillLogin(USERNAME, PASSWORD);
|
||||
loginPage.submit();
|
||||
|
||||
errorPage.assertCurrent();
|
||||
|
||||
// User is not allowed to register passwordless authenticator in this flow
|
||||
assertThat(events.poll().getError(), is("invalid_user_credentials"));
|
||||
assertThat(errorPage.getError(), is("Cannot login, credential setup required."));
|
||||
}
|
||||
|
||||
private void assertUserRegistered(String userId, String username, String email) {
|
||||
UserRepresentation user = getUser(userId);
|
||||
assertThat(user, notNullValue());
|
||||
assertThat(user.getCreatedTimestamp(), notNullValue());
|
||||
|
||||
// test that timestamp is current with 60s tollerance
|
||||
assertThat((System.currentTimeMillis() - user.getCreatedTimestamp()) < 60000, is(true));
|
||||
|
||||
// test user info is set from form
|
||||
assertThat(user.getUsername(), is(username.toLowerCase()));
|
||||
assertThat(user.getEmail(), is(email.toLowerCase()));
|
||||
assertThat(user.getFirstName(), is("firstName"));
|
||||
assertThat(user.getLastName(), is("lastName"));
|
||||
}
|
||||
|
||||
private void assertRegisteredCredentials(String userId, String aaguid, String attestationStatementFormat) {
|
||||
List<CredentialRepresentation> credentials = getCredentials(userId);
|
||||
credentials.forEach(i -> {
|
||||
if (WebAuthnCredentialModel.TYPE_TWOFACTOR.equals(i.getType())) {
|
||||
try {
|
||||
WebAuthnCredentialData data = JsonSerialization.readValue(i.getCredentialData(), WebAuthnCredentialData.class);
|
||||
assertThat(data.getAaguid(), is(aaguid));
|
||||
assertThat(data.getAttestationStatementFormat(), is(attestationStatementFormat));
|
||||
} catch (IOException e) {
|
||||
Assertions.fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private UserRepresentation getUser(String userId) {
|
||||
return managedRealm.admin().users().get(userId).toRepresentation();
|
||||
}
|
||||
|
||||
private List<CredentialRepresentation> getCredentials(String userId) {
|
||||
return managedRealm.admin().users().get(userId).credentials();
|
||||
}
|
||||
|
||||
private void updateRealmWithDefaultWebAuthnSettings() {
|
||||
managedRealm.updateWithCleanup(r -> r.webAuthnPolicySignatureAlgorithms(List.of("ES256")));
|
||||
managedRealm.updateWithCleanup(r -> r.webAuthnPolicyAttestationConveyancePreference("none"));
|
||||
managedRealm.updateWithCleanup(r -> r.webAuthnPolicyAuthenticatorAttachment("cross-platform"));
|
||||
managedRealm.updateWithCleanup(r -> r.webAuthnPolicyRequireResidentKey("No"));
|
||||
managedRealm.updateWithCleanup(r -> r.webAuthnPolicyRpId(null));
|
||||
managedRealm.updateWithCleanup(r -> r.webAuthnPolicyUserVerificationRequirement("preferred"));
|
||||
managedRealm.updateWithCleanup(r -> r.webAuthnPolicyAcceptableAaguids(List.of(ALL_ZERO_AAGUID)));
|
||||
}
|
||||
|
||||
/**
|
||||
* This flow contains:
|
||||
* <p>
|
||||
* UsernameForm REQUIRED
|
||||
* Subflow REQUIRED
|
||||
* ** WebAuthnPasswordlessAuthenticator ALTERNATIVE
|
||||
* ** sub-subflow ALTERNATIVE
|
||||
* **** PasswordForm ALTERNATIVE
|
||||
* **** WebAuthnAuthenticator ALTERNATIVE
|
||||
*
|
||||
* @return flow alias
|
||||
*/
|
||||
private String webAuthnTogetherPasswordlessFlow() {
|
||||
final String newFlowAlias = "browser-together-webauthn-flow";
|
||||
runOnServer.run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
|
||||
runOnServer.run(session -> {
|
||||
FlowUtil.inCurrentRealm(session)
|
||||
.selectFlow(newFlowAlias)
|
||||
.inForms(forms -> forms
|
||||
.clear()
|
||||
.addAuthenticatorExecution(REQUIRED, UsernameFormFactory.PROVIDER_ID)
|
||||
.addSubFlowExecution(REQUIRED, subFlow -> subFlow
|
||||
.addAuthenticatorExecution(ALTERNATIVE, WebAuthnPasswordlessAuthenticatorFactory.PROVIDER_ID)
|
||||
.addSubFlowExecution(ALTERNATIVE, passwordFlow -> passwordFlow
|
||||
.addAuthenticatorExecution(REQUIRED, PasswordFormFactory.PROVIDER_ID)
|
||||
.addAuthenticatorExecution(REQUIRED, WebAuthnAuthenticatorFactory.PROVIDER_ID))
|
||||
))
|
||||
.defineAsBrowserFlow();
|
||||
});
|
||||
return newFlowAlias;
|
||||
}
|
||||
}
|
||||
17
tests/webauthn/src/test/resources/keycloak-test.properties
Normal file
17
tests/webauthn/src/test/resources/keycloak-test.properties
Normal file
@@ -0,0 +1,17 @@
|
||||
kc.test.browser=chrome-headless
|
||||
|
||||
kc.test.server=distribution
|
||||
|
||||
kc.test.log.level=WARN
|
||||
|
||||
kc.test.log.filter=true
|
||||
|
||||
kc.test.log.category."org.keycloak.tests".level=INFO
|
||||
|
||||
kc.test.log.category."testinfo".level=INFO
|
||||
kc.test.log.category."org.keycloak.it".level=INFO
|
||||
kc.test.log.category."org.keycloak".level=WARN
|
||||
kc.test.log.category."managed.keycloak".level=WARN
|
||||
kc.test.log.category."managed.db".level=WARN
|
||||
kc.test.log.category."managed.infinispan".level=WARN
|
||||
kc.test.log.category."org.keycloak.testframework.clustering".level=WARN
|
||||
@@ -1,578 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.WebAuthnConstants;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory;
|
||||
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.util.SecretGenerator;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AbstractAdminTest;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.IgnoreBrowserDriver;
|
||||
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
|
||||
import org.keycloak.testsuite.pages.PasswordPage;
|
||||
import org.keycloak.testsuite.pages.SelectAuthenticatorPage;
|
||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.FlowUtil;
|
||||
import org.keycloak.testsuite.webauthn.pages.WebAuthnAuthenticatorsList;
|
||||
import org.keycloak.testsuite.webauthn.updaters.WebAuthnRealmAttributeUpdater;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.openqa.selenium.firefox.FirefoxDriver;
|
||||
|
||||
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.ALTERNATIVE;
|
||||
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.hasItem;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
|
||||
|
||||
@Page
|
||||
protected ErrorPage errorPage;
|
||||
|
||||
@Page
|
||||
protected LoginUsernameOnlyPage loginUsernamePage;
|
||||
|
||||
@Page
|
||||
protected PasswordPage passwordPage;
|
||||
|
||||
@Page
|
||||
protected SelectAuthenticatorPage selectAuthenticatorPage;
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
RealmRepresentation realmRepresentation = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/webauthn/testrealm-webauthn.json"), RealmRepresentation.class);
|
||||
|
||||
List<String> acceptableAaguids = new ArrayList<>();
|
||||
acceptableAaguids.add("00000000-0000-0000-0000-000000000000");
|
||||
acceptableAaguids.add("6d44ba9b-f6ec-2e49-b930-0c8fe920cb73");
|
||||
|
||||
realmRepresentation.setWebAuthnPolicyAcceptableAaguids(acceptableAaguids);
|
||||
|
||||
testRealms.add(realmRepresentation);
|
||||
configureTestRealm(realmRepresentation);
|
||||
}
|
||||
|
||||
@Test
|
||||
@IgnoreBrowserDriver(FirefoxDriver.class) // See https://github.com/keycloak/keycloak/issues/10368
|
||||
public void registerUserSuccess() throws IOException {
|
||||
String username = "registerUserSuccess";
|
||||
String email = "registerUserSuccess@email";
|
||||
String userId = null;
|
||||
|
||||
try (RealmAttributeUpdater rau = updateRealmWithDefaultWebAuthnSettings(testRealm()).update()) {
|
||||
|
||||
loginPage.open();
|
||||
loginPage.clickRegister();
|
||||
registerPage.assertCurrent();
|
||||
|
||||
String authenticatorLabel = SecretGenerator.getInstance().randomString(24);
|
||||
registerPage.register("firstName", "lastName", email, username, generatePassword(username));
|
||||
|
||||
// User was registered. Now he needs to register WebAuthn credential
|
||||
webAuthnRegisterPage.assertCurrent();
|
||||
webAuthnRegisterPage.clickRegister();
|
||||
webAuthnRegisterPage.registerWebAuthnCredential(authenticatorLabel);
|
||||
|
||||
appPage.assertCurrent();
|
||||
assertThat(appPage.getRequestType(), is(RequestType.AUTH_RESPONSE));
|
||||
appPage.openAccount();
|
||||
|
||||
// confirm that registration is successfully completed
|
||||
userId = events.expectRegister(username, email).assertEvent().getUserId();
|
||||
// confirm registration event
|
||||
EventRepresentation eventRep1 = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
|
||||
.user(userId)
|
||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel)
|
||||
.detail(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, ALL_ZERO_AAGUID)
|
||||
.assertEvent();
|
||||
EventRepresentation eventRep2 = events.expectRequiredAction(EventType.UPDATE_CREDENTIAL)
|
||||
.user(userId)
|
||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel)
|
||||
.detail(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, ALL_ZERO_AAGUID)
|
||||
.assertEvent();
|
||||
String regPubKeyCredentialId1 = eventRep1.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
|
||||
String regPubKeyCredentialId2 = eventRep2.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
|
||||
|
||||
assertThat(regPubKeyCredentialId1, equalTo(regPubKeyCredentialId2));
|
||||
|
||||
// confirm login event
|
||||
String sessionId = events.expectLogin()
|
||||
.user(userId)
|
||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel)
|
||||
.assertEvent().getSessionId();
|
||||
// confirm user registered
|
||||
assertUserRegistered(userId, username.toLowerCase(), email.toLowerCase());
|
||||
assertRegisteredCredentials(userId, ALL_ZERO_AAGUID, "none");
|
||||
|
||||
events.clear();
|
||||
|
||||
// logout by user
|
||||
logout();
|
||||
|
||||
// confirm logout event
|
||||
events.expectLogout(sessionId)
|
||||
.removeDetail(Details.REDIRECT_URI)
|
||||
.user(userId)
|
||||
.client("account")
|
||||
.assertEvent();
|
||||
|
||||
// login by user
|
||||
loginPage.open();
|
||||
loginPage.login(username, getPassword(username));
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
|
||||
final WebAuthnAuthenticatorsList authenticators = webAuthnLoginPage.getAuthenticators();
|
||||
assertThat(authenticators.getCount(), is(1));
|
||||
assertThat(authenticators.getLabels(), Matchers.contains(authenticatorLabel));
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
appPage.assertCurrent();
|
||||
assertThat(appPage.getRequestType(), is(RequestType.AUTH_RESPONSE));
|
||||
appPage.openAccount();
|
||||
|
||||
// confirm login event
|
||||
sessionId = events.expectLogin()
|
||||
.user(userId)
|
||||
.detail(WebAuthnConstants.PUBKEY_CRED_ID_ATTR, regPubKeyCredentialId2)
|
||||
.detail(WebAuthnConstants.USER_VERIFICATION_CHECKED, Boolean.FALSE.toString())
|
||||
.assertEvent().getSessionId();
|
||||
|
||||
events.clear();
|
||||
// logout by user
|
||||
logout();
|
||||
|
||||
// confirm logout event
|
||||
events.expectLogout(sessionId)
|
||||
.removeDetail(Details.REDIRECT_URI)
|
||||
.client("account")
|
||||
.user(userId)
|
||||
.assertEvent();
|
||||
} finally {
|
||||
removeFirstCredentialForUser(userId, WebAuthnCredentialModel.TYPE_TWOFACTOR);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@IgnoreBrowserDriver(FirefoxDriver.class) // See https://github.com/keycloak/keycloak/issues/10368
|
||||
public void webAuthnPasswordlessAlternativeWithWebAuthnAndPassword() throws IOException {
|
||||
String userId = null;
|
||||
|
||||
final String WEBAUTHN_LABEL = "webauthn";
|
||||
final String PASSWORDLESS_LABEL = "passwordless";
|
||||
|
||||
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm())
|
||||
.setBrowserFlow(webAuthnTogetherPasswordlessFlow())
|
||||
.update()) {
|
||||
|
||||
UserRepresentation user = ApiUtil.findUserByUsername(testRealm(), "test-user@localhost");
|
||||
assertThat(user, notNullValue());
|
||||
user.getRequiredActions().add(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
|
||||
|
||||
UserResource userResource = testRealm().users().get(user.getId());
|
||||
assertThat(userResource, notNullValue());
|
||||
userResource.update(user);
|
||||
|
||||
user = userResource.toRepresentation();
|
||||
assertThat(user, notNullValue());
|
||||
assertThat(user.getRequiredActions(), hasItem(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID));
|
||||
|
||||
userId = user.getId();
|
||||
|
||||
loginUsernamePage.open();
|
||||
loginUsernamePage.login("test-user@localhost");
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.login(getPassword("test-user@localhost"));
|
||||
|
||||
events.clear();
|
||||
|
||||
webAuthnRegisterPage.assertCurrent();
|
||||
webAuthnRegisterPage.clickRegister();
|
||||
webAuthnRegisterPage.registerWebAuthnCredential(WEBAUTHN_LABEL);
|
||||
|
||||
webAuthnRegisterPage.assertCurrent();
|
||||
|
||||
events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
|
||||
.user(userId)
|
||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, WEBAUTHN_LABEL)
|
||||
.assertEvent();
|
||||
events.expectRequiredAction(EventType.UPDATE_CREDENTIAL)
|
||||
.user(userId)
|
||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, WEBAUTHN_LABEL)
|
||||
.assertEvent();
|
||||
|
||||
webAuthnRegisterPage.clickRegister();
|
||||
webAuthnRegisterPage.registerWebAuthnCredential(PASSWORDLESS_LABEL);
|
||||
|
||||
appPage.assertCurrent();
|
||||
|
||||
events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
|
||||
.user(userId)
|
||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID)
|
||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, PASSWORDLESS_LABEL)
|
||||
.assertEvent();
|
||||
events.expectRequiredAction(EventType.UPDATE_CREDENTIAL)
|
||||
.user(userId)
|
||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID)
|
||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, PASSWORDLESS_LABEL)
|
||||
.assertEvent();
|
||||
|
||||
final String sessionID = events.expectLogin()
|
||||
.user(userId)
|
||||
.assertEvent()
|
||||
.getSessionId();
|
||||
|
||||
events.clear();
|
||||
|
||||
logout();
|
||||
|
||||
events.expectLogout(sessionID)
|
||||
.removeDetail(Details.REDIRECT_URI)
|
||||
.user(userId)
|
||||
.client("account")
|
||||
.assertEvent();
|
||||
|
||||
// Password + WebAuthn Passkey
|
||||
loginUsernamePage.open();
|
||||
loginUsernamePage.assertCurrent();
|
||||
loginUsernamePage.login("test-user@localhost");
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.login(getPassword("test-user@localhost"));
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
|
||||
final WebAuthnAuthenticatorsList authenticators = webAuthnLoginPage.getAuthenticators();
|
||||
assertThat(authenticators.getCount(), is(1));
|
||||
assertThat(authenticators.getLabels(), Matchers.contains(WEBAUTHN_LABEL));
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
appPage.assertCurrent();
|
||||
logout();
|
||||
|
||||
// Only passwordless login
|
||||
loginUsernamePage.open();
|
||||
loginUsernamePage.login("test-user@localhost");
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.assertTryAnotherWayLinkAvailability(true);
|
||||
passwordPage.clickTryAnotherWayLink();
|
||||
|
||||
selectAuthenticatorPage.assertCurrent();
|
||||
assertThat(selectAuthenticatorPage.getLoginMethodHelpText(SelectAuthenticatorPage.SECURITY_KEY),
|
||||
is("Use your Passkey for passwordless sign in."));
|
||||
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.SECURITY_KEY);
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
assertThat(webAuthnLoginPage.getAuthenticators().getCount(), is(0));
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
appPage.assertCurrent();
|
||||
logout();
|
||||
} finally {
|
||||
removeFirstCredentialForUser(userId, WebAuthnCredentialModel.TYPE_TWOFACTOR, WEBAUTHN_LABEL);
|
||||
removeFirstCredentialForUser(userId, WebAuthnCredentialModel.TYPE_PASSWORDLESS, PASSWORDLESS_LABEL);
|
||||
}
|
||||
}
|
||||
|
||||
// See: https://github.com/keycloak/keycloak/issues/29586
|
||||
@Test
|
||||
@IgnoreBrowserDriver(FirefoxDriver.class)
|
||||
public void webAuthnPasswordlessShouldFailIfUserIsDeletedInBetween() throws IOException {
|
||||
|
||||
final String WEBAUTHN_LABEL = "webauthn";
|
||||
final String PASSWORDLESS_LABEL = "passwordless";
|
||||
|
||||
RealmResource realmResource = testRealm();
|
||||
|
||||
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(realmResource)
|
||||
.setBrowserFlow(webAuthnTogetherPasswordlessFlow())
|
||||
.update()) {
|
||||
|
||||
String username = "webauthn-tester@localhost";
|
||||
String password = generatePassword("webauthn-tester@localhost");
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername(username);
|
||||
user.setEnabled(true);
|
||||
user.setFirstName("WebAuthN");
|
||||
user.setLastName("Tester");
|
||||
|
||||
String userId = ApiUtil.createUserAndResetPasswordWithAdminClient(realmResource, user, password, false);
|
||||
|
||||
user = ApiUtil.findUserByUsername(realmResource, username);
|
||||
|
||||
assertThat(user, notNullValue());
|
||||
user.getRequiredActions().add(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
|
||||
|
||||
UserResource userResource = realmResource.users().get(user.getId());
|
||||
assertThat(userResource, notNullValue());
|
||||
userResource.update(user);
|
||||
|
||||
user = userResource.toRepresentation();
|
||||
assertThat(user, notNullValue());
|
||||
assertThat(user.getRequiredActions(), hasItem(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID));
|
||||
|
||||
loginUsernamePage.open();
|
||||
loginUsernamePage.login(username);
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.login(password);
|
||||
|
||||
events.clear();
|
||||
|
||||
webAuthnRegisterPage.assertCurrent();
|
||||
webAuthnRegisterPage.clickRegister();
|
||||
webAuthnRegisterPage.registerWebAuthnCredential(WEBAUTHN_LABEL);
|
||||
|
||||
webAuthnRegisterPage.assertCurrent();
|
||||
|
||||
events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
|
||||
.user(userId)
|
||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, WEBAUTHN_LABEL)
|
||||
.assertEvent();
|
||||
events.expectRequiredAction(EventType.UPDATE_CREDENTIAL)
|
||||
.user(userId)
|
||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, WEBAUTHN_LABEL)
|
||||
.assertEvent();
|
||||
|
||||
webAuthnRegisterPage.clickRegister();
|
||||
webAuthnRegisterPage.registerWebAuthnCredential(PASSWORDLESS_LABEL);
|
||||
|
||||
appPage.assertCurrent();
|
||||
|
||||
logout();
|
||||
|
||||
// Password + WebAuthn Passkey
|
||||
loginUsernamePage.open();
|
||||
loginUsernamePage.assertCurrent();
|
||||
loginUsernamePage.login(username);
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.login(password);
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
|
||||
final WebAuthnAuthenticatorsList authenticators = webAuthnLoginPage.getAuthenticators();
|
||||
assertThat(authenticators.getCount(), is(1));
|
||||
assertThat(authenticators.getLabels(), Matchers.contains(WEBAUTHN_LABEL));
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
appPage.assertCurrent();
|
||||
logout();
|
||||
|
||||
// Only passwordless login
|
||||
loginUsernamePage.open();
|
||||
loginUsernamePage.login(username);
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.assertTryAnotherWayLinkAvailability(true);
|
||||
passwordPage.clickTryAnotherWayLink();
|
||||
|
||||
selectAuthenticatorPage.assertCurrent();
|
||||
assertThat(selectAuthenticatorPage.getLoginMethodHelpText(SelectAuthenticatorPage.SECURITY_KEY),
|
||||
is("Use your Passkey for passwordless sign in."));
|
||||
selectAuthenticatorPage.selectLoginMethod(SelectAuthenticatorPage.SECURITY_KEY);
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
assertThat(webAuthnLoginPage.getAuthenticators().getCount(), is(0));
|
||||
|
||||
// remove testuser before user authenticates via webauthn
|
||||
try (Response resp = realmResource.users().delete(userId)) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
webAuthnLoginPage.clickAuthenticate();
|
||||
|
||||
webAuthnErrorPage.assertCurrent();
|
||||
assertThat(webAuthnErrorPage.getError(), is("Unknown user authenticated by the Passkey."));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void webAuthnTwoFactorAndWebAuthnPasswordlessTogether() throws IOException {
|
||||
// Change binding to browser-webauthn-passwordless. This is flow, which contains both "webauthn" and "webauthn-passwordless" authenticator
|
||||
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm()).setBrowserFlow("browser-webauthn-passwordless").update()) {
|
||||
// Login as test-user@localhost with password
|
||||
loginPage.open();
|
||||
loginPage.login("test-user@localhost", getPassword("test-user@localhost"));
|
||||
|
||||
errorPage.assertCurrent();
|
||||
|
||||
// User is not allowed to register passwordless authenticator in this flow
|
||||
assertThat(events.poll().getError(), is("invalid_user_credentials"));
|
||||
assertThat(errorPage.getError(), is("Cannot login, credential setup required."));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertUserRegistered(String userId, String username, String email) {
|
||||
UserRepresentation user = getUser(userId);
|
||||
assertThat(user, notNullValue());
|
||||
assertThat(user.getCreatedTimestamp(), notNullValue());
|
||||
|
||||
// test that timestamp is current with 60s tollerance
|
||||
assertThat((System.currentTimeMillis() - user.getCreatedTimestamp()) < 60000, is(true));
|
||||
|
||||
// test user info is set from form
|
||||
assertThat(user.getUsername(), is(username.toLowerCase()));
|
||||
assertThat(user.getEmail(), is(email.toLowerCase()));
|
||||
assertThat(user.getFirstName(), is("firstName"));
|
||||
assertThat(user.getLastName(), is("lastName"));
|
||||
}
|
||||
|
||||
private void assertRegisteredCredentials(String userId, String aaguid, String attestationStatementFormat) {
|
||||
List<CredentialRepresentation> credentials = getCredentials(userId);
|
||||
credentials.forEach(i -> {
|
||||
if (WebAuthnCredentialModel.TYPE_TWOFACTOR.equals(i.getType())) {
|
||||
try {
|
||||
WebAuthnCredentialData data = JsonSerialization.readValue(i.getCredentialData(), WebAuthnCredentialData.class);
|
||||
assertThat(data.getAaguid(), is(aaguid));
|
||||
assertThat(data.getAttestationStatementFormat(), is(attestationStatementFormat));
|
||||
} catch (IOException e) {
|
||||
Assert.fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected UserRepresentation getUser(String userId) {
|
||||
return testRealm().users().get(userId).toRepresentation();
|
||||
}
|
||||
|
||||
protected List<CredentialRepresentation> getCredentials(String userId) {
|
||||
return testRealm().users().get(userId).credentials();
|
||||
}
|
||||
|
||||
private static WebAuthnRealmAttributeUpdater updateRealmWithDefaultWebAuthnSettings(RealmResource resource) {
|
||||
return new WebAuthnRealmAttributeUpdater(resource)
|
||||
.setWebAuthnPolicySignatureAlgorithms(Collections.singletonList("ES256"))
|
||||
.setWebAuthnPolicyAttestationConveyancePreference("none")
|
||||
.setWebAuthnPolicyAuthenticatorAttachment("cross-platform")
|
||||
.setWebAuthnPolicyRequireResidentKey("No")
|
||||
.setWebAuthnPolicyRpId(null)
|
||||
.setWebAuthnPolicyUserVerificationRequirement("preferred")
|
||||
.setWebAuthnPolicyAcceptableAaguids(Collections.singletonList(ALL_ZERO_AAGUID));
|
||||
}
|
||||
|
||||
/**
|
||||
* This flow contains:
|
||||
* <p>
|
||||
* UsernameForm REQUIRED
|
||||
* Subflow REQUIRED
|
||||
* ** WebAuthnPasswordlessAuthenticator ALTERNATIVE
|
||||
* ** sub-subflow ALTERNATIVE
|
||||
* **** PasswordForm ALTERNATIVE
|
||||
* **** WebAuthnAuthenticator ALTERNATIVE
|
||||
*
|
||||
* @return flow alias
|
||||
*/
|
||||
private String webAuthnTogetherPasswordlessFlow() {
|
||||
final String newFlowAlias = "browser-together-webauthn-flow";
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
FlowUtil.inCurrentRealm(session)
|
||||
.selectFlow(newFlowAlias)
|
||||
.inForms(forms -> forms
|
||||
.clear()
|
||||
.addAuthenticatorExecution(REQUIRED, UsernameFormFactory.PROVIDER_ID)
|
||||
.addSubFlowExecution(REQUIRED, subFlow -> subFlow
|
||||
.addAuthenticatorExecution(ALTERNATIVE, WebAuthnPasswordlessAuthenticatorFactory.PROVIDER_ID)
|
||||
.addSubFlowExecution(ALTERNATIVE, passwordFlow -> passwordFlow
|
||||
.addAuthenticatorExecution(REQUIRED, PasswordFormFactory.PROVIDER_ID)
|
||||
.addAuthenticatorExecution(REQUIRED, WebAuthnAuthenticatorFactory.PROVIDER_ID))
|
||||
))
|
||||
.defineAsBrowserFlow();
|
||||
});
|
||||
return newFlowAlias;
|
||||
}
|
||||
|
||||
private void removeFirstCredentialForUser(String userId, String credentialType) {
|
||||
removeFirstCredentialForUser(userId, credentialType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove first occurring credential from user with specific credentialType
|
||||
*
|
||||
* @param userId userId
|
||||
* @param credentialType type of credential
|
||||
* @param assertUserLabel user label of credential
|
||||
*/
|
||||
private void removeFirstCredentialForUser(String userId, String credentialType, String assertUserLabel) {
|
||||
if (userId == null || credentialType == null) return;
|
||||
|
||||
final UserResource userResource = testRealm().users().get(userId);
|
||||
|
||||
final CredentialRepresentation credentialRep = userResource.credentials()
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(credential -> credentialType.equals(credential.getType()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (credentialRep != null) {
|
||||
if (assertUserLabel != null) {
|
||||
assertThat(credentialRep.getUserLabel(), is(assertUserLabel));
|
||||
}
|
||||
userResource.removeCredential(credentialRep.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user