8000 Migrate OIDC/SAML broker config-variant and infrastructure-dependent tests to the new testsuite by sguilhen · Pull Request #51949 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7167796
Migrate basic broker login tests to new testsuite
sguilhen Aug 25, 2026
ef85a89
Move OIDC config-variant and parameter-forwarding tests to new testsu…
sguilhen Aug 25, 2026
8f7d18e
Migrate OIDC broker config-variant and parameter-forwarding tests
sguilhen Aug 25, 2026
68f970c
Move client-secret-jwt, private-key-jwt, and sub-mismatch tests to ne…
sguilhen Aug 25, 2026
b616b01
Migrate client-secret-jwt, private-key-jwt, and sub-mismatch broker t…
sguilhen Aug 25, 2026
ff0e86b
Move IDP hint and nonce parameter tests to new testsuite oidc package
sguilhen Aug 25, 2026
7d15211
Migrate IDP hint and nonce parameter broker tests
sguilhen Aug 25, 2026
4e2b00d
Move JWE and ECDH-ES broker tests to new testsuite oidc package
sguilhen Aug 25, 2026
1ed23c3
Migrate JWE and ECDH-ES broker tests to new testsuite
sguilhen Aug 25, 2026
3ff93df
Move custom broker providers to tests/custom-providers
sguilhen Aug 25, 2026
c4eea53
Port custom broker providers to tests/custom-providers and remove leg…
sguilhen Aug 25, 2026
86b5ae0
Move KcOidcBrokerIdpPublicKeyMissingUseTest to new testsuite oidc pac…
sguilhen Aug 25, 2026
1374566
Migrate KcOidcBrokerIdpPublicKeyMissingUseTest to new testsuite
sguilhen Aug 25, 2026
459fffa
Move private-key-jwt custom provider tests to new testsuite oidc package
sguilhen Aug 25, 2026
2922b0e
Migrate private-key-jwt custom provider broker tests to new testsuite
sguilhen Aug 25, 2026
63dadac
Move KcOidcBrokerVaultTest to new testsuite oidc package
sguilhen Aug 25, 2026
69e2a38
Migrate KcOidcBrokerVaultTest to new testsuite
sguilhen Aug 25, 2026
aef26cd
Move KcOidcBrokerPassMaxAgeTest to new testsuite oidc package
sguilhen Aug 25, 2026
98d2b6a
Migrate KcOidcBrokerPassMaxAgeTest to new testsuite
sguilhen Aug 25, 2026
56e5e18
Move SAML broker tests to new testsuite saml package
sguilhen Aug 25, 2026
30f8dea
Migrate KcSamlSignedDocumentOnlyBrokerTest to new testsuite
sguilhen Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package org.keycloak.tests.broker;

import java.util.List;
import java.util.Set;

import org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testframework.oauth.OAuthClient;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.ui.page.IdpReviewUserProfilePage;
import org.keycloak.testframework.ui.page.LoginPage;
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
import org.keycloak.testsuite.util.AccountHelper;
import org.keycloak.testsuite.util.userprofile.UserProfileUtil;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.openqa.selenium.TimeoutException;

