Merge pull request #78 from vrockai/KEYCLOAK-108

KEYCLOAK-108 add warning alerts to req action forms
This commit is contained in:
stianst 2013-10-19 02:09:09 -07:00
commit 8de6224501
18 changed files with 180 additions and 97 deletions

View file

@ -26,18 +26,18 @@ import org.keycloak.services.resources.flows.FormFlows;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ErrorBean {
public class MessageBean {
private String summary;
private FormFlows.ErrorType type;
private FormFlows.MessageType type;
// Message is considered ERROR by default
public ErrorBean(String summary) {
this(summary, FormFlows.ErrorType.ERROR);
public MessageBean(String summary) {
this(summary, FormFlows.MessageType.ERROR);
}
public ErrorBean(String summary, FormFlows.ErrorType type) {
public MessageBean(String summary, FormFlows.MessageType type) {
this.summary = summary;
this.type = type;
}
@ -47,15 +47,15 @@ public class ErrorBean {
}
public boolean isSuccess(){
return FormFlows.ErrorType.SUCCESS.equals(this.type);
return FormFlows.MessageType.SUCCESS.equals(this.type);
}
public boolean isWarning(){
return FormFlows.ErrorType.WARNING.equals(this.type);
return FormFlows.MessageType.WARNING.equals(this.type);
}
public boolean isError(){
return FormFlows.ErrorType.ERROR.equals(this.type);
return FormFlows.MessageType.ERROR.equals(this.type);
}
}

View file

@ -32,7 +32,7 @@ import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.jboss.resteasy.logging.Logger;
import org.keycloak.forms.ErrorBean;
import org.keycloak.forms.MessageBean;
import org.keycloak.forms.LoginBean;
import org.keycloak.forms.OAuthGrantBean;
import org.keycloak.forms.RealmBean;
@ -69,8 +69,7 @@ public class FormServiceImpl implements FormService {
commandMap.put(Pages.TOTP, new CommandTotp());
commandMap.put(Pages.LOGIN_CONFIG_TOTP, new CommandTotp());
commandMap.put(Pages.LOGIN_TOTP, new CommandLoginTotp());
commandMap.put(Pages.LOGIN_VERIFY_EMAIL, new CommandLoginTotp());
commandMap.put(Pages.ERROR, new CommandError());
commandMap.put(Pages.LOGIN_VERIFY_EMAIL, new CommandVerifyEmail());
commandMap.put(Pages.OAUTH_GRANT, new CommandOAuthGrant());
}
@ -82,8 +81,8 @@ public class FormServiceImpl implements FormService {
Map<String, Object> attributes = new HashMap<String, Object>();
if (dataBean.getError() != null){
attributes.put("message", new ErrorBean(dataBean.getError(), dataBean.getErrorType()));
if (dataBean.getMessage() != null){
attributes.put("message", new MessageBean(dataBean.getMessage(), dataBean.getMessageType()));
}
RealmBean realm = new RealmBean(dataBean.getRealm());
@ -161,9 +160,6 @@ public class FormServiceImpl implements FormService {
private class CommandLoginTotp implements Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
if (dataBean.getError() != null){
attributes.put("error", new ErrorBean(dataBean.getError()));
}
RealmBean realm = new RealmBean(dataBean.getRealm());
@ -206,10 +202,6 @@ public class FormServiceImpl implements FormService {
private class CommandLogin implements Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
if (dataBean.getError() != null){
attributes.put("error", new ErrorBean(dataBean.getError()));
}
RealmBean realm = new RealmBean(dataBean.getRealm());
attributes.put("realm", realm);
@ -230,9 +222,6 @@ public class FormServiceImpl implements FormService {
private class CommandRegister implements Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
if (dataBean.getError() != null){
attributes.put("error", new ErrorBean(dataBean.getError()));
}
RealmBean realm = new RealmBean(dataBean.getRealm());
@ -252,14 +241,6 @@ public class FormServiceImpl implements FormService {
}
}
private class CommandError implements Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
if (dataBean.getError() != null){
attributes.put("error", new ErrorBean(dataBean.getError()));
}
}
}
private class CommandOAuthGrant implements Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
@ -274,6 +255,20 @@ public class FormServiceImpl implements FormService {
}
}
private class CommandVerifyEmail implements Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
RealmBean realm = new RealmBean(dataBean.getRealm());
attributes.put("realm", realm);
UrlBean url = new UrlBean(realm, dataBean.getBaseURI());
url.setSocialRegistration(dataBean.getSocialRegistration());
attributes.put("url", url);
}
}
private interface Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean);
}

