From db303d4220817711caaac95938dac4b6adf1cb05 Mon Sep 17 00:00:00 2001 From: cgeorgilakis-grnet Date: Wed, 17 Dec 2025 14:13:48 +0200 Subject: [PATCH] Being possible to accept terms and conditions befure User is saved in Keycloak during first broker login. Follow GDPR. Closes #28714 Signed-off-by: cgeorgilakis-grnet --- .../forms/login/LoginFormsProvider.java | 1 + .../utils/DefaultAuthenticationFlows.java | 1 + .../userprofile/UserProfileProvider.java | 15 +++++++ .../broker/IdpReviewProfileAuthenticator.java | 42 +++++++++++++++---- .../IdpReviewProfileAuthenticatorFactory.java | 9 +++- .../DeclarativeUserProfileProvider.java | 30 +++++++++++++ ...DeclarativeUserProfileProviderFactory.java | 2 +- .../authentication/InitialFlowsTest.java | 2 +- .../pages/UpdateAccountInformationPage.java | 23 ++++++++++ .../testsuite/broker/AbstractBrokerTest.java | 16 +++++++ .../broker/AbstractFirstBrokerLoginTest.java | 30 +++++++++++++ .../AbstractInitializedBaseBrokerTest.java | 12 ++++++ .../base/login/idp-review-user-profile.ftl | 2 + .../theme/base/login/login-update-profile.ftl | 10 +++-- .../theme/base/login/register-commons.ftl | 2 +- .../keycloak.v2/login/register-commons.ftl | 2 +- 16 files changed, 183 insertions(+), 16 deletions(-) diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java index fe7243dab31..ecc78e42f77 100755 --- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java @@ -38,6 +38,7 @@ import org.keycloak.sessions.AuthenticationSessionModel; public interface LoginFormsProvider extends Provider { String UPDATE_PROFILE_CONTEXT_ATTR = "updateProfileCtx"; + String TERMS_ACCEPTANCE_REQUIRED = "termsAcceptanceRequired"; String IDENTITY_PROVIDER_BROKER_CONTEXT = "identityProviderBrokerCtx"; diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java index 40058867bce..d668a268d86 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java @@ -531,6 +531,7 @@ public class DefaultAuthenticationFlows { reviewProfileConfig.setAlias(IDP_REVIEW_PROFILE_CONFIG_ALIAS); Map config = new HashMap<>(); config.put("update.profile.on.first.login", IdentityProviderRepresentation.UPFLM_MISSING); + config.put("terms_and_conditions", "false"); reviewProfileConfig.setConfig(config); reviewProfileConfig = realm.addAuthenticatorConfig(reviewProfileConfig); diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileProvider.java b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileProvider.java index 5237eb9de00..1f9566e3e22 100644 --- a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileProvider.java @@ -55,6 +55,21 @@ public interface UserProfileProvider extends Provider { */ UserProfile create(UserProfileContext context, Map attributes); + /** + *

Creates a new {@link UserProfile} instance for a given {@code context} and {@code attributes} for update purposes. + * + *

