From 72e0c26a3588a0f656322e63eda7c365e151aab2 Mon Sep 17 00:00:00 2001 From: Marek Posolda Date: Fri, 17 Apr 2026 15:15:48 +0200 Subject: [PATCH] Update password after email verification during registration of users (#47538) closes #45568 Signed-off-by: mposolda --- ...s-set-password-on-register-form-config.png | Bin 0 -> 30768 bytes .../topics/users/con-user-registration.adoc | 16 + .../topics/changes/changes-26_7_0.adoc | 26 ++ .../upgrading/topics/changes/changes.adoc | 4 + .../migration/migrators/MigrateTo26_7_0.java | 73 ++++ .../datastore/DefaultMigrationManager.java | 4 +- .../models/utils/DefaultRequiredActions.java | 4 +- .../VerifyEmailActionTokenHandler.java | 33 +- .../forms/RegistrationPassword.java | 56 ++- .../realm/RealmConfigBuilder.java | 5 + .../testframework/ui/page/LoginPage.java | 7 + .../testframework/ui/page/RegisterPage.java | 32 +- .../ui/page/VerifyEmailPage.java | 44 +++ .../authentication/InitialFlowsTest.java | 2 +- .../RegisterWithEmailVerificationTest.java | 337 ++++++++++++++++++ .../tests/utils/PasswordGenerateUtil.java | 14 + .../testsuite/pages/RegisterPage.java | 31 +- .../RequiredActionEmailVerificationTest.java | 97 ++++- .../actions/RequiredActionPriorityTest.java | 14 +- ...henticationSessionFailoverClusterTest.java | 16 +- .../federation/storage/UserStorageTest.java | 5 +- .../testsuite/forms/BrowserButtonsTest.java | 64 ++-- .../forms/MultipleTabsLoginTest.java | 44 +-- .../testsuite/forms/RegisterTest.java | 108 ------ .../migration/AbstractMigrationTest.java | 25 +- 25 files changed, 828 insertions(+), 233 deletions(-) create mode 100644 docs/documentation/server_admin/images/registration-always-set-password-on-register-form-config.png create mode 100644 docs/documentation/upgrading/topics/changes/changes-26_7_0.adoc create mode 100644 model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo26_7_0.java create mode 100644 test-framework/ui/src/main/java/org/keycloak/testframework/ui/page/VerifyEmailPage.java create mode 100644 tests/base/src/test/java/org/keycloak/tests/forms/RegisterWithEmailVerificationTest.java create mode 100644 tests/base/src/test/java/org/keycloak/tests/utils/PasswordGenerateUtil.java diff --git a/docs/documentation/server_admin/images/registration-always-set-password-on-register-form-config.png b/docs/documentation/server_admin/images/registration-always-set-password-on-register-form-config.png new file mode 100644 index 0000000000000000000000000000000000000000..de00118476f0126f1a69e9034a3fd60c1b953e1b GIT binary patch literal 30768 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 literal 0 HcmV?d00001 diff --git a/docs/documentation/server_admin/topics/users/con-user-registration.adoc b/docs/documentation/server_admin/topics/users/con-user-registration.adoc index 27bcf1ead19..fc6f1ae3ac2 100644 --- a/docs/documentation/server_admin/topics/users/con-user-registration.adoc +++ b/docs/documentation/server_admin/topics/users/con-user-registration.adoc @@ -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 <>, 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}]. diff --git a/docs/documentation/upgrading/topics/changes/changes-26_7_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_7_0.adoc new file mode 100644 index 00000000000..0ac95530979 --- /dev/null +++ b/docs/documentation/upgrading/topics/changes/changes-26_7_0.adoc @@ -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. diff --git a/docs/documentation/upgrading/topics/changes/changes.adoc b/docs/documentation/upgrading/topics/changes/changes.adoc index 88c2e35b5dd..bc11fd2275a 100644 --- a/docs/documentation/upgrading/topics/changes/changes.adoc +++ b/docs/documentation/upgrading/topics/changes/changes.adoc @@ -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] diff --git a/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo26_7_0.java b/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo26_7_0.java new file mode 100644 index 00000000000..9b5343b06a1 --- /dev/null +++ b/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo26_7_0.java @@ -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 reqActionsByAlias = new HashMap<>(); + List 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 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; + } +} diff --git a/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultMigrationManager.java b/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultMigrationManager.java index fd4b5312737..8e38e355f69 100644 --- a/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultMigrationManager.java +++ b/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultMigrationManager.java @@ -50,6 +50,7 @@ import org.keycloak.migration.migrators.MigrateTo26_3_0; 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; @@ -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; diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultRequiredActions.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultRequiredActions.java index c34fd647c0e..61bc925bed7 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultRequiredActions.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultRequiredActions.java @@ -156,7 +156,7 @@ public class DefaultRequiredActions { totp.setName("Configure OTP"); totp.setProviderId(UserModel.RequiredAction.CONFIGURE_TOTP.name()); totp.setDefaultAction(false); - totp.setPriority(10); + totp.setPriority(54); realm.addRequiredActionProvider(totp); } } @@ -169,7 +169,7 @@ public class DefaultRequiredActions { updatePassword.setName("Update Password"); updatePassword.setProviderId(UserModel.RequiredAction.UPDATE_PASSWORD.name()); updatePassword.setDefaultAction(false); - updatePassword.setPriority(30); + updatePassword.setPriority(57); realm.addRequiredActionProvider(updatePassword); } } diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java index c4356c5bc32..a549a959ce7 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java @@ -46,6 +46,7 @@ import org.keycloak.services.managers.AuthenticationSessionManager; 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. @@ -130,21 +131,31 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHandler 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 formData = context.getHttpRequest().getDecodedFormParameters(); List errors = new ArrayList<>(); context.getEvent().detail(Details.REGISTER_METHOD, "form"); @@ -90,11 +113,16 @@ public class RegistrationPassword implements FormAction, FormActionFactory { @Override public void success(FormContext context) { - MultivaluedMap 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 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); } @@ -103,7 +131,22 @@ public class RegistrationPassword implements FormAction, FormActionFactory { @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 @@ -118,7 +161,6 @@ public class RegistrationPassword implements FormAction, FormActionFactory { @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { - } @Override @@ -143,7 +185,7 @@ public class RegistrationPassword implements FormAction, FormActionFactory { @Override public boolean isConfigurable() { - return false; + return true; } private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java index 187bd28b255..ca4b1ac1580 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java @@ -125,6 +125,11 @@ public class RealmConfigBuilder { return this; } + public RealmConfigBuilder verifyEmail(boolean verifyEmail) { + rep.setVerifyEmail(verifyEmail); + return this; + } + public RealmConfigBuilder editUsernameAllowed(boolean allowed) { rep.setEditUsernameAllowed(allowed); return this; diff --git a/test-framework/ui/src/main/java/org/keycloak/testframework/ui/page/LoginPage.java b/test-framework/ui/src/main/java/org/keycloak/testframework/ui/page/LoginPage.java index fe18ea5fd94..26ee71bc7e0 100644 --- a/test-framework/ui/src/main/java/org/keycloak/testframework/ui/page/LoginPage.java +++ b/test-framework/ui/src/main/java/org/keycloak/testframework/ui/page/LoginPage.java @@ -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; @@ -76,6 +79,10 @@ public class LoginPage extends AbstractLoginPage { return rememberMe.isSelected(); } + public void clickRegister() { + registerLink.click(); + } + public void resetPassword() { resetPasswordLink.click(); } diff --git a/test-framework/ui/src/main/java/org/keycloak/testframework/ui/page/RegisterPage.java b/test-framework/ui/src/main/java/org/keycloak/testframework/ui/page/RegisterPage.java index 53278c50ab3..3e4c1b1a6b3 100644 --- a/test-framework/ui/src/main/java/org/keycloak/testframework/ui/page/RegisterPage.java +++ b/test-framework/ui/src/main/java/org/keycloak/testframework/ui/page/RegisterPage.java @@ -23,6 +23,7 @@ import java.util.Map.Entry; import org.keycloak.models.Constants; import org.keycloak.testframework.ui.webdriver.ManagedWebDriver; +import org.junit.jupiter.api.Assertions; import org.openqa.selenium.By; import org.openqa.selenium.Keys; import org.openqa.selenium.NoSuchElementException; @@ -65,6 +66,11 @@ public class RegisterPage extends AbstractLoginPage { super(driver); } + // Register user with the registration-page expected to NOT have "password" and "password-confirmation" fields + public void registerWithoutPassword(String firstName, String lastName, String email, String username) { + register(firstName, lastName, email, username, null, null, null, null, null); + } + public void register(String firstName, String lastName, String email, String username, String password) { register(firstName, lastName, email, username, password, password, null, null, null); } @@ -96,14 +102,20 @@ public class RegisterPage extends AbstractLoginPage { usernameInput.sendKeys(username); } - passwordInput.clear(); - if (password != null) { - passwordInput.sendKeys(password); + if (!isPasswordPresent() && password != null) { + Assertions.fail("Password expected to be filled, but password field not present on the registration page"); } - passwordConfirmInput.clear(); - if (passwordConfirm != null) { - passwordConfirmInput.sendKeys(passwordConfirm); + if (isPasswordPresent()) { + passwordInput.clear(); + if (password != null) { + passwordInput.sendKeys(password); + } + + passwordConfirmInput.clear(); + if (passwordConfirm != null) { + passwordConfirmInput.sendKeys(passwordConfirm); + } } @@ -163,6 +175,14 @@ public class RegisterPage extends AbstractLoginPage { } } + public boolean isPasswordPresent() { + try { + return driver.findElement(By.name("password")).isDisplayed(); + } catch (NoSuchElementException nse) { + return false; + } + } + @Override public String getExpectedPageId() { return "login-register"; diff --git a/test-framework/ui/src/main/java/org/keycloak/testframework/ui/page/VerifyEmailPage.java b/test-framework/ui/src/main/java/org/keycloak/testframework/ui/page/VerifyEmailPage.java new file mode 100644 index 00000000000..ce25a53d129 --- /dev/null +++ b/test-framework/ui/src/main/java/org/keycloak/testframework/ui/page/VerifyEmailPage.java @@ -0,0 +1,44 @@ +package org.keycloak.testframework.ui.page; + + +import org.keycloak.testframework.ui.webdriver.ManagedWebDriver; + +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +public class VerifyEmailPage extends AbstractLoginPage { + + @FindBy(linkText = "Click here") + private WebElement resendEmailLink; + + @FindBy(name = "cancel-aia") + private WebElement cancelAIAButton; + + @FindBy(className = "kc-feedback-text") + private WebElement feedbackText; + + public VerifyEmailPage(ManagedWebDriver driver) { + super(driver); + } + + public void clickResendEmail() { + resendEmailLink.click(); + } + + public String getResendEmailLink() { + return resendEmailLink.getAttribute("href"); + } + + public String getFeedbackText() { + return feedbackText.getText(); + } + + public void cancel() { + cancelAIAButton.click(); + } + + @Override + public String getExpectedPageId() { + return "login-login-verify-email"; + } +} 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 5debe112564..b3371a9b774 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 @@ -222,7 +222,7 @@ public class InitialFlowsTest extends AbstractAuthenticationTest { execs = new LinkedList<>(); addExecInfo(execs, "registration form", "registration-page-form", false, 0, 0, REQUIRED, true, new String[]{REQUIRED, DISABLED}, 10); addExecInfo(execs, "Registration User Profile Creation", "registration-user-creation", false, 1, 0, REQUIRED, null, new String[]{REQUIRED, DISABLED}, 20); - addExecInfo(execs, "Password Validation", "registration-password-action", false, 1, 1, REQUIRED, null, new String[]{REQUIRED, DISABLED}, 50); + addExecInfo(execs, "Password Validation", "registration-password-action", true, 1, 1, REQUIRED, null, new String[]{REQUIRED, DISABLED}, 50); addExecInfo(execs, "reCAPTCHA", "registration-recaptcha-action", true, 1, 2, DISABLED, null, new String[]{REQUIRED, DISABLED}, 60); addExecInfo(execs, "Terms and conditions", "registration-terms-and-conditions", false, 1, 3, DISABLED, null, new String[]{REQUIRED, DISABLED}, 70); expected.add(new FlowExecutions(flow, execs)); diff --git a/tests/base/src/test/java/org/keycloak/tests/forms/RegisterWithEmailVerificationTest.java b/tests/base/src/test/java/org/keycloak/tests/forms/RegisterWithEmailVerificationTest.java new file mode 100644 index 00000000000..e7409b2edcf --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/forms/RegisterWithEmailVerificationTest.java @@ -0,0 +1,337 @@ +package org.keycloak.tests.forms; + +import java.io.IOException; +import java.util.Map; +import java.util.function.Function; + +import jakarta.mail.internet.MimeMessage; +import jakarta.ws.rs.core.Response; + +import org.keycloak.admin.client.resource.AuthenticationManagementResource; +import org.keycloak.authentication.forms.RegistrationPassword; +import org.keycloak.events.Details; +import org.keycloak.events.EventType; +import org.keycloak.models.UserModel; +import org.keycloak.models.credential.PasswordCredentialModel; +import org.keycloak.models.utils.DefaultAuthenticationFlows; +import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; +import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; +import org.keycloak.representations.idm.EventRepresentation; +import org.keycloak.representations.idm.RequiredActionProviderRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testframework.annotations.InjectEvents; +import org.keycloak.testframework.annotations.InjectRealm; +import org.keycloak.testframework.annotations.KeycloakIntegrationTest; +import org.keycloak.testframework.events.EventAssertion; +import org.keycloak.testframework.events.Events; +import org.keycloak.testframework.mail.MailServer; +import org.keycloak.testframework.mail.annotations.InjectMailServer; +import org.keycloak.testframework.oauth.OAuthClient; +import org.keycloak.testframework.oauth.annotations.InjectOAuthClient; +import org.keycloak.testframework.realm.ManagedRealm; +import org.keycloak.testframework.realm.RealmConfig; +import org.keycloak.testframework.realm.RealmConfigBuilder; +import org.keycloak.testframework.remote.timeoffset.InjectTimeOffSet; +import org.keycloak.testframework.remote.timeoffset.TimeOffSet; +import org.keycloak.testframework.ui.annotations.InjectPage; +import org.keycloak.testframework.ui.annotations.InjectWebDriver; +import org.keycloak.testframework.ui.page.LoginPage; +import org.keycloak.testframework.ui.page.LoginPasswordUpdatePage; +import org.keycloak.testframework.ui.page.RegisterPage; +import org.keycloak.testframework.ui.page.VerifyEmailPage; +import org.keycloak.testframework.ui.webdriver.ManagedWebDriver; +import org.keycloak.testframework.util.ApiUtil; +import org.keycloak.tests.utils.MailUtils; + +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.jupiter.api.Test; + +import static org.keycloak.authentication.forms.RegistrationPassword.ALWAYS_SET_PASSWORD_ON_REGISTER_FORM; +import static org.keycloak.tests.admin.authentication.AbstractAuthenticationTest.findExecutionByProvider; +import static org.keycloak.tests.utils.PasswordGenerateUtil.generatePassword; + +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@KeycloakIntegrationTest +public class RegisterWithEmailVerificationTest { + + @InjectWebDriver + ManagedWebDriver driver; + + @InjectRealm(config = RegisterTestRealmConfig.class) + ManagedRealm realm; + + @InjectOAuthClient + OAuthClient oauth; + + @InjectEvents + Events events; + + @InjectMailServer + MailServer mailServer; + + @InjectPage + LoginPage loginPage; + + @InjectPage + RegisterPage registerPage; + + @InjectPage + protected LoginPasswordUpdatePage changePasswordPage; + + @InjectPage + VerifyEmailPage verifyEmailPage; + + @InjectTimeOffSet + TimeOffSet timeOffSet; + + @Test + public void registerUserSuccessWithEmailVerification() { + realm.updateWithCleanup((realmm) -> realmm.verifyEmail(true)); + + registerUserSuccessWithEmailVerification(userId -> { + try { + MimeMessage message = mailServer.getReceivedMessages()[0]; + return MailUtils.getPasswordResetEmailLink(message); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + }); + } + + @Test + public void registerUserSuccessWithEmailVerification_emailVerifyDefaultAction() { + // Don't enable "Verify email" realm switch, but rather switch VERIFY_EMAIL as a default required action + AuthenticationManagementResource authMgmt = realm.admin().flows(); + RequiredActionProviderRepresentation reqAction = authMgmt.getRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL.name()); + reqAction.setDefaultAction(true); + authMgmt.updateRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL.name(), reqAction); + + try { + registerUserSuccessWithEmailVerification(userId -> { + try { + MimeMessage message = mailServer.getReceivedMessages()[0]; + return MailUtils.getPasswordResetEmailLink(message); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + }); + } finally { + reqAction.setDefaultAction(false); + authMgmt.updateRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL.name(), reqAction); + } + } + + @Test + public void registerUserSuccessWithEmailVerificationWithResend() { + realm.updateWithCleanup((realmm) -> realmm.verifyEmail(true)); + + registerUserSuccessWithEmailVerification(userId -> { + try { + timeOffSet.set(40); + + // Re-send email verification link + verifyEmailPage.clickResendEmail(); + verifyEmailPage.assertCurrent(); + + EventRepresentation sendVerifyEmailEvent = events.poll(); + EventAssertion.assertSuccess(sendVerifyEmailEvent) + .details(Details.EMAIL, "registerUserSuccessWithEmailVerification@email".toLowerCase()) + .userId(userId) + .type(EventType.SEND_VERIFY_EMAIL); + + // Get the last email + MimeMessage message = mailServer.getLastReceivedMessage(); + return MailUtils.getPasswordResetEmailLink(message); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + }); + } + + /** + * @param receiveEmailFunction Income is userId. Outcome is link to password reset + * @throws Exception + */ + private void registerUserSuccessWithEmailVerification(Function receiveEmailFunction) { + oauth.openLoginForm(); + loginPage.clickRegister(); + registerPage.assertCurrent(); + + // Password not shown initially on the registration page since verify-email is required + Assert.assertFalse(registerPage.isPasswordPresent()); + registerPage.registerWithoutPassword("firstName", "lastName", "registerUserSuccessWithEmailVerification@email", "registerUserSuccessWithEmailVerification"); + verifyEmailPage.assertCurrent(); + + EventRepresentation registerEvent = events.poll(); + EventAssertion.assertSuccess(registerEvent) + .clientId("test-app") + .details(Details.USERNAME, "registerUserSuccessWithEmailVerification") + .details(Details.EMAIL, "registerUserSuccessWithEmailVerification@email") + .details(Details.REGISTER_METHOD, "form") + .type(EventType.REGISTER); + String userId = registerEvent.getUserId(); + + try { + EventRepresentation sendVerifyEmailEvent = events.poll(); + EventAssertion.assertSuccess(sendVerifyEmailEvent) + .details(Details.EMAIL, "registerUserSuccessWithEmailVerification@email".toLowerCase()) + .userId(userId) + .type(EventType.SEND_VERIFY_EMAIL); + + String link = receiveEmailFunction.apply(userId); + + driver.open(link); + + EventRepresentation reqActionEmailEvent = events.poll(); + EventAssertion.assertSuccess(reqActionEmailEvent) + .details(Details.EMAIL, "registerUserSuccessWithEmailVerification@email".toLowerCase()) + .userId(userId) + .type(EventType.VERIFY_EMAIL); + + // User is required to update password as a next step after email is verified + updatePasswordOnChangePasswordPage(userId); + + assertUserRegistered(userId, "registerUserSuccessWithEmailVerification", "registerUserSuccessWithEmailVerification@email"); + + String code = oauth.parseLoginResponse().getCode(); + assertNotNull(code); + } finally { + realm.admin().users().delete(userId).close(); + } + } + + @Test + public void registerUserSuccessWithEmailVerification_passwordOnRegisterForm() throws Exception { + String authConfigId = enableAlwaysSetPasswordOnRegisterForm(); + realm.updateWithCleanup((realmm) -> realmm.verifyEmail(true)); + String userId = null; + try { + oauth.openLoginForm(); + loginPage.clickRegister(); + registerPage.assertCurrent(); + + registerPage.register("firstName", "lastName", "registerUserSuccessWithEmailVerification@email", "registerUserSuccessWithEmailVerification", generatePassword()); + verifyEmailPage.assertCurrent(); + + EventRepresentation registerEvent = events.poll(); + EventAssertion.assertSuccess(registerEvent) + .clientId("test-app") + .details(Details.USERNAME, "registerUserSuccessWithEmailVerification") + .details(Details.EMAIL, "registerUserSuccessWithEmailVerification@email") + .details(Details.REGISTER_METHOD, "form") + .type(EventType.REGISTER); + userId = registerEvent.getUserId(); + + EventRepresentation sendVerifyEmailEvent = events.poll(); + EventAssertion.assertSuccess(sendVerifyEmailEvent) + .details(Details.EMAIL, "registerUserSuccessWithEmailVerification@email".toLowerCase()) + .userId(userId) + .type(EventType.SEND_VERIFY_EMAIL); + + MimeMessage message = mailServer.getReceivedMessages()[0]; + String link = MailUtils.getPasswordResetEmailLink(message); + + driver.open(link); + + EventRepresentation reqActionEmailEvent = events.poll(); + EventAssertion.assertSuccess(reqActionEmailEvent) + .details(Details.EMAIL, "registerUserSuccessWithEmailVerification@email".toLowerCase()) + .userId(userId) + .type(EventType.VERIFY_EMAIL); + + assertUserRegistered(userId, "registerUserSuccessWithEmailVerification", "registerUserSuccessWithEmailVerification@email"); + + String code = oauth.parseLoginResponse().getCode(); + assertNotNull(code); + } finally { + disableAlwaysSetPasswordOnRegisterForm(authConfigId); + if (userId != null) { + realm.admin().users().delete(userId).close(); + } + } + } + + private void updatePasswordOnChangePasswordPage(String userId) { + changePasswordPage.assertCurrent(); + String password = generatePassword(); + changePasswordPage.changePassword(password, password); + + EventRepresentation event = events.poll(); + EventAssertion.assertSuccess(event) + .details(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE) + .userId(userId) + .type(EventType.UPDATE_PASSWORD); + event = events.poll(); + EventAssertion.assertSuccess(event) + .details(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE) + .userId(userId) + .type(EventType.UPDATE_CREDENTIAL); + } + + private UserRepresentation assertUserRegistered(String userId, String username, String email) { + EventRepresentation loginEvent = events.poll(); + EventAssertion.assertSuccess(loginEvent) + .details("username", username.toLowerCase()) + .type(EventType.LOGIN); + + UserRepresentation user = getUser(userId); + Assert.assertNotNull(user); + Assert.assertNotNull(user.getCreatedTimestamp()); + // test that timestamp is current with 10s tollerance + assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 10000); + assertUserBasicRegisterAttributes(userId, username, email, "firstName", "lastName"); + return user; + } + + private UserRepresentation getUser(String userId) { + return realm.admin().users().get(userId).toRepresentation(); + } + + private void assertUserBasicRegisterAttributes(String userId, String username, String email, String firstName, String lastName) { + UserRepresentation user = getUser(userId); + assertThat(user, notNullValue()); + + if (username != null) { + assertThat(username, Matchers.equalToIgnoringCase(user.getUsername())); + } + assertThat(email.toLowerCase(), is(user.getEmail())); + assertThat(firstName, is(user.getFirstName())); + assertThat(lastName, is(user.getLastName())); + } + + private String enableAlwaysSetPasswordOnRegisterForm() { + AuthenticatorConfigRepresentation cfg = new AuthenticatorConfigRepresentation(); + cfg.setAlias("reg-password"); + Map cfgMap = Map.of(ALWAYS_SET_PASSWORD_ON_REGISTER_FORM, "true"); + cfg.setConfig(cfgMap); + + AuthenticationManagementResource authMgmtResource = realm.admin().flows(); + AuthenticationExecutionInfoRepresentation authExecution = findExecutionByProvider(RegistrationPassword.PROVIDER_ID, authMgmtResource.getExecutions(DefaultAuthenticationFlows.REGISTRATION_FLOW)); + Response resp = authMgmtResource.newExecutionConfig(authExecution.getId(), cfg); + resp.close(); + return ApiUtil.getCreatedId(resp); + } + + private void disableAlwaysSetPasswordOnRegisterForm(String configId) { + AuthenticationManagementResource authMgmtResource = realm.admin().flows(); + AuthenticatorConfigRepresentation cfg = authMgmtResource.getAuthenticatorConfig(configId); + cfg.getConfig().put(ALWAYS_SET_PASSWORD_ON_REGISTER_FORM, "false"); + authMgmtResource.updateAuthenticatorConfig(configId, cfg); + } + + public static class RegisterTestRealmConfig implements RealmConfig { + + @Override + public RealmConfigBuilder configure(RealmConfigBuilder realm) { + realm.registrationAllowed(true); + return realm; + } + } + +} diff --git a/tests/base/src/test/java/org/keycloak/tests/utils/PasswordGenerateUtil.java b/tests/base/src/test/java/org/keycloak/tests/utils/PasswordGenerateUtil.java new file mode 100644 index 00000000000..142300cb89e --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/utils/PasswordGenerateUtil.java @@ -0,0 +1,14 @@ +package org.keycloak.tests.utils; + +import org.keycloak.common.util.SecretGenerator; + +public class PasswordGenerateUtil { + + public static String generatePassword() { + return generatePassword(64); + } + + public static String generatePassword(int length) { + return SecretGenerator.getInstance().randomString(length); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/RegisterPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/RegisterPage.java index 8f4b71761c1..569b1ce5a8f 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/RegisterPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/RegisterPage.java @@ -85,6 +85,11 @@ public class RegisterPage extends LanguageComboboxAwarePage register(firstName, lastName, email, username, password, password, null, null, null); } + // Register user with the registration-page expected to NOT have "password" and "password-confirmation" fields + public void registerWithoutPassword(String firstName, String lastName, String email, String username) { + register(firstName, lastName, email, username, null, null, null, null, null); + } + public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm) { register(firstName, lastName, email, username, password, passwordConfirm, null, null, null); } @@ -120,14 +125,20 @@ public class RegisterPage extends LanguageComboboxAwarePage usernameInput.sendKeys(username); } - passwordInput.clear(); - if (password != null) { - passwordInput.sendKeys(password); + if (!isPasswordPresent() && password != null) { + Assert.fail("Password expected to be filled, but password field not present on the registration page"); } - passwordConfirmInput.clear(); - if (passwordConfirm != null) { - passwordConfirmInput.sendKeys(passwordConfirm); + if (isPasswordPresent()) { + passwordInput.clear(); + if (password != null) { + passwordInput.sendKeys(password); + } + + passwordConfirmInput.clear(); + if (passwordConfirm != null) { + passwordConfirmInput.sendKeys(passwordConfirm); + } } if(isDepartmentPresent()) { @@ -271,6 +282,14 @@ public class RegisterPage extends LanguageComboboxAwarePage } } + public boolean isPasswordPresent() { + try { + return driver.findElement(By.name("password")).isDisplayed(); + } catch (NoSuchElementException nse) { + return false; + } + } + public boolean isCurrent() { return isCurrent("Register"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java index 6e8072b35c1..71c495faa7c 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java @@ -36,6 +36,7 @@ import org.keycloak.models.Constants; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -52,6 +53,7 @@ import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.InfoPage; import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; import org.keycloak.testsuite.pages.ProceedPage; import org.keycloak.testsuite.pages.RegisterPage; import org.keycloak.testsuite.pages.VerifyEmailPage; @@ -117,6 +119,9 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo @Page protected RegisterPage registerPage; + @Page + protected LoginPasswordUpdatePage updatePasswordPage; + @Page protected InfoPage infoPage; @@ -132,6 +137,14 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo @SecondBrowser protected WebDriver driver2; + @Page + @SecondBrowser + protected LoginPasswordUpdatePage updatePasswordPageSecondBrowser; + + @Page + @SecondBrowser + protected InfoPage infoPageSecondBrowser; + @Override public void configureTestRealm(RealmRepresentation testRealm) { testRealm.setVerifyEmail(Boolean.TRUE); @@ -220,7 +233,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo public void verifyEmailRegister() throws IOException { oauth.openLoginForm(); loginPage.clickRegister(); - registerPage.register("firstName", "lastName", "email@mail.com", "verifyEmail", "password", "password"); + registerPage.registerWithoutPassword("firstName", "lastName", "email@mail.com", "verifyEmail"); String userId = events.expectRegister("verifyEmail", "email@mail.com").assertEvent().getUserId(); @@ -237,8 +250,6 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo driver.navigate().to(verificationUrl.trim()); - Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectRequiredAction(EventType.VERIFY_EMAIL) .user(userId) .detail(Details.USERNAME, "verifyemail") @@ -246,9 +257,69 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo .detail(Details.CODE_ID, mailCodeId) .assertEvent(); + updatePasswordOnChangePasswordPage(updatePasswordPage, userId); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + events.expectLogin().user(userId).session(mailCodeId).detail(Details.USERNAME, "verifyemail").assertEvent(); } + private void updatePasswordOnChangePasswordPage(LoginPasswordUpdatePage updatePasswordPage, String userId) { + updatePasswordPage.assertCurrent(); + updatePasswordPage.changePassword("password", "password"); + events.expectRequiredAction(EventType.UPDATE_PASSWORD) + .detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE) + .user(userId) + .removeDetail(Details.REDIRECT_URI) + .assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE) + .user(userId) + .removeDetail(Details.REDIRECT_URI) + .assertEvent(); + } + + @Test + public void verifyEmailRegisterWithSecondBrowser() throws IOException { + oauth.openLoginForm(); + loginPage.clickRegister(); + registerPage.registerWithoutPassword("firstName", "lastName", "email-br2@mail.com", "verifyemail-br2"); + + String userId = events.expectRegister("verifyemail-br2", "email-br2@mail.com").assertEvent().getUserId(); + verifyEmailPage.assertCurrent(); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + MimeMessage message = greenMail.getReceivedMessages()[0]; + events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).user(userId).detail(Details.USERNAME, "verifyemail-br2").detail("email", "email-br2@mail.com").assertEvent(); + + // open link in the second browser without the session and follow the link + String verificationUrl = getEmailLink(message); + driver2.navigate().to(verificationUrl.trim()); + final WebElement proceedLink = driver2.findElement(By.linkText("» Click here to proceed")); + assertThat(proceedLink, Matchers.notNullValue()); + + // check if the initial client is preserved + String link = proceedLink.getAttribute("href"); + assertThat(link, Matchers.containsString("client_id=test-app")); + proceedLink.click(); + events.clear(); + + // should update password in the second browser + updatePasswordPageSecondBrowser.setDriver(driver2); + updatePasswordOnChangePasswordPage(updatePasswordPageSecondBrowser, userId); + infoPageSecondBrowser.setDriver(driver2); + infoPageSecondBrowser.assertCurrent(); + Assert.assertEquals("Your account has been updated.", infoPageSecondBrowser.getInfo()); + + // Refresh in the original browser. Authentication session is expired and user needs to login from the beginning + infoPage.setDriver(driver); + driver.navigate().refresh(); + loginPage.assertCurrent(); + Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError()); + loginPage.login("verifyemail-br2", "password"); + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + } + @Test public void verifyEmailRegisterSetLocale() throws IOException { RealmRepresentation realm = testRealm().toRepresentation(); @@ -258,7 +329,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo oauth.openLoginForm(); loginPage.clickRegister(); loginPage.openLanguage("Português"); - registerPage.register("firstName", "lastName", "locale@mail.com", "locale", "password", "password"); + registerPage.registerWithoutPassword("firstName", "lastName", "locale@mail.com", "locale"); Assert.assertEquals(1, greenMail.getReceivedMessages().length); MimeMessage message = greenMail.getReceivedMessages()[0]; @@ -274,7 +345,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo oauth.openLoginForm(); loginPage.clickRegister(); String username1 = KeycloakModelUtils.generateId(); - registerPage.register("firstName", "lastName", username1 + "@mail.com", username1, "password", "password"); + registerPage.registerWithoutPassword("firstName", "lastName", username1 + "@mail.com", username1); verifyEmailPage.assertCurrent(); Assert.assertEquals(1, greenMail.getReceivedMessages().length); MimeMessage message = greenMail.getReceivedMessages()[0]; @@ -283,12 +354,14 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo oauth.openLoginForm(); loginPage.clickRegister(); String username2 = KeycloakModelUtils.generateId(); - registerPage.register("firstName", "lastName", username2 + "@mail.com", username2, "password", "password"); + registerPage.registerWithoutPassword("firstName", "lastName", username2 + "@mail.com", username2); verifyEmailPage.assertCurrent(); Assert.assertEquals(2, greenMail.getReceivedMessages().length); message = greenMail.getReceivedMessages()[1]; String verificationLink2 = getEmailLink(message); driver.navigate().to(verificationLink2.trim()); + updatePasswordPage.assertCurrent(); + updatePasswordPage.changePassword("password", "password"); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); driver.navigate().to(verificationLink1.trim()); assertTrue(errorPage.getError().contains("You are already authenticated as different user")); @@ -303,7 +376,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo oauth.openLoginForm(); loginPage.clickRegister(); String username1 = KeycloakModelUtils.generateId(); - registerPage.register("firstName", "lastName", username1 + "@mail.com", username1, "password", "password"); + registerPage.registerWithoutPassword("firstName", "lastName", username1 + "@mail.com", username1); verifyEmailPage.assertCurrent(); Assert.assertEquals(1, greenMail.getReceivedMessages().length); MimeMessage message = greenMail.getReceivedMessages()[0]; @@ -312,13 +385,16 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo oauth.openLoginForm(); loginPage.clickRegister(); String username2 = KeycloakModelUtils.generateId(); - registerPage.register("firstName", "lastName", username2 + "@mail.com", username2, "password", "password"); + registerPage.registerWithoutPassword("firstName", "lastName", username2 + "@mail.com", username2); verifyEmailPage.assertCurrent(); Assert.assertEquals(2, greenMail.getReceivedMessages().length); message = greenMail.getReceivedMessages()[1]; String verificationLink2 = getEmailLink(message); driver.navigate().to(verificationLink1.trim()); + updatePasswordPage.assertCurrent(); + updatePasswordPage.changePassword("password", "password"); + driver.navigate().to(verificationLink2.trim()); assertTrue(errorPage.getError().contains("You are already authenticated as different user")); } @@ -967,8 +1043,9 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo assertThat(driver2.getPageSource(), Matchers.containsString("kc-info-message")); assertThat(driver2.getPageSource(), Matchers.containsString("Your email address has been verified.")); - // Browser 1: Expect land back to app after refresh + // Browser 1: Expect that it needs to authenticate from scratch after browser refresh driver.navigate().refresh(); + testAppHelper.login("test-user@localhost", "password"); appPage.assertCurrent(); } } @@ -1102,7 +1179,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo driver.navigate().to(oauth.registrationForm().build()); registerPage.assertCurrent(); - registerPage.register(COMMON_ATTR, COMMON_ATTR, COMMON_ATTR + "@" + COMMON_ATTR, COMMON_ATTR, COMMON_ATTR, COMMON_ATTR); + registerPage.registerWithoutPassword(COMMON_ATTR, COMMON_ATTR, COMMON_ATTR + "@" + COMMON_ATTR, COMMON_ATTR); verifyEmailPage.assertCurrent(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionPriorityTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionPriorityTest.java index 37c7a002d97..cd024a0f12d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionPriorityTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionPriorityTest.java @@ -139,13 +139,7 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest { events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).removeDetail(Details.REDIRECT_URI) .detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID).assertEvent(); - // Second, change password - changePasswordPage.assertCurrent(); - changePasswordPage.changePassword(NEW_PASSWORD, NEW_PASSWORD); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); - events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); - - // Finally, update profile + // Second, update profile updateProfilePage.assertCurrent(); updateProfilePage.prepareUpdate().firstName(NEW_FIRST_NAME).lastName(NEW_LAST_NAME) .email(NEW_EMAIL).submit(); @@ -155,6 +149,12 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest { .detail(Details.UPDATED_EMAIL, NEW_EMAIL) .assertEvent(); + // Finally, change password + changePasswordPage.assertCurrent(); + changePasswordPage.changePassword(NEW_PASSWORD, NEW_PASSWORD); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); + // Logged in appPage.assertCurrent(); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionFailoverClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionFailoverClusterTest.java index 88b73c3ef88..046a7086b02 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionFailoverClusterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionFailoverClusterTest.java @@ -67,9 +67,9 @@ public class AuthenticationSessionFailoverClusterTest extends AbstractFailoverCl String cookieValue1 = getAuthSessionCookieValue(driver); - // Login and assert on "updatePassword" page + // Login and assert on "updateProfile" page loginPage.login("login-test", "password"); - updatePasswordPage.assertCurrent(); + updateProfilePage.assertCurrent(); // Route didn't change Assert.assertEquals(cookieValue1, getAuthSessionCookieValue(driver)); @@ -83,11 +83,11 @@ public class AuthenticationSessionFailoverClusterTest extends AbstractFailoverCl logFailoverSetup(); // Trigger the action now - updatePasswordPage.changePassword("password", "password"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3").email("john@doe3.com").submit(); if (expectSuccessfulFailover) { //Action was successful - updateProfilePage.assertCurrent(); + updatePasswordPage.assertCurrent(); String cookieValue2 = getAuthSessionCookieValue(driver); @@ -104,14 +104,14 @@ public class AuthenticationSessionFailoverClusterTest extends AbstractFailoverCl Assert.assertNotNull(error); loginPage.login("login-test", "password"); - updatePasswordPage.changePassword("password", "password"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3").email("john@doe3.com").submit(); } - updateProfilePage.assertCurrent(); + updatePasswordPage.assertCurrent(); - // Successfully update profile and assert user logged - updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3").email("john@doe3.com").submit(); + // Successfully update password and assert user logged + updatePasswordPage.changePassword("password", "password"); appPage.assertCurrent(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java index 988dc893d92..a2b82fa4759 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java @@ -401,7 +401,7 @@ public class UserStorageTest extends AbstractAuthTest { oauth.openLoginForm(); loginPage.clickRegister(); - registerPage.register("firstName", "lastName", "email@mail.com", "verifyEmail", "password", "password"); + registerPage.registerWithoutPassword("firstName", "lastName", "email@mail.com", "verifyEmail"); verifyEmailPage.assertCurrent(); @@ -413,6 +413,9 @@ public class UserStorageTest extends AbstractAuthTest { driver.navigate().to(verificationUrl.trim()); + // Update password after email verified + updatePasswordPage.updatePasswords("password", "password"); + appPage.assertCurrent(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java index a798b8d17a8..6de7c3c24cc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java @@ -147,25 +147,33 @@ public class BrowserButtonsTest extends AbstractChangeImportedUserPasswordsTest // Login and assert on "updatePassword" page loginPage.login("login-test", getPassword("login-test")); - updatePasswordPage.assertCurrent(); - - // Update password and assert on "updateProfile" page - updatePasswordPage.changePassword(getPassword("login-test"), getPassword("login-test")); updateProfilePage.assertCurrent(); + // Update profile and assert on "updatePassword" page + updateProfile(); + updatePasswordPage.assertCurrent(); + // Click browser back. Assert on "Page expired" page UIUtils.navigateBackWithRefresh(driver, loginExpiredPage); // Click browser forward. Assert on "updateProfile" page again driver.navigate().forward(); - updateProfilePage.assertCurrent(); + updatePasswordPage.assertCurrent(); - // Successfully update profile and assert user logged - updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3").email("john@doe3.com").submit(); + // Successfully update password and assert user logged + updatePassword(); appPage.assertCurrent(); } + private void updateProfile() { + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3").email("john@doe3.com").submit(); + } + + private void updatePassword() { + updatePasswordPage.changePassword(getPassword("login-test"), getPassword("login-test")); + } + // KEYCLOAK-4670 - Flow 3 extended @Test @@ -174,16 +182,16 @@ public class BrowserButtonsTest extends AbstractChangeImportedUserPasswordsTest // Login and assert on "updatePassword" page loginPage.login("login-test", getPassword("login-test")); - updatePasswordPage.assertCurrent(); + updateProfilePage.assertCurrent(); // Click browser refresh. Assert still on updatePassword page driver.navigate().refresh(); - updatePasswordPage.assertCurrent(); - - // Update password and assert on "updateProfile" page - updatePasswordPage.changePassword(getPassword("login-test"), getPassword("login-test")); updateProfilePage.assertCurrent(); + // Update profile and assert on "updatePassword" page + updateProfile(); + updatePasswordPage.assertCurrent(); + // Click browser back. Assert on "Page expired" page UIUtils.navigateBackWithRefresh(driver, loginExpiredPage); @@ -195,19 +203,19 @@ public class BrowserButtonsTest extends AbstractChangeImportedUserPasswordsTest loginExpiredPage.clickLoginRestartLink(); loginPage.assertCurrent(); - // Login again and assert on "updateProfile" page + // Login again and assert on "updatePassword" page loginPage.login("login-test", getPassword("login-test")); - updateProfilePage.assertCurrent(); + updatePasswordPage.assertCurrent(); // Click browser back. Assert on "Page expired" page UIUtils.navigateBackWithRefresh(driver, loginExpiredPage); - // Click "login continue" and assert on updateProfile page + // Click "login continue" and assert on updatePassword page loginExpiredPage.clickLoginContinueLink(); - updateProfilePage.assertCurrent(); + updatePasswordPage.assertCurrent(); - // Successfully update profile and assert user logged - updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3").email("john@doe3.com").submit(); + // Successfully update password and assert user logged + updatePassword(); appPage.assertCurrent(); } @@ -220,8 +228,8 @@ public class BrowserButtonsTest extends AbstractChangeImportedUserPasswordsTest // Login and go through required actions oauth.openLoginForm(); loginPage.login("login-test", getPassword("login-test")); - updatePasswordPage.changePassword(getPassword("login-test"), getPassword("login-test")); - updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3").email("john@doe3.com").submit(); + updateProfile(); + updatePassword(); // Assert on consent screen grantPage.assertCurrent(); @@ -300,7 +308,7 @@ public class BrowserButtonsTest extends AbstractChangeImportedUserPasswordsTest // Login and assert on "updatePassword" page loginPage.login("login-test", getPassword("login-test")); - updatePasswordPage.assertCurrent(); + updateProfilePage.assertCurrent(); // Click browser back. I should be on login page . URL corresponds to OIDC AuthorizationEndpoint driver.navigate().back(); @@ -329,21 +337,21 @@ public class BrowserButtonsTest extends AbstractChangeImportedUserPasswordsTest driver.navigate().to(changePasswordUrl.trim()); - updatePasswordPage.assertCurrent(); + updateProfilePage.assertCurrent(); // Click browser back. Should be on loginPage for "forked flow" driver.navigate().back(); loginPage.assertCurrent(); - // When clicking browser forward, back on updatePasswordPage + // When clicking browser forward, back on updateProfilePage driver.navigate().forward(); - updatePasswordPage.assertCurrent(); + updateProfilePage.assertCurrent(); - // Click browser back. And continue login. Should be on updatePasswordPage + // Click browser back. And continue login. Should be on updateProfilePage driver.navigate().back(); loginPage.assertCurrent(); loginPage.login("login-test", getPassword("login-test")); - updatePasswordPage.assertCurrent(); + updateProfilePage.assertCurrent(); } @@ -360,14 +368,14 @@ public class BrowserButtonsTest extends AbstractChangeImportedUserPasswordsTest // Login loginPage.login("login-test", getPassword("login-test")); - updatePasswordPage.assertCurrent(); + updateProfilePage.assertCurrent(); // Click browser back. Should be on 'page expired' UIUtils.navigateBackWithRefresh(driver, loginExpiredPage); // Click 'continue' should be on updatePasswordPage loginExpiredPage.clickLoginContinueLink(); - updatePasswordPage.assertCurrent(); + updateProfilePage.assertCurrent(); // Click browser back. Should be on 'page expired' driver.navigate().back(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java index 378ec3ac93d..9882f02b03d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java @@ -174,7 +174,7 @@ public class MultipleTabsLoginTest extends AbstractChangeImportedUserPasswordsTe loginPage.assertCurrent(); loginPage.login("login-test", getPassword("login-test")); - updatePasswordPage.assertCurrent(); + updateProfilePage.assertCurrent(); // Simulate login in different browser tab tab2. I will be on loginPage again. tabUtil.newTab(oauth.loginForm().build()); @@ -285,10 +285,18 @@ public class MultipleTabsLoginTest extends AbstractChangeImportedUserPasswordsTe private void loginSuccessAndDoRequiredActions() { loginPage.login("login-test", getPassword("login-test")); - updatePasswordPage.changePassword(getPassword("login-test"), getPassword("login-test")); + updateProfile(); + updatePassword(); + appPage.assertCurrent(); + } + + private void updateProfile() { updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") .email("john@doe3.com").submit(); - appPage.assertCurrent(); + } + + private void updatePassword() { + updatePasswordPage.changePassword(getPassword("login-test"), getPassword("login-test")); } // Assert browser was redirected to the appPage with "error=temporarily_unavailable" and error_description corresponding to Constants.AUTHENTICATION_EXPIRED_MESSAGE @@ -340,7 +348,7 @@ public class MultipleTabsLoginTest extends AbstractChangeImportedUserPasswordsTe oauth.openLoginForm(); loginPage.assertCurrent(); loginPage.login("login-test", getPassword("login-test")); - updatePasswordPage.assertCurrent(); + updateProfilePage.assertCurrent(); getLogger().info("URL in tab1: " + driver.getCurrentUrl()); // Open new tab 2 @@ -366,7 +374,7 @@ public class MultipleTabsLoginTest extends AbstractChangeImportedUserPasswordsTe tabUtil.closeTab(1); assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1)); - waitForAppPage(() -> updatePasswordPage.changePassword(getPassword("login-test"), getPassword("login-test"))); + waitForAppPage(() -> updateProfile()); assertOnAppPageWithAlreadyLoggedInError(EventType.CUSTOM_REQUIRED_ACTION); } } @@ -459,7 +467,7 @@ public class MultipleTabsLoginTest extends AbstractChangeImportedUserPasswordsTe // continue with the login in the first tab util.switchToTab(originalTab); loginPage.login("login-test", getPassword("login-test")); - updatePasswordPage.assertCurrent(); + updateProfilePage.assertCurrent(); } } @@ -522,7 +530,7 @@ public class MultipleTabsLoginTest extends AbstractChangeImportedUserPasswordsTe // Authenticate in tab2 loginPage.login("login-test", getPassword("login-test")); - updatePasswordPage.assertCurrent(); + updateProfilePage.assertCurrent(); // Simulate going back to tab1 and confirm login form. Page "Page expired" should be shown (NOTE: WebDriver does it with GET, when real browser would do it with POST. Improve test if needed...) driver.navigate().to(actionUrl1); @@ -530,11 +538,9 @@ public class MultipleTabsLoginTest extends AbstractChangeImportedUserPasswordsTe // Finish login loginExpiredPage.clickLoginContinueLink(); - updatePasswordPage.assertCurrent(); - - updatePasswordPage.changePassword(getPassword("login-test"), getPassword("login-test")); - updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") - .email("john@doe3.com").submit(); + updateProfilePage.assertCurrent(); + updateProfile(); + updatePassword(); appPage.assertCurrent(); } @@ -560,7 +566,7 @@ public class MultipleTabsLoginTest extends AbstractChangeImportedUserPasswordsTe loginPage.assertCurrent(); loginPage.login("login-test", getPassword("login-test")); - updatePasswordPage.assertCurrent(); + updateProfilePage.assertCurrent(); // Manually remove execution from the URL and try to simulate the request just with "code" parameter String actionUrl = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource()); @@ -568,12 +574,10 @@ public class MultipleTabsLoginTest extends AbstractChangeImportedUserPasswordsTe driver.navigate().to(actionUrl); - // Back on updatePasswordPage now - updatePasswordPage.assertCurrent(); - - updatePasswordPage.changePassword(getPassword("login-test"), getPassword("login-test")); - updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") - .email("john@doe3.com").submit(); + // Back on updateProfilePage now + updateProfilePage.assertCurrent(); + updateProfile(); + updatePassword(); appPage.assertCurrent(); } @@ -686,7 +690,7 @@ public class MultipleTabsLoginTest extends AbstractChangeImportedUserPasswordsTe loginPage.assertCurrent(); loginPage.login("login-test", getPassword("login-test")); - updatePasswordPage.assertCurrent(); + updateProfilePage.assertCurrent(); String tab1Url = driver.getCurrentUrl(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java index bf49c617325..350239528e4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java @@ -21,7 +21,6 @@ import java.util.List; import java.util.Map; import java.util.UUID; -import jakarta.mail.internet.MimeMessage; import jakarta.ws.rs.core.Response; import org.keycloak.authentication.AuthenticationFlow; @@ -34,7 +33,6 @@ import org.keycloak.authentication.requiredactions.TermsAndConditions; import org.keycloak.common.util.Time; import org.keycloak.events.Details; import org.keycloak.events.Errors; -import org.keycloak.events.EventType; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.DefaultAuthenticationFlows; @@ -51,12 +49,9 @@ import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPasswordResetPage; import org.keycloak.testsuite.pages.RegisterPage; -import org.keycloak.testsuite.pages.VerifyEmailPage; import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.util.AccountHelper; import org.keycloak.testsuite.util.FlowUtil; -import org.keycloak.testsuite.util.GreenMailRule; -import org.keycloak.testsuite.util.MailUtils; import org.keycloak.testsuite.util.UIUtils; import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.oauth.AccessTokenResponse; @@ -96,15 +91,9 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest { @Page protected RegisterPage registerPage; - @Page - protected VerifyEmailPage verifyEmailPage; - @Page protected LoginPasswordResetPage resetPasswordPage; - @Rule - public GreenMailRule greenMail = new GreenMailRule(); - private String idTokenHint; @Override @@ -477,103 +466,6 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest { return user; } - @Test - // GreenMailRule is not working atm - public void registerUserSuccessWithEmailVerification() throws Exception { - try (RealmAttributeUpdater rau = setVerifyEmail(true).update()) { - oauth.openLoginForm(); - loginPage.clickRegister(); - registerPage.assertCurrent(); - - registerPage.register("firstName", "lastName", "registerUserSuccessWithEmailVerification@email", "registerUserSuccessWithEmailVerification", generatePassword()); - verifyEmailPage.assertCurrent(); - - String userId = events.expectRegister("registerUserSuccessWithEmailVerification", "registerUserSuccessWithEmailVerification@email").assertEvent().getUserId(); - - { - assertTrue("Expecting verify email", greenMail.waitForIncomingEmail(1000, 1)); - - events.expect(EventType.SEND_VERIFY_EMAIL) - .detail(Details.EMAIL, "registerUserSuccessWithEmailVerification@email".toLowerCase()) - .user(userId) - .assertEvent(); - - MimeMessage message = greenMail.getLastReceivedMessage(); - String link = MailUtils.getPasswordResetEmailLink(message); - - driver.navigate().to(link); - } - - events.expectRequiredAction(EventType.VERIFY_EMAIL) - .detail(Details.EMAIL, "registerUserSuccessWithEmailVerification@email".toLowerCase()) - .user(userId) - .assertEvent(); - - assertUserRegistered(userId, "registerUserSuccessWithEmailVerification", "registerUserSuccessWithEmailVerification@email"); - - appPage.assertCurrent(); - assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - - // test that timestamp is current with 10s tollerance - // test user info is set from form - } - } - - @Test - // GreenMailRule is not working atm - public void registerUserSuccessWithEmailVerificationWithResend() throws Exception { - try (RealmAttributeUpdater rau = setVerifyEmail(true).update()) { - oauth.openLoginForm(); - loginPage.clickRegister(); - registerPage.assertCurrent(); - - registerPage.register("firstName", "lastName", "registerUserSuccessWithEmailVerificationWithResend@email", "registerUserSuccessWithEmailVerificationWithResend", generatePassword()); - verifyEmailPage.assertCurrent(); - - String userId = events.expectRegister("registerUserSuccessWithEmailVerificationWithResend", "registerUserSuccessWithEmailVerificationWithResend@email").assertEvent().getUserId(); - - { - assertTrue("Expecting verify email", greenMail.waitForIncomingEmail(1000, 1)); - - events.expect(EventType.SEND_VERIFY_EMAIL) - .detail(Details.EMAIL, "registerUserSuccessWithEmailVerificationWithResend@email".toLowerCase()) - .user(userId) - .assertEvent(); - - setTimeOffset(40); - verifyEmailPage.clickResendEmail(); - verifyEmailPage.assertCurrent(); - - assertTrue("Expecting second verify email", greenMail.waitForIncomingEmail(1000, 1)); - - events.expect(EventType.SEND_VERIFY_EMAIL) - .detail(Details.EMAIL, "registerUserSuccessWithEmailVerificationWithResend@email".toLowerCase()) - .user(userId) - .assertEvent(); - - MimeMessage message = greenMail.getLastReceivedMessage(); - String link = MailUtils.getPasswordResetEmailLink(message); - - driver.navigate().to(link); - } - - events.expectRequiredAction(EventType.VERIFY_EMAIL) - .detail(Details.EMAIL, "registerUserSuccessWithEmailVerificationWithResend@email".toLowerCase()) - .user(userId) - .assertEvent(); - - assertUserRegistered(userId, "registerUserSuccessWithEmailVerificationWithResend", "registerUserSuccessWithEmailVerificationWithResend@email"); - - appPage.assertCurrent(); - assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - - // test that timestamp is current with 10s tollerance - // test user info is set from form - } finally { - setTimeOffset(0); - } - } - @Test public void registerUserUmlats() { oauth.openLoginForm(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java index 580f0051caf..84526c7b23e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java @@ -1004,23 +1004,14 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { log.info("Taking required actions from realm: " + realm.toRepresentation().getRealm()); List actions = realm.flows().getRequiredActions(); - // Checking the priority - int priority = 10; - for (RequiredActionProviderRepresentation action : actions) { - if (action.getAlias().equals("update_user_locale")) { - assertEquals(1000, action.getPriority()); - } else if (action.getAlias().equals("delete_credential")) { - assertEquals(110, action.getPriority()); - } else if (action.getAlias().equals("idp_link")) { - assertEquals(120, action.getPriority()); - } else if (action.getAlias().equals("verifiable_credential_offer")) { - assertEquals(130, action.getPriority()); - } else { - assertEquals(priority, action.getPriority()); - } - - priority += 10; - } + // Checking the priority. Assert that specified required actions are in expected order + List expectedReqActionAliases = Arrays.stream(new String[] { "TERMS_AND_CONDITIONS", "UPDATE_PROFILE", "VERIFY_EMAIL", "CONFIGURE_TOTP", "UPDATE_PASSWORD", + "delete_credential", "idp_link", "update_user_locale" }).toList(); + List reqActionsAliases = actions.stream() + .map(RequiredActionProviderRepresentation::getAlias) + .filter(expectedReqActionAliases::contains) + .toList(); + assertEquals(reqActionsAliases, expectedReqActionAliases); } }