Integrate registration with terms and conditions required action

Closes #25891

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2024-01-10 13:29:41 +01:00 committed by Marek Posolda
parent a8eca6add0
commit e162974a8d
4 changed files with 76 additions and 4 deletions

View file

@ -2983,3 +2983,4 @@ viewGuides=View guides
joinCommunity=Join community joinCommunity=Join community
readBlog=Read blog readBlog=Read blog
customValue=Custom value customValue=Custom value
termsAndConditionsUserAttribute=Terms and conditions accepted timestamp

View file

@ -25,6 +25,8 @@ import org.keycloak.authentication.FormAction;
import org.keycloak.authentication.FormActionFactory; import org.keycloak.authentication.FormActionFactory;
import org.keycloak.authentication.FormContext; import org.keycloak.authentication.FormContext;
import org.keycloak.authentication.ValidationContext; import org.keycloak.authentication.ValidationContext;
import org.keycloak.authentication.requiredactions.TermsAndConditions;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
@ -33,6 +35,7 @@ import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -137,6 +140,17 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
user.setEnabled(true); user.setEnabled(true);
if ("on".equals(formData.getFirst(RegistrationTermsAndConditions.FIELD))) {
// if accepted terms and conditions checkbox, remove action and add the attribute if enabled
RequiredActionProviderModel tacModel = context.getRealm().getRequiredActionProviderByAlias(
UserModel.RequiredAction.TERMS_AND_CONDITIONS.name());
if (tacModel != null && tacModel.isEnabled()) {
user.setSingleAttribute(TermsAndConditions.USER_ATTRIBUTE, Integer.toString(Time.currentTime()));
context.getAuthenticationSession().removeRequiredAction(UserModel.RequiredAction.TERMS_AND_CONDITIONS);
user.removeRequiredAction(UserModel.RequiredAction.TERMS_AND_CONDITIONS);
}
}
context.setUser(user); context.setUser(user);
context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username); context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);

View file

@ -30,6 +30,7 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.requiredactions.TermsAndConditions;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.component.AmphibianProviderFactory; import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
@ -38,6 +39,7 @@ import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.provider.ProviderConfigurationBuilder;
@ -173,6 +175,13 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide
return realm.isInternationalizationEnabled(); return realm.isInternationalizationEnabled();
} }
private static boolean isTermAndConditionsEnabled(AttributeContext context) {
RealmModel realm = context.getSession().getContext().getRealm();
RequiredActionProviderModel tacModel = realm.getRequiredActionProviderByAlias(
UserModel.RequiredAction.TERMS_AND_CONDITIONS.name());
return tacModel != null && tacModel.isEnabled();
}
private static boolean isNewUser(AttributeContext c) { private static boolean isNewUser(AttributeContext c) {
return c.getUser() == null; return c.getUser() == null;
} }
@ -440,6 +449,11 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide
metadata.addAttribute(UserModel.LOCALE, -1, DeclarativeUserProfileProviderFactory::isInternationalizationEnabled, DeclarativeUserProfileProviderFactory::isInternationalizationEnabled) metadata.addAttribute(UserModel.LOCALE, -1, DeclarativeUserProfileProviderFactory::isInternationalizationEnabled, DeclarativeUserProfileProviderFactory::isInternationalizationEnabled)
.setRequired(AttributeMetadata.ALWAYS_FALSE); .setRequired(AttributeMetadata.ALWAYS_FALSE);
metadata.addAttribute(TermsAndConditions.USER_ATTRIBUTE, -1, AttributeMetadata.ALWAYS_FALSE,
DeclarativeUserProfileProviderFactory::isTermAndConditionsEnabled)
.setAttributeDisplayName("${termsAndConditionsUserAttribute}")
.setRequired(AttributeMetadata.ALWAYS_FALSE);
return metadata; return metadata;
} }

View file