public interface BrokerConfigSupport {

String PROVIDER_REALM = "provider";
String CONSUMER_REALM = "consumer";
String USER_LOGIN = "testuser";
String USER_EMAIL = "user@localhost.com";
String USER_PASSWORD = "password";
String CONSUMER_BROKER_APP_CLIENT_ID = "broker-app";
String CONSUMER_BROKER_APP_SECRET = "broker-app-secret";

ManagedRealm getProviderRealm();
ManagedRealm getConsumerRealm();
OAuthClient getOAuthClient();
ManagedWebDriver getWebDriver();
LoginPage getLoginPage();
IdpReviewUserProfilePage getUpdateProfilePage();

String getIdpAlias();

// The provider user is created without a first/last name so the consumer's first-broker-login
// review-profile page appears. The new-testsuite provider realm enables the VERIFY_PROFILE required
// action by default, which would otherwise stop the incomplete user at the provider; disable it so
// the incomplete profile reaches the consumer (the legacy suite did not enable VERIFY_PROFILE).
@BeforeEach
default void relaxProviderProfileVerification() {
for (RequiredActionProviderRepresentation action : getProviderRealm().admin().flows().getRequiredActions()) {
if (UserModel.RequiredAction.VERIFY_PROFILE.name().equals(action.getAlias())) {
action.setEnabled(false);
getProviderRealm().admin().flows().updateRequiredAction(action.getAlias(), action);
}
}
}

@BeforeEach
default void enableUnmanagedAttributes() {
UserProfileUtil.enableUnmanagedAttributes(getConsumerRealm().admin().users().userProfile());
UserProfileUtil.enableUnmanagedAttributes(getProviderRealm().admin().users().userProfile());
}

default String getUserLogin() {
return USER_LOGIN;
}

default String getUserPassword() {
return USER_PASSWORD;
}

default String getUserEmail() {
return USER_EMAIL;
}

default void logInWithBroker() {
LoginPage loginPage = getLoginPage();
loginPage.clickSocial(getIdpAlias());
}

default void logInWithIdp() {
logInWithBroker();
}

default void logInAsUserInIDP() {
OAuthClient oauth = getOAuthClient();
oauth.openLoginForm();
logInWithBroker();
logInAsUserInIDPForFirstTime();
}

default void logInAsUserInIDPForFirstTime() {
LoginPage loginPage = getLoginPage();
loginPage.fillLogin(getUserLogin(), getUserPassword());
loginPage.submit();
}

// Drives the consumer's first-broker-login review-profile page, which must appear the first time a
// user logs in through a broker because the imported user is missing its first and last name. Fails
// the test if the page does not show, mirroring the legacy assertCurrent() on the update-profile page.
default void updateAccountInformation() {
Assertions.assertTrue(profilePageAppeared(),
"The first-broker-login review-profile page was expected but did not appear");
getUpdateProfilePage().update("Firstname", "Lastname");
}

// Tolerant variant for flows that intentionally skip the review-profile page (e.g. when
// update-profile-on-first-login is turned off). Fills the page only if it actually appears.
default void updateAccountInformationIfPresent() {
if (profilePageAppeared()) {
getUpdateProfilePage().update("Firstname", "Lastname");
}
}

private boolean profilePageAppeared() {
Set<String> profilePageIds = Set.of("login-login-update-profile", "login-idp-review-user-profile");
ManagedWebDriver driver = getWebDriver();
try {
driver.waiting().until(d -> {
String currentPageId = driver.page().getCurrentPageId();
if (currentPageId != null && profilePageIds.contains(currentPageId)) {
return true;
}
return null;
});
return true;
} catch (TimeoutException e) {
return false;
}
}

default UserRepresentation createUser(String username, String email) {
ManagedRealm consumerRealm = getConsumerRealm();
UserRepresentation user = new UserRepresentation();
user.setUsername(username);
user.setEmail(email);
user.setEmailVerified(true);
user.setEnabled(true);
consumerRealm.admin().users().create(user).close();
return user;
}

// Mid-test helper used by testSingleLogout() to verify login state after logout.
default void logoutFromConsumerRealm() {
AccountHelper.logout(getConsumerRealm().admin(), getUserLogin());
}

default void assertUserCreatedInConsumerRealm() {
ManagedRealm consumerRealm = getConsumerRealm();
UserRepresentation userRep = AccountHelper.getUserRepresentation(
consumerRealm.admin(), getUserLogin());
Assertions.assertNotNull(userRep, "There must be user " + getUserLogin() + " in consumer realm");
// The review-profile page filled in these names via the UI; assert they were actually persisted
// rather than overwriting them here, so a broken UI flow is caught instead of masked.
Assertions.assertEquals("Firstname", userRep.getFirstName(),
"First name should have been persisted by the review-profile page");
Assertions.assertEquals("Lastname", userRep.getLastName(),
"Last name should have been persisted by the review-profile page");

int userCount = consumerRealm.admin().users().count();
Assertions.assertTrue(userCount > 0, "There must be at least one user");

// userRep was fetched above via an exact username search, so assert its email directly instead of
// scanning the (paginated) users().list(), which could miss the user beyond the first page.
Assertions.assertEquals(getUserEmail(), userRep.getEmail(),
"There must be user " + getUserLogin() + " with the expected email in consumer realm");
}

default void disableUpdateProfileOnFirstLogin() {
var flows = getConsumerRealm().admin().flows();
for (AuthenticationExecutionInfoRepresentation execution :
flows.getExecutions(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW)) {
if (IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID.equals(execution.getProviderId())) {
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE.name());
flows.updateExecutions(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW, execution);
} else if (execution.getAlias() != null
&& execution.getAlias().equals(DefaultAuthenticationFlows.IDP_REVIEW_PROFILE_CONFIG_ALIAS)) {
AuthenticatorConfigRepresentation config = flows.getAuthenticatorConfig(execution.getAuthenticationConfig());
config.getConfig().put("update.profile.on.first.login", IdentityProviderRepresentation.UPFLM_OFF);
flows.updateAuthenticatorConfig(config.getId(), config);
}
}
// Turning off the review-profile step is not enough: the new-testsuite consumer realm enables the
// VERIFY_PROFILE required action by default, which would still intercept the incomplete imported user
// (the provider user has no first/last name on purpose). Disable it so login reaches the callback,
// mirroring the legacy suite, which did not enable VERIFY_PROFILE at all.
for (RequiredActionProviderRepresentation action : getConsumerRealm().admin().flows().getRequiredActions()) {
if (UserModel.RequiredAction.VERIFY_PROFILE.name().equals(action.getAlias())) {
action.setEnabled(false);
getConsumerRealm().admin().flows().updateRequiredAction(action.getAlias(), action);
}
}
}

