8000 Update password after email verification during registration of users by mposolda · Pull Request #47538 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ See the <<_identity_broker_first_login, First login flow section in the Identity

Also users coming from the <<_user-storage-federation, 3rd-party user storage>> (for example LDAP) are automatically available in {project_name} when the particular user storage is enabled

.Clarification on verify email
When self-registrations is enabled together with *Verify email* realm switch, then password will not be set by default
on the registration form. User will need first to verify his email and he will be able to setup his password on the subsequent screen. This is recommended
for security reason, so that self-registered user can set his password or other credentials after verifying email. Note it is also recommended
to enable <<enabling-forgot-password, Forget password>>, so that if user fails to verify his email during self-registration, he can do it later by following
*Forget password* flow without being stuck.

If you still prefer to keep password on the initial registration form and make email verification to be done once user self-registers with his password, you can
setup the configuration option on the <<_authentication-flows,Registration authentication flow>>. It can be done in the admin console by following tab *Authentication* ->
flow *registration* (or other flow, which you bind as registration flow) -> Settings of *Password validation* -> Enable *Always set password on register form*.

Note that this option is deprecated and exists mostly for the backwards compatibility. It may be removed in the future.

.Registration validation configuration
image:images/registration-always-set-password-on-register-form-config.png[]

[role="_additional-resources"]
.Additional resources
* For more information on customizing user registration, see the link:{developerguide_link}[{developerguide_name}].
26 changes: 26 additions & 0 deletions docs/documentation/upgrading/topics/changes/changes-26_7_0.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// ------------------------ Breaking changes ------------------------ //
== Breaking changes

Breaking changes are identified as those that might require changes for existing users to their configurations or applications.
In minor or patch releases, {project_name} will only introduce breaking changes to fix bugs.

// ------------------------ Notable changes ------------------------ //
== Notable changes

Notable changes may include internal behavior changes that prevent common misconfigurations, bugs that are fixed, or changes to simplify running {project_name}.
It also lists significant changes to internal APIs.

=== Verify email required before credentials setup during user self-registration

When user self-registration is enabled for the realm together with *Verify Email*, then users will not see password by default
on the registration screen. After registration of user's profile, user would be required to verify email and then he can setup password
(or eventually other credentials like OTP or Passkey) as a subsequent step. It is possible to disable and stick to the old behavior by
enable switch *Always set password on register form* on the realm registration flow on the *Password validation* authenticator. Note
that this option is deprecated and exists mostly for the backwards compatibility. It may be removed in the future.

For the details, see the link:{adminguide_link}[{adminguide_name}].

=== Configure TOTP and Update password required actions moved after Verify Email

In relation to the previous point, the required actions *Configure OTP* and *Update password* are moved in the order of required actions after *Verify Email*
required action. This is done automatically for new realm or during server update. If you prefer the old order, you can manually move required actions per your preference.
4 changes: 4 additions & 0 deletions docs/documentation/upgrading/topics/changes/changes.adoc
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
[[migration-changes]]
== Migration Changes

=== Migrating to 26.7.0

include::changes-26_7_0.adoc[leveloffset=2]

=== Migrating to 26.6.1

include::changes-26_6_1.adoc[leveloffset=2]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.keycloak.migration.migrators;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.keycloak.migration.ModelVersion;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.UserModel;

