KEYCLOAK-6455 Ability to require email to be verified before changing (#7943)

Closes #11875
This commit is contained in:
Réda Housni Alaoui 2022-05-09 18:52:22 +02:00 committed by GitHub
parent 76f83f0ab2
commit 5d87cdf1c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 1976 additions and 277 deletions

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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;

View file

@ -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;

View file

@ -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);
}

View file

@ -27,5 +27,9 @@ import java.util.Map;
*/
public interface EmailSenderProvider extends Provider {
void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException;
default void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
send(config, user.getEmail(), subject, textBody, htmlBody);
}
void send(Map<String, String> config, String address, String subject, String textBody, String htmlBody) throws EmailException;
}

View file

@ -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
*

View file

@ -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;
}

View file

@ -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;

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -305,7 +305,8 @@ public interface UserModel extends RoleMapperModel {
CONFIGURE_RECOVERY_AUTHN_CODES,
UPDATE_PASSWORD,
TERMS_AND_CONDITIONS,
VERIFY_PROFILE
VERIFY_PROFILE,
UPDATE_EMAIL
}
/**

View file

@ -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;
}
}

View file

@ -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<UpdateEmailActionToken> {
public UpdateEmailActionTokenHandler() {
super(UpdateEmailActionToken.TOKEN_TYPE, UpdateEmailActionToken.class, Messages.STALE_VERIFY_EMAIL_LINK,
EventType.EXECUTE_ACTIONS, Errors.INVALID_TOKEN);
}
@Override
public TokenVerifier.Predicate<? super UpdateEmailActionToken>[] getVerifiers(
ActionTokenContext<UpdateEmailActionToken> 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<UpdateEmailActionToken> 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<FormMessage> 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<UpdateEmailActionToken> tokenContext) {
return false;
}
}

View file

@ -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);
}

View file

@ -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<String, String> 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<FormMessage> 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<String, String> 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);
}
}

View file

@ -40,6 +40,8 @@ public interface UpdateProfileContext {
void setUsername(String username);
boolean isEditEmailAllowed();
String getEmail();
void setEmail(String email);

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -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();

View file

@ -61,9 +61,13 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
@Override
public void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
send(config, retrieveEmailAddress(user), subject, textBody, htmlBody);
}
@Override
public void send(Map<String, String> config, String address, String subject, String textBody, String htmlBody) throws EmailException {
Transport transport = null;
try {
String address = retrieveEmailAddress(user);
Properties props = new Properties();

View file

@ -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<String, Object> 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<Object> subjectAttributes, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException {
send(subjectFormatKey, subjectAttributes, bodyTemplate, bodyAttributes, null);
}
protected void send(String subjectFormatKey, List<Object> subjectAttributes, String bodyTemplate, Map<String, Object> 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<String, String> config, String subject, String textBody, String htmlBody) throws EmailException {
send(config, subject, textBody, htmlBody, null);
}
protected void send(Map<String, String> config, String subject, String textBody, String htmlBody, String address) throws EmailException {
EmailSenderProvider emailSender = session.getProvider(EmailSenderProvider.class);
if (address == null) {
emailSender.send(config, user, subject, textBody, htmlBody);
} else {
emailSender.send(config, address, subject, textBody, htmlBody);
}
}
@Override

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -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);

View file

@ -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:

View file

@ -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<String, String> formData;
public EmailBean(UserModel user, MultivaluedMap<String, String> formData) {
this.user = user;
this.formData = formData;
}
public String getValue() {
return formData != null ? formData.getFirst("email") : user.getEmail();
}
}

View file

@ -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() {

View file

@ -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";

View file

@ -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);

View file

@ -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;
/**
* <p>A base class for {@link UserProfileProvider} implementations providing the main hooks for customizations.
@ -97,11 +97,21 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
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<String> readOnlyAttributes = new ArrayList<>(Arrays.asList(builtinReadOnlyAttributes));
@ -177,6 +187,9 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
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)));
}
@ -305,6 +318,8 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
new AttributeValidatorMetadata(UsernameMutationValidator.ID)).setAttributeDisplayName("${username}");
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))

View file

@ -26,3 +26,4 @@ org.keycloak.authentication.requiredactions.UpdateUserLocaleAction
org.keycloak.authentication.requiredactions.DeleteAccount
org.keycloak.authentication.requiredactions.VerifyUserProfile
org.keycloak.authentication.requiredactions.RecoveryAuthnCodesAction
org.keycloak.authentication.requiredactions.UpdateEmail

View file

@ -2,3 +2,4 @@ org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHan
org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionTokenHandler
org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionTokenHandler
org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionTokenHandler
org.keycloak.authentication.actiontoken.updateemail.UpdateEmailActionTokenHandler

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -56,30 +56,16 @@ public class LoginUpdateProfilePage extends AbstractPage {
@FindBy(className = "alert-error")
private WebElement loginAlertErrorMessage;
public void update(String firstName, String lastName) {
prepareUpdate().firstName(firstName).lastName(lastName).submit();
}
public void update(String firstName, String lastName, String email) {
updateWithDepartment(firstName, lastName, null, email);
prepareUpdate().firstName(firstName).lastName(lastName).email(email).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 Update prepareUpdate() {
return new Update(this);
}
public void cancel() {
@ -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 {

View file

@ -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());
}
}

View file

@ -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();
}
}

View file

@ -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());
}
}

View file

@ -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();
}
}

View file

@ -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();

View file

@ -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")

View file

@ -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 <a href="mailto:wadahiro@gmail.com">Hiroyuki Wada</a>
*/
@ -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")

