Update password after email verification during registration of users (#47538)

closes #45568

Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
Marek Posolda
2026-04-17 15:15:48 +02:00
committed by GitHub
parent 08432969a4
commit 72e0c26a35
25 changed files with 828 additions and 233 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -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}].
@@ -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.
@@ -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]
@@ -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;
}
}
@@ -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;
@@ -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);
}
}
@@ -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<Ve
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);
}
@@ -34,6 +34,7 @@ import org.keycloak.models.AuthenticationExecutionModel;
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;
@@ -41,6 +42,7 @@ 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;
@@ -51,6 +53,13 @@ import org.keycloak.services.validation.Validation;
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.";
@@ -58,11 +67,25 @@ public class RegistrationPassword implements FormAction, FormActionFactory {
@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");
@@ -90,11 +113,16 @@ public class RegistrationPassword implements FormAction, FormActionFactory {
@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);
}
@@ -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 = {
@@ -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;
@@ -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();
}
@@ -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";
@@ -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";
}
}
@@ -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));
@@ -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<String, String> 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<String, String> 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;
}
}
}
@@ -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);
}
}
@@ -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");
@@ -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();
@@ -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());
@@ -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();
}
@@ -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();
}
}
@@ -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();
@@ -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();
@@ -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();
@@ -1004,23 +1004,14 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
log.info("Taking required actions from realm: " + realm.toRepresentation().getRealm());
List<RequiredActionProviderRepresentation> 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<String> 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<String> reqActionsAliases = actions.stream()
.map(RequiredActionProviderRepresentation::getAlias)
.filter(expectedReqActionAliases::contains)
.toList();
assertEquals(reqActionsAliases, expectedReqActionAliases);
}
}