KEYCLOAK-1113 - LoginFormProvider extended to allow per field errors,
freemarker implementation extended (backward compatible), used for registration form
This commit is contained in:
parent
48050de1a6
commit
8727aef647
6 changed files with 213 additions and 78 deletions
|
@ -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) + "]";
|
||||
}
|
||||
|
||||
}
|
|
@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
|
@ -48,8 +49,21 @@ public interface LoginFormsProvider extends Provider {
|
|||
public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> 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<FormMessage> messages);
|
||||
|
||||
public LoginFormsProvider setSuccess(String message, Object ... parameters);
|
||||
|
||||
public LoginFormsProvider setWarning(String message, Object ... parameters);
|
||||
|
|
|
@ -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<String, String> httpResponseHeaders = new HashMap<String, String>();
|
||||
private String accessRequestMessage;
|
||||
private URI actionUri;
|
||||
private Object[] parameters;
|
||||
|
||||
private String message;
|
||||
private List<FormMessage> messages = null;
|
||||
private MessageType messageType = MessageType.ERROR;
|
||||
|
||||
private MultivaluedMap<String, String> 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<String, MessageBean> messagesPerField = new HashMap<String, MessageBean>();
|
||||
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<FormMessage> 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 + "<br>" + newLine;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return this.type.toString().toLowerCase();
|
||||
}
|
||||
|
|
|
@ -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<FormMessage> 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)
|
||||
|
|
|
@ -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<String, String> formData, List<String> requiredCredentialTypes) {
|
||||
if (isEmpty(formData.getFirst("firstName"))) {
|
||||
return Messages.MISSING_FIRST_NAME;
|
||||
public static List<FormMessage> validateRegistrationForm(RealmModel realm, MultivaluedMap<String, String> formData, List<String> requiredCredentialTypes, PasswordPolicy policy) {
|
||||
List<FormMessage> 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;
|
||||
}
|
||||
|
||||
public static PasswordPolicy.Error validatePassword(MultivaluedMap<String, String> formData, PasswordPolicy policy) {
|
||||
return policy.validate(formData.getFirst("username"), formData.getFirst("password"));
|
||||
private static void addError(List<FormMessage> errors, String field, String message){
|
||||
errors.add(new FormMessage(field, message));
|
||||
}
|
||||
|
||||
|
||||
public static String validateUpdateProfileForm(MultivaluedMap<String, String> 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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue