From 5d87cdf1c6d420ece4691495701f956773fc2c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9da=20Housni=20Alaoui?= Date: Mon, 9 May 2022 18:52:22 +0200 Subject: [PATCH] KEYCLOAK-6455 Ability to require email to be verified before changing (#7943) Closes #11875 --- .../java/org/keycloak/common/Profile.java | 8 +- .../java/org/keycloak/common/ProfileTest.java | 8 +- .../kerberos/KerberosFederationProvider.java | 4 + .../keycloak/email/EmailSenderProvider.java | 6 +- .../keycloak/email/EmailTemplateProvider.java | 2 + .../keycloak/forms/login/LoginFormsPages.java | 2 +- .../migration/migrators/MigrateTo18_0_0.java | 1 - .../models/utils/DefaultRequiredActions.java | 15 ++ .../userprofile/UserProfileContext.java | 3 +- .../java/org/keycloak/models/UserModel.java | 3 +- .../updateemail/UpdateEmailActionToken.java | 57 +++++ .../UpdateEmailActionTokenHandler.java | 95 +++++++++ .../SerializedBrokeredIdentityContext.java | 6 + .../requiredactions/UpdateEmail.java | 199 ++++++++++++++++++ .../util/UpdateProfileContext.java | 2 + .../util/UserUpdateProfileContext.java | 13 +- .../email/DefaultEmailSenderProvider.java | 6 +- .../FreeMarkerEmailTemplateProvider.java | 36 +++- .../FreeMarkerLoginFormsProvider.java | 48 +++-- .../forms/login/freemarker/Templates.java | 2 + .../login/freemarker/model/EmailBean.java | 35 +++ .../login/freemarker/model/ProfileBean.java | 4 + .../keycloak/services/messages/Messages.java | 2 + .../resources/account/AccountConsole.java | 69 +++--- .../AbstractUserProfileProvider.java | 23 +- ...cloak.authentication.RequiredActionFactory | 3 +- ...tion.actiontoken.ActionTokenHandlerFactory | 3 +- .../auth/page/login/UpdateEmailPage.java | 87 ++++++++ .../testsuite/pages/EmailUpdatePage.java | 72 +++++++ ...nUpdateProfileEditUsernameAllowedPage.java | 37 +++- .../pages/LoginUpdateProfilePage.java | 95 ++++++--- .../testsuite/pages/VerifyProfilePage.java | 8 +- ...ractAppInitiatedActionUpdateEmailTest.java | 130 ++++++++++++ ...AbstractRequiredActionUpdateEmailTest.java | 155 ++++++++++++++ .../AppInitiatedActionUpdateEmailTest.java | 49 +++++ ...ActionUpdateEmailWithVerificationTest.java | 138 ++++++++++++ .../AppInitiatedActionUpdateProfileTest.java | 22 +- .../RequiredActionMultipleActionsTest.java | 3 +- .../actions/RequiredActionPriorityTest.java | 14 +- .../RequiredActionUpdateEmailTest.java | 53 +++++ ...onUpdateEmailTestWithVerificationTest.java | 144 +++++++++++++ .../RequiredActionUpdateProfileTest.java | 38 ++-- ...ctionUpdateProfileWithUserProfileTest.java | 43 ++-- ...henticationSessionFailoverClusterTest.java | 13 +- .../kerberos/AbstractKerberosTest.java | 3 - .../kerberos/KerberosStandaloneTest.java | 7 +- .../testsuite/forms/BrowserButtonsTest.java | 14 +- .../forms/MultipleTabsLoginTest.java | 35 +-- .../testsuite/forms/VerifyProfileTest.java | 2 +- .../ui/account2/page/PersonalInfoPage.java | 29 ++- .../ui/account2/LDAPAccountTest.java | 26 +-- .../ui/account2/PersonalInfoTest.java | 19 +- .../ui/account2/UpdateEmailTest.java | 164 +++++++++++++++ .../email/messages/messages_fr.properties | 3 + .../login/messages/messages_cs.properties | 2 + .../login/messages/messages_da.properties | 4 +- .../login/messages/messages_de.properties | 2 + .../login/messages/messages_fi.properties | 4 +- .../login/messages/messages_fr.properties | 6 + .../login/messages/messages_hu.properties | 2 + .../login/messages/messages_it.properties | 2 + .../login/messages/messages_ja.properties | 4 +- .../login/messages/messages_pl.properties | 2 + .../login/messages/messages_pt_BR.properties | 1 + .../login/messages/messages_sk.properties | 2 + .../login/messages/messages_tr.properties | 2 + .../account/messages/messages_fr.properties | 2 + .../email/html/email-update-confirmation.ftl | 5 + .../email/messages/messages_en.properties | 3 + .../email/text/email-update-confirmation.ftl | 2 + .../theme/base/login/login-update-profile.ftl | 32 +-- .../login/messages/messages_en.properties | 7 + .../theme/base/login/update-email.ftl | 42 ++++ .../theme/keycloak.v2/account/index.ftl | 4 +- .../account/messages/messages_en.properties | 2 + .../app/content/account-page/AccountPage.tsx | 58 ++++- .../account/src/app/widgets/features.ts | 2 + .../keycloak/login/resources/css/login.css | 3 +- 78 files changed, 1976 insertions(+), 277 deletions(-) create mode 100644 services/src/main/java/org/keycloak/authentication/actiontoken/updateemail/UpdateEmailActionToken.java create mode 100644 services/src/main/java/org/keycloak/authentication/actiontoken/updateemail/UpdateEmailActionTokenHandler.java create mode 100644 services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java create mode 100644 services/src/main/java/org/keycloak/forms/login/freemarker/model/EmailBean.java create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/UpdateEmailPage.java create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/EmailUpdatePage.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractAppInitiatedActionUpdateEmailTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractRequiredActionUpdateEmailTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateEmailTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateEmailWithVerificationTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateEmailTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateEmailTestWithVerificationTest.java create mode 100644 testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/UpdateEmailTest.java create mode 100644 themes/src/main/resources/theme/base/email/html/email-update-confirmation.ftl create mode 100644 themes/src/main/resources/theme/base/email/text/email-update-confirmation.ftl create mode 100644 themes/src/main/resources/theme/base/login/update-email.ftl diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index e5bdf46c05..c8c540e7a0 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -17,17 +17,16 @@ package org.keycloak.common; +import static org.keycloak.common.Profile.Type.DEPRECATED; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.HashSet; import java.util.Properties; import java.util.Set; - import org.jboss.logging.Logger; -import static org.keycloak.common.Profile.Type.DEPRECATED; - /** * @author Bill Burke * @version $Revision: 1 $ @@ -163,7 +162,8 @@ public class Profile { DYNAMIC_SCOPES("Dynamic OAuth 2.0 scopes", Type.EXPERIMENTAL), CLIENT_SECRET_ROTATION("Client Secret Rotation", Type.PREVIEW), STEP_UP_AUTHENTICATION("Step-up Authentication", Type.DEFAULT), - RECOVERY_CODES("Recovery codes", Type.PREVIEW); + RECOVERY_CODES("Recovery codes", Type.PREVIEW), + UPDATE_EMAIL("Update Email Action", Type.PREVIEW); private final Type typeProject; diff --git a/common/src/test/java/org/keycloak/common/ProfileTest.java b/common/src/test/java/org/keycloak/common/ProfileTest.java index ee13d96dd9..ee8bfaeab5 100644 --- a/common/src/test/java/org/keycloak/common/ProfileTest.java +++ b/common/src/test/java/org/keycloak/common/ProfileTest.java @@ -24,8 +24,8 @@ public class ProfileTest { @Test public void checkDefaultsKeycloak() { Assert.assertEquals("community", Profile.getName()); - assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.ADMIN2, Profile.Feature.DOCKER, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION); - assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN2, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION); + assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.ADMIN2, Profile.Feature.DOCKER, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION, Feature.UPDATE_EMAIL); + assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN2, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION, Feature.UPDATE_EMAIL); } @Test @@ -36,8 +36,8 @@ public class ProfileTest { Profile.init(); Assert.assertEquals("product", Profile.getName()); - assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.ADMIN2, Profile.Feature.DOCKER, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION); - assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN2, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION); + assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.ADMIN2, Profile.Feature.DOCKER, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION, Feature.UPDATE_EMAIL); + assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN2, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION, Feature.UPDATE_EMAIL); System.setProperty("keycloak.profile", "community"); Version.NAME = backUpName; diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java index eb924c0fd3..6d510ae213 100755 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java @@ -18,6 +18,7 @@ package org.keycloak.federation.kerberos; import org.jboss.logging.Logger; +import org.keycloak.common.Profile; import org.keycloak.common.constants.KerberosConstants; import org.keycloak.credential.CredentialAuthentication; import org.keycloak.credential.CredentialInput; @@ -271,6 +272,9 @@ public class KerberosFederationProvider implements UserStorageProvider, user.setSingleAttribute(KERBEROS_PRINCIPAL, username + "@" + kerberosConfig.getKerberosRealm()); if (kerberosConfig.isUpdateProfileFirstLogin()) { + if (Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL)) { + user.addRequiredAction(UserModel.RequiredAction.UPDATE_EMAIL); + } user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); } diff --git a/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java b/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java index a12b028e95..5539ee8354 100755 --- a/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java @@ -27,5 +27,9 @@ import java.util.Map; */ public interface EmailSenderProvider extends Provider { - void send(Map config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException; + default void send(Map config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException { + send(config, user.getEmail(), subject, textBody, htmlBody); + } + + void send(Map config, String address, String subject, String textBody, String htmlBody) throws EmailException; } diff --git a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java index fa8784bd6c..e5ddbcccbd 100755 --- a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java @@ -77,6 +77,8 @@ public interface EmailTemplateProvider extends Provider { void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException; + void sendEmailUpdateConfirmation(String link, long expirationInMinutes, String address) throws EmailException; + /** * Send formatted email * diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java index d0ff6db193..b0225c8f81 100755 --- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java +++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java @@ -28,6 +28,6 @@ public enum LoginFormsPages { LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM, LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE, UPDATE_USER_PROFILE, IDP_REVIEW_USER_PROFILE, LOGIN_RECOVERY_AUTHN_CODES_INPUT, LOGIN_RECOVERY_AUTHN_CODES_CONFIG, - FRONTCHANNEL_LOGOUT, LOGOUT_CONFIRM; + FRONTCHANNEL_LOGOUT, LOGOUT_CONFIRM, UPDATE_EMAIL; } diff --git a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo18_0_0.java b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo18_0_0.java index 5f73dfdfa2..ec253443fe 100644 --- a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo18_0_0.java +++ b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo18_0_0.java @@ -22,7 +22,6 @@ import org.jboss.logging.Logger; import org.keycloak.common.Profile; import org.keycloak.migration.MigrationProvider; import org.keycloak.migration.ModelVersion; -import org.keycloak.models.ClientScopeModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.representations.idm.RealmRepresentation; 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 acfad2c940..972cab6b8a 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 @@ -98,6 +98,7 @@ public class DefaultRequiredActions { addUpdateLocaleAction(realm); addDeleteAccountAction(realm); + addUpdateEmailAction(realm); } public static void addDeleteAccountAction(RealmModel realm) { @@ -125,4 +126,18 @@ public class DefaultRequiredActions { realm.addRequiredActionProvider(updateUserLocale); } } + + public static void addUpdateEmailAction(RealmModel realm){ + if (realm.getRequiredActionProviderByAlias(UserModel.RequiredAction.UPDATE_EMAIL.name()) == null + && Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL)){ + RequiredActionProviderModel updateEmail = new RequiredActionProviderModel(); + updateEmail.setEnabled(true); + updateEmail.setAlias(UserModel.RequiredAction.UPDATE_EMAIL.name()); + updateEmail.setName("Update Email"); + updateEmail.setProviderId(UserModel.RequiredAction.UPDATE_EMAIL.name()); + updateEmail.setDefaultAction(false); + updateEmail.setPriority(70); + realm.addRequiredActionProvider(updateEmail); + } + } } diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileContext.java b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileContext.java index 90a2347d41..d3c37c3d0c 100644 --- a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileContext.java +++ b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileContext.java @@ -36,7 +36,8 @@ public enum UserProfileContext { ACCOUNT_OLD(true), IDP_REVIEW(false), REGISTRATION_PROFILE(false), - REGISTRATION_USER_CREATION(false); + REGISTRATION_USER_CREATION(false), + UPDATE_EMAIL(false); protected boolean resetEmailVerified; diff --git a/server-spi/src/main/java/org/keycloak/models/UserModel.java b/server-spi/src/main/java/org/keycloak/models/UserModel.java index 65b6cd60c5..bce3f41eff 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserModel.java @@ -305,7 +305,8 @@ public interface UserModel extends RoleMapperModel { CONFIGURE_RECOVERY_AUTHN_CODES, UPDATE_PASSWORD, TERMS_AND_CONDITIONS, - VERIFY_PROFILE + VERIFY_PROFILE, + UPDATE_EMAIL } /** diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/updateemail/UpdateEmailActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/updateemail/UpdateEmailActionToken.java new file mode 100644 index 0000000000..8824d64a80 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/updateemail/UpdateEmailActionToken.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.authentication.actiontoken.updateemail; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.keycloak.authentication.actiontoken.DefaultActionToken; + +public class UpdateEmailActionToken extends DefaultActionToken { + + public static final String TOKEN_TYPE = "update-email"; + + @JsonProperty("oldEmail") + private String oldEmail; + @JsonProperty("newEmail") + private String newEmail; + + public UpdateEmailActionToken(String userId, int absoluteExpirationInSecs, String oldEmail, String newEmail){ + super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null); + this.oldEmail = oldEmail; + this.newEmail = newEmail; + } + + private UpdateEmailActionToken(){ + + } + + public String getOldEmail() { + return oldEmail; + } + + public void setOldEmail(String oldEmail) { + this.oldEmail = oldEmail; + } + + public String getNewEmail() { + return newEmail; + } + + public void setNewEmail(String newEmail) { + this.newEmail = newEmail; + } +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/updateemail/UpdateEmailActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/updateemail/UpdateEmailActionTokenHandler.java new file mode 100644 index 0000000000..f4fdc3c76a --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/updateemail/UpdateEmailActionTokenHandler.java @@ -0,0 +1,95 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.authentication.actiontoken.updateemail; + +import java.util.List; +import java.util.Objects; +import javax.ws.rs.core.Response; +import org.keycloak.TokenVerifier; +import org.keycloak.authentication.actiontoken.AbstractActionTokenHandler; +import org.keycloak.authentication.actiontoken.ActionTokenContext; +import org.keycloak.authentication.actiontoken.TokenUtils; +import org.keycloak.authentication.requiredactions.UpdateEmail; +import org.keycloak.events.Errors; +import org.keycloak.events.EventType; +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.FormMessage; +import org.keycloak.services.messages.Messages; +import org.keycloak.services.validation.Validation; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.userprofile.UserProfile; +import org.keycloak.userprofile.ValidationException; + +public class UpdateEmailActionTokenHandler extends AbstractActionTokenHandler { + + public UpdateEmailActionTokenHandler() { + super(UpdateEmailActionToken.TOKEN_TYPE, UpdateEmailActionToken.class, Messages.STALE_VERIFY_EMAIL_LINK, + EventType.EXECUTE_ACTIONS, Errors.INVALID_TOKEN); + } + + @Override + public TokenVerifier.Predicate[] getVerifiers( + ActionTokenContext tokenContext) { + return TokenUtils.predicates(TokenUtils.checkThat( + t -> Objects.equals(t.getOldEmail(), tokenContext.getAuthenticationSession().getAuthenticatedUser().getEmail()), + Errors.INVALID_EMAIL, getDefaultErrorMessage())); + } + + @Override + public Response handleToken(UpdateEmailActionToken token, ActionTokenContext tokenContext) { + AuthenticationSessionModel authenticationSession = tokenContext.getAuthenticationSession(); + UserModel user = authenticationSession.getAuthenticatedUser(); + + KeycloakSession session = tokenContext.getSession(); + + LoginFormsProvider forms = session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authenticationSession) + .setUser(user); + + String newEmail = token.getNewEmail(); + + UserProfile emailUpdateValidationResult; + try { + emailUpdateValidationResult = UpdateEmail.validateEmailUpdate(session, user, newEmail); + } catch (ValidationException pve) { + List errors = Validation.getFormErrorsFromValidation(pve.getErrors()); + return forms.setErrors(errors).createErrorPage(Response.Status.BAD_REQUEST); + } + + UpdateEmail.updateEmailNow(tokenContext.getEvent(), user, emailUpdateValidationResult); + + tokenContext.getEvent().success(); + + // verify user email as we know it is valid as this entry point would never have gotten here. + user.setEmailVerified(true); + user.removeRequiredAction(UserModel.RequiredAction.UPDATE_EMAIL); + tokenContext.getAuthenticationSession().removeRequiredAction(UserModel.RequiredAction.UPDATE_EMAIL); + user.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL); + tokenContext.getAuthenticationSession().removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL); + + return forms.setAttribute("messageHeader", forms.getMessage("emailUpdatedTitle")).setSuccess("emailUpdated", newEmail) + .createInfoPage(); + } + + @Override + public boolean canUseTokenRepeatedly(UpdateEmailActionToken token, + ActionTokenContext tokenContext) { + return false; + } +} diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java index 1749bd7a95..e8c77941be 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java @@ -92,6 +92,12 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext { setSingleAttribute(UserModel.USERNAME, username); } + @JsonIgnore + @Override + public boolean isEditEmailAllowed() { + return true; + } + public String getModelUsername() { return getFirstAttribute(UserModel.USERNAME); } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java new file mode 100644 index 0000000000..560c49fbea --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java @@ -0,0 +1,199 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.authentication.requiredactions; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.UriInfo; +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.authentication.InitiatedActionSupport; +import org.keycloak.authentication.RequiredActionContext; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.authentication.actiontoken.updateemail.UpdateEmailActionToken; +import org.keycloak.common.Profile; +import org.keycloak.common.util.Time; +import org.keycloak.email.EmailException; +import org.keycloak.email.EmailTemplateProvider; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.forms.login.LoginFormsPages; +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.forms.login.freemarker.Templates; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.FormMessage; +import org.keycloak.provider.EnvironmentDependentProviderFactory; +import org.keycloak.services.Urls; +import org.keycloak.services.validation.Validation; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.userprofile.EventAuditingAttributeChangeListener; +import org.keycloak.userprofile.UserProfile; +import org.keycloak.userprofile.UserProfileContext; +import org.keycloak.userprofile.UserProfileProvider; +import org.keycloak.userprofile.ValidationException; + +public class UpdateEmail implements RequiredActionProvider, RequiredActionFactory, EnvironmentDependentProviderFactory { + + private static final Logger logger = Logger.getLogger(UpdateEmail.class); + + @Override + public InitiatedActionSupport initiatedActionSupport() { + return InitiatedActionSupport.SUPPORTED; + } + + @Override + public String getDisplayText() { + return "Update Email"; + } + + @Override + public void evaluateTriggers(RequiredActionContext context) { + + } + + @Override + public void requiredActionChallenge(RequiredActionContext context) { + context.challenge(context.form().createResponse(UserModel.RequiredAction.UPDATE_EMAIL)); + } + + @Override + public void processAction(RequiredActionContext context) { + MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); + String newEmail = formData.getFirst(UserModel.EMAIL); + + RealmModel realm = context.getRealm(); + UserModel user = context.getUser(); + UserProfile emailUpdateValidationResult; + try { + emailUpdateValidationResult = validateEmailUpdate(context.getSession(), user, newEmail); + } catch (ValidationException pve) { + List errors = Validation.getFormErrorsFromValidation(pve.getErrors()); + context.challenge(context.form().setErrors(errors).setFormData(formData) + .createResponse(UserModel.RequiredAction.UPDATE_EMAIL)); + return; + } + + if (!realm.isVerifyEmail() || Validation.isBlank(newEmail) + || Objects.equals(user.getEmail(), newEmail) && user.isEmailVerified()) { + updateEmailWithoutConfirmation(context, emailUpdateValidationResult); + return; + } + + sendEmailUpdateConfirmation(context); + } + + private void sendEmailUpdateConfirmation(RequiredActionContext context) { + UserModel user = context.getUser(); + String oldEmail = user.getEmail(); + String newEmail = context.getHttpRequest().getDecodedFormParameters().getFirst(UserModel.EMAIL); + + RealmModel realm = context.getRealm(); + int validityInSecs = realm.getActionTokenGeneratedByUserLifespan(UpdateEmailActionToken.TOKEN_TYPE); + + UriInfo uriInfo = context.getUriInfo(); + KeycloakSession session = context.getSession(); + AuthenticationSessionModel authenticationSession = context.getAuthenticationSession(); + + UpdateEmailActionToken actionToken = new UpdateEmailActionToken(user.getId(), Time.currentTime() + validityInSecs, + oldEmail, newEmail); + + String link = Urls + .actionTokenBuilder(uriInfo.getBaseUri(), actionToken.serialize(session, realm, uriInfo), + authenticationSession.getClient().getClientId(), authenticationSession.getTabId()) + + .build(realm.getName()).toString(); + + context.getEvent().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, newEmail); + try { + session.getProvider(EmailTemplateProvider.class).setAuthenticationSession(authenticationSession).setRealm(realm) + .setUser(user).sendEmailUpdateConfirmation(link, TimeUnit.SECONDS.toMinutes(validityInSecs), newEmail); + } catch (EmailException e) { + logger.error("Failed to send email for email update", e); + context.getEvent().error(Errors.EMAIL_SEND_FAILED); + return; + } + context.getEvent().success(); + + LoginFormsProvider forms = context.form(); + context.challenge(forms.setAttribute("messageHeader", forms.getMessage("emailUpdateConfirmationSentTitle")) + .setInfo("emailUpdateConfirmationSent", newEmail).createForm(Templates.getTemplate(LoginFormsPages.INFO))); + } + + private void updateEmailWithoutConfirmation(RequiredActionContext context, + UserProfile emailUpdateValidationResult) { + + updateEmailNow(context.getEvent(), context.getUser(), emailUpdateValidationResult); + context.success(); + } + + public static UserProfile validateEmailUpdate(KeycloakSession session, UserModel user, String newEmail) { + MultivaluedMap formData = new MultivaluedHashMap<>(); + formData.putSingle(UserModel.EMAIL, newEmail); + UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class); + UserProfile profile = profileProvider.create(UserProfileContext.UPDATE_EMAIL, formData, user); + profile.validate(); + return profile; + } + + public static void updateEmailNow(EventBuilder event, UserModel user, UserProfile emailUpdateValidationResult) { + + String oldEmail = user.getEmail(); + String newEmail = emailUpdateValidationResult.getAttributes().getFirstValue(UserModel.EMAIL); + event.event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, newEmail); + emailUpdateValidationResult.update(false, new EventAuditingAttributeChangeListener(emailUpdateValidationResult, event)); + } + + @Override + public RequiredActionProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return UserModel.RequiredAction.UPDATE_EMAIL.name(); + } + + @Override + public boolean isSupported() { + return Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL); + } +} diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java index f6fb87a21d..d8286be6f7 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java @@ -40,6 +40,8 @@ public interface UpdateProfileContext { void setUsername(String username); + boolean isEditEmailAllowed(); + String getEmail(); void setEmail(String email); diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java index b2d24e35c0..dea0d67999 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java @@ -17,13 +17,13 @@ package org.keycloak.authentication.requiredactions.util; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.userprofile.UserProfileContext; - import java.util.List; import java.util.Map; import java.util.stream.Stream; +import org.keycloak.common.Profile; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.userprofile.UserProfileContext; /** * @author Marek Posolda @@ -58,6 +58,11 @@ public class UserUpdateProfileContext implements UpdateProfileContext { user.setUsername(username); } + @Override + public boolean isEditEmailAllowed() { + return !Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL); + } + @Override public String getEmail() { return user.getEmail(); diff --git a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java index c4680f3f7c..6473c22e51 100644 --- a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java +++ b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java @@ -61,9 +61,13 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider { @Override public void send(Map config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException { + send(config, retrieveEmailAddress(user), subject, textBody, htmlBody); + } + + @Override + public void send(Map config, String address, String subject, String textBody, String htmlBody) throws EmailException { Transport transport = null; try { - String address = retrieveEmailAddress(user); Properties props = new Properties(); diff --git a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java index d10172c4bc..73286f9983 100755 --- a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java +++ b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java @@ -181,6 +181,22 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { send("emailVerificationSubject", "email-verification.ftl", attributes); } + @Override + public void sendEmailUpdateConfirmation(String link, long expirationInMinutes, String newEmail) throws EmailException { + if (newEmail == null) { + throw new IllegalArgumentException("The new email is mandatory"); + } + + Map attributes = new HashMap<>(this.attributes); + attributes.put("user", new ProfileBean(user)); + attributes.put("newEmail", newEmail); + addLinkInfoIntoAttributes(link, expirationInMinutes, attributes); + + attributes.put("realmName", getRealmName()); + + send("emailUpdateConfirmationSubject", Collections.emptyList(), "email-update-confirmation.ftl", attributes, newEmail); + } + /** * Add link info into template attributes. * @@ -245,9 +261,13 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { @Override public void send(String subjectFormatKey, List subjectAttributes, String bodyTemplate, Map bodyAttributes) throws EmailException { + send(subjectFormatKey, subjectAttributes, bodyTemplate, bodyAttributes, null); + } + + protected void send(String subjectFormatKey, List subjectAttributes, String bodyTemplate, Map bodyAttributes, String address) throws EmailException { try { EmailTemplate email = processTemplate(subjectFormatKey, subjectAttributes, bodyTemplate, bodyAttributes); - send(email.getSubject(), email.getTextBody(), email.getHtmlBody()); + send(email.getSubject(), email.getTextBody(), email.getHtmlBody(), address); } catch (EmailException e) { throw e; } catch (Exception e) { @@ -255,13 +275,21 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { } } - protected void send(String subject, String textBody, String htmlBody) throws EmailException { - send(realm.getSmtpConfig(), subject, textBody, htmlBody); + protected void send(String subject, String textBody, String htmlBody, String address) throws EmailException { + send(realm.getSmtpConfig(), subject, textBody, htmlBody, address); } protected void send(Map config, String subject, String textBody, String htmlBody) throws EmailException { + send(config, subject, textBody, htmlBody, null); + } + + protected void send(Map config, String subject, String textBody, String htmlBody, String address) throws EmailException { EmailSenderProvider emailSender = session.getProvider(EmailSenderProvider.class); - emailSender.send(config, user, subject, textBody, htmlBody); + if (address == null) { + emailSender.send(config, user, subject, textBody, htmlBody); + } else { + emailSender.send(config, address, subject, textBody, htmlBody); + } } @Override diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java index ad2ec949ec..1519733221 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -16,6 +16,24 @@ */ package org.keycloak.forms.login.freemarker; +import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD; + +import java.io.IOException; +import java.net.URI; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; import org.jboss.logging.Logger; import org.keycloak.OAuth2Constants; import org.keycloak.authentication.AuthenticationFlowContext; @@ -32,6 +50,7 @@ import org.keycloak.forms.login.freemarker.model.RecoveryAuthnCodeInputLoginBean import org.keycloak.forms.login.freemarker.model.RecoveryAuthnCodesBean; import org.keycloak.forms.login.freemarker.model.ClientBean; import org.keycloak.forms.login.freemarker.model.CodeBean; +import org.keycloak.forms.login.freemarker.model.EmailBean; import org.keycloak.forms.login.freemarker.model.IdentityProviderBean; import org.keycloak.forms.login.freemarker.model.IdpReviewProfileBean; import org.keycloak.forms.login.freemarker.model.LoginBean; @@ -74,25 +93,6 @@ import org.keycloak.userprofile.UserProfileContext; import org.keycloak.userprofile.UserProfileProvider; import org.keycloak.utils.MediaType; -import javax.ws.rs.core.MultivaluedHashMap; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; -import java.io.IOException; -import java.net.URI; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Properties; - -import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD; - /** * @author Stian Thorgersen */ @@ -159,8 +159,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { page = LoginFormsPages.LOGIN_RECOVERY_AUTHN_CODES_CONFIG; break; case UPDATE_PROFILE: - UpdateProfileContext userBasedContext = new UserUpdateProfileContext(realm, user); - this.attributes.put(UPDATE_PROFILE_CONTEXT_ATTR, userBasedContext); + this.attributes.put(UPDATE_PROFILE_CONTEXT_ATTR, new UserUpdateProfileContext(realm, user)); actionMessage = Messages.UPDATE_PROFILE; if(isDynamicUserProfile()) { @@ -169,6 +168,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { page = LoginFormsPages.LOGIN_UPDATE_PROFILE; } break; + case UPDATE_EMAIL: + actionMessage = Messages.UPDATE_EMAIL; + page = LoginFormsPages.UPDATE_EMAIL; + break; case UPDATE_PASSWORD: boolean isRequestedByAdmin = user.getRequiredActionsStream().filter(Objects::nonNull).anyMatch(UPDATE_PASSWORD.toString()::contains); actionMessage = isRequestedByAdmin ? Messages.UPDATE_PASSWORD : Messages.RESET_PASSWORD; @@ -237,6 +240,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { UpdateProfileContext userCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR); attributes.put("user", new ProfileBean(userCtx, formData)); break; + case UPDATE_EMAIL: + attributes.put("email", new EmailBean(user, formData)); + break; case LOGIN_IDP_LINK_CONFIRM: case LOGIN_IDP_LINK_EMAIL: BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT); diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java b/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java index 828c474aef..574c04ccd5 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java @@ -70,6 +70,8 @@ public class Templates { return "webauthn-error.ftl"; case LOGIN_UPDATE_PROFILE: return "login-update-profile.ftl"; + case UPDATE_EMAIL: + return "update-email.ftl"; case CODE: return "code.ftl"; case LOGIN_PAGE_EXPIRED: diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/EmailBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/EmailBean.java new file mode 100644 index 0000000000..11de05e864 --- /dev/null +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/EmailBean.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.forms.login.freemarker.model; + +import javax.ws.rs.core.MultivaluedMap; +import org.keycloak.models.UserModel; + +public class EmailBean { + + private final UserModel user; + private final MultivaluedMap formData; + + public EmailBean(UserModel user, MultivaluedMap formData) { + this.user = user; + this.formData = formData; + } + + public String getValue() { + return formData != null ? formData.getFirst("email") : user.getEmail(); + } +} diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/ProfileBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/ProfileBean.java index 577d8a1f9c..68de38af58 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/ProfileBean.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/ProfileBean.java @@ -69,6 +69,10 @@ public class ProfileBean { return user.isEditUsernameAllowed(); } + public boolean isEditEmailAllowed() { + return user.isEditEmailAllowed(); + } + public String getUsername() { return formData != null ? formData.getFirst("username") : user.getUsername(); } public String getFirstName() { diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java index 9ebf8d03b6..5998b54313 100755 --- a/services/src/main/java/org/keycloak/services/messages/Messages.java +++ b/services/src/main/java/org/keycloak/services/messages/Messages.java @@ -102,6 +102,8 @@ public class Messages { public static final String VERIFY_EMAIL = "verifyEmailMessage"; + public static final String UPDATE_EMAIL = "updateEmailMessage"; + public static final String LINK_IDP = "linkIdpMessage"; public static final String EMAIL_VERIFIED = "emailVerifiedMessage"; diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java index 691ef871f5..e7a0851d3a 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java @@ -1,38 +1,5 @@ package org.keycloak.services.resources.account; -import org.jboss.logging.Logger; -import org.jboss.resteasy.annotations.cache.NoCache; -import org.keycloak.common.Profile; -import org.keycloak.authentication.requiredactions.DeleteAccount; -import org.keycloak.common.Version; -import org.keycloak.events.EventStoreProvider; -import org.keycloak.models.AccountRoles; -import org.keycloak.models.ClientModel; -import org.keycloak.models.Constants; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; -import org.keycloak.protocol.oidc.utils.RedirectUtils; -import org.keycloak.services.Urls; -import org.keycloak.services.managers.AppAuthManager; -import org.keycloak.services.managers.Auth; -import org.keycloak.services.managers.AuthenticationManager; -import org.keycloak.services.util.ResolveRelative; -import org.keycloak.services.validation.Validation; -import org.keycloak.theme.FreeMarkerException; -import org.keycloak.theme.FreeMarkerUtil; -import org.keycloak.theme.Theme; -import org.keycloak.theme.beans.MessageFormatterMethod; -import org.keycloak.urls.UrlType; -import org.keycloak.utils.MediaType; - -import javax.json.Json; -import javax.json.JsonObjectBuilder; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -45,9 +12,42 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import javax.json.Json; +import javax.json.JsonObjectBuilder; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; +import org.jboss.logging.Logger; +import org.jboss.resteasy.annotations.cache.NoCache; +import org.keycloak.authentication.requiredactions.DeleteAccount; +import org.keycloak.common.Profile; +import org.keycloak.common.Version; +import org.keycloak.events.EventStoreProvider; +import org.keycloak.models.AccountRoles; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RequiredActionProviderModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.protocol.oidc.utils.RedirectUtils; +import org.keycloak.services.Urls; +import org.keycloak.services.managers.AppAuthManager; +import org.keycloak.services.managers.Auth; +import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.resources.RealmsResource; +import org.keycloak.services.util.ResolveRelative; +import org.keycloak.services.validation.Validation; +import org.keycloak.theme.FreeMarkerException; +import org.keycloak.theme.FreeMarkerUtil; +import org.keycloak.theme.Theme; +import org.keycloak.theme.beans.MessageFormatterMethod; +import org.keycloak.urls.UrlType; +import org.keycloak.utils.MediaType; /** * Created by st on 29/03/17. @@ -144,6 +144,9 @@ public class AccountConsole { map.put("isTotpConfigured", isTotpConfigured); map.put("deleteAccountAllowed", deleteAccountAllowed); + map.put("updateEmailFeatureEnabled", Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL)); + RequiredActionProviderModel updateEmailActionProvider = realm.getRequiredActionProviderByAlias(UserModel.RequiredAction.UPDATE_EMAIL.name()); + map.put("updateEmailActionEnabled", updateEmailActionProvider != null && updateEmailActionProvider.isEnabled()); FreeMarkerUtil freeMarkerUtil = new FreeMarkerUtil(); String result = freeMarkerUtil.processTemplate(map, "index.ftl", theme); diff --git a/services/src/main/java/org/keycloak/userprofile/AbstractUserProfileProvider.java b/services/src/main/java/org/keycloak/userprofile/AbstractUserProfileProvider.java index af8757b0e3..b4ac6d0ba5 100644 --- a/services/src/main/java/org/keycloak/userprofile/AbstractUserProfileProvider.java +++ b/services/src/main/java/org/keycloak/userprofile/AbstractUserProfileProvider.java @@ -25,6 +25,7 @@ import static org.keycloak.userprofile.UserProfileContext.ACCOUNT_OLD; import static org.keycloak.userprofile.UserProfileContext.IDP_REVIEW; import static org.keycloak.userprofile.UserProfileContext.REGISTRATION_PROFILE; import static org.keycloak.userprofile.UserProfileContext.REGISTRATION_USER_CREATION; +import static org.keycloak.userprofile.UserProfileContext.UPDATE_EMAIL; import static org.keycloak.userprofile.UserProfileContext.UPDATE_PROFILE; import static org.keycloak.userprofile.UserProfileContext.USER_API; @@ -36,8 +37,8 @@ import java.util.Map; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; - import org.keycloak.Config; +import org.keycloak.common.Profile; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -57,7 +58,6 @@ import org.keycloak.userprofile.validator.UsernameHasValueValidator; import org.keycloak.userprofile.validator.UsernameIDNHomographValidator; import org.keycloak.userprofile.validator.UsernameMutationValidator; import org.keycloak.validate.ValidatorConfig; -import org.keycloak.validate.validators.EmailValidator; /** *

A base class for {@link UserProfileProvider} implementations providing the main hooks for customizations. @@ -97,11 +97,21 @@ public abstract class AbstractUserProfileProvider return !realm.isRegistrationEmailAsUsername(); case UPDATE_PROFILE: return realm.isEditUsernameAllowed(); + case UPDATE_EMAIL: + return false; default: return true; } } + private static boolean editEmailCondition(AttributeContext c) { + return !Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL) || (c.getContext() != UPDATE_PROFILE && c.getContext() != ACCOUNT); + } + + private static boolean readEmailCondition(AttributeContext c) { + return !Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL) || c.getContext() != UPDATE_PROFILE; + } + public static Pattern getRegexPatternString(String[] builtinReadOnlyAttributes) { if (builtinReadOnlyAttributes != null) { List readOnlyAttributes = new ArrayList<>(Arrays.asList(builtinReadOnlyAttributes)); @@ -177,6 +187,9 @@ public abstract class AbstractUserProfileProvider addContextualProfileMetadata(configureUserProfile(createDefaultProfile(ACCOUNT_OLD, readOnlyValidator))); addContextualProfileMetadata(configureUserProfile(createDefaultProfile(REGISTRATION_PROFILE, readOnlyValidator))); addContextualProfileMetadata(configureUserProfile(createDefaultProfile(UPDATE_PROFILE, readOnlyValidator))); + if (Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL)) { + addContextualProfileMetadata(configureUserProfile(createDefaultProfile(UPDATE_EMAIL, readOnlyValidator))); + } addContextualProfileMetadata(configureUserProfile(createRegistrationUserCreationProfile())); addContextualProfileMetadata(configureUserProfile(createUserResourceValidation(config))); } @@ -304,7 +317,9 @@ public abstract class AbstractUserProfileProvider new AttributeValidatorMetadata(DuplicateUsernameValidator.ID), new AttributeValidatorMetadata(UsernameMutationValidator.ID)).setAttributeDisplayName("${username}"); - metadata.addAttribute(UserModel.EMAIL, -1, + metadata.addAttribute(UserModel.EMAIL, -1, + AbstractUserProfileProvider::editEmailCondition, + AbstractUserProfileProvider::readEmailCondition, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL, false)), new AttributeValidatorMetadata(DuplicateEmailValidator.ID), new AttributeValidatorMetadata(EmailExistsAsUsernameValidator.ID)) @@ -361,4 +376,4 @@ public abstract class AbstractUserProfileProvider return metadata; } -} \ No newline at end of file +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory index 89b8c9a38d..ac3961633a 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory @@ -25,4 +25,5 @@ org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory org.keycloak.authentication.requiredactions.UpdateUserLocaleAction org.keycloak.authentication.requiredactions.DeleteAccount org.keycloak.authentication.requiredactions.VerifyUserProfile -org.keycloak.authentication.requiredactions.RecoveryAuthnCodesAction \ No newline at end of file +org.keycloak.authentication.requiredactions.RecoveryAuthnCodesAction +org.keycloak.authentication.requiredactions.UpdateEmail diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory index 2a5b9ec3e5..a345bfaf71 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory @@ -1,4 +1,5 @@ org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionTokenHandler org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionTokenHandler -org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionTokenHandler \ No newline at end of file +org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionTokenHandler +org.keycloak.authentication.actiontoken.updateemail.UpdateEmailActionTokenHandler diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/UpdateEmailPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/UpdateEmailPage.java new file mode 100644 index 0000000000..96392e3457 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/UpdateEmailPage.java @@ -0,0 +1,87 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.auth.page.login; + +import static org.keycloak.testsuite.util.UIUtils.clickLink; +import static org.keycloak.testsuite.util.UIUtils.getTextFromElement; + +import org.keycloak.models.UserModel; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +public class UpdateEmailPage extends RequiredActions { + + @FindBy(id = "email") + private WebElement emailInput; + + @FindBy(id = "input-error-email") + private WebElement inputErrorEmail; + + @FindBy(css = "button[name='cancel-aia']") + private WebElement cancelActionButton; + + @FindBy(css = "input[type='submit']") + private WebElement submitActionButton; + + @Override + public String getActionId() { + return UserModel.RequiredAction.UPDATE_EMAIL.name(); + } + + @Override + public boolean isCurrent() { + return driver.getCurrentUrl().contains("login-actions/required-action") + && driver.getCurrentUrl().contains("execution=" + getActionId()); + } + + public void changeEmail(String email){ + emailInput.clear(); + emailInput.sendKeys(email); + + submit(); + } + + public String getEmail() { + return emailInput.getAttribute("value"); + } + + public String getEmailInputError() { + try { + return getTextFromElement(inputErrorEmail); + } catch (NoSuchElementException e) { + return null; + } + } + + public boolean isCancelDisplayed() { + try { + return cancelActionButton.isDisplayed(); + } catch (NoSuchElementException e) { + return false; + } + } + + public void clickCancelAIA() { + clickLink(cancelActionButton); + } + + public void clickSubmitAction() { + clickLink(submitActionButton); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/EmailUpdatePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/EmailUpdatePage.java new file mode 100644 index 0000000000..a3ffebab6c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/EmailUpdatePage.java @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.pages; + +import static org.keycloak.testsuite.util.UIUtils.getTextFromElement; +import static org.keycloak.testsuite.util.UIUtils.isElementVisible; + +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +public class EmailUpdatePage extends AbstractPage { + + @FindBy(id = "email") + private WebElement emailInput; + + @FindBy(css = "input[type=\"submit\"]") + private WebElement submitButton; + + @FindBy(name = "cancel-aia") + private WebElement cancelAIAButton; + + @FindBy(id = "input-error-email") + private WebElement emailError; + + public void changeEmail(String newEmail) { + emailInput.clear(); + emailInput.sendKeys(newEmail); + + submitButton.click(); + } + + public void cancel() { + cancelAIAButton.click(); + } + + @Override + public boolean isCurrent() { + return PageUtils.getPageTitle(driver).equals("Update email"); + } + + @Override + public void open() throws Exception { + throw new UnsupportedOperationException(); + } + + public String getEmailError() { + try { + return getTextFromElement(emailError); + } catch (NoSuchElementException e) { + return null; + } + } + + public boolean isCancelDisplayed() { + return isElementVisible(cancelAIAButton); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfileEditUsernameAllowedPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfileEditUsernameAllowedPage.java index ed675668b2..15e3b58808 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfileEditUsernameAllowedPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfileEditUsernameAllowedPage.java @@ -26,16 +26,8 @@ public class LoginUpdateProfileEditUsernameAllowedPage extends LoginUpdateProfil @FindBy(id = "username") private WebElement usernameInput; - public void update(String firstName, String lastName, String email, String username) { - usernameInput.clear(); - usernameInput.sendKeys(username); - update(firstName, lastName, email); - } - - public void updateWithDepartment(String firstName, String lastName, String department, String email, String username) { - usernameInput.clear(); - usernameInput.sendKeys(username); - super.updateWithDepartment(firstName, lastName, department, email); + public Update prepareUpdate() { + return new Update(this); } public String getUsername() { @@ -59,4 +51,29 @@ public class LoginUpdateProfileEditUsernameAllowedPage extends LoginUpdateProfil throw new UnsupportedOperationException(); } + public static class Update extends LoginUpdateProfilePage.Update { + + private final LoginUpdateProfileEditUsernameAllowedPage page; + private String username; + + protected Update(LoginUpdateProfileEditUsernameAllowedPage page) { + super(page); + this.page = page; + } + + public Update username(String username) { + this.username = username; + return this; + } + + @Override + public void submit() { + if (username != null) { + page.usernameInput.clear(); + page.usernameInput.sendKeys(username); + } + super.submit(); + } + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java index 37bf9134e2..5d0b1ee50e 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java @@ -17,6 +17,9 @@ package org.keycloak.testsuite.pages; +import static org.keycloak.testsuite.util.UIUtils.clickLink; +import static org.keycloak.testsuite.util.UIUtils.getTextFromElement; + import org.jboss.arquillian.graphene.page.Page; import org.keycloak.testsuite.util.UIUtils; import org.openqa.selenium.By; @@ -24,9 +27,6 @@ import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; -import static org.keycloak.testsuite.util.UIUtils.clickLink; -import static org.keycloak.testsuite.util.UIUtils.getTextFromElement; - /** * @author Stian Thorgersen */ @@ -56,30 +56,16 @@ public class LoginUpdateProfilePage extends AbstractPage { @FindBy(className = "alert-error") private WebElement loginAlertErrorMessage; - public void update(String firstName, String lastName, String email) { - updateWithDepartment(firstName, lastName, null, email); + public void update(String firstName, String lastName) { + prepareUpdate().firstName(firstName).lastName(lastName).submit(); } - - public void updateWithDepartment(String firstName, String lastName, String department, String email) { - if (firstName != null) { - firstNameInput.clear(); - firstNameInput.sendKeys(firstName); - } - if (lastName != null) { - lastNameInput.clear(); - lastNameInput.sendKeys(lastName); - } - if (email != null) { - emailInput.clear(); - emailInput.sendKeys(email); - } - if(department != null) { - departmentInput.clear(); - departmentInput.sendKeys(department); - } - - clickLink(submitButton); + public void update(String firstName, String lastName, String email) { + prepareUpdate().firstName(firstName).lastName(lastName).email(email).submit(); + } + + public Update prepareUpdate() { + return new Update(this); } public void cancel() { @@ -101,11 +87,11 @@ public class LoginUpdateProfilePage extends AbstractPage { public String getLastName() { return lastNameInput.getAttribute("value"); } - + public String getEmail() { return emailInput.getAttribute("value"); } - + public String getDepartment() { return departmentInput.getAttribute("value"); } @@ -148,6 +134,61 @@ public class LoginUpdateProfilePage extends AbstractPage { } } + public static class Update { + private final LoginUpdateProfilePage page; + private String firstName; + private String lastName; + private String department; + private String email; + + protected Update(LoginUpdateProfilePage page) { + this.page = page; + } + + public Update firstName(String firstName) { + this.firstName = firstName; + return this; + } + + public Update lastName(String lastName) { + this.lastName = lastName; + return this; + } + + public Update department(String department) { + this.department = department; + return this; + } + + public Update email(String email) { + this.email = email; + return this; + } + + public void submit() { + if (firstName != null) { + page.firstNameInput.clear(); + page.firstNameInput.sendKeys(firstName); + } + if (lastName != null) { + page.lastNameInput.clear(); + page.lastNameInput.sendKeys(lastName); + } + + if(department != null) { + page.departmentInput.clear(); + page.departmentInput.sendKeys(department); + } + + if (email != null) { + page.emailInput.clear(); + page.emailInput.sendKeys(email); + } + + clickLink(page.submitButton); + } + } + // For managing input errors public static class UpdateProfileErrors { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/VerifyProfilePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/VerifyProfilePage.java index 3f449453cf..a916655d53 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/VerifyProfilePage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/VerifyProfilePage.java @@ -75,14 +75,14 @@ public class VerifyProfilePage extends AbstractPage { update(firstName, lastName); } - + public void updateEmail(String email, String firstName, String lastName) { - + emailInput.clear(); if (emailInput != null) { emailInput.sendKeys(email); } - + firstNameInput.clear(); if (firstName != null) { firstNameInput.sendKeys(firstName); @@ -158,4 +158,4 @@ public class VerifyProfilePage extends AbstractPage { throw new UnsupportedOperationException(); } -} \ No newline at end of file +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractAppInitiatedActionUpdateEmailTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractAppInitiatedActionUpdateEmailTest.java new file mode 100644 index 0000000000..71fd3f947c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractAppInitiatedActionUpdateEmailTest.java @@ -0,0 +1,130 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.actions; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.common.Profile; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.pages.EmailUpdatePage; +import org.keycloak.testsuite.util.UserBuilder; + +@EnableFeature(Profile.Feature.UPDATE_EMAIL) +public abstract class AbstractAppInitiatedActionUpdateEmailTest extends AbstractAppInitiatedActionTest { + + @Page + protected EmailUpdatePage emailUpdatePage; + + @Override + protected String getAiaAction() { + return UserModel.RequiredAction.UPDATE_EMAIL.name(); + } + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + } + + @Before + public void beforeTest() { + ApiUtil.removeUserByUsername(testRealm(), "test-user@localhost"); + UserRepresentation user = UserBuilder.create().enabled(true).username("test-user@localhost") + .email("test-user@localhost").firstName("Tom").lastName("Brady").build(); + prepareUser(user); + ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password"); + + ApiUtil.removeUserByUsername(testRealm(), "john-doh@localhost"); + user = UserBuilder.create().enabled(true).username("john-doh@localhost").email("john-doh@localhost").firstName("John") + .lastName("Doh").build(); + prepareUser(user); + ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password"); + } + + protected void prepareUser(UserRepresentation user){ + + } + + @Test + public void cancelUpdateEmail() { + doAIA(); + + loginPage.login("test-user@localhost", "password"); + + emailUpdatePage.assertCurrent(); + emailUpdatePage.cancel(); + + assertKcActionStatus("cancelled"); + + // assert nothing was updated in persistent store + UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); + Assert.assertEquals("test-user@localhost", user.getEmail()); + } + + @Test + public void updateToExistingEmail() { + doAIA(); + + loginPage.login("test-user@localhost", "password"); + + emailUpdatePage.assertCurrent(); + emailUpdatePage.changeEmail("john-doh@localhost"); + emailUpdatePage.assertCurrent(); + + Assert.assertEquals("Email already exists.", emailUpdatePage.getEmailError()); + + UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); + Assert.assertEquals("test-user@localhost", user.getEmail()); + } + + @Test + public void updateToInvalidEmail(){ + doAIA(); + + loginPage.login("test-user@localhost", "password"); + + emailUpdatePage.assertCurrent(); + emailUpdatePage.changeEmail("invalidemail"); + emailUpdatePage.assertCurrent(); + + Assert.assertEquals("Invalid email address.", emailUpdatePage.getEmailError()); + + UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); + Assert.assertEquals("test-user@localhost", user.getEmail()); + } + + @Test + public void updateToBlankEmail(){ + doAIA(); + + loginPage.login("test-user@localhost", "password"); + + emailUpdatePage.assertCurrent(); + emailUpdatePage.changeEmail(""); + emailUpdatePage.assertCurrent(); + + Assert.assertEquals("Please specify email.", emailUpdatePage.getEmailError()); + + UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); + Assert.assertEquals("test-user@localhost", user.getEmail()); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractRequiredActionUpdateEmailTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractRequiredActionUpdateEmailTest.java new file mode 100644 index 0000000000..0e86c73ac0 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractRequiredActionUpdateEmailTest.java @@ -0,0 +1,155 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.actions; + +import static org.junit.Assert.assertFalse; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.common.Profile; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.auth.page.login.UpdateEmailPage; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.util.UserBuilder; + +@EnableFeature(Profile.Feature.UPDATE_EMAIL) +public abstract class AbstractRequiredActionUpdateEmailTest extends AbstractTestRealmKeycloakTest { + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Page + protected LoginPage loginPage; + + @Page + protected UpdateEmailPage updateEmailPage; + + @Page + protected AppPage appPage; + + @Before + public void beforeTest() { + ApiUtil.removeUserByUsername(testRealm(), "test-user@localhost"); + UserRepresentation user = UserBuilder.create().enabled(true) + .username("test-user@localhost") + .email("test-user@localhost") + .firstName("Tom") + .lastName("Brady") + .requiredAction(UserModel.RequiredAction.UPDATE_EMAIL.name()).build(); + prepareUser(user); + ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password"); + + ApiUtil.removeUserByUsername(testRealm(), "john-doh@localhost"); + user = UserBuilder.create().enabled(true) + .username("john-doh@localhost") + .email("john-doh@localhost") + .firstName("John") + .lastName("Doh") + .requiredAction(UserModel.RequiredAction.UPDATE_EMAIL.name()).build(); + prepareUser(user); + ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password"); + } + + protected void prepareUser(UserRepresentation user){ + + } + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + + } + + @Test + public void cancelIsNotDisplayed(){ + loginPage.open(); + + loginPage.login("test-user@localhost", "password"); + + updateEmailPage.assertCurrent(); + assertFalse(updateEmailPage.isCancelDisplayed()); + } + + @Test + public void updateEmailMissing() { + loginPage.open(); + + loginPage.login("test-user@localhost", "password"); + + updateEmailPage.assertCurrent(); + + updateEmailPage.changeEmail(""); + + updateEmailPage.assertCurrent(); + + // assert that form holds submitted values during validation error + Assert.assertEquals("", updateEmailPage.getEmail()); + + Assert.assertEquals("Please specify email.", updateEmailPage.getEmailInputError()); + + events.assertEmpty(); + } + + @Test + public void updateEmailDuplicate() { + loginPage.open(); + + loginPage.login("john-doh@localhost", "password"); + + updateEmailPage.assertCurrent(); + + updateEmailPage.changeEmail("test-user@localhost"); + + updateEmailPage.assertCurrent(); + + // assert that form holds submitted values during validation error + Assert.assertEquals("test-user@localhost", updateEmailPage.getEmail()); + + Assert.assertEquals("Email already exists.", updateEmailPage.getEmailInputError()); + + events.assertEmpty(); + } + + @Test + public void updateEmailInvalid() { + loginPage.open(); + + loginPage.login("test-user@localhost", "password"); + + updateEmailPage.assertCurrent(); + + updateEmailPage.changeEmail("invalid"); + + updateEmailPage.assertCurrent(); + + // assert that form holds submitted values during validation error + Assert.assertEquals("invalid", updateEmailPage.getEmail()); + + Assert.assertEquals("Invalid email address.", updateEmailPage.getEmailInputError()); + + events.assertEmpty(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateEmailTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateEmailTest.java new file mode 100644 index 0000000000..8401db54b3 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateEmailTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.actions; + +import static org.junit.Assert.assertTrue; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.events.Details; +import org.keycloak.events.EventType; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +public class AppInitiatedActionUpdateEmailTest extends AbstractAppInitiatedActionUpdateEmailTest { + + @Test + public void updateEmail() { + doAIA(); + + loginPage.login("test-user@localhost", "password"); + + emailUpdatePage.assertCurrent(); + assertTrue(emailUpdatePage.isCancelDisplayed()); + + emailUpdatePage.changeEmail("new@email.com"); + + events.expect(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost") + .detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent(); + + assertKcActionStatus("success"); + + UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); + Assert.assertEquals("new@email.com", user.getEmail()); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateEmailWithVerificationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateEmailWithVerificationTest.java new file mode 100644 index 0000000000..c03023dd73 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateEmailWithVerificationTest.java @@ -0,0 +1,138 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.actions; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.events.Details; +import org.keycloak.events.EventType; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.InfoPage; +import org.keycloak.testsuite.util.GreenMailRule; +import org.keycloak.testsuite.util.MailUtils; + +public class AppInitiatedActionUpdateEmailWithVerificationTest extends AbstractAppInitiatedActionUpdateEmailTest { + + @Rule + public GreenMailRule greenMail = new GreenMailRule(); + + @Page + protected InfoPage infoPage; + + @Page + protected ErrorPage errorPage; + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + testRealm.setVerifyEmail(true); + } + + @Override + protected void prepareUser(UserRepresentation user) { + user.setEmailVerified(true); + } + + @Test + public void updateEmail() throws IOException, MessagingException { + doAIA(); + loginPage.login("test-user@localhost", "password"); + + emailUpdatePage.assertCurrent(); + assertTrue(emailUpdatePage.isCancelDisplayed()); + emailUpdatePage.changeEmail("new@localhost"); + + events.expect(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, "new@localhost").assertEvent(); + Assert.assertEquals("test-user@localhost", ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost").getEmail()); + + driver.navigate().to(fetchEmailConfirmationLink("new@localhost")); + + infoPage.assertCurrent(); + assertEquals("The account email has been successfully updated to new@localhost.", infoPage.getInfo()); + + events.expect(EventType.UPDATE_EMAIL) + .detail(Details.PREVIOUS_EMAIL, "test-user@localhost") + .detail(Details.UPDATED_EMAIL, "new@localhost"); + + Assert.assertEquals("new@localhost", ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost").getEmail()); + } + + @Test + public void confirmEmailUpdateAfterThirdPartyEmailUpdate() throws MessagingException, IOException { + doAIA(); + loginPage.login("test-user@localhost", "password"); + + emailUpdatePage.assertCurrent(); + emailUpdatePage.changeEmail("new@localhost"); + + String confirmationLink = fetchEmailConfirmationLink("new@localhost"); + + UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); + user.setEmail("very-new@localhost"); + user.setEmailVerified(true); + testRealm().users().get(user.getId()).update(user); + + driver.navigate().to(confirmationLink); + + errorPage.assertCurrent(); + assertEquals("The link you clicked is an old stale link and is no longer valid. Maybe you have already verified your email.", errorPage.getError()); + } + + @Test + public void confirmEmailAfterDuplicateEmailSetForThirdPartyAccount() throws MessagingException, IOException { + doAIA(); + loginPage.login("test-user@localhost", "password"); + + emailUpdatePage.assertCurrent(); + emailUpdatePage.changeEmail("new@localhost"); + + String confirmationLink = fetchEmailConfirmationLink("new@localhost"); + + UserRepresentation otherUser = ActionUtil.findUserWithAdminClient(adminClient, "john-doh@localhost"); + otherUser.setEmail("new@localhost"); + otherUser.setEmailVerified(true); + testRealm().users().get(otherUser.getId()).update(otherUser); + + driver.navigate().to(confirmationLink); + + errorPage.assertCurrent(); + assertEquals("Email already exists.", errorPage.getError()); + } + + private String fetchEmailConfirmationLink(String emailRecipient) throws MessagingException, IOException { + MimeMessage[] receivedMessages = greenMail.getReceivedMessages(); + Assert.assertEquals(1, receivedMessages.length); + MimeMessage message = receivedMessages[0]; + Address[] recipients = message.getRecipients(Message.RecipientType.TO); + Assert.assertTrue(recipients.length >= 1); + assertEquals(emailRecipient, recipients[0].toString()); + + return MailUtils.getPasswordResetEmailLink(message).trim(); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateProfileTest.java index 37cdd81f59..c0584d54cb 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateProfileTest.java @@ -85,7 +85,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); + updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last").email("new@email.com").submit(); events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.PREVIOUS_FIRST_NAME, "Tom").detail(Details.UPDATED_FIRST_NAME, "New first") .detail(Details.PREVIOUS_LAST_NAME, "Brady").detail(Details.UPDATED_LAST_NAME, "New last") @@ -115,7 +115,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); + updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last").email("new@email.com").submit(); events.expectLogin().assertEvent(); events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.PREVIOUS_FIRST_NAME, "Tom").detail(Details.UPDATED_FIRST_NAME, "New first") @@ -165,7 +165,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "john-doh@localhost", "new"); + updateProfilePage.prepareUpdate().username("new").firstName("New first").lastName("New last").email("john-doh@localhost").submit(); events.expectLogin() .event(EventType.UPDATE_PROFILE) @@ -199,7 +199,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct updateProfilePage.assertCurrent(); - updateProfilePage.update("", "New last", "new@email.com", "new"); + updateProfilePage.prepareUpdate().username("new").firstName("").lastName("New last").email("new@email.com").submit(); updateProfilePage.assertCurrent(); @@ -224,7 +224,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "", "new@email.com", "new"); + updateProfilePage.prepareUpdate().username("new").firstName("New first").lastName("").email("new@email.com").submit(); updateProfilePage.assertCurrent(); @@ -249,7 +249,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "", "new"); + updateProfilePage.prepareUpdate().username("new").firstName("New first").lastName("New last").email("").submit(); updateProfilePage.assertCurrent(); @@ -271,7 +271,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "invalidemail", "invalid"); + updateProfilePage.prepareUpdate().username("invalid").firstName("New first").lastName("New last").email("invalidemail").submit(); updateProfilePage.assertCurrent(); @@ -293,7 +293,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "new@email.com", ""); + updateProfilePage.prepareUpdate().username("").firstName("New first").lastName("New last").email("new@email.com").submit(); updateProfilePage.assertCurrent(); @@ -316,7 +316,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); + updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last").email("new@email.com").submit(); updateProfilePage.assertCurrent(); @@ -339,7 +339,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "keycloak-user@localhost", "test-user@localhost"); + updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last").email("keycloak-user@localhost").submit(); updateProfilePage.assertCurrent(); @@ -363,7 +363,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct // Expire cookies and assert the page with "back to application" link present driver.manage().deleteAllCookies(); - updateProfilePage.update("New first", "New last", "keycloak-user@localhost", "test-user@localhost"); + updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last").email("keycloak-user@localhost").submit(); errorPage.assertCurrent(); String backToAppLink = errorPage.getBackToApplicationLink(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java index 196d980f51..9b8b3a9d4d 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java @@ -94,7 +94,8 @@ public class RequiredActionMultipleActionsTest extends AbstractTestRealmKeycloak } public String updateProfile(String codeId) { - updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); + updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last") + .email("new@email.com").submit(); AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_PROFILE) .detail(Details.UPDATED_FIRST_NAME, "New first") 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 53bb5e3d89..226cfc37ae 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 @@ -16,6 +16,11 @@ */ package org.keycloak.testsuite.actions; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + import org.jboss.arquillian.graphene.page.Page; import org.junit.Assert; import org.junit.Before; @@ -43,11 +48,6 @@ import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage; import org.keycloak.testsuite.pages.TermsAndConditionsPage; import org.keycloak.testsuite.util.UserBuilder; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; - /** * @author Hiroyuki Wada */ @@ -116,7 +116,7 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest { // Finally, update profile updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); + updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last").email("new@email.com").submit(); events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.UPDATED_FIRST_NAME, "New first") .detail(Details.UPDATED_LAST_NAME, "New last") .detail(Details.PREVIOUS_EMAIL, "test-user@localhost") @@ -150,7 +150,7 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest { // Second, update profile updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); + updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last").email("new@email.com").submit(); events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.UPDATED_FIRST_NAME, "New first") .detail(Details.UPDATED_LAST_NAME, "New last") .detail(Details.PREVIOUS_EMAIL, "test-user@localhost") diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateEmailTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateEmailTest.java new file mode 100644 index 0000000000..c35890b337 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateEmailTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.actions; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.events.Details; +import org.keycloak.events.EventType; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.pages.AppPage; + +public class RequiredActionUpdateEmailTest extends AbstractRequiredActionUpdateEmailTest { + + @Test + public void updateEmail() { + loginPage.open(); + + loginPage.login("test-user@localhost", "password"); + + updateEmailPage.assertCurrent(); + + updateEmailPage.changeEmail("new-email@localhost"); + + events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost") + .detail(Details.UPDATED_EMAIL, "new-email@localhost").assertEvent(); + assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin().assertEvent(); + + // assert user is really updated in persistent store + UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); + assertEquals("new-email@localhost", user.getEmail()); + assertFalse(user.getRequiredActions().contains(UserModel.RequiredAction.UPDATE_EMAIL.name())); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateEmailTestWithVerificationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateEmailTestWithVerificationTest.java new file mode 100644 index 0000000000..89ce799b20 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateEmailTestWithVerificationTest.java @@ -0,0 +1,144 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.actions; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.events.Details; +import org.keycloak.events.EventType; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.InfoPage; +import org.keycloak.testsuite.util.GreenMailRule; +import org.keycloak.testsuite.util.MailUtils; + +public class RequiredActionUpdateEmailTestWithVerificationTest extends AbstractRequiredActionUpdateEmailTest { + + @Rule + public GreenMailRule greenMail = new GreenMailRule(); + + @Page + private InfoPage infoPage; + + @Page + private ErrorPage errorPage; + + protected void prepareUser(UserRepresentation user){ + user.setEmailVerified(true); + } + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + testRealm.setVerifyEmail(true); + } + + @Test + public void updateEmail() throws IOException, MessagingException { + loginPage.open(); + + loginPage.login("test-user@localhost", "password"); + + updateEmailPage.assertCurrent(); + updateEmailPage.changeEmail("new@localhost"); + + events.expect(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, "new@localhost").assertEvent(); + UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); + assertEquals("test-user@localhost", user.getEmail()); + assertTrue(user.getRequiredActions().contains(UserModel.RequiredAction.UPDATE_EMAIL.name())); + + driver.navigate().to(fetchEmailConfirmationLink("new@localhost")); + + infoPage.assertCurrent(); + assertEquals("The account email has been successfully updated to new@localhost.", infoPage.getInfo()); + + events.expect(EventType.UPDATE_EMAIL) + .detail(Details.PREVIOUS_EMAIL, "test-user@localhost") + .detail(Details.UPDATED_EMAIL, "new@localhost"); + + user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); + assertEquals("new@localhost", user.getEmail()); + assertFalse(user.getRequiredActions().contains(UserModel.RequiredAction.UPDATE_EMAIL.name())); + } + + @Test + public void confirmEmailUpdateAfterThirdPartyEmailUpdate() throws MessagingException, IOException { + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + + updateEmailPage.assertCurrent(); + updateEmailPage.changeEmail("new@localhost"); + + String confirmationLink = fetchEmailConfirmationLink("new@localhost"); + + UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); + user.setEmail("very-new@localhost"); + user.setEmailVerified(true); + testRealm().users().get(user.getId()).update(user); + + driver.navigate().to(confirmationLink); + + errorPage.assertCurrent(); + assertEquals("The link you clicked is an old stale link and is no longer valid. Maybe you have already verified your email.", errorPage.getError()); + assertTrue(ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost").getRequiredActions().contains(UserModel.RequiredAction.UPDATE_EMAIL.name())); + } + + @Test + public void confirmEmailAfterDuplicateEmailSetForThirdPartyAccount() throws MessagingException, IOException { + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + + updateEmailPage.assertCurrent(); + updateEmailPage.changeEmail("new@localhost"); + + String confirmationLink = fetchEmailConfirmationLink("new@localhost"); + + UserRepresentation otherUser = ActionUtil.findUserWithAdminClient(adminClient, "john-doh@localhost"); + otherUser.setEmail("new@localhost"); + otherUser.setEmailVerified(true); + testRealm().users().get(otherUser.getId()).update(otherUser); + + driver.navigate().to(confirmationLink); + + errorPage.assertCurrent(); + assertEquals("Email already exists.", errorPage.getError()); + assertTrue(ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost").getRequiredActions().contains(UserModel.RequiredAction.UPDATE_EMAIL.name())); + } + + private String fetchEmailConfirmationLink(String emailRecipient) throws MessagingException, IOException { + MimeMessage[] receivedMessages = greenMail.getReceivedMessages(); + assertEquals(1, receivedMessages.length); + MimeMessage message = receivedMessages[0]; + Address[] recipients = message.getRecipients(Message.RecipientType.TO); + assertTrue(recipients.length >= 1); + assertEquals(emailRecipient, recipients[0].toString()); + + return MailUtils.getPasswordResetEmailLink(message).trim(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java index 408632da10..e9c3d3140c 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java @@ -16,6 +16,10 @@ */ package org.keycloak.testsuite.actions; +import static org.junit.Assert.assertFalse; + +import java.util.Arrays; +import java.util.HashMap; import org.hamcrest.Matchers; import org.jboss.arquillian.graphene.page.Page; import org.junit.Assert; @@ -29,8 +33,8 @@ import org.keycloak.models.UserModel; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; @@ -40,11 +44,6 @@ import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage; import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.userprofile.UserProfileContext; -import static org.junit.Assert.assertFalse; - -import java.util.Arrays; -import java.util.HashMap; - /** * @author Stian Thorgersen */ @@ -107,8 +106,8 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe updateProfilePage.assertCurrent(); assertFalse(updateProfilePage.isCancelDisplayed()); - updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); - + updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last").email("new@email.com").submit(); + events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.PREVIOUS_FIRST_NAME, "Tom").detail(Details.UPDATED_FIRST_NAME, "New first") .detail(Details.PREVIOUS_LAST_NAME, "Brady").detail(Details.UPDATED_LAST_NAME, "New last") .detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com") @@ -137,7 +136,7 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "john-doh@localhost", "new"); + updateProfilePage.prepareUpdate().username("new").firstName("New first").lastName("New last").email("john-doh@localhost").submit(); events.expectLogin().event(EventType.UPDATE_PROFILE).detail(Details.UPDATED_FIRST_NAME, "New first").user(userId).session(Matchers.nullValue(String.class)).removeDetail(Details.CONSENT) .detail(Details.UPDATED_LAST_NAME, "New last").user(userId).session(Matchers.nullValue(String.class)).removeDetail(Details.CONSENT) @@ -167,7 +166,7 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe updateProfilePage.assertCurrent(); - updateProfilePage.update("", "New last", "new@email.com", "new"); + updateProfilePage.prepareUpdate().username("new").firstName("").lastName("New last").email("new@email.com").submit(); updateProfilePage.assertCurrent(); @@ -192,7 +191,7 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "", "new@email.com", "new"); + updateProfilePage.prepareUpdate().username("new").firstName("New first").lastName("").email("new@email.com").submit(); updateProfilePage.assertCurrent(); @@ -217,7 +216,8 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "", "new"); + updateProfilePage.prepareUpdate().username("new").firstName("New first").lastName("New last") + .email("").submit(); updateProfilePage.assertCurrent(); @@ -239,7 +239,8 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "invalidemail", "invalid"); + updateProfilePage.prepareUpdate().username("invalid").firstName("New first").lastName("New last") + .email("invalidemail").submit(); updateProfilePage.assertCurrent(); @@ -261,7 +262,7 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "new@email.com", ""); + updateProfilePage.prepareUpdate().username("").firstName("New first").lastName("New last").email("new@email.com").submit(); updateProfilePage.assertCurrent(); @@ -284,7 +285,7 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); + updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last").email("new@email.com").submit(); updateProfilePage.assertCurrent(); @@ -307,7 +308,8 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "keycloak-user@localhost", "test-user@localhost"); + updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last") + .email("keycloak-user@localhost").submit(); updateProfilePage.assertCurrent(); @@ -331,7 +333,7 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe // Expire cookies and assert the page with "back to application" link present driver.manage().deleteAllCookies(); - updateProfilePage.update("New first", "New last", "keycloak-user@localhost", "test-user@localhost"); + updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last").email("keycloak-user@localhost").submit(); errorPage.assertCurrent(); String backToAppLink = errorPage.getBackToApplicationLink(); @@ -357,7 +359,7 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe updateProfilePage.assertCurrent(); assertFalse(updateProfilePage.isCancelDisplayed()); - updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); + updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last").email("new@email.com").submit(); events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.CONTEXT, UserProfileContext.UPDATE_PROFILE.name()).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java index e5a6cc784b..d8074d730c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java @@ -284,7 +284,7 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi updateProfilePage.assertCurrent(); assertFalse(updateProfilePage.isCancelDisplayed()); - updateProfilePage.update("New first", "", "new@email.com", USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("New first").lastName("").email("new@email.com").submit(); events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.PREVIOUS_FIRST_NAME, "Tom").detail(Details.UPDATED_FIRST_NAME, "New first") .detail(Details.PREVIOUS_LAST_NAME, "Brady") @@ -319,11 +319,11 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi updateProfilePage.assertCurrent(); //submit with error - updateProfilePage.update("First", "L", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("First").lastName("L").email(USERNAME1).submit(); updateProfilePage.assertCurrent(); //submit OK - updateProfilePage.update("First", "Last", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("First").lastName("Last").email(USERNAME1).submit(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); @@ -352,7 +352,7 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi Assert.assertFalse(updateProfilePage.isDepartmentEnabled()); //update of the other attributes must be successful in this case - updateProfilePage.update("First", "Last", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("First").lastName("Last").email(USERNAME1).submit(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); @@ -379,7 +379,7 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi Assert.assertFalse(updateProfilePage.isDepartmentEnabled()); //update of the other attributes must be successful in this case - updateProfilePage.update("First", "Last", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("First").lastName("Last").email(USERNAME1).submit(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); @@ -406,7 +406,7 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi Assert.assertFalse("'department' field is visible" , updateProfilePage.isDepartmentPresent()); //update of the other attributes must be successful in this case - updateProfilePage.update("First", "Last", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("First").lastName("Last").email(USERNAME1).submit(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); @@ -431,11 +431,13 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi updateProfilePage.assertCurrent(); //submit with error - updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("FirstCC").lastName("LastCC").email(USERNAME1) + .department("").submit(); updateProfilePage.assertCurrent(); //submit OK - updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "DepartmentCC", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("FirstCC").lastName("LastCC").email(USERNAME1) + .department("DepartmentCC").submit(); // we also test additional attribute configured to be audited in the event events.expectRequiredAction(EventType.UPDATE_PROFILE) @@ -470,11 +472,13 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi updateProfilePage.assertCurrent(); //submit with error - updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("FirstCC").lastName("LastCC").email(USERNAME1) + .department("").submit(); updateProfilePage.assertCurrent(); //submit OK - updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "DepartmentCC", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("FirstCC").lastName("LastCC").email(USERNAME1) + .department("DepartmentCC").submit(); events.expectRequiredAction(EventType.UPDATE_PROFILE).client(client_scope_optional.getClientId()) .detail(Details.PREVIOUS_FIRST_NAME, "Tom").detail(Details.UPDATED_FIRST_NAME, "FirstCC") @@ -508,11 +512,13 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi updateProfilePage.assertCurrent(); //submit with error - updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("FirstCC").lastName("LastCC").email(USERNAME1) + .department("").submit(); updateProfilePage.assertCurrent(); //submit OK - updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "DepartmentCC", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("FirstCC").lastName("LastCC").email(USERNAME1) + .department("DepartmentCC").submit(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); @@ -540,11 +546,13 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi updateProfilePage.assertCurrent(); //submit with error - updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("FirstCC").lastName("LastCC").email(USERNAME1) + .department("").submit(); updateProfilePage.assertCurrent(); //submit OK - updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "DepartmentCC", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("FirstCC").lastName("LastCC").email(USERNAME1) + .department("DepartmentCC").submit(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); @@ -572,7 +580,8 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi updateProfilePage.assertCurrent(); Assert.assertTrue(updateProfilePage.isDepartmentPresent()); - updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "DepartmentCC", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("FirstCC").lastName("LastCC").email(USERNAME1) + .department("DepartmentCC").submit(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); @@ -600,7 +609,7 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi updateProfilePage.assertCurrent(); Assert.assertFalse(updateProfilePage.isDepartmentPresent()); - updateProfilePage.update("FirstCC", "LastCC", USERNAME1, USERNAME1); + updateProfilePage.prepareUpdate().username(USERNAME1).firstName("FirstCC").lastName("LastCC").email(USERNAME1).submit(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); @@ -636,4 +645,4 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi testRealm().users().get(ur.getId()).update(ur); } -} \ No newline at end of file +} 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 282a278c18..2dd2508c1f 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 @@ -17,6 +17,11 @@ package org.keycloak.testsuite.cluster; +import static org.junit.Assert.assertEquals; +import static org.keycloak.testsuite.util.WaitUtils.pause; + +import java.io.IOException; +import javax.mail.MessagingException; import org.jboss.arquillian.graphene.page.Page; import org.junit.Test; import org.keycloak.services.managers.AuthenticationSessionManager; @@ -27,12 +32,6 @@ import org.keycloak.testsuite.pages.LoginUpdateProfilePage; import org.openqa.selenium.Cookie; import org.openqa.selenium.WebDriver; -import javax.mail.MessagingException; -import java.io.IOException; - -import static org.junit.Assert.assertEquals; -import static org.keycloak.testsuite.util.WaitUtils.pause; - /** * @author Marek Posolda */ @@ -109,7 +108,7 @@ public class AuthenticationSessionFailoverClusterTest extends AbstractFailoverCl updateProfilePage.assertCurrent(); // Successfully update profile and assert user logged - updateProfilePage.update("John", "Doe3", "john@doe3.com"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3").email("john@doe3.com").submit(); appPage.assertCurrent(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java index 57c325a92d..93a402b54e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java @@ -27,7 +27,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Consumer; - import javax.naming.Context; import javax.naming.NamingException; import javax.naming.directory.Attributes; @@ -35,7 +34,6 @@ import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.security.sasl.Sasl; import javax.ws.rs.core.Response; - import org.apache.http.NameValuePair; import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; @@ -75,7 +73,6 @@ import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; -import org.keycloak.testsuite.auth.page.AuthRealm; import org.keycloak.testsuite.pages.AccountPasswordPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.util.KerberosRule; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java index ac3e5e2982..92d7433963 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java @@ -19,12 +19,9 @@ package org.keycloak.testsuite.federation.kerberos; import java.net.URI; import java.util.List; - - import javax.ws.rs.client.Entity; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; - import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; @@ -36,11 +33,9 @@ import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.storage.UserStorageProvider; import org.keycloak.testsuite.ActionURIUtils; +import org.keycloak.testsuite.KerberosEmbeddedServer; import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected; import org.keycloak.testsuite.util.KerberosRule; -import org.keycloak.testsuite.KerberosEmbeddedServer; - -import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; /** * Test for the KerberosFederationProvider (kerberos without LDAP integration) 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 af5946a617..f9e03315fb 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 @@ -17,11 +17,11 @@ package org.keycloak.testsuite.forms; -import java.io.IOException; +import static org.junit.Assert.assertEquals; +import java.io.IOException; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; - import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Rule; @@ -33,6 +33,7 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; +import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.InfoPage; @@ -48,9 +49,6 @@ import org.keycloak.testsuite.util.GreenMailRule; import org.keycloak.testsuite.util.MailUtils; import org.keycloak.testsuite.util.UserBuilder; -import static org.junit.Assert.assertEquals; -import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; - /** * Test for browser back/forward/refresh buttons * @@ -167,7 +165,7 @@ public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest { // Successfully update profile and assert user logged - updateProfilePage.update("John", "Doe3", "john@doe3.com"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3").email("john@doe3.com").submit(); appPage.assertCurrent(); } @@ -214,7 +212,7 @@ public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest { updateProfilePage.assertCurrent(); // Successfully update profile and assert user logged - updateProfilePage.update("John", "Doe3", "john@doe3.com"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3").email("john@doe3.com").submit(); appPage.assertCurrent(); } @@ -228,7 +226,7 @@ public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest { loginPage.open(); loginPage.login("login-test", "password"); updatePasswordPage.changePassword("password", "password"); - updateProfilePage.update("John", "Doe3", "john@doe3.com"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3").email("john@doe3.com").submit(); // Assert on consent screen grantPage.assertCurrent(); 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 f7d61a32df..cbd66619ca 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 @@ -17,6 +17,10 @@ package org.keycloak.testsuite.forms; +import static org.junit.Assert.fail; +import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot; +import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; + import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Rule; @@ -48,10 +52,6 @@ import org.keycloak.testsuite.util.GreenMailRule; import org.keycloak.testsuite.util.UserBuilder; import org.openqa.selenium.NoSuchElementException; -import static org.junit.Assert.fail; -import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; -import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot; - /** * Tries to simulate testing with multiple browser tabs * @@ -145,7 +145,8 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { updatePasswordPage.assertCurrent(); updatePasswordPage.changePassword("password", "password"); - updateProfilePage.update("John", "Doe3", "john@doe3.com"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") + .email("john@doe3.com").submit(); appPage.assertCurrent(); // Try to go back to tab 1. We should have ALREADY_LOGGED_IN info page @@ -184,7 +185,8 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { // Login success now loginPage.login("login-test", "password"); updatePasswordPage.changePassword("password", "password"); - updateProfilePage.update("John", "Doe3", "john@doe3.com"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") + .email("john@doe3.com").submit(); appPage.assertCurrent(); } @@ -208,7 +210,8 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { // Login success now loginPage.login("login-test", "password"); updatePasswordPage.changePassword("password", "password"); - updateProfilePage.update("John", "Doe3", "john@doe3.com"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") + .email("john@doe3.com").submit(); appPage.assertCurrent(); } @@ -233,7 +236,8 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { updatePasswordPage.assertCurrent(); updatePasswordPage.changePassword("password", "password"); - updateProfilePage.update("John", "Doe3", "john@doe3.com"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") + .email("john@doe3.com").submit(); appPage.assertCurrent(); } @@ -271,7 +275,8 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { updatePasswordPage.assertCurrent(); updatePasswordPage.changePassword("password", "password"); - updateProfilePage.update("John", "Doe3", "john@doe3.com"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") + .email("john@doe3.com").submit(); appPage.assertCurrent(); } @@ -297,7 +302,8 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { driver.navigate().to(tab1Url); loginPage.login("login-test", "password"); updatePasswordPage.changePassword("password", "password"); - updateProfilePage.update("John", "Doe3", "john@doe3.com"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") + .email("john@doe3.com").submit(); // Assert I am redirected to the appPage in tab1 appPage.assertCurrent(); @@ -332,7 +338,8 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { driver.navigate().to(tab1Url); loginPage.login("login-test", "password"); updatePasswordPage.changePassword("password", "password"); - updateProfilePage.update("John", "Doe3", "john@doe3.com"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") + .email("john@doe3.com").submit(); // Assert I am redirected to the appPage in tab1 and have state corresponding to tab1 appPage.assertCurrent(); @@ -365,7 +372,8 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { // Continue in tab2 and finish login here loginPage.login("login-test", "password"); updatePasswordPage.changePassword("password", "password"); - updateProfilePage.update("John", "Doe3", "john@doe3.com"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") + .email("john@doe3.com").submit(); // Assert I am redirected to the appPage in tab2 and have state corresponding to tab2 appPage.assertCurrent(); @@ -407,7 +415,8 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { updatePasswordPage.assertCurrent(); updatePasswordPage.changePassword("password", "password"); - updateProfilePage.update("John", "Doe3", "john@doe3.com"); + updateProfilePage.prepareUpdate().firstName("John").lastName("Doe3") + .email("john@doe3.com").submit(); appPage.assertCurrent(); // Try to go back to tab 1. We should have ALREADY_LOGGED_IN info page diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java index 7b1ca0d008..ca4aca7638 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java @@ -993,4 +993,4 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { testRealm.users().get(userId).update(ur); } -} \ No newline at end of file +} diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/PersonalInfoPage.java b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/PersonalInfoPage.java index 65877a691d..3856d55150 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/PersonalInfoPage.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/main/java/org/keycloak/testsuite/ui/account2/page/PersonalInfoPage.java @@ -17,14 +17,8 @@ package org.keycloak.testsuite.ui.account2.page; -import org.keycloak.representations.idm.UserRepresentation; -import org.openqa.selenium.By; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.ui.Select; - +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import static org.keycloak.testsuite.util.UIAssert.assertElementDisabled; import static org.keycloak.testsuite.util.UIAssert.assertInputElementValid; import static org.keycloak.testsuite.util.UIUtils.clickLink; @@ -32,7 +26,11 @@ import static org.keycloak.testsuite.util.UIUtils.getTextInputValue; import static org.keycloak.testsuite.util.UIUtils.isElementVisible; import static org.keycloak.testsuite.util.UIUtils.setTextInputValue; -import static org.junit.Assert.assertEquals; +import org.keycloak.representations.idm.UserRepresentation; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.ui.Select; /** * @author Vaclav Muzikar @@ -54,6 +52,8 @@ public class PersonalInfoPage extends AbstractLoggedInPage { private WebElement cancelBtn; @FindBy(id = "delete-account") private WebElement deleteAccountSection; + @FindBy(id = "update-email-btn") + private WebElement updateEmailLink; @Override public String getPageId() { @@ -88,6 +88,18 @@ public class PersonalInfoPage extends AbstractLoggedInPage { assertInputElementValid(expected, email); } + public void assertUpdateEmailLinkVisible(boolean expected){ + if (updateEmailLink == null) { + assertFalse(expected); + return; + } + assertEquals(expected, isElementVisible(updateEmailLink)); + } + + public void clickUpdateEmailLink(){ + clickLink(updateEmailLink); + } + public String getFirstName() { return getTextInputValue(firstName); } @@ -149,7 +161,6 @@ public class PersonalInfoPage extends AbstractLoggedInPage { public void setValues(UserRepresentation user, boolean includeUsername) { if (includeUsername) {setUsername(user.getUsername());} - setEmail(user.getEmail()); setFirstName(user.getFirstName()); setLastName(user.getLastName()); } diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/LDAPAccountTest.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/LDAPAccountTest.java index 866996ae38..68e5e7eeda 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/LDAPAccountTest.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/LDAPAccountTest.java @@ -17,12 +17,24 @@ package org.keycloak.testsuite.ui.account2; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD; +import static org.keycloak.testsuite.admin.Users.setPasswordFor; +import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; + import org.jboss.arquillian.graphene.page.Page; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.models.LDAPConstants; import org.keycloak.models.RealmModel; import org.keycloak.models.credential.PasswordCredentialModel; -import org.keycloak.representations.idm.*; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.idm.model.LDAPObject; @@ -32,18 +44,6 @@ import org.keycloak.testsuite.ui.account2.page.PersonalInfoPage; import org.keycloak.testsuite.ui.account2.page.SigningInPage; import org.keycloak.testsuite.util.LDAPRule; import org.keycloak.testsuite.util.LDAPTestUtils; -import org.junit.Before; -import org.junit.Test; -import org.junit.ClassRule; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertNotNull; -import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD; -import static org.keycloak.testsuite.admin.Users.setPasswordFor; -import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; /** * @author Alfredo Moises Boullosa diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/PersonalInfoTest.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/PersonalInfoTest.java index 7f4f8e3d0a..1877e66fe2 100644 --- a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/PersonalInfoTest.java +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/PersonalInfoTest.java @@ -17,6 +17,16 @@ package org.keycloak.testsuite.ui.account2; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.admin.Users.setPasswordFor; +import static org.keycloak.testsuite.util.UIUtils.refreshPageAndWaitForLoad; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Test; @@ -28,15 +38,6 @@ import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage; import org.keycloak.testsuite.ui.account2.page.PersonalInfoPage; import org.keycloak.testsuite.util.UserBuilder; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.junit.Assert.*; -import static org.keycloak.testsuite.admin.Users.setPasswordFor; -import static org.keycloak.testsuite.util.UIUtils.refreshPageAndWaitForLoad; - /** * @author Vaclav Muzikar */ diff --git a/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/UpdateEmailTest.java b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/UpdateEmailTest.java new file mode 100644 index 0000000000..68d7759c99 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/base-ui/src/test/java/org/keycloak/testsuite/ui/account2/UpdateEmailTest.java @@ -0,0 +1,164 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.ui.account2; + +import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.util.UIUtils.refreshPageAndWaitForLoad; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.common.Profile; +import org.keycloak.events.Details; +import org.keycloak.events.EventType; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RequiredActionProviderRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.auth.page.login.UpdateEmailPage; +import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage; +import org.keycloak.testsuite.ui.account2.page.PersonalInfoPage; + +@EnableFeature(Profile.Feature.UPDATE_EMAIL) +public class UpdateEmailTest extends BaseAccountPageTest { + + @Page + private PersonalInfoPage personalInfoPage; + + @Page + private UpdateEmailPage updateEmailPage; + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Override + protected AbstractLoggedInPage getAccountPage() { + return personalInfoPage; + } + + @Before + public void setup() { + enableUpdateEmailRequiredAction(); + } + + @After + public void clean() { + disableUpdateEmailRequiredAction(); + disableRegistration(); + } + + @Test + public void updateEmailLinkVisibleWithUpdateEmailActionEnabled() { + refreshPageAndWaitForLoad(); + personalInfoPage.assertUpdateEmailLinkVisible(true); + } + + @Test + public void updateEmailLinkNotVisibleWithoutUpdateEmailActionEnabled() { + disableUpdateEmailRequiredAction(); + refreshPageAndWaitForLoad(); + personalInfoPage.assertUpdateEmailLinkVisible(false); + } + + @Test + public void updateEmailLinkVisibleWithUpdateEmailActionEnabledAndRegistrationEmailAsUsernameAndEditUsernameNotAllowed() { + enableRegistration(true, false); + refreshPageAndWaitForLoad(); + personalInfoPage.assertUpdateEmailLinkVisible(false); + } + + @Test + public void updateUserInfoWithRegistrationEnabled() { + enableRegistration(false, true); + refreshPageAndWaitForLoad(); + + assertTrue(personalInfoPage.valuesEqual(testUser)); + personalInfoPage.assertSaveDisabled(false); + + UserRepresentation newInfo = new UserRepresentation(); + newInfo.setUsername(testUser.getUsername()); + newInfo.setEmail(testUser.getEmail()); + newInfo.setFirstName("New First"); + newInfo.setLastName("New Last"); + + personalInfoPage.setValues(newInfo, true); + assertTrue(personalInfoPage.valuesEqual(newInfo)); + personalInfoPage.assertSaveDisabled(false); + personalInfoPage.clickSave(); + personalInfoPage.alert().assertSuccess(); + personalInfoPage.assertSaveDisabled(false); + + personalInfoPage.navigateTo(); + personalInfoPage.valuesEqual(newInfo); + } + + @Test + public void aiaCancellationSucceeds() { + refreshPageAndWaitForLoad(); + personalInfoPage.assertUpdateEmailLinkVisible(true); + personalInfoPage.clickUpdateEmailLink(); + Assert.assertTrue(updateEmailPage.isCurrent()); + updateEmailPage.clickCancelAIA(); + Assert.assertTrue(personalInfoPage.isCurrent()); + } + + @Test + public void updateEmailSucceeds() { + personalInfoPage.navigateTo(); + personalInfoPage.assertUpdateEmailLinkVisible(true); + personalInfoPage.clickUpdateEmailLink(); + Assert.assertTrue(updateEmailPage.isCurrent()); + updateEmailPage.changeEmail("new-email@example.org"); + events.expectAccount(EventType.UPDATE_EMAIL).detail(Details.UPDATED_EMAIL, "new-email@example.org"); + Assert.assertEquals("new-email@example.org", testRealmResource().users().get(testUser.getId()).toRepresentation().getEmail()); + } + + private void disableUpdateEmailRequiredAction() { + RequiredActionProviderRepresentation updateEmail = testRealmResource().flows().getRequiredAction(UserModel.RequiredAction.UPDATE_EMAIL.name()); + updateEmail.setEnabled(false); + testRealmResource().flows().updateRequiredAction(UserModel.RequiredAction.UPDATE_EMAIL.name(), updateEmail); + } + + private void enableUpdateEmailRequiredAction() { + RequiredActionProviderRepresentation updateEmail = testRealmResource().flows().getRequiredAction(UserModel.RequiredAction.UPDATE_EMAIL.name()); + updateEmail.setEnabled(true); + testRealmResource().flows().updateRequiredAction(UserModel.RequiredAction.UPDATE_EMAIL.name(), updateEmail); + } + + private void enableRegistration(boolean emailAsUsername, boolean usernameEditionAllowed) { + RealmRepresentation realmRepresentation = testRealmResource().toRepresentation(); + realmRepresentation.setRegistrationAllowed(true); + realmRepresentation.setRegistrationEmailAsUsername(emailAsUsername); + realmRepresentation.setEditUsernameAllowed(usernameEditionAllowed); + testRealmResource().update(realmRepresentation); + } + + private void disableRegistration() { + RealmRepresentation realmRepresentation = testRealmResource().toRepresentation(); + realmRepresentation.setRegistrationAllowed(false); + realmRepresentation.setRegistrationEmailAsUsername(false); + realmRepresentation.setEditUsernameAllowed(false); + testRealmResource().update(realmRepresentation); + } + +} diff --git a/themes/src/main/resources-community/theme/base/email/messages/messages_fr.properties b/themes/src/main/resources-community/theme/base/email/messages/messages_fr.properties index 6a21384f18..83a4fa6a79 100644 --- a/themes/src/main/resources-community/theme/base/email/messages/messages_fr.properties +++ b/themes/src/main/resources-community/theme/base/email/messages/messages_fr.properties @@ -1,6 +1,9 @@ emailVerificationSubject=V\u00e9rification du courriel emailVerificationBody=Quelqu''un vient de cr\u00e9er un compte "{2}" avec votre courriel. Si vous \u00eates \u00e0 l''origine de cette requ\u00eate, veuillez cliquer sur le lien ci-dessous afin de v\u00e9rifier votre adresse de courriel\n\n{0}\n\nCe lien expire dans {3}.\n\nSinon, veuillez ignorer ce message. emailVerificationBodyHtml=

Quelqu''un vient de cr\u00e9er un compte "{2}" avec votre courriel. Si vous \u00eates \u00e0 l''origine de cette requ\u00eate, veuillez cliquer sur le lien ci-dessous afin de v\u00e9rifier votre adresse de courriel

{0}

Ce lien expire dans {3}.

Sinon, veuillez ignorer ce message.

+emailUpdateConfirmationSubject=V\u00e9rification du nouveau courriel +emailUpdateConfirmationBody=Afin d''utiliser le courriel {1} dans votre compte {2}, cliquez sur le lien ci-dessous\n\n{0}\n\nCe lien expire dans {3}.\n\nSinon, veuillez ignorer ce message. +emailUpdateConfirmationBodyHtml=

Afin d''utiliser le courriel {1} dans votre compte {2}, cliquez sur le lien ci-dessous

{0}

Ce lien expirera dans {3}.

Sinon, veuillez ignorer ce message.

passwordResetSubject=R\u00e9initialiser le mot de passe passwordResetBody=Quelqu''un vient de demander une r\u00e9initialisation de mot de passe pour votre compte {2}. Si vous \u00eates \u00e0 l''origine de cette requ\u00eate, veuillez cliquer sur le lien ci-dessous pour le mettre \u00e0 jour.\n\n{0}\n\nCe lien expire dans {3}.\n\nSinon, veuillez ignorer ce message ; aucun changement ne sera effectu\u00e9 sur votre compte. passwordResetBodyHtml=

Quelqu''un vient de demander une r\u00e9initialisation de mot de passe pour votre compte {2}. Si vous \u00eates \u00e0 l''origine de cette requ\u00eate, veuillez cliquer sur le lien ci-dessous pour le mettre \u00e0 jour.

Lien pour r\u00e9initialiser votre mot de passe

Ce lien expire dans {3}.

Sinon, veuillez ignorer ce message ; aucun changement ne sera effectu\u00e9 sur votre compte.

diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_cs.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_cs.properties index 11a3c7eef6..7b8c7cc143 100644 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_cs.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_cs.properties @@ -421,3 +421,5 @@ frontchannel-logout.message=Odhlašujete se z následujících aplikací logoutConfirmTitle=Odhlašování logoutConfirmHeader=Chcete se odhlásit? doLogout=Odhlásit + +readOnlyUsernameMessage=Nemůžete aktualizovat své uživatelské jméno, protože je pouze pro čtení. diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_da.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_da.properties index 1eba02d8c6..89e5394b9a 100644 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_da.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_da.properties @@ -358,4 +358,6 @@ webauthn-error-different-user=Den første authenticatede bruger er ikke den der webauthn-error-auth-verification=Resultatet fra log ind med sikkerhedsnøgle er ugyldigt. webauthn-error-register-verification=Resultatet fra registrering med sikkerhedsnøglen er ugyldigt. webauthn-error-user-not-found=Ukendt bruger authenticated med sikkerhedsnøglen. -identity-provider-redirector=Forbind med en anden Identitetsudbyder \ No newline at end of file +identity-provider-redirector=Forbind med en anden Identitetsudbyder + +readOnlyUsernameMessage=Du kan ikke opdatere dit brugernavn da det er read-only. diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_de.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_de.properties index b12fe1c4c6..4195aa71df 100755 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_de.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_de.properties @@ -378,3 +378,5 @@ errasingData=L\u00F6schen aller Ihrer Daten loggingOutImmediately=Sofortige Abmeldung accountUnusable=Eine sp\u00E4tere Nutzung der Anwendung ist mit diesem Konto nicht mehr m\u00F6glich userDeletedSuccessfully=Nutzer erfolgreich gel\u00F6scht + +readOnlyUsernameMessage=Sie k\u00F6nnen Ihren Benutzernamen nicht \u00E4ndern, da er schreibgesch\u00FCtzt ist. diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_fi.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_fi.properties index f22eb3dcb0..f2c17af2e8 100644 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_fi.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_fi.properties @@ -438,4 +438,6 @@ loggingOutImmediately=Sinut kirjataan ulos välittömästi accountUnusable=Tämän sovelluksen käyttö ei myöhemmin enää ole mahdollista tällä käyttäjätilillä userDeletedSuccessfully=Käyttäjä poistettu onnistuneesti -access-denied=Pääsy evätty \ No newline at end of file +access-denied=Pääsy evätty + +readOnlyUsernameMessage=Et voi päivittää käyttäjänimeäsi, koska se on "vain-luku"-tilassa. diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_fr.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_fr.properties index 5f577b4683..2487d5138c 100644 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_fr.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_fr.properties @@ -34,6 +34,11 @@ errorTitle=Nous sommes d\u00e9sol\u00e9s... errorTitleHtml=Nous sommes d\u00e9sol\u00e9s... emailVerifyTitle=V\u00e9rification du courriel emailForgotTitle=Mot de passe oubli\u00e9 ? +updateEmailTitle=Mise \u00e0 jour du courriel +emailUpdateConfirmationSentTitle=Courriel de confirmation envoy\u00e9 +emailUpdateConfirmationSent=Un courriel de confirmation a \u00e9t\u00e9 envoy\u00e9 \u00e0 {0}. Vous devez suivre les instructions de ce dernier afin de compl\u00e9ter la mise \u00e0 jour. +emailUpdatedTitle=Adresse de courriel mis \u00e0 jour +emailUpdated=La mise \u00e0 jour de votre adresse de courriel vers {0} a \u00e9t\u00e9 compl\u00e9t\u00e9e avec succ\u00e8s. updatePasswordTitle=Mise \u00e0 jour du mot de passe codeSuccessTitle=Code succ\u00e8s codeErrorTitle=Code d''erreur \: {0} @@ -184,6 +189,7 @@ confirmLinkIdpContinue=Souhaitez-vous lier {0} \u00e0 votre compte existant configureTotpMessage=Vous devez configurer l''authentification par mobile pour activer votre compte. updateProfileMessage=Vous devez mettre \u00e0 jour votre profil pour activer votre compte. updatePasswordMessage=Vous devez changer votre mot de passe pour activer votre compte. +updateEmailMessage=Vous devez mettre \u00e0 votre addresse de courriel pour activer votre compte. resetPasswordMessage=Vous devez changer votre mot de passe. verifyEmailMessage=Vous devez v\u00e9rifier votre courriel pour activer votre compte. linkIdpMessage=Vous devez v\u00e9rifier votre courriel pour lier votre compte avec {0}. diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_hu.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_hu.properties index e2d7f3d21d..0a20d6c469 100644 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_hu.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_hu.properties @@ -352,3 +352,5 @@ webauthn-error-user-not-found=Ismeretlen felhasználót hitelesítettünk a bizt identity-provider-redirector=Összekötés másik személyazonosság-kezelővel identity-provider-login-label=Egyéb bejelentkezési módok + +readOnlyUsernameMessage=A felhasználó név nem módosítható. diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_it.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_it.properties index ca2cbdb5d1..91447d2278 100644 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_it.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_it.properties @@ -350,3 +350,5 @@ webauthn-error-register-verification=Il risultato della registrazione della chia webauthn-error-user-not-found=Utente sconosciuto autenticato con la chiave di sicurezza. identity-provider-redirector=Connettiti con un altro identity provider. + +readOnlyUsernameMessage=Non puoi aggiornare il tuo nome utente poich\u00E9 \u00e8 in modalit\u00e0 sola lettura. diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_ja.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_ja.properties index eef992538c..f2488189a7 100644 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_ja.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_ja.properties @@ -351,4 +351,6 @@ webauthn-error-auth-verification=セキュリティーキーの認証結果が webauthn-error-register-verification=セキュリティーキーの登録結果が無効です。 webauthn-error-user-not-found=セキュリティーキーで認証された不明なユーザー。 -identity-provider-redirector=別のアイデンティティー・プロバイダーと接続する \ No newline at end of file +identity-provider-redirector=別のアイデンティティー・プロバイダーと接続する + +readOnlyUsernameMessage=読み取り専用のため、ユーザー名を更新することはできません。 diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_pl.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_pl.properties index a754996c05..d8b649eb67 100644 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_pl.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_pl.properties @@ -314,3 +314,5 @@ console-verify-email=Musisz zweryfikować swój adres e-mail. Wiadomość e-mai console-email-code=Kod z e-mail\: console-accept-terms=Akceptujesz warunki? [t/n]\: console-accept=t + +readOnlyUsernameMessage=Zmiana nazwy użytkownika nie jest możliwa, ponieważ edycja konta jest zablokowana. diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_pt_BR.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_pt_BR.properties index 796bf29cc2..0ac2f05c1b 100755 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_pt_BR.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_pt_BR.properties @@ -378,3 +378,4 @@ loggingOutImmediately=Sair da aplica\u00e7\u00e3o imediatamente accountUnusable=Qualquer uso subsequente da aplica\u00e7\u00e3o n\u00e3o ser\u00e1 poss\u00edvel com esta conta userDeletedSuccessfully=Usu\u00e1rio exclu\u00eddo com sucesso +readOnlyUsernameMessage=Voc\u00ea^n\u00e3o pode atualizar o seu nome de usu\u00e1rio, uma vez que \u00e9 apenas de leitura. diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_sk.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_sk.properties index 7734b5bfb4..62d29de540 100644 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_sk.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_sk.properties @@ -261,3 +261,5 @@ noCertificate=[Bez certifikátu] pageNotFound=Stránka nebola nájdená internalServerError=Vyskytla sa interná chyba servera + +readOnlyUsernameMessage=Nemôžete aktualizovať svoje používateľské meno, pretože je iba na čítanie. diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_tr.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_tr.properties index 7d6c7a0479..a465a15119 100755 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_tr.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_tr.properties @@ -292,3 +292,5 @@ console-verify-email=E-posta adresinizi do\u011Frulaman\u0131z gerekiyor. Bir do console-email-code=E-posta Kodu: console-accept-terms=\u015Eartlar\u0131 kabul et? [e/h]: console-accept=e + +readOnlyUsernameMessage=Yazma korumal\u0131 oldu\u011Fundan kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 de\u011Fi\u015Ftiremezsiniz. diff --git a/themes/src/main/resources-community/theme/keycloak.v2/account/messages/messages_fr.properties b/themes/src/main/resources-community/theme/keycloak.v2/account/messages/messages_fr.properties index 0dbac3485c..8a74dd3bc5 100644 --- a/themes/src/main/resources-community/theme/keycloak.v2/account/messages/messages_fr.properties +++ b/themes/src/main/resources-community/theme/keycloak.v2/account/messages/messages_fr.properties @@ -40,3 +40,5 @@ thirdPartyApp=Tierce inUse=Utilis\u00e9(e) notInUse=Non utilis\u00e9(e) setUpNew=Configurer {0} + +updateEmail=Modifier le courriel diff --git a/themes/src/main/resources/theme/base/email/html/email-update-confirmation.ftl b/themes/src/main/resources/theme/base/email/html/email-update-confirmation.ftl new file mode 100644 index 0000000000..583308e047 --- /dev/null +++ b/themes/src/main/resources/theme/base/email/html/email-update-confirmation.ftl @@ -0,0 +1,5 @@ + + +${kcSanitize(msg("emailUpdateConfirmationBodyHtml",link, newEmail, realmName, linkExpirationFormatter(linkExpiration)))?no_esc} + + diff --git a/themes/src/main/resources/theme/base/email/messages/messages_en.properties b/themes/src/main/resources/theme/base/email/messages/messages_en.properties index 132fe2516e..ad6b61f35d 100755 --- a/themes/src/main/resources/theme/base/email/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/email/messages/messages_en.properties @@ -1,6 +1,9 @@ emailVerificationSubject=Verify email emailVerificationBody=Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {3}.\n\nIf you didn''t create this account, just ignore this message. emailVerificationBodyHtml=

Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address

Link to e-mail address verification

This link will expire within {3}.

If you didn''t create this account, just ignore this message.

+emailUpdateConfirmationSubject=Verify new email +emailUpdateConfirmationBody=To update your {2} account with email address {1}, click the link below\n\n{0}\n\nThis link will expire within {3}.\n\nIf you don''t want to proceed with this modification, just ignore this message. +emailUpdateConfirmationBodyHtml=

To update your {2} account with email address {1}, click the link below

{0}

This link will expire within {3}.

If you don''t want to proceed with this modification, just ignore this message.

emailTestSubject=[KEYCLOAK] - SMTP test message emailTestBody=This is a test message emailTestBodyHtml=

This is a test message

diff --git a/themes/src/main/resources/theme/base/email/text/email-update-confirmation.ftl b/themes/src/main/resources/theme/base/email/text/email-update-confirmation.ftl new file mode 100644 index 0000000000..335d9a848d --- /dev/null +++ b/themes/src/main/resources/theme/base/email/text/email-update-confirmation.ftl @@ -0,0 +1,2 @@ +<#ftl output_format="plainText"> +${msg("emailUpdateConfirmationBody",link, newEmail, realmName, linkExpirationFormatter(linkExpiration))} diff --git a/themes/src/main/resources/theme/base/login/login-update-profile.ftl b/themes/src/main/resources/theme/base/login/login-update-profile.ftl index 3a8610a55e..be579b01bd 100755 --- a/themes/src/main/resources/theme/base/login/login-update-profile.ftl +++ b/themes/src/main/resources/theme/base/login/login-update-profile.ftl @@ -23,23 +23,25 @@ -
-
- -
-
- + <#if user.editEmailAllowed> +
+
+ +
+
+ - <#if messagesPerField.existsError('email')> - - ${kcSanitize(messagesPerField.get('email'))?no_esc} - - + <#if messagesPerField.existsError('email')> + + ${kcSanitize(messagesPerField.get('email'))?no_esc} + + +
-
+
diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties index 4b5d2a77ea..d243324c73 100755 --- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -44,6 +44,11 @@ errorTitle=We are sorry... errorTitleHtml=We are sorry ... emailVerifyTitle=Email verification emailForgotTitle=Forgot Your Password? +updateEmailTitle=Update email +emailUpdateConfirmationSentTitle=Confirmation email sent +emailUpdateConfirmationSent=A confirmation email has been sent to {0}. You must follow the instructions of the former to complete the email update. +emailUpdatedTitle=Email updated +emailUpdated=The account email has been successfully updated to {0}. updatePasswordTitle=Update password codeSuccessTitle=Success code codeErrorTitle=Error code\: {0} @@ -264,6 +269,7 @@ configureTotpMessage=You need to set up Mobile Authenticator to activate your ac configureBackupCodesMessage=You need to set up Backup Codes to activate your account. updateProfileMessage=You need to update your user profile to activate your account. updatePasswordMessage=You need to change your password to activate your account. +updateEmailMessage=You need to update your email address to activate your account. resetPasswordMessage=You need to change your password. verifyEmailMessage=You need to verify your email address to activate your account. linkIdpMessage=You need to verify your email address to link your account with {0}. @@ -487,3 +493,4 @@ logoutConfirmTitle=Logging out logoutConfirmHeader=Do you want to logout? doLogout=Logout +readOnlyUsernameMessage=You can''t update your username as it is read-only. diff --git a/themes/src/main/resources/theme/base/login/update-email.ftl b/themes/src/main/resources/theme/base/login/update-email.ftl new file mode 100644 index 0000000000..4c85e5b5da --- /dev/null +++ b/themes/src/main/resources/theme/base/login/update-email.ftl @@ -0,0 +1,42 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('email'); section> + <#if section = "header"> + ${msg("updateEmailTitle")} + <#elseif section = "form"> +
+
+
+ +
+
+ + + <#if messagesPerField.existsError('email')> + + ${kcSanitize(messagesPerField.get('email'))?no_esc} + + +
+
+ +
+
+
+
+
+ +
+ <#if isAppInitiatedAction??> + + + <#else> + + +
+
+
+ + diff --git a/themes/src/main/resources/theme/keycloak.v2/account/index.ftl b/themes/src/main/resources/theme/keycloak.v2/account/index.ftl index 76d65c6347..80db491936 100644 --- a/themes/src/main/resources/theme/keycloak.v2/account/index.ftl +++ b/themes/src/main/resources/theme/keycloak.v2/account/index.ftl @@ -46,7 +46,9 @@ isEventsEnabled : ${isEventsEnabled?c}, isMyResourcesEnabled : ${(realm.userManagedAccessAllowed && isAuthorizationEnabled)?c}, isTotpConfigured : ${isTotpConfigured?c}, - deleteAccountAllowed : ${deleteAccountAllowed?c} + deleteAccountAllowed : ${deleteAccountAllowed?c}, + updateEmailFeatureEnabled: ${updateEmailFeatureEnabled?c}, + updateEmailActionEnabled: ${updateEmailActionEnabled?c} } var availableLocales = []; diff --git a/themes/src/main/resources/theme/keycloak.v2/account/messages/messages_en.properties b/themes/src/main/resources/theme/keycloak.v2/account/messages/messages_en.properties index b3d677bd81..369f365ff7 100644 --- a/themes/src/main/resources/theme/keycloak.v2/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/keycloak.v2/account/messages/messages_en.properties @@ -162,3 +162,5 @@ error-user-attribute-required=Please specify ''{0}''. error-invalid-date=''{0}'' is invalid date. error-username-invalid-character=''{0}'' contains invalid character. error-person-name-invalid-character='{0}' contains invalid character. + +updateEmail=Update email diff --git a/themes/src/main/resources/theme/keycloak.v2/account/src/app/content/account-page/AccountPage.tsx b/themes/src/main/resources/theme/keycloak.v2/account/src/app/content/account-page/AccountPage.tsx index d891946c7a..625b7298ca 100644 --- a/themes/src/main/resources/theme/keycloak.v2/account/src/app/content/account-page/AccountPage.tsx +++ b/themes/src/main/resources/theme/keycloak.v2/account/src/app/content/account-page/AccountPage.tsx @@ -14,14 +14,15 @@ * limitations under the License. */ import * as React from 'react'; -import { ActionGroup, - Button, - Form, - FormGroup, - TextInput, - Grid, - GridItem, - ExpandableSection, +import { ActionGroup, + Button, + Form, + FormGroup, + TextInput, + InputGroup, + Grid, + GridItem, + ExpandableSection, ValidatedOptions, PageSection, PageSectionVariants, @@ -40,6 +41,7 @@ import { LocaleSelector } from '../../widgets/LocaleSelectors'; import { KeycloakContext } from '../../keycloak-service/KeycloakContext'; import { KeycloakService } from '../../keycloak-service/keycloak.service'; import { AIACommand } from '../../util/AIACommand'; +import {ExternalLinkSquareAltIcon} from "@patternfly/react-icons"; declare const features: Features; declare const locale: string; @@ -69,6 +71,8 @@ export class AccountPage extends React.Component { + new AIACommand(keycloak, "UPDATE_EMAIL").execute(); + } + public render(): React.ReactNode { const fields: FormFields = this.state.formFields; return ( @@ -189,8 +197,8 @@ export class AccountPage extends React.Component )} - - + } + {this.isUpdateEmailFeatureEnabled && + + + + {this.isUpdateEmailActionEnabled && (!this.isRegistrationEmailAsUsername || this.isEditUserNameAllowed) && + + { (keycloak: KeycloakService) => ( + + )} + + } + + }