Skip to content

Commit

Permalink
Remove RegistrationProfile class and handle migration (keycloak#24215)
Browse files Browse the repository at this point in the history
closes keycloak#24182


Co-authored-by: andymunro <48995441+andymunro@users.noreply.github.com>
  • Loading branch information
mposolda and andymunro authored Oct 24, 2023
1 parent 6adce2a commit 1bd6aca
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 189 deletions.
103 changes: 61 additions & 42 deletions docs/documentation/server_development/topics/auth-spi.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1032,51 +1032,43 @@ Let's also look at the user profile plugin that is used to validate email addres
@Override
public void validate(ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> errors = new ArrayList<>();

String eventError = Errors.INVALID_REGISTRATION;
context.getEvent().detail(Details.REGISTER_METHOD, "form");

if (Validation.isBlank(formData.getFirst((RegistrationPage.FIELD_FIRST_NAME)))) {
errors.add(new FormMessage(RegistrationPage.FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME));
}
UserProfile profile = getOrCreateUserProfile(context, formData);

if (Validation.isBlank(formData.getFirst((RegistrationPage.FIELD_LAST_NAME)))) {
errors.add(new FormMessage(RegistrationPage.FIELD_LAST_NAME, Messages.MISSING_LAST_NAME));
}
try {
profile.validate();
} catch (ValidationException pve) {
List<FormMessage> errors = Validation.getFormErrorsFromValidation(pve.getErrors());

String email = formData.getFirst(Validation.FIELD_EMAIL);
if (Validation.isBlank(email)) {
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.MISSING_EMAIL));
} else if (!Validation.isEmailValid(email)) {
formData.remove(Validation.FIELD_EMAIL);
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.INVALID_EMAIL));
}
if (pve.hasError(Messages.EMAIL_EXISTS, Messages.INVALID_EMAIL)) {
context.getEvent().detail(Details.EMAIL, profile.getAttributes().getFirstValue(UserModel.EMAIL));
}

if (context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
formData.remove(Validation.FIELD_EMAIL);
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.EMAIL_EXISTS));
}
if (pve.hasError(Messages.EMAIL_EXISTS)) {
context.error(Errors.EMAIL_IN_USE);
} else if (pve.hasError(Messages.USERNAME_EXISTS)) {
context.error(Errors.USERNAME_IN_USE);
} else {
context.error(Errors.INVALID_REGISTRATION);
}

if (errors.size() > 0) {
context.validationError(formData, errors);
return;

} else {
context.success();
}
context.success();
}
----
As you can see, this validate() method of user profile processing makes sure that the email, first, and last name are filled in the form.
It also makes sure that email is in the right format.
If any of these validations fail, an error message is queued up for rendering.
Any fields in error are removed from the form data.
Error messages are represented by the FormMessage class.
The first parameter of the constructor of this class takes the HTML element id.
The input in error will be highlighted when the form is re-rendered.
The second parameter is a message reference id.
This id must correspond to a property in one of the localized message bundle files.
in the theme.
As you can see, this validate() method of user profile processing makes sure that the email and all other attributes are filled in the form.
It delegates to User Profile SPI, which makes sure that email is in the right format and does all other validations.
If any of these validations fail, an error message is queued up for rendering. It would contain the message for every field where the validation failed.
NOTE: As you can see, the user profile makes sure that registration form contains all the needed user profile fields. User profile also makes sure that correct validations
are used, attributes are correctly grouped on the page. There is a correct type used for each field (such as if a user needs to choose from predefined values), fields
are "conditionally" rendered just for some scopes (Progressive profiling) and others. So usually you will not need to implement new `FormAction` or registration fields, but
you can just properly configure user-profile to reflect this. For more details, see link:{adminguide_link}#user-profile[User Profile documentation].
In general, new FormAction might be useful for instance if you want to add new credentials to the registration form (such as ReCaptcha support as mentioned here) rather than new user profile fields.
After all validations have been processed then, the form flow then invokes the FormAction.success() method.
For recaptcha this is a no-op, so we won't go over it.
Expand All @@ -1087,17 +1079,44 @@ For user profile processing, this method fills in values in the registered user.

@Override
public void success(FormContext context) {
UserModel user = context.getUser();
checkNotOtherUserAuthenticating(context);

MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
user.setFirstName(formData.getFirst(RegistrationPage.FIELD_FIRST_NAME));
user.setLastName(formData.getFirst(RegistrationPage.FIELD_LAST_NAME));
user.setEmail(formData.getFirst(RegistrationPage.FIELD_EMAIL));

String email = formData.getFirst(UserModel.EMAIL);
String username = formData.getFirst(UserModel.USERNAME);

if (context.getRealm().isRegistrationEmailAsUsername()) {
username = email;
}

context.getEvent().detail(Details.USERNAME, username)
.detail(Details.REGISTER_METHOD, "form")
.detail(Details.EMAIL, email);

UserProfile profile = getOrCreateUserProfile(context, formData);
UserModel user = profile.create();

user.setEnabled(true);

// This means that following actions can retrieve user from the context by context.getUser() method
context.setUser(user);
}
----
Pretty simple implementation.
The UserModel of the newly registered user is obtained from the FormContext.
The appropriate methods are called to initialize UserModel data.
The new user is created and the UserModel of the newly registered user is added to the FormContext.
The appropriate methods are called to initialize UserModel data. In your own FormAction, you can possibly obtain user by using something like:
[source,java]
----