public class MigrateTo26_7_0 extends RealmMigration {

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

@Override
public ModelVersion getVersion() {
return VERSION;
}


@Override
public void migrateRealm(KeycloakSession session, RealmModel realm) {
Map<String, RequiredActionProviderModel> reqActionsByAlias = new HashMap<>();
List<Integer> reqActionPriorities = new ArrayList<>();
realm.getRequiredActionProvidersStream().forEach((reqAction) -> {
reqActionsByAlias.put(reqAction.getAlias(), reqAction);
reqActionPriorities.add(reqAction.getPriority());
});

RequiredActionProviderModel verifyEmail = reqActionsByAlias.get(UserModel.RequiredAction.VERIFY_EMAIL.name());
RequiredActionProviderModel configureTotp = reqActionsByAlias.get(UserModel.RequiredAction.CONFIGURE_TOTP.name());
RequiredActionProviderModel updatePassword = reqActionsByAlias.get(UserModel.RequiredAction.UPDATE_PASSWORD.name());

if (verifyEmail == null) {
return;
}

// Default case when admin did not changed anything. Set priorities same way like in DefaultRequiredActions
if (configureTotp != null && updatePassword != null &&
verifyEmail.getPriority() == 50 && configureTotp.getPriority() == 10 && updatePassword.getPriority() == 30) {
configureTotp.setPriority(54);
realm.updateRequiredActionProvider(configureTotp);
updatePassword.setPriority(57);
realm.updateRequiredActionProvider(updatePassword);
} else {
// Case when admin changed priorities of required actions. Add configureTotp and updatePassword to the first free places after verifyEmail
int nextAvailablePriority = getFirstAvailablePriorityAfter(verifyEmail.getPriority(), reqActionPriorities);
if (configureTotp != null) {
configureTotp.setPriority(nextAvailablePriority);
realm.updateRequiredActionProvider(configureTotp);
nextAvailablePriority = getFirstAvailablePriorityAfter(nextAvailablePriority, reqActionPriorities);
}
if (updatePassword != null) {
updatePassword.setPriority(nextAvailablePriority);
realm.updateRequiredActionProvider(updatePassword);
}
}
}

private int getFirstAvailablePriorityAfter(int priority, List<Integer> reqActionPriorities) {
for (int i = priority + 1 ; i < (priority + reqActionPriorities.size() + 2) ; i++) {
if (!reqActionPriorities.contains(i)) {
return i;
}
}

// Should not happen
return reqActionPriorities.get(reqActionPriorities.size() - 1) + 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.keycloak.migration.migrators.MigrateTo26_4_0;
import org.keycloak.migration.migrators.MigrateTo26_4_3;
import org.keycloak.migration.migrators.MigrateTo26_6_1;
import org.keycloak.migration.migrators.MigrateTo26_7_0;
import org.keycloak.migration.migrators.MigrateTo2_0_0;
import org.keycloak.migration.migrators.MigrateTo2_1_0;
import org.keycloak.migration.migrators.MigrateTo2_2_0;
Expand Down Expand Up @@ -133,7 +134,8 @@ public class DefaultMigrationManager implements MigrationManager {
new MigrateTo26_3_0(),
new MigrateTo26_4_0(),
new MigrateTo26_4_3(),
new MigrateTo26_6_1()
new MigrateTo26_6_1(),
new MigrateTo26_7_0()
};

private final KeycloakSession session;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public static void addConfigureTotpAction(RealmModel realm) {
totp.setName("Configure OTP");
totp.setProviderId(UserModel.RequiredAction.CONFIGURE_TOTP.name());
totp.setDefaultAction(false);
totp.setPriority(10);
totp.setPriority(54);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marek, would not be better to move the verify email action to the priority 1 (always the first to be presented). This way all the required actions are always after the verify email. It's only terms&Conditions and Update Profile with this change, so maybe it's enough. But I think that, if verify email is the first one, no action is never done at the registration side, everything is done at the verification email side.

Sorry for not thinking about this before.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this one, it seems to me that "Verify email" should be done after "Update profile" ?

Thinking that when user has both Update profile and Verify email required actions, it would makes sense that he first updates his profile and then email is verified as a subsequent step (as email was possibly updated by user during updating profile). That is also aligned with registration form (User in fact first registers, which pretty much updating his profile info and then email verification is a next step and then possibly password-update or other credentials update are another subsequent step). WDYT?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, as I commented the current PR order is OK. Verify email should be before any credential action, and that's OK now. About the update profile I didn't check that exact scenario, I just preferred the verify email to be first, but maybe you are right and update profile should be before verify email.

realm.addRequiredActionProvider(totp);
}
}
Expand All @@ -169,7 +169,7 @@ public static void addUpdatePasswordAction(RealmModel realm) {
updatePassword.setName("Update Password");
updatePassword.setProviderId(UserModel.RequiredAction.UPDATE_PASSWORD.name());
updatePassword.setDefaultAction(false);
updatePassword.setPriority(30);
updatePassword.setPriority(57);
realm.addRequiredActionProvider(updatePassword);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionCompoundId;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;

/**
* Action token handler for verification of e-mail address.
Expand Down Expand Up @@ -130,21 +131,31 @@ public Response handleToken(VerifyEmailActionToken token, ActionTokenContext<Ver

event.success();

if (token.getCompoundOriginalAuthenticationSessionId() != null) {
AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true);
String nextAction = AuthenticationManager.nextRequiredAction(session, authSession, tokenContext.getRequest(), event);

return forms
.setAuthenticationSession(authSession)
.setAttribute("messageHeader", forms.getMessage(Messages.EMAIL_VERIFIED_HEADER, user.getEmail()))
.setSuccess(Messages.EMAIL_VERIFIED)
.setUser(user)
.createInfoPage();
if (token.getCompoundOriginalAuthenticationSessionId() != null) {
// Email verified in other browser than the one originally started. Removing original authSession. This authenticationSession would be finished after requiredAction
AuthenticationSessionCompoundId origAuthSession = AuthenticationSessionCompoundId.encoded(token.getCompoundOriginalAuthenticationSessionId());
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, origAuthSession.getRootSessionId());
session.authenticationSessions().removeRootAuthenticationSession(realm, rootAuthSession);

if (nextAction == null) {
AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true);

return forms
.setAuthenticationSession(authSession)
.setAttribute("messageHeader", forms.getMessage(Messages.EMAIL_VERIFIED_HEADER, user.getEmail()))
.setSuccess(Messages.EMAIL_VERIFIED)
.setUser(user)
.createInfoPage();
} else {
// Updating current authSession to end after required actions (should be marked already, but just to re-add in case of some corner case scenarios)
authSession.setAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
}
}

tokenContext.setEvent(event.clone().removeDetail(Details.EMAIL).event(EventType.LOGIN));

String nextAction = AuthenticationManager.nextRequiredAction(session, authSession, tokenContext.getRequest(), event);
return AuthenticationManager.redirectToRequiredActions(session, realm, authSession, uriInfo, nextAction);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.policy.PasswordPolicyManagerProvider;
import org.keycloak.policy.PolicyError;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation;

Expand All @@ -51,18 +53,39 @@
public class RegistrationPassword implements FormAction, FormActionFactory {
public static final String PROVIDER_ID = "registration-password-action";

// Configuration option
public static final String ALWAYS_SET_PASSWORD_ON_REGISTER_FORM = "always_set_password_on_register_form";

// Authentication note to signal that password fields should not be rendered on the registration page, but rather "update password" required action should be added
// to the user account
private static final String UPDATE_PASSWORD_AFTER_EMAIL_VERIFICATION_NOTE = "update_password_after_email_verification_note";

@Override
public String getHelpText() {
return "Validates that password matches password confirmation field. It also will store password in user's credential store.";
}

@Override
public List<ProviderConfigProperty> getConfigProperties() {
return null;
return ProviderConfigurationBuilder.create()
.property()
.name(ALWAYS_SET_PASSWORD_ON_REGISTER_FORM)
.label("Always set password on register form")
.helpText("When this option is false and 'Verify Email' is enabled for the realm, then the password will not be set by the user on the registration form, but rather in the later stage once "
+ " user's email address is successfully verified. This is recommended for security reasons. When true, the password fields will be available directly on the registration form and can be set "
+ " by the user before his email is verified. This option is deprecated and might be removed in the future.")
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.add()
.build();
}

@Override
public void validate(ValidationContext context) {
if (isVerifyEmail(context)) {
context.success();
return;
}

MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> errors = new ArrayList<>();
context.getEvent().detail(Details.REGISTER_METHOD, "form");
Expand Down Expand Up @@ -90,11 +113,16 @@ public void validate(ValidationContext context) {

@Override
public void success(FormContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String password = formData.getFirst(RegistrationPage.FIELD_PASSWORD);
UserModel user = context.getUser();

if ("true".equals(context.getAuthenticationSession().getAuthNote(UPDATE_PASSWORD_AFTER_EMAIL_VERIFICATION_NOTE))) {
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
return;
}

MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
try {
user.credentialManager().updateCredential(UserCredentialModel.password(formData.getFirst("password"), false));
user.credentialManager().updateCredential(UserCredentialModel.password(formData.getFirst(RegistrationPage.FIELD_PASSWORD), false));
} catch (Exception me) {
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
}
Expand All @@ -103,7 +131,22 @@ public void success(FormContext context) {

@Override
public void buildPage(FormContext context, LoginFormsProvider form) {
form.setAttribute("passwordRequired", true);
if (isVerifyEmail(context)) {
context.getAuthenticationSession().setAuthNote(UPDATE_PASSWORD_AFTER_EMAIL_VERIFICATION_NOTE, "true");
} else {
form.setAttribute("passwordRequired", true);
}
}

private boolean isVerifyEmail(FormContext context) {
String alwaysSetPasswordCfg = context.getAuthenticatorConfig() == null ? null : context.getAuthenticatorConfig().getConfig().get(ALWAYS_SET_PASSWORD_ON_REGISTER_FORM);
if ("true".equals(alwaysSetPasswordCfg)) return false;

if (context.getRealm().isVerifyEmail()) return true;

// Check if verifyEmail is set as default required action. In that case, newly registered users are also required to verify their emails
RequiredActionProviderModel verifyEmailAction = context.getRealm().getRequiredActionProviderByAlias(UserModel.RequiredAction.VERIFY_EMAIL.name());
return verifyEmailAction != null && verifyEmailAction.isDefaultAction();
}

@Override
Expand All @@ -118,7 +161,6 @@ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserMode

@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {

}

@Override
Expand All @@ -143,7 +185,7 @@ public String getReferenceCategory() {

@Override
public boolean isConfigurable() {
return false;
return true;
}

private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ public RealmConfigBuilder registrationAllowed(boolean allowed) {
return this;
}

public RealmConfigBuilder verifyEmail(boolean verifyEmail) {
rep.setVerifyEmail(verifyEmail);
return this;
}

public RealmConfigBuilder editUsernameAllowed(boolean allowed) {
rep.setEditUsernameAllowed(allowed);
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public class LoginPage extends AbstractLoginPage {
@FindBy(id = "rememberMe")
private WebElement rememberMe;

@FindBy(linkText = "Register")
private WebElement registerLink;

@FindBy(linkText = "Forgot Password?")
private WebElement resetPasswordLink;

Expand Down Expand Up @@ -76,6 +79,10 @@ public boolean isRememberMe() {
return rememberMe.isSelected();
}

public void clickRegister() {
registerLink.click();
}

public void resetPassword() {
resetPasswordLink.click();
}
Expand Down
Loading
Loading
0