mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
[Test Framework] Migrate initial WebAuthn setup + WebAuthnRegisterAndLoginTest. (#44016)
Signed-off-by: Lukas Hanusovsky <lhanusov@redhat.com>
This commit is contained in:
parent
29fdcedbc8
commit
e8c6a7b98d
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@ -929,12 +929,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
browser:
|
|
||||||
- chrome
|
|
||||||
- firefox
|
|
||||||
fail-fast: false
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
@ -943,13 +937,16 @@ jobs:
|
|||||||
uses: ./.github/actions/integration-test-setup
|
uses: ./.github/actions/integration-test-setup
|
||||||
|
|
||||||
- uses: ./.github/actions/install-chrome
|
- uses: ./.github/actions/install-chrome
|
||||||
if: matrix.browser == 'chrome'
|
|
||||||
|
|
||||||
- name: Run WebAuthn IT
|
- name: Run WebAuthn IT
|
||||||
run: |
|
run: |
|
||||||
TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh webauthn`
|
TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh webauthn`
|
||||||
echo "Tests: $TESTS"
|
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
|
- uses: ./.github/actions/upload-flaky-tests
|
||||||
name: Upload flaky tests
|
name: Upload flaky tests
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package org.keycloak.testframework.events;
|
package org.keycloak.testframework.events;
|
||||||
|
|
||||||
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.representations.idm.EventRepresentation;
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
|
|
||||||
@ -37,6 +38,16 @@ public class EventAssertion {
|
|||||||
return this;
|
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) {
|
public EventAssertion clientId(String clientId) {
|
||||||
Assertions.assertEquals(clientId, event.getClientId());
|
Assertions.assertEquals(clientId, event.getClientId());
|
||||||
return this;
|
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.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientPolicyRepresentation;
|
import org.keycloak.representations.idm.ClientPolicyRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientProfileRepresentation;
|
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.GroupRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||||
import org.keycloak.representations.idm.RoleRepresentation;
|
import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
import org.keycloak.representations.idm.RolesRepresentation;
|
import org.keycloak.representations.idm.RolesRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
@ -88,11 +90,22 @@ public class RealmConfigBuilder {
|
|||||||
return RoleConfigBuilder.update(role).name(roleName);
|
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) {
|
public RealmConfigBuilder registrationEmailAsUsername(boolean registrationEmailAsUsername) {
|
||||||
rep.setRegistrationEmailAsUsername(registrationEmailAsUsername);
|
rep.setRegistrationEmailAsUsername(registrationEmailAsUsername);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RealmConfigBuilder registrationAllowed(boolean allowed) {
|
||||||
|
rep.setRegistrationAllowed(allowed);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public RealmConfigBuilder editUsernameAllowed(boolean allowed) {
|
public RealmConfigBuilder editUsernameAllowed(boolean allowed) {
|
||||||
rep.setEditUsernameAllowed(allowed);
|
rep.setEditUsernameAllowed(allowed);
|
||||||
return this;
|
return this;
|
||||||
@ -267,6 +280,106 @@ public class RealmConfigBuilder {
|
|||||||
return this;
|
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
|
* 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
|
* 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() {
|
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";
|
String nonExistingUrl = oauth.loginForm().build().split("protocol")[0] + "incorrect-path";
|
||||||
driver.open(nonExistingUrl);
|
driver.open(nonExistingUrl);
|
||||||
|
|
||||||
|
errorPage.assertCurrent();
|
||||||
|
|
||||||
assertThat(driver.page().getPageSource(), containsString(realmLocalizationMessageValue));
|
assertThat(driver.page().getPageSource(), containsString(realmLocalizationMessageValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,7 @@
|
|||||||
<module>custom-providers</module>
|
<module>custom-providers</module>
|
||||||
<module>custom-scripts</module>
|
<module>custom-scripts</module>
|
||||||
<module>clustering</module>
|
<module>clustering</module>
|
||||||
|
<module>webauthn</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<profiles>
|
<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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user