@Override
public void success(FormContext context) {
UserModel user = context.getUser();
if (user != null) {
// Do something useful with the user here ...
}
}
----
Finally, you are also required to define a FormActionFactory class.
This class is implemented similarly to AuthenticatorFactory, so we won't go over it.
Expand All @@ -1112,7 +1131,7 @@ For example:
[source]
----

org.keycloak.authentication.forms.RegistrationProfile
org.keycloak.authentication.forms.RegistrationUserCreation
org.keycloak.authentication.forms.RegistrationRecaptcha
----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,10 @@ Use a single escape:

```
bin/kc.sh start --db postgres --db-username keycloak --db-url "jdbc:postgresql://localhost:5432/keycloak?ssl=false&connectTimeout=30" --db-password keycloak --hostname localhost
```
```

= Removed RegistrationProfile form action

The form action `RegistrationProfile` (displayed in the UI of authentication flows as `Profile Validation`) was removed from the codebase and also from all authentication flows. By default, it was in
the built-in registration flow of every realm. The validation of user attributes as well as creation of the user including all that user's attributes is handled by `RegistrationUserCreation` form action and
hence `RegistrationProfile` is not needed anymore. There is usually no further action needed in relation to this change, unless you used `RegistrationProfile` class in your own providers.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
package org.keycloak.migration.migrators;

import java.util.Optional;

import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlow;
import org.keycloak.component.ComponentModel;
import org.keycloak.migration.ModelVersion;
import org.keycloak.models.KeycloakSession;
Expand All @@ -29,6 +32,8 @@

public class MigrateTo23_0_0 implements Migration {

private static final Logger LOG = Logger.getLogger(MigrateTo23_0_0.class);

public static final ModelVersion VERSION = new ModelVersion("23.0.0");

private static final String USER_PROFILE_ENABLED_PROP = "userProfileEnabled";
Expand All @@ -38,12 +43,17 @@ public class MigrateTo23_0_0 implements Migration {

@Override
public void migrate(KeycloakSession session) {
session.realms().getRealmsStream().forEach(this::updateUserProfileConfig);
session.realms().getRealmsStream().forEach(this::migrateRealm);
}

@Override
public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) {
migrateRealm(realm);
}

private void migrateRealm(RealmModel realm) {
updateUserProfileConfig(realm);
removeRegistrationProfileFormExecution(realm);
}

private void updateUserProfileConfig(RealmModel realm) {
Expand Down Expand Up @@ -74,6 +84,20 @@ private void updateUserProfileConfig(RealmModel realm) {
}
}

private void removeRegistrationProfileFormExecution(RealmModel realm) {
realm.getAuthenticationFlowsStream()
.filter(flow -> AuthenticationFlow.FORM_FLOW.equals(flow.getProviderId()))
.forEach(registrationFlow -> {
realm.getAuthenticationExecutionsStream(registrationFlow.getId())
.filter(authExecution -> "registration-profile-action".equals(authExecution.getAuthenticator()))
.forEach(registrationProfileExecution -> {
realm.removeAuthenticatorExecution(registrationProfileExecution);
LOG.debugf("Removed 'registration-profile-action' form action from authentication flow '%s' in the realm '%s'.", registrationFlow.getAlias(), realm.getName());
});
});

}

@Override
public ModelVersion getVersion() {
return VERSION;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
#

org.keycloak.authentication.forms.RegistrationPassword
org.keycloak.authentication.forms.RegistrationProfile
org.keycloak.authentication.forms.RegistrationUserCreation
org.keycloak.authentication.forms.RegistrationRecaptcha
org.keycloak.authentication.forms.RegistrationTermsAndConditions
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ public void testFormActionProviders() {
List<Map<String, Object>> result = authMgmtResource.getFormActionProviders();

List<Map<String, Object>> expected = new LinkedList<>();
addProviderInfo(expected, "registration-profile-action", "Profile Validation",
"Validates email, first name, and last name attributes and stores them in user data.");
addProviderInfo(expected, "registration-recaptcha-action", "Recaptcha",
"Adds Google Recaptcha button. Recaptchas verify that the entity that is registering is a human. " +
"This can only be used on the internet and must be configured after you add it.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import org.keycloak.authentication.AuthenticationFlow;
import org.keycloak.authentication.authenticators.browser.CookieAuthenticatorFactory;
import org.keycloak.authentication.forms.RegistrationPassword;
import org.keycloak.authentication.forms.RegistrationProfile;
import org.keycloak.authentication.forms.RegistrationRecaptcha;
import org.keycloak.authentication.forms.RegistrationTermsAndConditions;
import org.keycloak.authentication.forms.RegistrationUserCreation;
Expand Down
Loading

0 comments on commit 1bd6aca

Please sign in to comment.