Add a test for IdpUsernamePasswordForm in webauthn CI job

Closes #41259

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc
2025-07-21 13:40:24 +02:00
committed by Bruno Oliveira da Silva
parent 0b98cb7466
commit dd17f7d811
5 changed files with 286 additions and 8 deletions
@@ -81,12 +81,7 @@ public class IdpUsernamePasswordForm extends UsernamePasswordForm {
Optional<UserModel> existingUser = getExistingUser(context);
existingUser.ifPresent(context::setUser);
boolean result = validateUserAndPassword(context, formData);
// Restore formData for the case of error
setupForm(context, formData, existingUser);
return result;
return validateUserAndPassword(context, formData);
}
protected LoginFormsProvider setupForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData, Optional<UserModel> existingUser) {
@@ -53,7 +53,7 @@ public class IdpConfirmLinkPage extends LanguageComboboxAwarePage {
}
public void clickLinkAccount() {
linkAccountButton.click();
UIUtils.clickLink(linkAccountButton);
}
}
@@ -221,4 +221,9 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
rep.setAdminPermissionsEnabled(adminPermissionsEnabled);
return this;
}
public RealmAttributeUpdater setWebAuthnPolicyPasswordlessPasskeysEnabled(Boolean passkeysEnabled) {
rep.setWebAuthnPolicyPasswordlessPasskeysEnabled(passkeysEnabled);
return this;
}
}
@@ -244,7 +244,7 @@ public abstract class AbstractBrokerTest extends AbstractInitializedBaseBrokerTe
}
}
static void disableUpdateProfileOnFirstLogin(AuthenticationExecutionInfoRepresentation execution, AuthenticationManagementResource flows) {
public static void disableUpdateProfileOnFirstLogin(AuthenticationExecutionInfoRepresentation execution, AuthenticationManagementResource flows) {
if (execution.getProviderId() != null && execution.getProviderId().equals(IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID)) {
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE.name());
flows.updateExecutions(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW, execution);
@@ -0,0 +1,278 @@
/*
* Copyright 2025 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.passwordless;
import java.io.IOException;
import java.util.Optional;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.AuthenticationManagementResource;
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
import org.keycloak.common.Profile;
import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.arquillian.annotation.IgnoreBrowserDriver;
import org.keycloak.testsuite.broker.AbstractBrokerTest;
import org.keycloak.testsuite.broker.AbstractInitializedBaseBrokerTest;
import org.keycloak.testsuite.broker.BrokerConfiguration;
import org.keycloak.testsuite.broker.BrokerTestConstants;
import org.keycloak.testsuite.broker.BrokerTestTools;
import org.keycloak.testsuite.broker.KcOidcBrokerConfiguration;
import org.keycloak.testsuite.broker.oidc.TestKeycloakOidcIdentityProviderFactory;
import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.BrowserDriverUtil;
import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.testsuite.webauthn.AbstractWebAuthnVirtualTest;
import org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions;
import org.keycloak.testsuite.webauthn.authenticators.KcVirtualAuthenticator;
import org.keycloak.testsuite.webauthn.authenticators.VirtualAuthenticatorManager;
import org.keycloak.testsuite.webauthn.pages.WebAuthnErrorPage;
import org.keycloak.testsuite.webauthn.pages.WebAuthnLoginPage;
import org.keycloak.testsuite.webauthn.pages.WebAuthnRegisterPage;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.firefox.FirefoxDriver;
/**
*
* @author rmartinc
*/
@EnableFeature(value = Profile.Feature.PASSKEYS, skipRestart = true)
@IgnoreBrowserDriver(FirefoxDriver.class) // See https://github.com/keycloak/keycloak/issues/10368
public class PasskeysKcOidcFirstBrokerLoginTest extends AbstractInitializedBaseBrokerTest {
@Page
protected RegisterPage registerPage;
@Page
protected WebAuthnRegisterPage webAuthnRegisterPage;
@Page
protected WebAuthnLoginPage webAuthnLoginPage;
@Page
protected WebAuthnErrorPage webAuthnErrorPage;
private VirtualAuthenticatorManager virtualAuthenticatorManager;
@Before
@Override
public void beforeBrokerTest() {
super.beforeBrokerTest();
AuthenticationManagementResource authRes = adminClient.realm(bc.consumerRealmName()).flows();
RequiredActionProviderRepresentation reqAction = authRes.getRequiredAction(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
reqAction.setEnabled(Boolean.TRUE);
reqAction.setDefaultAction(Boolean.TRUE);
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
authRes.updateRequiredAction(reqAction.getAlias(), reqAction);
}
@Before
public void setUpVirtualAuthenticator() {
if (!BrowserDriverUtil.isDriverFirefox(driver)) {
virtualAuthenticatorManager = AbstractWebAuthnVirtualTest.createDefaultVirtualManager(driver, DefaultVirtualAuthOptions.DEFAULT_RESIDENT_KEY.getOptions());
}
}
@After
public void removeVirtualAuthenticator() {
if (!BrowserDriverUtil.isDriverFirefox(driver)) {
virtualAuthenticatorManager.removeAuthenticator();
}
}
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return new KcOidcBrokerConfiguration() {
private final String userPassword = generatePassword();
@Override
public IdentityProviderRepresentation setUpIdentityProvider(IdentityProviderSyncMode syncMode) {
IdentityProviderRepresentation idp = BrokerTestTools.createIdentityProvider(
BrokerTestConstants.IDP_OIDC_ALIAS, TestKeycloakOidcIdentityProviderFactory.ID);
applyDefaultConfiguration(idp.getConfig(), syncMode);
return idp;
}
@Override
public RealmRepresentation createConsumerRealm() {
RealmRepresentation consumerRealm = super.createConsumerRealm();
consumerRealm.setRegistrationAllowed(Boolean.TRUE);
consumerRealm.setWebAuthnPolicyPasswordlessPasskeysEnabled(Boolean.TRUE);
return consumerRealm;
}
@Override
public String getUserPassword() {
return userPassword;
}
};
}
@Test
public void testLinkAccountByReauthenticationWithPassword() {
oauth.realm(bc.consumerRealmName());
registerUser("consumer", bc.getUserPassword(), BrokerTestConstants.USER_EMAIL, generatePassword(24));
logout();
oauth.client("broker-app").realm(bc.consumerRealmName()).openLoginForm();
logInWithBroker(bc);
BrokerTestTools.waitForPage(driver, "account already exists", false);
Assert.assertTrue(idpConfirmLinkPage.isCurrent());
Assert.assertEquals("User with email user@localhost.com already exists. How do you want to continue?", idpConfirmLinkPage.getMessage());
idpConfirmLinkPage.clickLinkAccount();
Assert.assertEquals("Authenticate to link your account with " + bc.getIDPAlias(), loginPage.getInfoMessage());
Assert.assertThrows(NoSuchElementException.class, () -> loginPage.findSocialButton(bc.getIDPAlias()));
Assert.assertThrows(NoSuchElementException.class, () -> loginPage.clickRegister());
webAuthnLoginPage.isCurrent();
loginPage.login(bc.getUserPassword());
Assert.assertTrue(appPage.isCurrent());
assertNumFederatedIdentities(ApiUtil.findUserByUsername(adminClient.realm(bc.consumerRealmName()), "consumer").getId(), 1);
}
@Test
public void testLinkAccountByReauthenticationWithExternalPasskey() {
oauth.realm(bc.consumerRealmName());
registerUser("consumer", bc.getUserPassword(), BrokerTestConstants.USER_EMAIL, generatePassword(24));
logout();
oauth.client("broker-app").realm(bc.consumerRealmName()).openLoginForm();
logInWithBroker(bc);
BrokerTestTools.waitForPage(driver, "account already exists", false);
Assert.assertTrue(idpConfirmLinkPage.isCurrent());
Assert.assertEquals("User with email user@localhost.com already exists. How do you want to continue?", idpConfirmLinkPage.getMessage());
idpConfirmLinkPage.clickLinkAccount();
Assert.assertEquals("Authenticate to link your account with " + bc.getIDPAlias(), loginPage.getInfoMessage());
Assert.assertThrows(NoSuchElementException.class, () -> loginPage.findSocialButton(bc.getIDPAlias()));
Assert.assertThrows(NoSuchElementException.class, () -> loginPage.clickRegister());
webAuthnLoginPage.isCurrent();
loginPage.login(generatePassword()); // invalid password
Assert.assertEquals("Invalid username or password.", loginPage.getInputError());
webAuthnLoginPage.clickAuthenticate();
Assert.assertTrue(appPage.isCurrent());
assertNumFederatedIdentities(ApiUtil.findUserByUsername(adminClient.realm(bc.consumerRealmName()), "consumer").getId(), 1);
}
@Test
public void testLinkAccountByReauthenticationWithDiscoverablePasskey() throws IOException {
virtualAuthenticatorManager.useAuthenticator(DefaultVirtualAuthOptions.PASSKEYS.getOptions());
oauth.realm(bc.consumerRealmName());
registerUser("consumer", bc.getUserPassword(), BrokerTestConstants.USER_EMAIL, generatePassword(24));
logout();
// disable passkeys here to not login automatically using the discoverable passkey and allow select the IdP
try (RealmAttributeUpdater updater = new RealmAttributeUpdater(adminClient.realm(bc.consumerRealmName()))
.setWebAuthnPolicyPasswordlessPasskeysEnabled(Boolean.FALSE)
.update()) {
oauth.client("broker-app").realm(bc.consumerRealmName()).openLoginForm();
logInWithBroker(bc);
}
BrokerTestTools.waitForPage(driver, "account already exists", false);
Assert.assertTrue(idpConfirmLinkPage.isCurrent());
Assert.assertEquals("User with email user@localhost.com already exists. How do you want to continue?", idpConfirmLinkPage.getMessage());
idpConfirmLinkPage.clickLinkAccount();
// login is automatically now via discoverable passkey
Assert.assertTrue(appPage.isCurrent());
assertNumFederatedIdentities(ApiUtil.findUserByUsername(adminClient.realm(bc.consumerRealmName()), "consumer").getId(), 1);
}
protected void registerUser(String username, String password, String email, String authenticatorLabel) {
oauth.client("broker-app").realm(bc.consumerRealmName()).openLoginForm();
loginPage.clickRegister();
WaitUtils.waitForPageToLoad();
registerPage.assertCurrent();
registerPage.register("firstName", "lastName", email, username, password, password);
// User was registered. Now he needs to register WebAuthn credential
WaitUtils.waitForPageToLoad();
webAuthnRegisterPage.assertCurrent();
webAuthnRegisterPage.clickRegister();
tryRegisterAuthenticator(authenticatorLabel, 10);
WaitUtils.waitForPageToLoad();
}
private void tryRegisterAuthenticator(String authenticatorLabel, int numberOfAllowedRetries) {
final boolean hasResidentKey = Optional.ofNullable(virtualAuthenticatorManager)
.map(VirtualAuthenticatorManager::getCurrent)
.map(KcVirtualAuthenticator::getOptions)
.map(KcVirtualAuthenticator.Options::hasResidentKey)
.orElse(false);
if (hasResidentKey && !webAuthnRegisterPage.isRegisterAlertPresent()) {
for (int i = 0; i < numberOfAllowedRetries; i++) {
webAuthnErrorPage.clickTryAgain();
WaitUtils.waitForPageToLoad();
webAuthnRegisterPage.assertCurrent();
webAuthnRegisterPage.clickRegister();
if (webAuthnRegisterPage.isRegisterAlertPresent()) {
webAuthnRegisterPage.registerWebAuthnCredential(authenticatorLabel);
return;
} else {
WaitUtils.pause(200);
}
}
} else {
webAuthnRegisterPage.registerWebAuthnCredential(authenticatorLabel);
}
}
protected void logout() {
try {
WaitUtils.waitForPageToLoad();
oauth.openLogoutForm();
logoutConfirmPage.assertCurrent();
logoutConfirmPage.confirmLogout();
infoPage.assertCurrent();
Assert.assertEquals("You are logged out", infoPage.getInfo());
} catch (Exception e) {
throw new RuntimeException("Cannot logout user", e);
}
}
}