diff --git a/forms/login-api/src/main/java/org/keycloak/login/FormMessage.java b/forms/login-api/src/main/java/org/keycloak/login/FormMessage.java
new file mode 100644
index 0000000000..707ad31295
--- /dev/null
+++ b/forms/login-api/src/main/java/org/keycloak/login/FormMessage.java
@@ -0,0 +1,69 @@
+/*
+ * JBoss, Home of Professional Open Source
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @authors tag. All rights reserved.
+ */
+package org.keycloak.login;
+
+import java.util.Arrays;
+
+/**
+ * Message (eg. error) to be shown in form.
+ *
+ * @author Vlastimil Elias (velias at redhat dot com)
+ */
+public class FormMessage {
+
+ /**
+ * Value used for {@link #field} if message is global (not tied to any specific form field)
+ */
+ public static final String GLOBAL = "global";
+
+ private String field;
+ private String message;
+ private Object[] parameters;
+
+ /**
+ * Create message.
+ *
+ * @param field this message is for. {@link #GLOBAL} is used if null
+ * @param message key for the message
+ * @param parameters to be formatted into message
+ */
+ public FormMessage(String field, String message, Object... parameters) {
+ this(field, message);
+ this.parameters = parameters;
+ }
+
+ /**
+ * Create message without parameters.
+ *
+ * @param field this message is for. {@link #GLOBAL} is used if null
+ * @param message key for the message
+ */
+ public FormMessage(String field, String message) {
+ super();
+ if (field == null)
+ field = GLOBAL;
+ this.field = field;
+ this.message = message;
+ }
+
+ public String getField() {
+ return field;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public Object[] getParameters() {
+ return parameters;
+ }
+
+ @Override
+ public String toString() {
+ return "FormMessage [field=" + field + ", message=" + message + ", parameters=" + Arrays.toString(parameters) + "]";
+ }
+
+}
diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
index 36a5252515..ac73087bca 100755
--- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
+++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
@@ -1,5 +1,13 @@
package org.keycloak.login;
+import java.net.URI;
+import java.util.List;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.RealmModel;
@@ -7,13 +15,6 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriInfo;
-import java.net.URI;
-import java.util.List;
-
/**
* @author Stian Thorgersen
*/
@@ -48,7 +49,20 @@ public interface LoginFormsProvider extends Provider {
public LoginFormsProvider setAccessRequest(List realmRolesRequested, MultivaluedMap resourceRolesRequested);
public LoginFormsProvider setAccessRequest(String message);
+ /**
+ * Set one global error message.
+ *
+ * @param message key of message
+ * @param parameters to be formatted into message
+ */
public LoginFormsProvider setError(String message, Object ... parameters);
+
+ /**
+ * Set multiple error messages.
+ *
+ * @param messages to be set
+ */
+ public LoginFormsProvider setErrors(List messages);
public LoginFormsProvider setSuccess(String message, Object ... parameters);
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
index 9aca77f006..6d600f060c 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -8,6 +8,7 @@ import org.keycloak.email.EmailProvider;
import org.keycloak.freemarker.*;
import org.keycloak.freemarker.beans.AdvancedMessageFormatterMethod;
import org.keycloak.freemarker.beans.MessageFormatterMethod;
+import org.keycloak.login.FormMessage;
import org.keycloak.login.LoginFormsPages;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.login.freemarker.model.ClientBean;
@@ -32,6 +33,7 @@ import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.flows.Urls;
import javax.ws.rs.core.*;
+
import java.io.IOException;
import java.net.URI;
import java.text.MessageFormat;
@@ -55,9 +57,8 @@ import java.util.concurrent.TimeUnit;
private Map httpResponseHeaders = new HashMap();
private String accessRequestMessage;
private URI actionUri;
- private Object[] parameters;
- private String message;
+ private List messages = null;
private MessageType messageType = MessageType.ERROR;
private MultivaluedMap formData;
@@ -134,7 +135,7 @@ import java.util.concurrent.TimeUnit;
return Response.serverError().build();
}
- if (message == null) {
+ if (messages == null) {
setWarning(actionMessage);
}
@@ -175,24 +176,34 @@ import java.util.concurrent.TimeUnit;
logger.warn("Failed to load properties", e);
}
- Properties messages;
+ Properties messagesBundle;
Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, httpHeaders);
try {
- messages = theme.getMessages(locale);
- attributes.put("msg", new MessageFormatterMethod(locale, messages));
+ messagesBundle = theme.getMessages(locale);
+ attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle));
} catch (IOException e) {
logger.warn("Failed to load messages", e);
- messages = new Properties();
+ messagesBundle = new Properties();
}
- if (message != null) {
- String formattedMessage;
- if(messages.containsKey(message)){
- formattedMessage = new MessageFormat(messages.getProperty(message),locale).format(parameters);
- }else{
- formattedMessage = message;
+ if (messages != null) {
+ Map messagesPerField = new HashMap();
+ MessageBean wholeMessage = new MessageBean(null, messageType);
+ for (FormMessage message : this.messages) {
+ String formattedMessageText = formatMessageMessage(message, messagesBundle, locale);
+ if (formattedMessageText != null) {
+ wholeMessage.appendSummaryLine(formattedMessageText);
+ MessageBean fm = messagesPerField.get(message.getField());
+ if (fm == null) {
+ messagesPerField.put(message.getField(), new MessageBean(formattedMessageText, messageType));
+ } else {
+ fm.appendSummaryLine(formattedMessageText);
+ }
+ }
}
- attributes.put("message", new MessageBean(formattedMessage, messageType));
+
+ attributes.put("message", wholeMessage);
+ attributes.put("messagePerField", messagesPerField);
}
if (page == LoginFormsPages.OAUTH_GRANT) {
// for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param
@@ -218,7 +229,7 @@ import java.util.concurrent.TimeUnit;
b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath());
break;
}
- attributes.put("locale", new LocaleBean(realm, locale, b, messages));
+ attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
}
}
@@ -240,10 +251,10 @@ import java.util.concurrent.TimeUnit;
break;
case OAUTH_GRANT:
attributes.put("oauth", new OAuthGrantBean(accessCode, clientSession, client, realmRolesRequested, resourceRolesRequested, this.accessRequestMessage));
- attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messages));
+ attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle));
break;
case CODE:
- attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? message : null));
+ attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? getFirstMessageUnformatted() : null));
break;
}
@@ -303,24 +314,46 @@ import java.util.concurrent.TimeUnit;
return createResponse(LoginFormsPages.CODE);
}
- public FreeMarkerLoginFormsProvider setError(String message, Object ... parameters) {
- this.message = message;
+ protected void setMessage(MessageType type, String message, Object... parameters) {
+ messageType = type;
+ messages = new ArrayList<>();
+ messages.add(new FormMessage(null, message, parameters));
+ }
+
+ protected String getFirstMessageUnformatted() {
+ if (messages != null && !messages.isEmpty()) {
+ return messages.get(0).getMessage();
+ }
+ return null;
+ }
+
+ protected String formatMessageMessage(FormMessage message, Properties messagesBundle, Locale locale) {
+ if (message == null)
+ return null;
+ if (messagesBundle.containsKey(message.getMessage())) {
+ return new MessageFormat(messagesBundle.getProperty(message.getMessage()), locale).format(message.getParameters());
+ } else {
+ return message.getMessage();
+ }
+ }
+ public FreeMarkerLoginFormsProvider setError(String message, Object... parameters) {
+ setMessage(MessageType.ERROR, message, parameters);
+ return this;
+ }
+
+ public LoginFormsProvider setErrors(List messages) {
this.messageType = MessageType.ERROR;
- this.parameters = parameters;
+ this.messages = new ArrayList<>(messages);
return this;
}
public FreeMarkerLoginFormsProvider setSuccess(String message, Object ... parameters) {
- this.message = message;
- this.messageType = MessageType.SUCCESS;
- this.parameters = parameters;
+ setMessage(MessageType.SUCCESS, message, parameters);
return this;
}
public FreeMarkerLoginFormsProvider setWarning(String message, Object ... parameters) {
- this.message = message;
- this.messageType = MessageType.WARNING;
- this.parameters = parameters;
+ setMessage(MessageType.WARNING, message, parameters);
return this;
}
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java
index 72d48b7d88..f5e69c4145 100644
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java
@@ -41,6 +41,15 @@ public class MessageBean {
return summary;
}
+ public void appendSummaryLine(String newLine) {
+ if (newLine == null)
+ return;
+ if (summary == null)
+ summary = newLine;
+ else
+ summary = summary + "
" + newLine;
+ }
+
public String getType() {
return this.type.toString().toLowerCase();
}
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index 5b405355a6..0a7e8b1df4 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -31,6 +31,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.login.FormMessage;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.*;
import org.keycloak.models.UserModel.RequiredAction;
@@ -63,6 +64,8 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
+
+import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -422,8 +425,8 @@ public class LoginActionsService {
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_CODE);
}
- String username = formData.getFirst("username");
- String email = formData.getFirst("email");
+ String username = formData.getFirst(Validation.FIELD_USERNAME);
+ String email = formData.getFirst(Validation.FIELD_EMAIL);
if (realm.isRegistrationEmailAsUsername()) {
username = email;
formData.putSingle(AuthenticationManager.FORM_USERNAME, username);
@@ -458,20 +461,12 @@ public class LoginActionsService {
}
// Validate here, so user is not created if password doesn't validate to passwordPolicy of current realm
- String errorMessage = Validation.validateRegistrationForm(realm, formData, requiredCredentialTypes);
- Object[] parameters = new Object[0];
- if (errorMessage == null) {
- PasswordPolicy.Error error = Validation.validatePassword(formData, realm.getPasswordPolicy());
- if(error != null){
- errorMessage = error.getMessage();
- parameters = error.getParameters();
- }
- }
+ List errors = Validation.validateRegistrationForm(realm, formData, requiredCredentialTypes, realm.getPasswordPolicy());
- if (errorMessage != null) {
+ if (errors != null && !errors.isEmpty()) {
event.error(Errors.INVALID_REGISTRATION);
return Flows.forms(session, realm, client, uriInfo, headers)
- .setError(errorMessage, parameters)
+ .setErrors(errors)
.setFormData(formData)
.setClientSessionCode(clientCode.getCode())
.createRegistration();
@@ -488,7 +483,7 @@ public class LoginActionsService {
}
// Validate that user with this email doesn't exist in realm or any federation provider
- if (session.users().getUserByEmail(email, realm) != null) {
+ if (email != null && session.users().getUserByEmail(email, realm) != null) {
event.error(Errors.EMAIL_IN_USE);
return Flows.forms(session, realm, client, uriInfo, headers)
.setError(Messages.EMAIL_EXISTS)
diff --git a/services/src/main/java/org/keycloak/services/validation/Validation.java b/services/src/main/java/org/keycloak/services/validation/Validation.java
index f72a63023a..5fb98f9d96 100755
--- a/services/src/main/java/org/keycloak/services/validation/Validation.java
+++ b/services/src/main/java/org/keycloak/services/validation/Validation.java
@@ -1,71 +1,86 @@
package org.keycloak.services.validation;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.keycloak.login.FormMessage;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages;
-import javax.ws.rs.core.MultivaluedMap;
-import java.util.List;
-import java.util.regex.Pattern;
-
public class Validation {
+ public static final String FIELD_PASSWORD_CONFIRM = "password-confirm";
+ public static final String FIELD_EMAIL = "email";
+ public static final String FIELD_LAST_NAME = "lastName";
+ public static final String FIELD_FIRST_NAME = "firstName";
+ public static final String FIELD_PASSWORD = "password";
+ public static final String FIELD_USERNAME = "username";
+
// Actually allow same emails like angular. See ValidationTest.testEmailValidation()
private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*");
- public static String validateRegistrationForm(RealmModel realm, MultivaluedMap formData, List requiredCredentialTypes) {
- if (isEmpty(formData.getFirst("firstName"))) {
- return Messages.MISSING_FIRST_NAME;
+ public static List validateRegistrationForm(RealmModel realm, MultivaluedMap formData, List requiredCredentialTypes, PasswordPolicy policy) {
+ List errors = new ArrayList<>();
+
+ if (!realm.isRegistrationEmailAsUsername() && isEmpty(formData.getFirst(FIELD_USERNAME))) {
+ addError(errors, FIELD_USERNAME, Messages.MISSING_USERNAME);
}
- if (isEmpty(formData.getFirst("lastName"))) {
- return Messages.MISSING_LAST_NAME;
+ if (isEmpty(formData.getFirst(FIELD_FIRST_NAME))) {
+ addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME);
}
- if (isEmpty(formData.getFirst("email"))) {
- return Messages.MISSING_EMAIL;
+ if (isEmpty(formData.getFirst(FIELD_LAST_NAME))) {
+ addError(errors, FIELD_LAST_NAME, Messages.MISSING_LAST_NAME);
}
- if (!isEmailValid(formData.getFirst("email"))) {
- return Messages.INVALID_EMAIL;
- }
-
- if (!realm.isRegistrationEmailAsUsername() && isEmpty(formData.getFirst("username"))) {
- return Messages.MISSING_USERNAME;
+ if (isEmpty(formData.getFirst(FIELD_EMAIL))) {
+ addError(errors, FIELD_EMAIL, Messages.MISSING_EMAIL);
+ } else if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) {
+ addError(errors, FIELD_EMAIL, Messages.INVALID_EMAIL);
}
if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) {
- if (isEmpty(formData.getFirst(CredentialRepresentation.PASSWORD))) {
- return Messages.MISSING_PASSWORD;
- }
-
- if (!formData.getFirst("password").equals(formData.getFirst("password-confirm"))) {
- return Messages.INVALID_PASSWORD_CONFIRM;
+ if (isEmpty(formData.getFirst(FIELD_PASSWORD))) {
+ addError(errors, FIELD_PASSWORD, Messages.MISSING_PASSWORD);
+ } else if (!formData.getFirst(FIELD_PASSWORD).equals(formData.getFirst(FIELD_PASSWORD_CONFIRM))) {
+ addError(errors, FIELD_PASSWORD_CONFIRM, Messages.INVALID_PASSWORD_CONFIRM);
}
}
- return null;
+ if (formData.getFirst(FIELD_PASSWORD) != null) {
+ PasswordPolicy.Error err = policy.validate(realm.isRegistrationEmailAsUsername()?formData.getFirst(FIELD_EMAIL):formData.getFirst(FIELD_USERNAME), formData.getFirst(FIELD_PASSWORD));
+ if (err != null)
+ errors.add(new FormMessage(FIELD_PASSWORD, err.getMessage(), err.getParameters()));
+ }
+
+ return errors;
+ }
+
+ private static void addError(List errors, String field, String message){
+ errors.add(new FormMessage(field, message));
}
- public static PasswordPolicy.Error validatePassword(MultivaluedMap formData, PasswordPolicy policy) {
- return policy.validate(formData.getFirst("username"), formData.getFirst("password"));
- }
public static String validateUpdateProfileForm(MultivaluedMap formData) {
- if (isEmpty(formData.getFirst("firstName"))) {
+ if (isEmpty(formData.getFirst(FIELD_FIRST_NAME))) {
return Messages.MISSING_FIRST_NAME;
}
- if (isEmpty(formData.getFirst("lastName"))) {
+ if (isEmpty(formData.getFirst(FIELD_LAST_NAME))) {
return Messages.MISSING_LAST_NAME;
}
- if (isEmpty(formData.getFirst("email"))) {
+ if (isEmpty(formData.getFirst(FIELD_EMAIL))) {
return Messages.MISSING_EMAIL;
}
- if (!isEmailValid(formData.getFirst("email"))) {
+ if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) {
return Messages.INVALID_EMAIL;
}