View file

@ -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()));
}
}

View file

@ -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();
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -107,7 +106,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.PREVIOUS_FIRST_NAME, "Tom").detail(Details.UPDATED_FIRST_NAME, "New first")
.detail(Details.PREVIOUS_LAST_NAME, "Brady").detail(Details.UPDATED_LAST_NAME, "New last")
@ -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();

View file

@ -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));

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ -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();
}

View file

@ -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;

View file

@ -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)

View file

@ -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();

View file

@ -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

View file

@ -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 <vmuzikar@redhat.com>
@ -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());
}

View file

@ -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 <aboullos@redhat.com>

View file

@ -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 <vmuzikar@redhat.com>
*/

View file

@ -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);
}
}

View file

@ -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=<p>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</p><p><a href="{0}">{0}</a></p><p>Ce lien expire dans {3}.</p><p>Sinon, veuillez ignorer ce message.</p>
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=<p>Afin d''utiliser le courriel {1} dans votre compte {2}, cliquez sur le lien ci-dessous</p><p><a href="{0}">{0}</a></p><p>Ce lien expirera dans {3}.</p><p>Sinon, veuillez ignorer ce message.</p>
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=<p>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.</p><p><a href="{0}">Lien pour r\u00e9initialiser votre mot de passe</a></p><p>Ce lien expire dans {3}.</p><p>Sinon, veuillez ignorer ce message ; aucun changement ne sera effectu\u00e9 sur votre compte.</p>

View file

@ -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í.

View file

@ -359,3 +359,5 @@ webauthn-error-auth-verification=Resultatet fra log ind med sikkerhedsnøgle er
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
readOnlyUsernameMessage=Du kan ikke opdatere dit brugernavn da det er read-only.

View file

@ -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.

View file

@ -439,3 +439,5 @@ accountUnusable=Tämän sovelluksen käyttö ei myöhemmin enää ole mahdollist
userDeletedSuccessfully=Käyttäjä poistettu onnistuneesti
access-denied=Pääsy evätty
readOnlyUsernameMessage=Et voi päivittää käyttäjänimeäsi, koska se on "vain-luku"-tilassa.

View file

@ -34,6 +34,11 @@ errorTitle=Nous sommes d\u00e9sol\u00e9s...
errorTitleHtml=Nous sommes <strong>d\u00e9sol\u00e9s</strong>...
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}.

View file

@ -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ó.

View file

@ -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.

View file

@ -352,3 +352,5 @@ webauthn-error-register-verification=セキュリティーキーの登録結果
webauthn-error-user-not-found=セキュリティーキーで認証された不明なユーザー。
identity-provider-redirector=別のアイデンティティー・プロバイダーと接続する
readOnlyUsernameMessage=読み取り専用のため、ユーザー名を更新することはできません。

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -40,3 +40,5 @@ thirdPartyApp=Tierce
inUse=Utilis\u00e9(e)
notInUse=Non utilis\u00e9(e)
setUpNew=Configurer {0}
updateEmail=Modifier le courriel

View file

@ -0,0 +1,5 @@
<html>
<body>
${kcSanitize(msg("emailUpdateConfirmationBodyHtml",link, newEmail, realmName, linkExpirationFormatter(linkExpiration)))?no_esc}
</body>
</html>

View file