View file

@ -291,9 +291,6 @@ a.zocial:before {
.rcue-login-register.reset .background-area .section.app-form {
width: 43.2em;
}
.rcue-login-register.reset .feedback {
left: 35.7em;
}
.rcue-login-register.oauth .form-actions {
margin-bottom: 0;

View file

@ -12,7 +12,7 @@
<#elseif section = "form">
<p class="instruction">Something happened and we could not process your request.</p>
<p id="error-summary" class="instruction second">${error.summary}</p>
<p id="error-summary" class="instruction second">${message.summary}</p>
<#elseif section = "info" >

View file

@ -8,11 +8,6 @@
Google Authenticator Setup
<#elseif section = "feedback">
<div class="feedback warning show">
<p><strong>Your account is not enabled because you need to set up the Google Authenticator.</strong><br>Please follow the steps below.</p>
</div>
<#elseif section = "form">
<div id="form">

View file

@ -11,15 +11,6 @@
<#elseif section = "form">
<div id="form">
<#if message?has_content>
<#if message.success>
<div class="feedback success bottom-left show"><p><strong>${rb.getString('successHeader')}</strong> ${rb.getString(message.summary)}</p></div>
</#if>
<#if message.error>
<div class="feedback error bottom-left show"><p><strong>${rb.getString('errorHeader')}</strong><br/>${rb.getString(message.summary)}</p></div>
</#if>
</#if>
<p class="instruction">${rb.getString('emailInstruction')}</p>
<form action="${url.loginPasswordResetUrl}" method="post">
<div>

View file

@ -8,11 +8,6 @@
Email verification
<#elseif section = "feedback">
<div class="feedback warning show">
<p><strong>Your account is not enabled because you need to verify your email.</strong><br>Please follow the steps below.</p>
</div>
<#elseif section = "form">
<div class="app-form">

View file

@ -17,7 +17,11 @@
<body class="rcue-login-register ${bodyClass}">
<div class="feedback-aligner">
<#nested "feedback">
<#if message?has_content && message.warning>
<div class="feedback warning show">
<p><strong>${rb.getString('actionWarningHeader')} ${rb.getString(message.summary)}</strong><br/>${rb.getString('actionFollow')}</p>
</div>
</#if>
</div>
<#if (template.themeConfig.logo)?has_content>
<h1>
@ -33,18 +37,26 @@
<div class="background-area">
<div class="form-area clearfix">
<div class="section app-form">
<#if !isErrorPage && message?has_content>
<#if message.error>
<div class="feedback error bottom-left show">
<p>
<strong id="loginError">${rb.getString(message.summary)}</strong><br/>${rb.getString('emailErrorInfo')}
</p>
</div>
<#elseif message.success>
<div class="feedback success bottom-left show">
<p>
<strong>${rb.getString('successHeader')}</strong> ${rb.getString(message.summary)}
</p>
</div>
</#if>
</#if>
<h3>Application login area</h3>
<#nested "form">
</div>
<#if !isErrorPage && error?has_content>
<div class="feedback error bottom-left show">
<p>
<strong id="loginError">${rb.getString(error.summary)}</strong>
</p>
</div>
</#if>
<div class="section info-area">
<h3>Info area</h3>
<#nested "info">

View file

@ -31,10 +31,10 @@
<div class="form-area ${(realm.social)?string('social','')} clearfix">
<div class="section app-form">
<h3>Application login area</h3>
<#if error?has_content>
<#if message?has_content && message.error>
<div class="feedback error bottom-left show">
<p>
<strong id="loginError">${rb.getString(error.summary)}</strong><br/>${rb.getString('emailErrorInfo')}
<strong id="loginError">${rb.getString(message.summary)}</strong><br/>${rb.getString('emailErrorInfo')}
</p>
</div>
</#if>

View file

@ -31,6 +31,7 @@ missingLastName=Please specify last name
missingEmail=Please specify email
missingUsername=Please specify username
missingPassword=Please specify password
notMatchPassword=Passwords don't match
missingTotp=Please specify authenticator code
invalidPasswordExisting=Invalid existing password
@ -43,6 +44,12 @@ successTotpRemoved=Google authenticator removed.
usernameExists=Username already exists
error=A system error has occured, contact admin
actionWarningHeader=Your account is not enabled.
actionTotpWarning=You need to set up the Google Authenticator to activate your account.
actionProfileWarning=You need to update your user profile to activate your account.
actionPasswordWarning=You need to change your password to activate your account.
actionEmailWarning=You need to verify your email address to activate your account.
actionFollow=Please follow the steps below.
successHeader=Success!
errorHeader=Error!

View file

@ -44,9 +44,9 @@ public interface FormService {
private RealmModel realm;
private UserModel userModel;
private String error;
private String message;
private FormFlows.ErrorType errorType;
private FormFlows.MessageType messageType;
private MultivaluedMap<String, String> formData;
private URI baseURI;
@ -81,11 +81,11 @@ public interface FormService {
private String contextPath;
public FormServiceDataBean(RealmModel realm, UserModel userModel, MultivaluedMap<String, String> formData, String error){
public FormServiceDataBean(RealmModel realm, UserModel userModel, MultivaluedMap<String, String> formData, String message){
this.realm = realm;
this.userModel = userModel;
this.formData = formData;
this.error = error;
this.message = message;
}
public URI getBaseURI() {
@ -96,12 +96,12 @@ public interface FormService {
this.baseURI = baseURI;
}
public String getError() {
return error;
public String getMessage() {
return message;
}
public void setError(String error) {
this.error = error;
public void setMessage(String message) {
this.message = message;
}
public MultivaluedMap<String, String> getFormData() {
@ -128,12 +128,12 @@ public interface FormService {
this.userModel = userModel;
}
public FormFlows.ErrorType getErrorType() {
return errorType;
public FormFlows.MessageType getMessageType() {
return messageType;
}
public void setErrorType(FormFlows.ErrorType errorType) {
this.errorType = errorType;
public void setMessageType(FormFlows.MessageType messageType) {
this.messageType = messageType;
}
/* OAuth Part */

View file

@ -44,6 +44,8 @@ public class Messages {
public static final String MISSING_PASSWORD = "missingPassword";
public static final String NOTMATCH_PASSWORD = "notMatchPassword";
public static final String MISSING_USERNAME = "missingUsername";
public static final String MISSING_TOTP = "missingTotp";
@ -52,6 +54,14 @@ public class Messages {
public static final String USERNAME_EXISTS = "usernameExists";
public static final String ACTION_WARN_TOTP = "actionTotpWarning";
public static final String ACTION_WARN_PROFILE = "actionProfileWarning";
public static final String ACTION_WARN_PASSWD = "actionPasswordWarning";
public static final String ACTION_WARN_EMAIL = "actionEmailWarning";
public static final String ERROR = "error";
}

View file

@ -115,7 +115,7 @@ public class AccountService {
public Response processTotpRemove() {
UserModel user = getUserFromAuthManager();
user.setTotp(false);
return Flows.forms(realm, request, uriInfo).setError("successTotpRemoved").setErrorType(FormFlows.ErrorType.SUCCESS)
return Flows.forms(realm, request, uriInfo).setError("successTotpRemoved").setErrorType(FormFlows.MessageType.SUCCESS)
.setUser(user).forwardToTotp();
}
@ -152,7 +152,7 @@ public class AccountService {
user.setTotp(true);
return Flows.forms(realm, request, uriInfo).setError("successTotp").setErrorType(FormFlows.ErrorType.SUCCESS)
return Flows.forms(realm, request, uriInfo).setError("successTotp").setErrorType(FormFlows.MessageType.SUCCESS)
.setUser(user).forwardToTotp();
}

View file

@ -88,6 +88,12 @@ public class RequiredActionsService {
}
UserModel user = getUser(accessCode);
String error = Validation.validateUpdateProfileForm(formData);
if (error != null) {
return Flows.forms(realm, request, uriInfo).setError(error).forwardToAction(RequiredAction.UPDATE_PROFILE);
}
user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName"));
user.setEmail(formData.getFirst("email"));
@ -146,15 +152,14 @@ public class RequiredActionsService {
UserModel user = getUser(accessCode);
String password = formData.getFirst("password");
String passwordNew = formData.getFirst("password-new");
String passwordConfirm = formData.getFirst("password-confirm");
FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user);
if (Validation.isEmpty(passwordNew)) {
forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
return forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
} else if (!passwordNew.equals(passwordConfirm)) {
forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
return forms.setError(Messages.NOTMATCH_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
}
UserCredentialModel credentials = new UserCredentialModel();
@ -257,7 +262,7 @@ public class RequiredActionsService {
new EmailSender().sendPasswordReset(user, realm, accessCode, uriInfo);
return Flows.forms(realm, request, uriInfo).setError("emailSent").setErrorType(FormFlows.ErrorType.SUCCESS)
return Flows.forms(realm, request, uriInfo).setError("emailSent").setErrorType(FormFlows.MessageType.SUCCESS)
.forwardToPasswordReset();
}

View file

@ -34,6 +34,7 @@ import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.services.messages.Messages;
import org.picketlink.idm.model.sample.Realm;
import javax.imageio.spi.ServiceRegistry;
@ -58,8 +59,8 @@ public class FormFlows {
// TODO refactor/rename "error" to "message" everywhere where it makes sense
private String error;
public static enum ErrorType {SUCCESS, WARNING, ERROR};
private ErrorType errorType;
public static enum MessageType {SUCCESS, WARNING, ERROR};
private MessageType messageType = MessageType.ERROR;
private MultivaluedMap<String, String> formData;
@ -79,16 +80,17 @@ public class FormFlows {
}
public Response forwardToAction(RequiredAction action) {
switch (action) {
case CONFIGURE_TOTP:
return forwardToForm(Pages.LOGIN_CONFIG_TOTP);
return forwardToActionForm(Pages.LOGIN_CONFIG_TOTP, Messages.ACTION_WARN_TOTP);
case UPDATE_PROFILE:
return forwardToForm(Pages.LOGIN_UPDATE_PROFILE);
return forwardToActionForm(Pages.LOGIN_UPDATE_PROFILE, Messages.ACTION_WARN_PROFILE);
case UPDATE_PASSWORD:
return forwardToForm(Pages.LOGIN_UPDATE_PASSWORD);
return forwardToActionForm(Pages.LOGIN_UPDATE_PASSWORD, Messages.ACTION_WARN_PASSWD);
case VERIFY_EMAIL:
new EmailSender().sendEmailVerification(userModel, realm, accessCode, uriInfo);
return forwardToForm(Pages.LOGIN_VERIFY_EMAIL);
return forwardToActionForm(Pages.LOGIN_VERIFY_EMAIL, Messages.ACTION_WARN_EMAIL);
default:
return Response.serverError().build();
}
@ -103,7 +105,6 @@ public class FormFlows {
}
private Response forwardToForm(String template, FormService.FormServiceDataBean formDataBean) {
formDataBean.setErrorType(errorType == null ? ErrorType.ERROR : errorType);
// Getting URI needed by form processing service
ResteasyUriInfo uriInfo = request.getUri();
@ -143,8 +144,21 @@ public class FormFlows {
private Response forwardToForm(String template) {
FormService.FormServiceDataBean formDataBean = new FormService.FormServiceDataBean(realm, userModel, formData, error);
return forwardToForm(template, formDataBean);
formDataBean.setMessageType(messageType);
return forwardToForm(template, formDataBean);
}
private Response forwardToActionForm(String template, String warningSummary) {
// If no other message is set, notify user about required action in the warning window
// so it's clear that this is a req. action form not a login form
if (error == null){
messageType = MessageType.WARNING;
error = warningSummary;
}
return forwardToForm(template);
}
public Response forwardToLogin() {
@ -202,8 +216,8 @@ public class FormFlows {
return this;
}
public FormFlows setErrorType(ErrorType errorType) {
this.errorType = errorType;
public FormFlows setErrorType(MessageType errorType) {
this.messageType = errorType;
return this;
}

View file

@ -38,6 +38,22 @@ public class Validation {
return null;
}
public static String validateUpdateProfileForm(MultivaluedMap<String, String> formData) {
if (isEmpty(formData.getFirst("firstName"))) {
return Messages.MISSING_FIRST_NAME;
}
if (isEmpty(formData.getFirst("lastName"))) {
return Messages.MISSING_LAST_NAME;
}
if (isEmpty(formData.getFirst("email"))) {
return Messages.MISSING_EMAIL;
}
return null;
}
public static boolean isEmpty(String s) {
return s == null || s.length() == 0;
}

View file

@ -44,8 +44,8 @@ import org.openqa.selenium.WebDriver;
*/
public class RequiredActionUpdateProfileTest {
@ClassRule
public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() {
@Rule
public KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
@ -83,4 +83,50 @@ public class RequiredActionUpdateProfileTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
}
@Test
public void updateProfileMissingFirstName() {
loginPage.open();
loginPage.login("test-user@localhost", "password");
updateProfilePage.assertCurrent();
updateProfilePage.update("", "New last", "new@email.com");
updateProfilePage.assertCurrent();
Assert.assertEquals("Please specify first name", updateProfilePage.getError());
}
@Test
public void updateProfileMissingLastName() {
loginPage.open();
loginPage.login("test-user@localhost", "password");
updateProfilePage.assertCurrent();
updateProfilePage.update("New first", "", "new@email.com");
updateProfilePage.assertCurrent();
Assert.assertEquals("Please specify last name", updateProfilePage.getError());
}
@Test
public void updateProfileMissingEmail() {
loginPage.open();
loginPage.login("test-user@localhost", "password");
updateProfilePage.assertCurrent();
updateProfilePage.update("New first", "New last", "");
updateProfilePage.assertCurrent();
Assert.assertEquals("Please specify email", updateProfilePage.getError());
}
}

View file

@ -123,7 +123,7 @@ public class ResetPasswordTest {
resetPasswordPage.assertCurrent();
Assert.assertNotEquals("Success!", resetPasswordPage.getMessage());
Assert.assertEquals("Error!", resetPasswordPage.getMessage());
Assert.assertEquals("Invalid username or email.", resetPasswordPage.getMessage());
}
@Test
@ -138,7 +138,7 @@ public class ResetPasswordTest {
resetPasswordPage.assertCurrent();
Assert.assertNotEquals("Success!", resetPasswordPage.getMessage());
Assert.assertEquals("Error!", resetPasswordPage.getMessage());
Assert.assertEquals("Invalid username or email.", resetPasswordPage.getMessage());
}
}