default void assertNumFederatedIdentities(String username, int expected) {
ManagedRealm consumerRealm = getConsumerRealm();
List<UserRepresentation> users = consumerRealm.admin().users().search(username, true);
Assertions.assertEquals(1, users.size(), "Expected exactly one user with username " + username);
List<FederatedIdentityRepresentation> fedIdentities =
consumerRealm.admin().users().get(users.get(0).getId()).getFederatedIdentity();
Assertions.assertEquals(expected, fedIdentities.size());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.keycloak.tests.broker;

import org.keycloak.testframework.oauth.OAuthClient;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.ui.page.LoginPage;
import org.keycloak.testsuite.util.AccountHelper;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public interface BrokerLoginTest extends BrokerConfigSupport {

@Test
default void testLogInAsUserInIDP() {
loginUser();
testSingleLogout();
}

default void loginUser() {
logInAsUserInIDP();
updateAccountInformation();
assertUserCreatedInConsumerRealm();
}

default void testSingleLogout() {
OAuthClient oauth = getOAuthClient();
ManagedRealm consumerRealm = getConsumerRealm();
ManagedRealm providerRealm = getProviderRealm();

oauth.openLoginForm();
Assertions.assertTrue(oauth.parseLoginResponse().isSuccess(), "Should be logged in");

AccountHelper.logout(consumerRealm.admin(), getUserLogin());
AccountHelper.logout(providerRealm.admin(), getUserLogin());

oauth.openLoginForm();
LoginPage loginPage = getLoginPage();
loginPage.assertCurrent();
}

@Test
default void loginWithExistingUser() {
ManagedRealm consumerRealm = getConsumerRealm();
OAuthClient oauth = getOAuthClient();

int userCountBefore = consumerRealm.admin().users().count();

testLogInAsUserInIDP();

int userCount = consumerRealm.admin().users().count();
Assertions.assertEquals(userCountBefore + 1, userCount,
"First broker login should have created exactly one user in the consumer realm");

oauth.openLoginForm();

logInWithBroker();

LoginPage loginPage = getLoginPage();
loginPage.fillLogin(getUserLogin(), getUserPassword());
loginPage.submit();

Assertions.assertTrue(oauth.parseLoginResponse().isSuccess());
Assertions.assertEquals(userCount, consumerRealm.admin().users().count());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.keycloak.tests.broker;

import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.keys.KeyProvider;
import org.keycloak.models.utils.DefaultKeyProviders;
import org.keycloak.representations.idm.ComponentRepresentation;

public interface EcdhEsJweBrokerConfigSupport extends JweBrokerConfigSupport {
Comment thread
sguilhen marked this conversation as resolved.
Resolved

String getCurve();

@Override
default ComponentRepresentation getProviderKeyComponent() {
ComponentRepresentation component = new ComponentRepresentation();
component.setName("ecdsa-generated");
< 8B2C /td> component.setProviderId("ecdsa-generated");
component.setProviderType(KeyProvider.class.getName());
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
config.putSingle("priority", DefaultKeyProviders.DEFAULT_PRIORITY);
config.putSingle("ecdsaEllipticCurveKey", getCurve());
component.setConfig(config);
return component;
}

@Override
default ComponentRepresentation getConsumerKeyComponent() {
ComponentRepresentation component = new ComponentRepresentation();
component.setName("ecdh-generated");
component.setProviderId("ecdh-generated");
component.setProviderType(KeyProvider.class.getName());
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
config.putSingle("priority", DefaultKeyProviders.DEFAULT_PRIORITY);
config.putSingle("ecdhAlgorithm", getEncAlg());
config.putSingle("ecdhEllipticCurveKey", getCurve());
component.setConfig(config);
return component;
}
}
Loading
Loading
0