@ -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=<p>Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address</p><p><a href="{0}">Link to e-mail address verification</a></p><p>This link will expire within {3}.</p><p>If you didn''t create this account, just ignore this message.</p>
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=<p>To update your {2} account with email address {1}, click the link below</p><p><a href="{0}">{0}</a></p><p>This link will expire within {3}.</p><p>If you don''t want to proceed with this modification, just ignore this message.</p>
emailTestSubject=[KEYCLOAK] - SMTP test message
emailTestBody=This is a test message
emailTestBodyHtml=<p>This is a test message</p>

View file

@ -0,0 +1,2 @@
<#ftl output_format="plainText">
${msg("emailUpdateConfirmationBody",link, newEmail, realmName, linkExpirationFormatter(linkExpiration))}

View file

@ -23,6 +23,7 @@
</div>
</div>
</#if>
<#if user.editEmailAllowed>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label>
@ -40,6 +41,7 @@
</#if>
</div>
</div>
</#if>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">

View file

@ -44,6 +44,11 @@ errorTitle=We are sorry...
errorTitleHtml=We are <strong>sorry</strong> ...
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.

View file

@ -0,0 +1,42 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('email'); section>
<#if section = "header">
${msg("updateEmailTitle")}
<#elseif section = "form">
<form id="kc-update-email-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="email" name="email" value="${(email.value!'')}"
class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('email')>true</#if>"
/>
<#if messagesPerField.existsError('email')>
<span id="input-error-email" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('email'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
<div class="${properties.kcFormOptionsWrapperClass!}">
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<#if isAppInitiatedAction??>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
<button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" type="submit" name="cancel-aia" value="true" />${msg("doCancel")}</button>
<#else>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
</#if>
</div>
</div>
</form>
</#if>
</@layout.registrationLayout>

View file

@ -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 = [];

View file

@ -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

View file

@ -19,6 +19,7 @@ import { ActionGroup,
Form,
FormGroup,
TextInput,
InputGroup,
Grid,
GridItem,
ExpandableSection,
@ -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<AccountPageProps, AccountPageSt
private isRegistrationEmailAsUsername: boolean = features.isRegistrationEmailAsUsername;
private isEditUserNameAllowed: boolean = features.isEditUserNameAllowed;
private isDeleteAccountAllowed: boolean = features.deleteAccountAllowed;
private isUpdateEmailFeatureEnabled: boolean = features.updateEmailFeatureEnabled;
private isUpdateEmailActionEnabled: boolean = features.updateEmailActionEnabled;
private readonly DEFAULT_STATE: AccountPageState = {
errors: {
username: '',
@ -155,6 +159,10 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
new AIACommand(keycloak, "delete_account").execute();
}
private handleEmailUpdate = (keycloak: KeycloakService): void => {
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<AccountPageProps, AccountPageSt
)}
</FormGroup>
)}
<FormGroup
label={Msg.localize("email")}
{!this.isUpdateEmailFeatureEnabled && <FormGroup
label={Msg.localize('email')}
fieldId="email-address"
helperTextInvalid={this.state.errors.email}
validated={
@ -213,7 +221,35 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
: ValidatedOptions.default
}
></TextInput>
</FormGroup>
</FormGroup> }
{this.isUpdateEmailFeatureEnabled && <FormGroup
label={Msg.localize('email')}
fieldId="email-address"
>
<InputGroup>
<TextInput
isDisabled
type="email"
id="email-address"
name="email"
value={fields.email}
>
</TextInput>
{this.isUpdateEmailActionEnabled && (!this.isRegistrationEmailAsUsername || this.isEditUserNameAllowed) &&
<KeycloakContext.Consumer>
{ (keycloak: KeycloakService) => (
<Button id="update-email-btn"
variant="link"
onClick={() => this.handleEmailUpdate(keycloak)}
icon={<ExternalLinkSquareAltIcon/>}
iconPosition="right">
<Msg msgKey="updateEmail" />
</Button>
)}
</KeycloakContext.Consumer>
}
</InputGroup>
</FormGroup> }
<FormGroup
label={Msg.localize("firstName")}
fieldId="first-name"

View file

@ -24,6 +24,8 @@
isMyResourcesEnabled: boolean;
isTotpConfigured: boolean;
deleteAccountAllowed: boolean;
updateEmailFeatureEnabled: boolean;
updateEmailActionEnabled: boolean;
}

View file

@ -540,7 +540,8 @@ ul#kc-totp-supported-apps {
#kc-form-login div.form-group:last-of-type,
#kc-register-form div.form-group:last-of-type,
#kc-update-profile-form div.form-group:last-of-type {
#kc-update-profile-form div.form-group:last-of-type,
#kc-update-email-form div.form-group:last-of-type{
margin-bottom: 0px;
}