Instances created from this method are going to run validations and updates based on the given {@code user}. This + * might be useful when updating an existing user. + * + * @param context the context + * @param attributes the attributes to associate with the instance returned from this method + * @param user the user to eventually update with the given {@code attributes} + * @param terms if terms and condition required action user attribute neeed to be saved + * + * @return the user profile instance + */ + UserProfile create(UserProfileContext context, Map attributes, UserModel user, boolean terms); + /** *

Creates a new {@link UserProfile} instance for a given {@code context} and {@code attributes} for update purposes. * diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java index 808261eab94..baf620c8510 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java @@ -17,6 +17,7 @@ package org.keycloak.authentication.authenticators.broker; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -28,8 +29,10 @@ import jakarta.ws.rs.core.Response; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext; +import org.keycloak.authentication.requiredactions.TermsAndConditions; import org.keycloak.broker.provider.AbstractIdentityProvider; import org.keycloak.broker.provider.BrokeredIdentityContext; +import org.keycloak.common.util.Time; import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; @@ -56,7 +59,8 @@ import org.jboss.logging.Logger; public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator { private static final Logger logger = Logger.getLogger(IdpReviewProfileAuthenticator.class); - + private static final String TERMS_FIELD ="termsAccepted"; + private boolean enabledRequiredAction= false; @Override public boolean requiresUser() { return false; @@ -65,14 +69,20 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator { @Override protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext userCtx, BrokeredIdentityContext brokerContext) { IdentityProviderModel idpConfig = brokerContext.getIdpConfig(); + enabledRequiredAction = context.getRealm().getRequiredActionProviderByAlias(UserModel.RequiredAction.TERMS_AND_CONDITIONS.name()).isEnabled() && Boolean.valueOf(context.getAuthenticatorConfig().getConfig().get(IdpReviewProfileAuthenticatorFactory.TERMS_AND_CONDITIONS)); + boolean updateProfile = requiresUpdateProfilePage(context.getAuthenticatorConfig(), context, userCtx); - if (requiresUpdateProfilePage(context, userCtx, brokerContext)) { + if ( enabledRequiredAction || updateProfile) { + // set up form only if + // 1. terms and conditions is enabled and terms and condition configuration is true + // 2. based on UPDATE_PROFILE_ON_FIRST_LOGIN value and IdP release data logger.debugf("Identity provider '%s' requires update profile action for broker user '%s'.", idpConfig.getAlias(), userCtx.getUsername()); // No formData for first render. The profile is rendered from userCtx Response challengeResponse = context.form() .setAttribute(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR, userCtx) + .setAttribute(LoginFormsProvider.TERMS_ACCEPTANCE_REQUIRED, enabledRequiredAction) .setFormData(null) .createUpdateProfilePage(); context.challenge(challengeResponse); @@ -82,14 +92,12 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator { } } - protected boolean requiresUpdateProfilePage(AuthenticationFlowContext context, SerializedBrokeredIdentityContext userCtx, BrokeredIdentityContext brokerContext) { - String enforceUpdateProfile = context.getAuthenticationSession().getAuthNote(ENFORCE_UPDATE_PROFILE); - if (Boolean.parseBoolean(enforceUpdateProfile)) { + protected boolean requiresUpdateProfilePage(AuthenticatorConfigModel authenticatorConfig, AuthenticationFlowContext context, SerializedBrokeredIdentityContext userCtx) { + if (Boolean.parseBoolean(context.getAuthenticationSession().getAuthNote(ENFORCE_UPDATE_PROFILE))) { return true; } String updateProfileFirstLogin; - AuthenticatorConfigModel authenticatorConfig = context.getAuthenticatorConfig(); if (authenticatorConfig == null || !authenticatorConfig.getConfig().containsKey(IdpReviewProfileAuthenticatorFactory.UPDATE_PROFILE_ON_FIRST_LOGIN)) { updateProfileFirstLogin = IdentityProviderRepresentation.UPFLM_MISSING; } else { @@ -114,6 +122,19 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator { EventBuilder event = context.getEvent(); event.event(EventType.UPDATE_PROFILE).detail(Details.CONTEXT, UserProfileContext.IDP_REVIEW.name()); MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); + + if (enabledRequiredAction && ! formData.containsKey(TERMS_FIELD)) { + Response challengeForTerms = context.form() + .setErrors(Collections.singletonList(new FormMessage(TERMS_FIELD, "termsAcceptanceRequired"))) + .setAttribute(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR, userCtx) + .setAttribute(LoginFormsProvider.TERMS_ACCEPTANCE_REQUIRED, enabledRequiredAction) + .setFormData(formData) + .createUpdateProfilePage(); + + context.challenge(challengeForTerms); + + return; + } UserModelDelegate updatedProfile = new UserModelDelegate(null) { @Override @@ -205,7 +226,13 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator { UserProfileProvider profileProvider = context.getSession().getProvider(UserProfileProvider.class); Map> attributes = new HashMap<>(formData); attributes.putIfAbsent(UserModel.USERNAME, Collections.singletonList(updatedProfile.getUsername())); - UserProfile profile = profileProvider.create(UserProfileContext.IDP_REVIEW, attributes, updatedProfile); + if (attributes.containsKey(TERMS_FIELD)) { + //if form contains terms and condition remove this field and add TermsAndConditions.USER_ATTRIBUTE + attributes.remove(TERMS_FIELD); + attributes.put(TermsAndConditions.USER_ATTRIBUTE, Arrays.asList(Integer.toString(Time.currentTime()))); + } + + UserProfile profile = profileProvider.create(UserProfileContext.IDP_REVIEW, attributes, updatedProfile, formData.containsKey(TERMS_FIELD)); try { profile.update((attributeName, userModel, oldValue) -> { @@ -231,6 +258,7 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator { Response challenge = context.form() .setErrors(errors) .setAttribute(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR, userCtx) + .setAttribute(LoginFormsProvider.TERMS_ACCEPTANCE_REQUIRED, enabledRequiredAction) .setFormData(formData) .createUpdateProfilePage(); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticatorFactory.java index a3864ead1c5..c4aa3280560 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticatorFactory.java @@ -39,6 +39,7 @@ public class IdpReviewProfileAuthenticatorFactory implements AuthenticatorFactor static IdpReviewProfileAuthenticator SINGLETON = new IdpReviewProfileAuthenticator(); public static final String UPDATE_PROFILE_ON_FIRST_LOGIN = "update.profile.on.first.login"; + public static final String TERMS_AND_CONDITIONS = "terms_and_conditions"; @Override public Authenticator create(KeycloakSession session) { @@ -110,7 +111,13 @@ public class IdpReviewProfileAuthenticatorFactory implements AuthenticatorFactor + " page for reviewing profile will be displayed and user can review and update his profile. Value 'off' means that page won't be displayed." + " Value 'missing' means that page is displayed just when some required attribute is missing (wasn't downloaded from identity provider). Value 'missing' is the default one." + " WARN: In case that user clicks 'Review profile info' on link duplications page, the update page will be always displayed. You would need to disable this authenticator to never display the page."); - + configProperties.add(property); + property = new ProviderConfigProperty(); + property.setName(TERMS_AND_CONDITIONS); + property.setLabel("Accept Terms and Conditions"); + property.setType(ProviderConfigProperty.BOOLEAN_TYPE); + property.setHelpText("Enable this option to require users to accept terms and conditions before their profile is recorded." + + "This ensures compliance with data regulation frameworks like GDPR on first login. You also need to enable the 'Terms and Conditions' Required action for this to take effect."); configProperties.add(property); } diff --git a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java index 365f1a23ad5..13c39f1d597 100644 --- a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java +++ b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java @@ -30,8 +30,10 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import org.keycloak.authentication.requiredactions.TermsAndConditions; import org.keycloak.component.ComponentModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; @@ -50,6 +52,7 @@ import org.keycloak.userprofile.config.UPConfigUtils; import org.keycloak.userprofile.validator.AttributeRequiredByMetadataValidator; import org.keycloak.userprofile.validator.ImmutableAttributeValidator; import org.keycloak.userprofile.validator.MultiValueValidator; +import org.keycloak.userprofile.validator.ReadOnlyAttributeUnchangedValidator; import org.keycloak.util.JsonSerialization; import org.keycloak.validate.AbstractSimpleValidator; import org.keycloak.validate.ValidatorConfig; @@ -128,6 +131,33 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider { return createUserProfile(context, attributes, null); } + @Override + public UserProfile create(UserProfileContext context, Map attributes, UserModel user, boolean terms) { + return terms ? createUserProfileWithTerms(context, attributes, user) : createUserProfile(context, attributes, user); + } + + private UserProfile createUserProfileWithTerms(UserProfileContext context, Map attributes, UserModel user) { + UserProfileMetadata defaultMetadata = contextualMetadataRegistry.get(context); + + if (defaultMetadata == null) { + // some contexts (and their metadata) are available enabled when the corresponding feature is enabled + throw new RuntimeException("No metadata is bound to the " + context + " context"); + } + + UserProfileMetadata metadata = configureUserProfile(defaultMetadata, session); + List readonlyValidators = new ArrayList<>(); + readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(DeclarativeUserProfileProviderFactory.readOnlyAttributesPattern)); + metadata.addAttribute(TermsAndConditions.USER_ATTRIBUTE, 1000, readonlyValidators); + Attributes profileAttributes = createAttributes(context, attributes, user, metadata); + return new DefaultUserProfile(metadata, profileAttributes, createUserFactory(), user, session); + } + + private AttributeValidatorMetadata createReadOnlyAttributeUnchangedValidator(Pattern pattern) { + return new AttributeValidatorMetadata(ReadOnlyAttributeUnchangedValidator.ID, + ValidatorConfig.builder().config(ReadOnlyAttributeUnchangedValidator.CFG_PATTERN, pattern) + .build()); + } + private UserProfile createUserProfile(UserProfileContext context, Map attributes, UserModel user) { UserProfileMetadata defaultMetadata = contextualMetadataRegistry.get(context); diff --git a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java index 962063f6c7c..f428635f9ee 100644 --- a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java +++ b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java @@ -97,7 +97,7 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide */ private static final String[] DEFAULT_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp", "userCertificate", "saml.persistent.name.id.for.*", "ENABLED", "EMAIL_VERIFIED", "disabledReason", UserModel.EMAIL_PENDING }; private static final String[] DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp" }; - private static final Pattern readOnlyAttributesPattern = getRegexPatternString(DEFAULT_READ_ONLY_ATTRIBUTES); + public static final Pattern readOnlyAttributesPattern = getRegexPatternString(DEFAULT_READ_ONLY_ATTRIBUTES); private static final Pattern adminReadOnlyAttributesPattern = getRegexPatternString(DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES); private static volatile UPConfig PARSED_DEFAULT_RAW_CONFIG; diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/authentication/InitialFlowsTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/authentication/InitialFlowsTest.java index 24180cb6c66..40e4bf7b841 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/authentication/InitialFlowsTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/authentication/InitialFlowsTest.java @@ -45,7 +45,7 @@ public class InitialFlowsTest extends AbstractAuthenticationTest { private HashMap expectedConfigs = new HashMap<>(); { - expectedConfigs.put("idp-review-profile", newConfig("review profile config", new String[]{"update.profile.on.first.login", "missing"})); + expectedConfigs.put("idp-review-profile", newConfig("review profile config", new String[]{"update.profile.on.first.login", "missing","terms_and_conditions","false"})); expectedConfigs.put("idp-create-user-if-unique", newConfig("create unique user config", new String[]{"require.password.update.after.registration", "false"})); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/UpdateAccountInformationPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/UpdateAccountInformationPage.java index 81d9d08ede5..6a5f72b1e61 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/UpdateAccountInformationPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/UpdateAccountInformationPage.java @@ -24,6 +24,9 @@ public class UpdateAccountInformationPage extends LanguageComboboxAwarePage { @FindBy(name = "department") private WebElement departmentInput; + @FindBy(name = "termsAccepted") + private WebElement termsAccepted; + @FindBy(css = "input[type=\"submit\"]") private WebElement submitButton; @@ -46,6 +49,26 @@ public class UpdateAccountInformationPage extends LanguageComboboxAwarePage { clickLink(submitButton); } + public void acceptTerms(String userName, + String email, + String firstName, + String lastName) { + usernameInput.clear(); + usernameInput.sendKeys(userName); + + emailInput.clear(); + emailInput.sendKeys(email); + + firstNameInput.clear(); + firstNameInput.sendKeys(firstName); + + lastNameInput.clear(); + lastNameInput.sendKeys(lastName); + termsAccepted.click(); + + clickLink(submitButton); + } + public void updateAccountInformation(String userName, String email, String firstName, diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java index 9599c09eb6e..b17576e24e6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java @@ -212,6 +212,7 @@ public abstract class AbstractBrokerTest extends AbstractInitializedBaseBrokerTe } else if (execution.getAlias() != null && execution.getAlias().equals(IDP_REVIEW_PROFILE_CONFIG_ALIAS)) { AuthenticatorConfigRepresentation config = flows.getAuthenticatorConfig(execution.getAuthenticationConfig()); config.getConfig().put("update.profile.on.first.login", IdentityProviderRepresentation.UPFLM_ON); + config.getConfig().put("terms_and_conditions", "false"); flows.updateAuthenticatorConfig(config.getId(), config); } } @@ -223,6 +224,7 @@ public abstract class AbstractBrokerTest extends AbstractInitializedBaseBrokerTe } else if (execution.getAlias() != null && execution.getAlias().equals(IDP_REVIEW_PROFILE_CONFIG_ALIAS)) { AuthenticatorConfigRepresentation config = flows.getAuthenticatorConfig(execution.getAuthenticationConfig()); config.getConfig().put("update.profile.on.first.login", IdentityProviderRepresentation.UPFLM_MISSING); + config.getConfig().put("terms_and_conditions", "false"); flows.updateAuthenticatorConfig(config.getId(), config); } } @@ -253,10 +255,24 @@ public abstract class AbstractBrokerTest extends AbstractInitializedBaseBrokerTe } else if (execution.getAlias() != null && execution.getAlias().equals(IDP_REVIEW_PROFILE_CONFIG_ALIAS)) { AuthenticatorConfigRepresentation config = flows.getAuthenticatorConfig(execution.getAuthenticationConfig()); config.getConfig().put("update.profile.on.first.login", IdentityProviderRepresentation.UPFLM_OFF); + config.getConfig().put("terms_and_conditions", "false"); flows.updateAuthenticatorConfig(config.getId(), config); } } + static void makeTermsAndCondionRequiredInLogin(AuthenticationExecutionInfoRepresentation execution, AuthenticationManagementResource flows) { + if (execution.getProviderId() != null && execution.getProviderId().equals(IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID)) { + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED.name()); + flows.updateExecutions(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW, execution); + } else if (execution.getAlias() != null && execution.getAlias().equals(IDP_REVIEW_PROFILE_CONFIG_ALIAS)) { + AuthenticatorConfigRepresentation config = flows.getAuthenticatorConfig(execution.getAuthenticationConfig()); + config.getConfig().put("update.profile.on.first.login", IdentityProviderRepresentation.UPFLM_ON); + config.getConfig().put("terms_and_conditions", "true"); + flows.updateAuthenticatorConfig(config.getId(), config); + } + } + + static void disableExistingUser(AuthenticationExecutionInfoRepresentation execution, AuthenticationManagementResource flows) { if (execution.getProviderId() != null && (execution.getProviderId().equals(IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID) || execution.getProviderId().equals(IdpConfirmLinkAuthenticatorFactory.PROVIDER_ID))) { execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED.name()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java index 6abe2ca32a3..7ddc6383536 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java @@ -1,10 +1,12 @@ package org.keycloak.testsuite.broker; import java.util.List; +import java.util.function.BiConsumer; import jakarta.mail.internet.MimeMessage; import jakarta.ws.rs.core.Response; +import org.keycloak.admin.client.resource.AuthenticationManagementResource; import org.keycloak.admin.client.resource.IdentityProviderResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.UserResource; @@ -640,6 +642,29 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa assertEquals("User with username consumer already exists. Please login to account management to link the account.", errorPage.getError()); } + @Test + public void testUserExistsFirstBrokerLoginFlowUpdateProfileOnAndTermsAccepted() { + createUser("consumer"); + + updateExecutionsAndEnableTermsAndCondition(AbstractBrokerTest::makeTermsAndCondionRequiredInLogin); + + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + + logInWithBroker(bc); + + Assert.assertTrue(updateAccountInformationPage.isCurrent()); + Assert.assertTrue("We must be on correct realm right now", + driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); + + log.debug("Updating info on updateAccount page"); + updateAccountInformationPage.acceptTerms("consumer", "consumer-user@redhat.com", "FirstName", "LastName"); + + waitForPage(driver, "we are sorry...", false); + assertEquals("User with username consumer already exists. Please login to account management to link the account.", errorPage.getError()); + + } + /** * Refers to in old test suite: org.keycloak.testsuite.broker.AbstractFirstBrokerLoginTest#testRegistrationWithPasswordUpdateRequired @@ -1659,4 +1684,9 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa return () -> toggleRegistrationAllowed(realmName, genuineValue); } + + private void updateExecutionsAndEnableTermsAndCondition(BiConsumer action) { + updateExecutions(action); + changeRequiredAction(true); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractInitializedBaseBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractInitializedBaseBrokerTest.java index 69e0e44018b..a3a98a73113 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractInitializedBaseBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractInitializedBaseBrokerTest.java @@ -21,8 +21,10 @@ import java.util.function.BiConsumer; import org.keycloak.admin.client.resource.AuthenticationManagementResource; import org.keycloak.admin.client.resource.IdentityProviderResource; import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.authentication.requiredactions.TermsAndConditions; import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; +import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.junit.Before; @@ -65,6 +67,16 @@ public abstract class AbstractInitializedBaseBrokerTest extends AbstractBaseBrok addClientsToProviderAndConsumer(); testContext.setInitialized(true); + changeRequiredAction(false); + + } + + protected void changeRequiredAction(boolean enabled) { + AuthenticationManagementResource flows = adminClient.realm(bc.consumerRealmName()).flows(); + RequiredActionProviderRepresentation rep = flows.getRequiredAction(TermsAndConditions.PROVIDER_ID); + rep.setEnabled(enabled); + flows.updateRequiredAction(TermsAndConditions.PROVIDER_ID, rep); + } protected void updateExecutions(BiConsumer action) { diff --git a/themes/src/main/resources/theme/base/login/idp-review-user-profile.ftl b/themes/src/main/resources/theme/base/login/idp-review-user-profile.ftl index 1b70aeccb9a..76790f83a7f 100644 --- a/themes/src/main/resources/theme/base/login/idp-review-user-profile.ftl +++ b/themes/src/main/resources/theme/base/login/idp-review-user-profile.ftl @@ -1,5 +1,6 @@ <#import "template.ftl" as layout> <#import "user-profile-commons.ftl" as userProfileCommons> +<#import "register-commons.ftl" as registerCommons> <@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section> <#if section = "header"> ${msg("loginIdpReviewProfileTitle")} @@ -7,6 +8,7 @@

<@userProfileCommons.userProfileFormFields/> + <@registerCommons.termsAcceptance/>
diff --git a/themes/src/main/resources/theme/base/login/login-update-profile.ftl b/themes/src/main/resources/theme/base/login/login-update-profile.ftl index f2df9d47f4c..1d710adb00a 100755 --- a/themes/src/main/resources/theme/base/login/login-update-profile.ftl +++ b/themes/src/main/resources/theme/base/login/login-update-profile.ftl @@ -1,5 +1,6 @@ <#import "template.ftl" as layout> <#import "user-profile-commons.ftl" as userProfileCommons> +<#import "register-commons.ftl" as registerCommons> <@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section> <#if section = "header"> ${msg("loginProfileTitle")} @@ -7,10 +8,11 @@ <@userProfileCommons.userProfileFormFields/> - + <@registerCommons.termsAcceptance/>
-
-
+ +
+
@@ -25,4 +27,4 @@
- \ No newline at end of file + diff --git a/themes/src/main/resources/theme/base/login/register-commons.ftl b/themes/src/main/resources/theme/base/login/register-commons.ftl index 7007797ee0e..92f4869c59b 100644 --- a/themes/src/main/resources/theme/base/login/register-commons.ftl +++ b/themes/src/main/resources/theme/base/login/register-commons.ftl @@ -1,5 +1,5 @@ <#macro termsAcceptance> - <#if termsAcceptanceRequired??> + <#if termsAcceptanceRequired!false>
${msg("termsTitle")} diff --git a/themes/src/main/resources/theme/keycloak.v2/login/register-commons.ftl b/themes/src/main/resources/theme/keycloak.v2/login/register-commons.ftl index 7007797ee0e..92f4869c59b 100644 --- a/themes/src/main/resources/theme/keycloak.v2/login/register-commons.ftl +++ b/themes/src/main/resources/theme/keycloak.v2/login/register-commons.ftl @@ -1,5 +1,5 @@ <#macro termsAcceptance> - <#if termsAcceptanceRequired??> + <#if termsAcceptanceRequired!false>
${msg("termsTitle")}