Skip to content

Commit

Permalink
KEYCLOAK-6455 Ability to require email to be verified before changing
Browse files Browse the repository at this point in the history
  • Loading branch information
reda-alaoui committed Apr 20, 2021
1 parent ada7f37 commit cf1596c
Show file tree
Hide file tree
Showing 66 changed files with 1,676 additions and 301 deletions.
Expand Up @@ -271,6 +271,7 @@ protected UserModel importUserToKeycloak(RealmModel realm, String username) {
user.setSingleAttribute(KERBEROS_PRINCIPAL, username + "@" + kerberosConfig.getKerberosRealm());

if (kerberosConfig.isUpdateProfileFirstLogin()) {
user.addRequiredAction(UserModel.RequiredAction.UPDATE_EMAIL);
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
}

Expand Down
Expand Up @@ -27,5 +27,9 @@
*/
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;
}
Expand Up @@ -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
*
Expand Down
Expand Up @@ -26,6 +26,6 @@ public enum LoginFormsPages {
LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL,
OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_SELECT_AUTHENTICATOR, REGISTER, INFO, ERROR, ERROR_WEBAUTHN, LOGIN_UPDATE_PROFILE,
LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM,
LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE;
LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE, UPDATE_EMAIL;

}
Expand Up @@ -23,6 +23,7 @@
import org.jboss.logging.Logger;
import org.keycloak.common.Version;
import org.keycloak.migration.migrators.MigrateTo12_0_0;
import org.keycloak.migration.migrators.MigrateTo13_0_0;
import org.keycloak.migration.migrators.MigrateTo1_2_0;
import org.keycloak.migration.migrators.MigrateTo1_3_0;
import org.keycloak.migration.migrators.MigrateTo1_4_0;
Expand Down Expand Up @@ -94,7 +95,8 @@ public class MigrationModelManager {
new MigrateTo8_0_2(),
new MigrateTo9_0_0(),
new MigrateTo9_0_4(),
new MigrateTo12_0_0()
new MigrateTo12_0_0(),
new MigrateTo13_0_0()
};

public static void migrate(KeycloakSession session) {
Expand Down
@@ -0,0 +1,37 @@
/*
* 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.migration.migrators;

import org.keycloak.migration.ModelVersion;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.utils.DefaultRequiredActions;

public class MigrateTo13_0_0 implements Migration {

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

@Override
public void migrate(KeycloakSession session) {
session.realms().getRealmsStream().forEach(DefaultRequiredActions::addUpdateEmailAction);
}

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

}
Expand Up @@ -85,6 +85,7 @@ public static void addActions(RealmModel realm) {

addUpdateLocaleAction(realm);
addDeleteAccountAction(realm);
addUpdateEmailAction(realm);
}

public static void addDeleteAccountAction(RealmModel realm) {
Expand Down Expand Up @@ -112,4 +113,17 @@ public static void addUpdateLocaleAction(RealmModel realm) {
realm.addRequiredActionProvider(updateUserLocale);
}
}

public static void addUpdateEmailAction(RealmModel realm){
if (realm.getRequiredActionProviderByAlias(UserModel.RequiredAction.UPDATE_EMAIL.name()) == null){
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);
}
}
}
Expand Up @@ -22,6 +22,7 @@
*/
public enum UserUpdateEvent {
UpdateProfile,
UpdateEmail,
UserResource,
Account,
IdpReview,
Expand Down
Expand Up @@ -298,7 +298,7 @@ default long getGroupsCountByNameContaining(String search) {
void setServiceAccountClientLink(String clientInternalId);

enum RequiredAction {
VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD, TERMS_AND_CONDITIONS
VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD, TERMS_AND_CONDITIONS, UPDATE_EMAIL
}

/**
Expand Down
@@ -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;
}
}
@@ -0,0 +1,94 @@
/*
* 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.AbstractActionTokenHander;
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.validation.UserProfileValidationResult;

public class UpdateEmailActionTokenHandler extends AbstractActionTokenHander<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();

UserProfileValidationResult emailUpdateValidationResult = UpdateEmail.validateEmailUpdate(session, user, newEmail);
List<FormMessage> errors = Validation.getFormErrorsFromValidation(emailUpdateValidationResult);
if (!errors.isEmpty()) {
return forms.setErrors(errors).createErrorPage(Response.Status.BAD_REQUEST);
}

UpdateEmail.updateEmailNow(tokenContext.getEvent(), tokenContext.getRealm(), 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;
}
}
Expand Up @@ -85,6 +85,12 @@ public void setUsername(String username) {
setSingleAttribute(UserModel.USERNAME, username);
}

@JsonIgnore
@Override
public boolean isEditEmailAllowed() {
return true;
}

public String getModelUsername() {
return getFirstAttribute(UserModel.USERNAME);
}
Expand Down

0 comments on commit cf1596c

Please sign in to comment.