@ -19,7 +19,6 @@ package org.keycloak.testsuite.forms;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Assume;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.authentication.AuthenticationFlow; import org.keycloak.authentication.AuthenticationFlow;
@ -28,14 +27,18 @@ import org.keycloak.authentication.forms.RegistrationPassword;
import org.keycloak.authentication.forms.RegistrationRecaptcha; import org.keycloak.authentication.forms.RegistrationRecaptcha;
import org.keycloak.authentication.forms.RegistrationTermsAndConditions; import org.keycloak.authentication.forms.RegistrationTermsAndConditions;
import org.keycloak.authentication.forms.RegistrationUserCreation; import org.keycloak.authentication.forms.RegistrationUserCreation;
import org.keycloak.authentication.requiredactions.TermsAndConditions;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
@ -436,7 +439,7 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
assertUserRegistered(userId, username, email); assertUserRegistered(userId, username, email);
} }
private void assertUserRegistered(String userId, String username, String email) { private UserRepresentation assertUserRegistered(String userId, String username, String email) {
events.expectLogin().detail("username", username.toLowerCase()).user(userId).assertEvent(); events.expectLogin().detail("username", username.toLowerCase()).user(userId).assertEvent();
UserRepresentation user = getUser(userId); UserRepresentation user = getUser(userId);
@ -445,6 +448,7 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
// test that timestamp is current with 10s tollerance // test that timestamp is current with 10s tollerance
assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 10000); assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 10000);
assertUserBasicRegisterAttributes(userId, username, email, "firstName", "lastName"); assertUserBasicRegisterAttributes(userId, username, email, "firstName", "lastName");
return user;
} }
@Test @Test
@ -761,12 +765,51 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
String userId = events.expectRegister("registerUserSuccessTermsAcceptance", "registerUserSuccessTermsAcceptance@email") String userId = events.expectRegister("registerUserSuccessTermsAcceptance", "registerUserSuccessTermsAcceptance@email")
.assertEvent().getUserId(); .assertEvent().getUserId();
assertUserRegistered(userId, "registerUserSuccessTermsAcceptance", "registerUserSuccessTermsAcceptance@email"); UserRepresentation user = assertUserRegistered(userId, "registerUserSuccessTermsAcceptance", "registerUserSuccessTermsAcceptance@email");
Assert.assertNull(user.getAttributes());
} finally { } finally {
configureRegistrationFlowWithCustomRegistrationPageForm(UUID.randomUUID().toString()); configureRegistrationFlowWithCustomRegistrationPageForm(UUID.randomUUID().toString());
} }
} }
@Test
public void registerUserSuccessTermsAcceptanceWithRequiredActionEnabled() {
configureRegistrationFlowWithCustomRegistrationPageForm(UUID.randomUUID().toString(),
AuthenticationExecutionModel.Requirement.REQUIRED);
// configure Terms and Conditions required action as enabled and default
RequiredActionProviderRepresentation tacRep = testRealm().flows().getRequiredAction(UserModel.RequiredAction.TERMS_AND_CONDITIONS.name());
Assert.assertNotNull(tacRep);
tacRep.setEnabled(true);
tacRep.setDefaultAction(true);
testRealm().flows().updateRequiredAction(UserModel.RequiredAction.TERMS_AND_CONDITIONS.name(), tacRep);
try {
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
int currentTime = Time.currentTime();
registerPage.register("firstName", "lastName", "registerUserSuccessTermsAcceptance2@email",
"registerUserSuccessTermsAcceptance2", "password", "password", null, true, null);
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
String userId = events.expectRegister("registerUserSuccessTermsAcceptance2", "registerUserSuccessTermsAcceptance2@email")
.assertEvent().getUserId();
UserRepresentation user = assertUserRegistered(userId, "registerUserSuccessTermsAcceptance2", "registerUserSuccessTermsAcceptance2@email");
Assert.assertNotNull(user.getAttributes());
Assert.assertNotNull(user.getAttributes().get(TermsAndConditions.USER_ATTRIBUTE));
Assert.assertEquals(1, user.getAttributes().get(TermsAndConditions.USER_ATTRIBUTE).size());
Assert.assertTrue(Integer.parseInt(user.getAttributes().get(TermsAndConditions.USER_ATTRIBUTE).get(0)) >= currentTime);
} finally {
tacRep.setEnabled(false);
tacRep.setDefaultAction(false);
testRealm().flows().updateRequiredAction(UserModel.RequiredAction.TERMS_AND_CONDITIONS.name(), tacRep);
configureRegistrationFlowWithCustomRegistrationPageForm(UUID.randomUUID().toString());
}
}
@Test @Test
public void testRegisterShouldFailBeforeUserCreationWhenUserIsInContext() { public void testRegisterShouldFailBeforeUserCreationWhenUserIsInContext() {
loginPage.open(); loginPage.open();