Repository: gravitee-io/graviteeio-access-management
Pull request #533 (2019-02-22 10:27:34)

fix(client): allow to unset client certificate
fixes gravitee-io/issues#1960


Patch:
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/RootRouter.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/RootRouter.java
index 0011e3d392..b26043b3ee 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/RootRouter.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/RootRouter.java
@@ -29,10 +29,12 @@ import io.gravitee.am.gateway.handler.vertx.handler.root.endpoint.login.LoginCal
 import io.gravitee.am.gateway.handler.vertx.handler.root.endpoint.login.LoginEndpointHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.root.endpoint.login.LoginRequestParseHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.root.endpoint.logout.LogoutEndpointHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.root.endpoint.user.PasswordPolicyRequestParseHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.root.endpoint.user.UserTokenRequestParseHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.root.endpoint.user.password.*;
 import io.gravitee.am.gateway.handler.vertx.handler.root.endpoint.user.register.*;
 import io.gravitee.am.model.Domain;
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
 import io.vertx.core.Handler;
 import io.vertx.core.http.HttpMethod;
 import io.vertx.reactivex.core.Vertx;
@@ -71,6 +73,9 @@ public class RootRouter {
     @Autowired
     private UserAuthenticationManager userAuthenticationManager;
 
+    @Autowired
+    private PasswordValidator passwordValidator;
+
     public Router route(AuthProvider userAuthProvider) {
         // create the root router
         final Router router = Router.router(vertx);
@@ -95,6 +100,7 @@ public class RootRouter {
         Handler<RoutingContext> resetPasswordHandler = new ResetPasswordEndpointHandler(thymeleafTemplateEngine);
         Handler<RoutingContext> resetPasswordSubmissionRequestParseHandler = new ResetPasswordSubmissionRequestParseHandler();
         Handler<RoutingContext> resetPasswordSubmissionHandler = new ResetPasswordSubmissionEndpointHandler(userService);
+        Handler<RoutingContext> passwordPolicyRequestParseHandler = new PasswordPolicyRequestParseHandler(passwordValidator);
 
         // login route
         router.get("/login")
@@ -123,6 +129,7 @@ public class RootRouter {
                     .handler(registerHandler);
             router.route(HttpMethod.POST, "/register")
                     .handler(registerSubmissionRequestHandler)
+                    .handler(passwordPolicyRequestParseHandler)
                     .handler(registerSubmissionHandler);
         }
 
@@ -132,6 +139,7 @@ public class RootRouter {
         router.route(HttpMethod.POST, "/confirmRegistration")
                 .handler(registerConfirmationSubmissionRequestParseHandler)
                 .handler(userTokenRequestParseHandler)
+                .handler(passwordPolicyRequestParseHandler)
                 .handler(registerConfirmationSubmissionEndpointHandler);
 
         // mount forgot/reset password pages only if the option is enabled
@@ -149,6 +157,7 @@ public class RootRouter {
             router.route(HttpMethod.POST, "/resetPassword")
                     .handler(resetPasswordSubmissionRequestParseHandler)
                     .handler(userTokenRequestParseHandler)
+                    .handler(passwordPolicyRequestParseHandler)
                     .handler(resetPasswordSubmissionHandler);
         }
         return router;
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/PasswordPolicyRequestParseHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/PasswordPolicyRequestParseHandler.java
new file mode 100644
index 0000000000..135c228a44
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/PasswordPolicyRequestParseHandler.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * 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 io.gravitee.am.gateway.handler.vertx.handler.root.endpoint.user;
+
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
+import io.vertx.reactivex.ext.web.RoutingContext;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class PasswordPolicyRequestParseHandler extends UserRequestHandler {
+
+    public static final String PASSWORD_PARAM = "password";
+    public static final String TOKEN_PARAM = "token";
+    public static final String WARNING_PARAM = "warning";
+    public static final String CLIENT_ID_PARAM = "client_id";
+    private PasswordValidator passwordValidator;
+
+    public PasswordPolicyRequestParseHandler(PasswordValidator passwordValidator) {
+        this.passwordValidator = passwordValidator;
+    }
+
+    @Override
+    public void handle(RoutingContext context) {
+        if (!passwordValidator.validate(context.request().getParam(PASSWORD_PARAM))) {
+            Map<String, String> parameters = new HashMap<>();
+            if (context.request().getParam(CLIENT_ID_PARAM) != null) {
+                parameters.put(CLIENT_ID_PARAM, context.request().getParam(CLIENT_ID_PARAM));
+            }
+            if (context.request().getParam(TOKEN_PARAM) != null) {
+                parameters.put(TOKEN_PARAM, context.request().getParam(TOKEN_PARAM));
+            }
+            parameters.put(WARNING_PARAM, "invalid_password_value");
+            redirectToPage(context, parameters);
+        } else {
+            context.next();
+        }
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/UserTokenRequestParseHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/UserTokenRequestParseHandler.java
index 84c7b6bdb9..d30daa2625 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/UserTokenRequestParseHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/UserTokenRequestParseHandler.java
@@ -56,7 +56,7 @@ public class UserTokenRequestParseHandler extends UserRequestHandler {
         }
 
         // user has been redirected due to warning, continue
-        if (warning != null) {
+        if (warning != null && token == null) {
             context.next();
             return;
         }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/password/ResetPasswordEndpointHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/password/ResetPasswordEndpointHandler.java
index 697b54ed58..f0f700a643 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/password/ResetPasswordEndpointHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/password/ResetPasswordEndpointHandler.java
@@ -37,6 +37,7 @@ public class ResetPasswordEndpointHandler implements Handler<RoutingContext> {
     private static final String ERROR_PARAM = "error";
     private static final String SUCCESS_PARAM = "success";
     private static final String TOKEN_PARAM = "token";
+    public static final String WARNING_PARAM = "warning";
     private ThymeleafTemplateEngine engine;
 
     public ResetPasswordEndpointHandler(ThymeleafTemplateEngine engine) {
@@ -48,10 +49,12 @@ public class ResetPasswordEndpointHandler implements Handler<RoutingContext> {
         final HttpServerRequest request = routingContext.request();
         final String error = request.getParam(ERROR_PARAM);
         final String success = request.getParam(SUCCESS_PARAM);
+        final String warning = request.getParam(WARNING_PARAM);
         final String token = request.getParam(TOKEN_PARAM);
         // add query params to context
         routingContext.put(ERROR_PARAM, error);
         routingContext.put(SUCCESS_PARAM, success);
+        routingContext.put(WARNING_PARAM, warning);
         routingContext.put(TOKEN_PARAM, token);
 
         // retrieve user who want to reset password
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/register/RegisterSubmissionEndpointHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/register/RegisterSubmissionEndpointHandler.java
index ced50ae4f8..cbc170ffff 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/register/RegisterSubmissionEndpointHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/root/endpoint/user/register/RegisterSubmissionEndpointHandler.java
@@ -46,7 +46,9 @@ public class RegisterSubmissionEndpointHandler extends UserRequestHandler {
         MultiMap params = context.request().formAttributes();
 
         Map<String, String> queryParams = new HashMap<>();
-        queryParams.put(CLIENT_ID_PARAM, context.request().getParam(CLIENT_ID_PARAM));
+        if (context.request().getParam(CLIENT_ID_PARAM) != null) {
+            queryParams.put(CLIENT_ID_PARAM, context.request().getParam(CLIENT_ID_PARAM));
+        }
 
         userService.register(convert(params))
                 .subscribe(
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/SCIMRouter.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/SCIMRouter.java
index 8db4077a17..7ffd14d119 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/SCIMRouter.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/SCIMRouter.java
@@ -26,6 +26,7 @@ import io.gravitee.am.gateway.handler.vertx.handler.scim.endpoint.configuration.
 import io.gravitee.am.gateway.handler.vertx.handler.scim.endpoint.groups.*;
 import io.gravitee.am.gateway.handler.vertx.handler.scim.endpoint.users.*;
 import io.gravitee.am.gateway.handler.vertx.handler.scim.handler.BearerTokensParseHandler;
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
 import io.vertx.reactivex.core.Vertx;
 import io.vertx.reactivex.ext.web.Router;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -63,6 +64,9 @@ public class SCIMRouter {
     @Autowired
     private ClientSyncService clientSyncService;
 
+    @Autowired
+    private PasswordValidator passwordValidator;
+
     public Router route() {
         // Create the SCIM router
         final Router router = Router.router(vertx);
@@ -85,8 +89,10 @@ public class SCIMRouter {
         getUserEndpointHandler.setObjectMapper(objectMapper);
         CreateUserEndpointHandler createUserEndpointHandler = CreateUserEndpointHandler.create(userService);
         createUserEndpointHandler.setObjectMapper(objectMapper);
+        createUserEndpointHandler.setPasswordValidator(passwordValidator);
         UpdateUserEndpointHandler updateUserEndpointHandler = UpdateUserEndpointHandler.create(userService);
         updateUserEndpointHandler.setObjectMapper(objectMapper);
+        updateUserEndpointHandler.setPasswordValidator(passwordValidator);
         DeleteUserEndpointHandler deleteUserEndpointHandler = DeleteUserEndpointHandler.create(userService);
 
         router.get("/Users").handler(listUserEndpointHandler);
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/CreateUserEndpointHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/CreateUserEndpointHandler.java
index dfb1fdaa97..5fa9b95b60 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/CreateUserEndpointHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/CreateUserEndpointHandler.java
@@ -23,6 +23,7 @@ import io.gravitee.am.gateway.handler.scim.exception.InvalidValueException;
 import io.gravitee.am.gateway.handler.scim.model.EntrepriseUser;
 import io.gravitee.am.gateway.handler.scim.model.User;
 import io.gravitee.am.gateway.handler.vertx.utils.UriBuilderRequest;
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
 import io.gravitee.common.http.HttpHeaders;
 import io.gravitee.common.http.MediaType;
 import io.vertx.core.Handler;
@@ -86,6 +87,7 @@ public class CreateUserEndpointHandler implements Handler<RoutingContext> {
     private static final Logger logger = LoggerFactory.getLogger(CreateUserEndpointHandler.class);
     private UserService userService;
     private ObjectMapper objectMapper;
+    private PasswordValidator passwordValidator;
 
     public CreateUserEndpointHandler(UserService userService) {
         this.userService = userService;
@@ -110,6 +112,14 @@ public class CreateUserEndpointHandler implements Handler<RoutingContext> {
                 return;
             }
 
+            // password policy
+            if (user.getPassword() != null) {
+                if (!passwordValidator.validate(user.getPassword())) {
+                    context.fail(new InvalidValueException("Field [password] is invalid"));
+                    return;
+                }
+            }
+
             userService.create(user, location(context.request()))
                     .subscribe(
                             user1 -> context.response()
@@ -129,6 +139,10 @@ public class CreateUserEndpointHandler implements Handler<RoutingContext> {
         this.objectMapper = objectMapper;
     }
 
+    public void setPasswordValidator(PasswordValidator passwordValidator) {
+        this.passwordValidator = passwordValidator;
+    }
+
     public static CreateUserEndpointHandler create(UserService userService) {
         return new CreateUserEndpointHandler(userService);
     }
@@ -142,7 +156,7 @@ public class CreateUserEndpointHandler implements Handler<RoutingContext> {
         }
     }
 
-    private void checkSchemas(List<String> schemas) throws Exception {
+    private void checkSchemas(List<String> schemas) {
         if (schemas == null || schemas.isEmpty()) {
             throw new InvalidValueException("Field [schemas] is required");
         }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/UpdateUserEndpointHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/UpdateUserEndpointHandler.java
index 7e34d562b1..64ce4df523 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/UpdateUserEndpointHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/UpdateUserEndpointHandler.java
@@ -22,6 +22,7 @@ import io.gravitee.am.gateway.handler.scim.exception.InvalidValueException;
 import io.gravitee.am.gateway.handler.scim.model.EntrepriseUser;
 import io.gravitee.am.gateway.handler.scim.model.User;
 import io.gravitee.am.gateway.handler.vertx.utils.UriBuilderRequest;
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
 import io.gravitee.common.http.HttpHeaders;
 import io.gravitee.common.http.MediaType;
 import io.vertx.core.Handler;
@@ -79,6 +80,7 @@ public class UpdateUserEndpointHandler implements Handler<RoutingContext>  {
     private static final Logger logger = LoggerFactory.getLogger(UpdateUserEndpointHandler.class);
     private UserService userService;
     private ObjectMapper objectMapper;
+    private PasswordValidator passwordValidator;
 
     public UpdateUserEndpointHandler(UserService userService) {
         this.userService = userService;
@@ -104,6 +106,14 @@ public class UpdateUserEndpointHandler implements Handler<RoutingContext>  {
                 return;
             }
 
+            // password policy
+            if (user.getPassword() != null) {
+                if (!passwordValidator.validate(user.getPassword())) {
+                    context.fail(new InvalidValueException("Field [password] is invalid"));
+                    return;
+                }
+            }
+
             userService.update(userId, user, location(context.request()))
                     .subscribe(
                             user1 -> context.response()
@@ -122,6 +132,10 @@ public class UpdateUserEndpointHandler implements Handler<RoutingContext>  {
         this.objectMapper = objectMapper;
     }
 
+    public void setPasswordValidator(PasswordValidator passwordValidator) {
+        this.passwordValidator = passwordValidator;
+    }
+
     public static UpdateUserEndpointHandler create(UserService userService) {
         return new UpdateUserEndpointHandler(userService);
     }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/resources/webroot/views/registration.html b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/resources/webroot/views/registration.html
index aed17a6cc9..26818fd68b 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/resources/webroot/views/registration.html
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/resources/webroot/views/registration.html
@@ -25,7 +25,7 @@
                 <label>Create your account</label>
             </div>
             <div th:if="${warning}" class="register-error-info">
-                <p style="margin: 0;">An account already exists with this username.</p>
+                <p style="margin: 0;" th:text="${warning == 'invalid_password_value'} ? 'Invalid password value. It does not comply with the password policy.' : 'An account already exists with this username.'"></p>
             </div>
             <form role="form" th:action="@{register}" method="post" style="display: flex; flex-direction: column; margin-top: 30px;">
                 <div style="display: flex; flex-direction: row">
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/resources/webroot/views/reset_password.html b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/resources/webroot/views/reset_password.html
index 8a959ce0e5..93a239abdc 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/resources/webroot/views/reset_password.html
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/resources/webroot/views/reset_password.html
@@ -25,6 +25,9 @@
                 <label>Hi <span th:text="${user.firstName} + ' ' + ${user.lastName}"></span></label>
                 <p>Please enter a new password for your account.</p>
             </div>
+            <div th:if="${warning}" class="reset-password-error-info">
+                <p style="margin: 0;">Invalid password value. It does not comply with the password policy.</p>
+            </div>
             <form role="form" th:action="@{resetPassword}" method="post" style="display: flex; flex-direction: column; margin-top: 30px;">
                 <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label" style="width: 100%; display: flex;">
                     <input class="mdl-textfield__input" type="password" id="password" name="password"/>
diff --git a/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-distribution/src/main/resources/config/gravitee.yml b/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-distribution/src/main/resources/config/gravitee.yml
index c93a6c6944..585ee3572d 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-distribution/src/main/resources/config/gravitee.yml
+++ b/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-distribution/src/main/resources/config/gravitee.yml
@@ -167,6 +167,21 @@ email:
 
 # User management configuration
 user:
+  # Password complexity validation policy
+  # Applications should enforce password complexity rules to discourage easy to guess passwords.
+  # Passwords should require a minimum level of complexity that makes sense for the application and its user population.
+  #password:
+  #  policy:
+  #    pattern: ^(?=\s*\S).*$ # Regex pattern for password validation (default to ^(?=\s*\S).*$ (non empty string))
+                              # Example : ^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$
+                              # ^                # start-of-string
+                              #(?=.*[0-9])       # a digit must occur at least once
+                              #(?=.*[a-z])       # a lower case letter must occur at least once
+                              #(?=.*[A-Z])       # an upper case letter must occur at least once
+                              #(?=.*[@#$%^&+=])  # a special character must occur at least once
+                              #(?=\S+$)          # no whitespace allowed in the entire string
+                              #.{8,}             # anything, at least eight places though
+                              #$                 # end-of-string
   registration:
     email:
       #subject: New user registration
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/PasswordValue.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/PasswordValue.java
index dfdc372b7d..a7cc90b69f 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/PasswordValue.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/model/PasswordValue.java
@@ -16,7 +16,6 @@
 package io.gravitee.am.management.handlers.management.api.model;
 
 import javax.validation.constraints.NotNull;
-import javax.validation.constraints.Size;
 
 /**
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
@@ -25,7 +24,6 @@ import javax.validation.constraints.Size;
 public class PasswordValue {
 
     @NotNull
-    @Size(min = 3)
     private String password;
 
     public String getPassword() {
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/ClientResource.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/ClientResource.java
index 948bc96044..697e68e6e2 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/ClientResource.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/ClientResource.java
@@ -120,7 +120,7 @@ public class ClientResource extends AbstractResource {
         domainService.findById(domain)
                 .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
                 .flatMapSingle(irrelevant -> this.applyDefaultResponseType(patchClient))
-                .flatMap(patch -> clientService.patch(domain, client, patch))
+                .flatMap(patch -> clientService.patch(domain, client, patch, true))
                 .map(updatedClient -> Response.ok(updatedClient).build())
                 .subscribe(
                         result -> response.resume(result),
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserResource.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserResource.java
index 7460f49cb8..69c726ae55 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserResource.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UserResource.java
@@ -23,7 +23,9 @@ import io.gravitee.am.model.User;
 import io.gravitee.am.service.ClientService;
 import io.gravitee.am.service.DomainService;
 import io.gravitee.am.service.IdentityProviderService;
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
 import io.gravitee.am.service.exception.DomainNotFoundException;
+import io.gravitee.am.service.exception.UserInvalidException;
 import io.gravitee.am.service.exception.UserNotFoundException;
 import io.gravitee.am.service.model.UpdateUser;
 import io.gravitee.common.http.MediaType;
@@ -64,6 +66,9 @@ public class UserResource {
     @Autowired
     private ClientService clientService;
 
+    @Autowired
+    private PasswordValidator passwordValidator;
+
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     @ApiOperation(value = "Get a user")
@@ -141,6 +146,13 @@ public class UserResource {
                               @PathParam("user") String user,
                               @ApiParam(name = "password", required = true) @Valid @NotNull PasswordValue password,
                               @Suspended final AsyncResponse response) {
+
+        // check password policy
+        if (!passwordValidator.validate(password.getPassword())) {
+            response.resume(new UserInvalidException(("Field [password] is invalid")));
+            return;
+        }
+
         domainService.findById(domain)
                 .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
                 .flatMapCompletable(user1 -> userService.resetPassword(domain, user, password.getPassword()))
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UsersResource.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UsersResource.java
index fe05ce9b2e..b7c62b231c 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UsersResource.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UsersResource.java
@@ -20,6 +20,7 @@ import io.gravitee.am.model.User;
 import io.gravitee.am.model.common.Page;
 import io.gravitee.am.service.DomainService;
 import io.gravitee.am.service.IdentityProviderService;
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
 import io.gravitee.am.service.exception.DomainNotFoundException;
 import io.gravitee.am.service.exception.UserInvalidException;
 import io.gravitee.am.service.model.NewUser;
@@ -63,6 +64,9 @@ public class UsersResource extends AbstractResource {
     @Autowired
     private IdentityProviderService identityProviderService;
 
+    @Autowired
+    private PasswordValidator passwordValidator;
+
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     @ApiOperation(value = "List users for a security domain")
@@ -124,6 +128,14 @@ public class UsersResource extends AbstractResource {
             return;
         }
 
+        // check password policy
+        if (newUser.getPassword() != null) {
+            if (!passwordValidator.validate(newUser.getPassword())) {
+                response.resume(new UserInvalidException(("Field [password] is invalid")));
+                return;
+            }
+        }
+
         domainService.findById(domain)
                 .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
                 .flatMapSingle(userProvider -> userService.create(domain, newUser))
diff --git a/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-distribution/src/main/resources/config/gravitee.yml b/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-distribution/src/main/resources/config/gravitee.yml
index 620f309f2c..00c672ac44 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-distribution/src/main/resources/config/gravitee.yml
+++ b/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-distribution/src/main/resources/config/gravitee.yml
@@ -185,6 +185,21 @@ email:
 
 # User management configuration
 user:
+  # Password complexity validation policy
+  # Applications should enforce password complexity rules to discourage easy to guess passwords.
+  # Passwords should require a minimum level of complexity that makes sense for the application and its user population.
+  #password:
+  #  policy:
+  #    pattern: ^(?=\s*\S).*$ # Regex pattern for password validation (default to ^(?=\s*\S).*$ (non empty string))
+                              # Example : ^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$
+                              # ^                # start-of-string
+                              #(?=.*[0-9])       # a digit must occur at least once
+                              #(?=.*[a-z])       # a lower case letter must occur at least once
+                              #(?=.*[A-Z])       # an upper case letter must occur at least once
+                              #(?=.*[@#$%^&+=])  # a special character must occur at least once
+                              #(?=\S+$)          # no whitespace allowed in the entire string
+                              #.{8,}             # anything, at least eight places though
+                              #$                 # end-of-string
   registration:
     email:
       #subject: New user registration
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/ClientService.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/ClientService.java
index 10f6c1590f..be3a48dc51 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/ClientService.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/ClientService.java
@@ -56,7 +56,11 @@ public interface ClientService {
 
     Single<Client> update(Client client);
 
-    Single<Client> patch(String domain, String id, PatchClient patchClient);
+    default Single<Client> patch(String domain, String id, PatchClient patchClient) {
+        return patch(domain, id, patchClient, false);
+    }
+
+    Single<Client> patch(String domain, String id, PatchClient patchClient, boolean forceNull);
 
     Single<Set<Client>> findByIdentityProvider(String identityProvider);
 
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/authentication/crypto/password/PasswordValidator.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/authentication/crypto/password/PasswordValidator.java
new file mode 100644
index 0000000000..7e2d8faca3
--- /dev/null
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/authentication/crypto/password/PasswordValidator.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * 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 io.gravitee.am.service.authentication.crypto.password;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface PasswordValidator {
+
+    boolean validate(final String password);
+}
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/authentication/crypto/password/RegexPasswordValidator.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/authentication/crypto/password/RegexPasswordValidator.java
new file mode 100644
index 0000000000..a1a556cd95
--- /dev/null
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/authentication/crypto/password/RegexPasswordValidator.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * 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 io.gravitee.am.service.authentication.crypto.password;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Value;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class RegexPasswordValidator implements PasswordValidator, InitializingBean {
+
+    private Pattern pattern;
+    private Matcher matcher;
+
+    @Value("${user.password.policy.pattern:^(?=\\s*\\S).*$}")
+    private String passwordPattern;
+
+    public RegexPasswordValidator() { }
+
+    public RegexPasswordValidator(String pattern) {
+        this.pattern = Pattern.compile(pattern);
+    }
+
+    @Override
+    public boolean validate(final String password) {
+        matcher = pattern.matcher(password);
+        return matcher.matches();
+    }
+
+
+    @Override
+    public void afterPropertiesSet() {
+        pattern = Pattern.compile(passwordPattern);
+    }
+}
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ClientServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ClientServiceImpl.java
index 3fa320254e..683d9d7911 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ClientServiceImpl.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ClientServiceImpl.java
@@ -396,11 +396,11 @@ public class ClientServiceImpl implements ClientService {
     }
 
     @Override
-    public Single<Client> patch(String domain, String id, PatchClient patchClient) {
+    public Single<Client> patch(String domain, String id, PatchClient patchClient, boolean forceNull) {
         LOGGER.debug("Patch a client {} for domain {}", id, domain);
         return clientRepository.findById(id)
                 .switchIfEmpty(Maybe.error(new ClientNotFoundException(id)))
-                .flatMapSingle(toPatch -> Single.just(patchClient.patch(toPatch)))
+                .flatMapSingle(toPatch -> Single.just(patchClient.patch(toPatch, forceNull)))
                 .flatMap(client -> this.validateClientMetadata(domain, client))
                 .flatMap(client -> this.updateClientAndReloadDomain(domain, client))
                 .onErrorResumeNext(this::handleError);
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchClient.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchClient.java
index dfca15b765..1b2bc004e4 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchClient.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchClient.java
@@ -464,7 +464,7 @@ public class PatchClient {
         this.enhanceScopesWithUserPermissions = enhanceScopesWithUserPermissions;
     }
 
-    public Client patch(Client toPatch) {
+    public Client patch(Client toPatch, boolean forceNull) {
 
         SetterUtils.safeSet(toPatch::setRedirectUris, this.getRedirectUris());
         SetterUtils.safeSet(toPatch::setAuthorizedGrantTypes, this.getAuthorizedGrantTypes());
@@ -512,7 +512,12 @@ public class PatchClient {
         SetterUtils.safeSet(toPatch::setEnabled, this.getEnabled(), boolean.class);
         SetterUtils.safeSet(toPatch::setIdentities, this.getIdentities());
         SetterUtils.safeSet(toPatch::setOauth2Identities, this.getOauth2Identities());
-        SetterUtils.safeSet(toPatch::setCertificate, this.getCertificate());
+        // we should be able to unset the certificate
+        if (this.getCertificate() == null && forceNull) {
+            toPatch.setCertificate(null);
+        } else {
+            SetterUtils.safeSet(toPatch::setCertificate, this.getCertificate());
+        }
         SetterUtils.safeSet(toPatch::setEnhanceScopesWithUserPermissions, this.getEnhanceScopesWithUserPermissions(), boolean.class);
 
         return toPatch;
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/spring/ServiceConfiguration.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/spring/ServiceConfiguration.java
index c7e2ba0fb9..2ec48881ea 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/spring/ServiceConfiguration.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/spring/ServiceConfiguration.java
@@ -15,6 +15,8 @@
  */
 package io.gravitee.am.service.spring;
 
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
+import io.gravitee.am.service.authentication.crypto.password.RegexPasswordValidator;
 import io.vertx.ext.web.client.WebClientOptions;
 import io.vertx.reactivex.core.Vertx;
 import io.vertx.reactivex.ext.web.client.WebClient;
@@ -52,4 +54,9 @@ public class ServiceConfiguration {
 
         return WebClient.create(vertx,options);
     }
+
+    @Bean
+    public PasswordValidator passwordValidator() {
+        return new RegexPasswordValidator();
+    }
 }
diff --git a/gravitee-am-ui/src/app/domain/components/forms/form/dialog/form-info.component.html b/gravitee-am-ui/src/app/domain/components/forms/form/dialog/form-info.component.html
index 67c1e5817b..dd07ec6900 100644
--- a/gravitee-am-ui/src/app/domain/components/forms/form/dialog/form-info.component.html
+++ b/gravitee-am-ui/src/app/domain/components/forms/form/dialog/form-info.component.html
@@ -52,7 +52,7 @@
         <div>
           <div th:if="${error == null && success == null}">
             <div th:if="${warning}">
-              <p>An account already exists with this username.</p>
+              <p th:text="${warning == 'invalid_password_value'} ? 'Invalid password value. It does not comply with the password policy.' : 'An account already exists with this username.'"></p>
             </div>
             <form th:action="@{register}" method="post">
               <input class="mdl-textfield__input" type="text" id="firstName" name="firstName"/>
@@ -138,6 +138,9 @@
             <div>
               <label>Hi <span th:text="${user.firstName} + ' ' + ${user.lastName}"></span></label>
               <p>Please enter a new password for your account.</p>
+            </div>
+             <div th:if="${warning}">
+                <p>Invalid password value. It does not comply with the password policy.</p>
             </div>
             <form th:action="@{resetPassword}" method="post">
               <input type="password" id="password" name="password" required/>
diff --git a/gravitee-am-ui/src/app/services/client.service.ts b/gravitee-am-ui/src/app/services/client.service.ts
index 1f45f45b4f..0bf92da198 100644
--- a/gravitee-am-ui/src/app/services/client.service.ts
+++ b/gravitee-am-ui/src/app/services/client.service.ts
@@ -41,7 +41,7 @@ export class ClientService {
   }
 
   update(domainId, id, client): Observable<Response> {
-    return this.http.patch(this.clientsURL + domainId + "/clients/" + id, {
+    return this.http.put(this.clientsURL + domainId + "/clients/" + id, {
       'clientName': client.clientName,
       'redirectUris': client.redirectUris,
       'authorizedGrantTypes': client.authorizedGrantTypes,


Test patch:
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/CreateUserEndpointHandlerTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/CreateUserEndpointHandlerTest.java
new file mode 100644
index 0000000000..b34246b96c
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/CreateUserEndpointHandlerTest.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * 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 io.gravitee.am.gateway.handler.vertx.handler.scim.endpoint.users;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import io.gravitee.am.gateway.handler.scim.UserService;
+import io.gravitee.am.gateway.handler.scim.model.Meta;
+import io.gravitee.am.gateway.handler.scim.model.User;
+import io.gravitee.am.gateway.handler.vertx.RxWebTestBase;
+import io.gravitee.am.gateway.handler.vertx.handler.scim.handler.ErrorHandler;
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
+import io.reactivex.Single;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.core.json.Json;
+import io.vertx.reactivex.ext.web.handler.BodyHandler;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class CreateUserEndpointHandlerTest extends RxWebTestBase {
+
+    @Mock
+    private UserService userService;
+
+    @Mock
+    private PasswordValidator passwordValidator;
+
+    @Mock
+    private ObjectMapper objectMapper;
+
+    @Mock
+    private ObjectWriter objectWriter;
+
+    @InjectMocks
+    private CreateUserEndpointHandler createUserEndpointHandler = new CreateUserEndpointHandler(userService);
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        // set handler
+        createUserEndpointHandler.setPasswordValidator(passwordValidator);
+        createUserEndpointHandler.setObjectMapper(objectMapper);
+
+        // object mapper
+        when(objectWriter.writeValueAsString(any())).thenReturn("UserObject");
+        when(objectMapper.writerWithDefaultPrettyPrinter()).thenReturn(objectWriter);
+
+        router.route()
+                .handler(BodyHandler.create())
+                .failureHandler(new ErrorHandler());
+    }
+
+    @Test
+    public void shouldNotInvokeSCIMCreateUserEndpoint_invalid_password() throws Exception {
+        router.route("/Users").handler(createUserEndpointHandler);
+        when(passwordValidator.validate(anyString())).thenReturn(false);
+
+        testRequest(
+                HttpMethod.POST,
+                "/Users",
+                req -> {
+                    req.setChunked(true);
+                    req.write(Json.encode(getUser()));
+                    },
+                400,
+                "Bad Request",
+                "{\n" +
+                        "  \"status\" : \"400\",\n" +
+                        "  \"scimType\" : \"invalidValue\",\n" +
+                        "  \"detail\" : \"Field [password] is invalid\",\n" +
+                        "  \"schemas\" : [ \"urn:ietf:params:scim:api:messages:2.0:Error\" ]\n" +
+                        "}");
+    }
+
+    @Test
+    public void shouldInvokeSCIMCreateUserEndpoint_valid_password() throws Exception {
+        router.route("/Users").handler(createUserEndpointHandler);
+        when(passwordValidator.validate(anyString())).thenReturn(true);
+        when(userService.create(any(), any())).thenReturn(Single.just(getUser()));
+
+        testRequest(
+                HttpMethod.POST,
+                "/Users",
+                req -> {
+                    req.setChunked(true);
+                    req.write(Json.encode(getUser()));
+                },
+                201,
+                "Created", null);
+    }
+
+    private User getUser() {
+        User user = new User();
+        user.setUserName("username");
+        user.setSchemas(User.SCHEMAS);
+        user.setPassword("toto");
+
+        Meta meta = new Meta();
+        meta.setLocation("http://test");
+        user.setMeta(meta);
+
+        return user;
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/PasswordPolicyRequestParseHandlerTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/PasswordPolicyRequestParseHandlerTest.java
new file mode 100644
index 0000000000..940029f046
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/PasswordPolicyRequestParseHandlerTest.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * 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 io.gravitee.am.gateway.handler.vertx.handler.scim.endpoint.users;
+
+import io.gravitee.am.gateway.handler.vertx.RxWebTestBase;
+import io.gravitee.am.gateway.handler.vertx.handler.root.endpoint.user.PasswordPolicyRequestParseHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.scim.handler.ErrorHandler;
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.reactivex.core.buffer.Buffer;
+import io.vertx.reactivex.ext.web.handler.BodyHandler;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class PasswordPolicyRequestParseHandlerTest extends RxWebTestBase {
+
+    @Mock
+    private PasswordValidator passwordValidator;
+
+    @InjectMocks
+    private PasswordPolicyRequestParseHandler passwordPolicyRequestParseHandler = new PasswordPolicyRequestParseHandler(passwordValidator);
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        router.route()
+                .handler(BodyHandler.create())
+                .failureHandler(new ErrorHandler());
+    }
+
+    @Test
+    public void shouldNotHandle_invalid_password() throws Exception {
+        router.route("/")
+                .handler(passwordPolicyRequestParseHandler)
+                .handler(rc -> rc.response().end());
+
+        when(passwordValidator.validate(anyString())).thenReturn(false);
+
+        testRequest(HttpMethod.POST, "/", req -> {
+            Buffer buffer = Buffer.buffer();
+            buffer.appendString("password=password");
+            req.headers().set("content-length", String.valueOf(buffer.length()));
+            req.headers().set("content-type", "application/x-www-form-urlencoded");
+            req.write(buffer);
+        }, resp -> {
+            String location = resp.headers().get("location");
+            assertNotNull(location);
+            assertTrue(location.contains("warning=invalid_password_value"));
+        },302, "Found", null);
+    }
+
+    @Test
+    public void shouldHandle() throws Exception {
+        router.route("/")
+                .handler(passwordPolicyRequestParseHandler)
+                .handler(rc -> rc.response().end());
+
+        when(passwordValidator.validate(anyString())).thenReturn(true);
+
+        testRequest(HttpMethod.POST, "/", req -> {
+            Buffer buffer = Buffer.buffer();
+            buffer.appendString("password=password");
+            req.headers().set("content-length", String.valueOf(buffer.length()));
+            req.headers().set("content-type", "application/x-www-form-urlencoded");
+            req.write(buffer);
+        },200, "OK", null);
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/UpdateUserEndpointHandlerTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/UpdateUserEndpointHandlerTest.java
new file mode 100644
index 0000000000..c6c46da816
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/handler/scim/endpoint/users/UpdateUserEndpointHandlerTest.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * 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 io.gravitee.am.gateway.handler.vertx.handler.scim.endpoint.users;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import io.gravitee.am.gateway.handler.scim.UserService;
+import io.gravitee.am.gateway.handler.scim.model.Meta;
+import io.gravitee.am.gateway.handler.scim.model.User;
+import io.gravitee.am.gateway.handler.vertx.RxWebTestBase;
+import io.gravitee.am.gateway.handler.vertx.handler.scim.handler.ErrorHandler;
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
+import io.reactivex.Single;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.core.json.Json;
+import io.vertx.reactivex.ext.web.handler.BodyHandler;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class UpdateUserEndpointHandlerTest extends RxWebTestBase {
+
+    @Mock
+    private UserService userService;
+
+    @Mock
+    private PasswordValidator passwordValidator;
+
+    @Mock
+    private ObjectMapper objectMapper;
+
+    @Mock
+    private ObjectWriter objectWriter;
+
+    @InjectMocks
+    private UpdateUserEndpointHandler updateUserEndpointHandler = new UpdateUserEndpointHandler(userService);
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        // set handler
+        updateUserEndpointHandler.setPasswordValidator(passwordValidator);
+        updateUserEndpointHandler.setObjectMapper(objectMapper);
+
+        // object mapper
+        when(objectWriter.writeValueAsString(any())).thenReturn("UserObject");
+        when(objectMapper.writerWithDefaultPrettyPrinter()).thenReturn(objectWriter);
+
+        router.route()
+                .handler(BodyHandler.create())
+                .failureHandler(new ErrorHandler());
+    }
+
+    @Test
+    public void shouldNotInvokeSCIMUpdateUserEndpoint_invalid_password() throws Exception {
+        router.route("/Users").handler(updateUserEndpointHandler);
+        when(passwordValidator.validate(anyString())).thenReturn(false);
+
+        testRequest(
+                HttpMethod.PUT,
+                "/Users",
+                req -> {
+                    req.setChunked(true);
+                    req.write(Json.encode(getUser()));
+                },
+                400,
+                "Bad Request",
+                "{\n" +
+                        "  \"status\" : \"400\",\n" +
+                        "  \"scimType\" : \"invalidValue\",\n" +
+                        "  \"detail\" : \"Field [password] is invalid\",\n" +
+                        "  \"schemas\" : [ \"urn:ietf:params:scim:api:messages:2.0:Error\" ]\n" +
+                        "}");
+    }
+
+    @Test
+    public void shouldInvokeSCIMUpdateUserEndpoint_valid_password() throws Exception {
+        router.route("/Users").handler(updateUserEndpointHandler);
+        when(passwordValidator.validate(anyString())).thenReturn(true);
+        when(userService.update(any(), any(), any())).thenReturn(Single.just(getUser()));
+
+        testRequest(
+                HttpMethod.PUT,
+                "/Users",
+                req -> {
+                    req.setChunked(true);
+                    req.write(Json.encode(getUser()));
+                },
+                200,
+                "OK", null);
+    }
+
+    private User getUser() {
+        User user = new User();
+        user.setUserName("username");
+        user.setSchemas(User.SCHEMAS);
+        user.setPassword("toto");
+
+        Meta meta = new Meta();
+        meta.setLocation("http://test");
+        user.setMeta(meta);
+
+        return user;
+    }
+}
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/JerseySpringTest.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/JerseySpringTest.java
index 7adbc54447..7347980a2a 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/JerseySpringTest.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/JerseySpringTest.java
@@ -19,6 +19,7 @@ import io.gravitee.am.management.handlers.management.api.certificate.Certificate
 import io.gravitee.am.management.service.*;
 import io.gravitee.am.plugins.certificate.core.CertificatePluginManager;
 import io.gravitee.am.service.*;
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.test.JerseyTest;
 import org.junit.After;
@@ -97,6 +98,9 @@ public abstract class JerseySpringTest {
     @Autowired
     protected FormService formService;
 
+    @Autowired
+    protected PasswordValidator passwordValidator;
+
     @Configuration
     @ComponentScan("io.gravitee.am.management.handlers.management.api.resources.enhancer")
     static class ContextConfiguration {
@@ -189,6 +193,11 @@ public abstract class JerseySpringTest {
         public FormService formService() {
             return mock(FormService.class);
         }
+
+        @Bean
+        public PasswordValidator passwordValidator() {
+            return mock(PasswordValidator.class);
+        }
     }
 
     private JerseyTest _jerseyTest;
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UserResourceTest.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UserResourceTest.java
index 5a1a778840..fe5926a744 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UserResourceTest.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UserResourceTest.java
@@ -16,16 +16,20 @@
 package io.gravitee.am.management.handlers.management.api.resources;
 
 import io.gravitee.am.management.handlers.management.api.JerseySpringTest;
+import io.gravitee.am.management.handlers.management.api.model.PasswordValue;
 import io.gravitee.am.model.Domain;
 import io.gravitee.am.model.User;
 import io.gravitee.am.service.exception.TechnicalManagementException;
 import io.gravitee.common.http.HttpStatusCode;
+import io.reactivex.Completable;
 import io.reactivex.Maybe;
 import org.junit.Test;
 
+import javax.ws.rs.client.Entity;
 import javax.ws.rs.core.Response;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.anyString;
 import static org.mockito.Mockito.doReturn;
 
 /**
@@ -74,4 +78,52 @@ public class UserResourceTest extends JerseySpringTest {
         final Response response = target("domains/" + domainId).request().get();
         assertEquals(HttpStatusCode.INTERNAL_SERVER_ERROR_500, response.getStatus());
     }
+
+    @Test
+    public void shouldNotResetPassword_invalid_password() {
+        final String domainId = "domain-id";
+        final String userId = "user-id";
+
+        PasswordValue passwordValue = new PasswordValue();
+        passwordValue.setPassword("password");
+
+        doReturn(false).when(passwordValidator).validate(anyString());
+
+        final Response response = target("domains")
+                .path(domainId).path("users")
+                .path(userId)
+                .path("resetPassword")
+                .request()
+                .post(Entity.json(passwordValue));
+        assertEquals(HttpStatusCode.BAD_REQUEST_400, response.getStatus());
+    }
+
+    @Test
+    public void shouldResetPassword() {
+        final String domainId = "domain-id";
+        final Domain mockDomain = new Domain();
+        mockDomain.setId(domainId);
+
+        final String userId = "user-id";
+        final User mockUser = new User();
+        mockUser.setId(userId);
+        mockUser.setUsername("user-username");
+        mockUser.setDomain(domainId);
+
+        PasswordValue passwordValue = new PasswordValue();
+        passwordValue.setPassword("password");
+
+
+        doReturn(Maybe.just(mockDomain)).when(domainService).findById(domainId);
+        doReturn(Completable.complete()).when(userService).resetPassword(anyString(), anyString(), anyString());
+        doReturn(true).when(passwordValidator).validate(anyString());
+
+        final Response response = target("domains")
+                .path(domainId).path("users")
+                .path(userId)
+                .path("resetPassword")
+                .request()
+                .post(Entity.json(passwordValue));
+        assertEquals(HttpStatusCode.NO_CONTENT_204, response.getStatus());
+    }
 }
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UsersResourceTest.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UsersResourceTest.java
index 888a809720..47468f6b86 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UsersResourceTest.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/UsersResourceTest.java
@@ -20,17 +20,21 @@ import io.gravitee.am.model.Domain;
 import io.gravitee.am.model.User;
 import io.gravitee.am.model.common.Page;
 import io.gravitee.am.service.exception.TechnicalManagementException;
+import io.gravitee.am.service.model.NewUser;
 import io.gravitee.common.http.HttpStatusCode;
 import io.reactivex.Maybe;
 import io.reactivex.Single;
 import org.junit.Test;
 
+import javax.ws.rs.client.Entity;
 import javax.ws.rs.core.Response;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
 import static org.mockito.Mockito.doReturn;
 
 /**
@@ -80,4 +84,47 @@ public class UsersResourceTest extends JerseySpringTest {
         final Response response = target("domains").path(domainId).path("users").request().get();
         assertEquals(HttpStatusCode.INTERNAL_SERVER_ERROR_500, response.getStatus());
     }
+
+    @Test
+    public void shouldNotCreate_invalid_password() {
+        final String domainId = "domain-1";
+        final Domain mockDomain = new Domain();
+        mockDomain.setId(domainId);
+
+        NewUser newUser = new NewUser();
+        newUser.setUsername("username");
+        newUser.setPassword("password");
+        newUser.setEmail("test@test.com");
+
+        doReturn(Maybe.just(mockDomain)).when(domainService).findById(domainId);
+        doReturn(false).when(passwordValidator).validate(anyString());
+
+        final Response response = target("domains")
+                .path(domainId)
+                .path("users")
+                .request().post(Entity.json(newUser));
+        assertEquals(HttpStatusCode.BAD_REQUEST_400, response.getStatus());
+    }
+
+    @Test
+    public void shouldNotCreate() {
+        final String domainId = "domain-1";
+        final Domain mockDomain = new Domain();
+        mockDomain.setId(domainId);
+
+        NewUser newUser = new NewUser();
+        newUser.setUsername("username");
+        newUser.setPassword("password");
+        newUser.setEmail("test@test.com");
+
+        doReturn(true).when(passwordValidator).validate(anyString());
+        doReturn(Maybe.just(mockDomain)).when(domainService).findById(domainId);
+        doReturn(Single.just(new User())).when(userService).create(anyString(), any());
+
+        final Response response = target("domains")
+                .path(domainId)
+                .path("users")
+                .request().post(Entity.json(newUser));
+        assertEquals(HttpStatusCode.CREATED_201, response.getStatus());
+    }
 }
diff --git a/gravitee-am-service/src/test/java/io/gravitee/am/service/ClientServiceTest.java b/gravitee-am-service/src/test/java/io/gravitee/am/service/ClientServiceTest.java
index 3225e9819a..3682f8fb23 100644
--- a/gravitee-am-service/src/test/java/io/gravitee/am/service/ClientServiceTest.java
+++ b/gravitee-am-service/src/test/java/io/gravitee/am/service/ClientServiceTest.java
@@ -553,7 +553,7 @@ public class ClientServiceTest {
     @Test
     public void shouldPatch() {
         PatchClient patchClient = Mockito.mock(PatchClient.class);
-        when(patchClient.patch(any())).thenReturn(new Client());
+        when(patchClient.patch(any(), eq(false))).thenReturn(new Client());
         when(patchClient.getRedirectUris()).thenReturn(Optional.of(Arrays.asList("https://gravitee.io/callback")));
         when(domainService.findById(DOMAIN)).thenReturn(Maybe.just(new Domain()));
         when(domainService.reload(eq(DOMAIN), any())).thenReturn(Single.just(new Domain()));
diff --git a/gravitee-am-service/src/test/java/io/gravitee/am/service/authentication/crypto/password/PasswordValidatorTest.java b/gravitee-am-service/src/test/java/io/gravitee/am/service/authentication/crypto/password/PasswordValidatorTest.java
new file mode 100644
index 0000000000..6f1d419952
--- /dev/null
+++ b/gravitee-am-service/src/test/java/io/gravitee/am/service/authentication/crypto/password/PasswordValidatorTest.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * 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 io.gravitee.am.service.authentication.crypto.password;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class PasswordValidatorTest {
+
+    @Test
+    public void testPassword_min_8_characters_at_least_one_letter_one_number() {
+        PasswordValidator passwordValidator = new RegexPasswordValidator("^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$");
+
+        Assert.assertFalse(passwordValidator.validate("test"));
+        Assert.assertFalse(passwordValidator.validate("password"));
+        Assert.assertTrue(passwordValidator.validate("password01"));
+    }
+
+    @Test
+    public void testPassword_min_8_characters_at_least_one_letter_one_number_one_special_character() {
+        PasswordValidator passwordValidator = new RegexPasswordValidator("^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$");
+
+        Assert.assertFalse(passwordValidator.validate("test"));
+        Assert.assertFalse(passwordValidator.validate("password"));
+        Assert.assertFalse(passwordValidator.validate("password01"));
+        Assert.assertTrue(passwordValidator.validate("password01*"));
+    }
+
+    @Test
+    public void testPassword_min_8_characters_at_least_one_uppercase_letter_one_lowercase_letter_one_number_one_special_character() {
+        PasswordValidator passwordValidator = new RegexPasswordValidator("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$");
+
+        Assert.assertFalse(passwordValidator.validate("test"));
+        Assert.assertFalse(passwordValidator.validate("password"));
+        Assert.assertFalse(passwordValidator.validate("password01"));
+        Assert.assertFalse(passwordValidator.validate("password01*"));
+        Assert.assertTrue(passwordValidator.validate("Password01*"));
+    }
+
+    @Test
+    public void testPassword_min_8_characters_max_10_characters_at_least_one_uppercase_letter_one_lowercase_letter_one_number_one_special_character() {
+        PasswordValidator passwordValidator = new RegexPasswordValidator("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,10}$");
+
+        Assert.assertFalse(passwordValidator.validate("test"));
+        Assert.assertFalse(passwordValidator.validate("password"));
+        Assert.assertFalse(passwordValidator.validate("password01"));
+        Assert.assertFalse(passwordValidator.validate("password01*"));
+        Assert.assertFalse(passwordValidator.validate("Password01*"));
+        Assert.assertTrue(passwordValidator.validate("Password0*"));
+    }
+}
diff --git a/gravitee-am-service/src/test/java/io/gravitee/am/service/model/PatchClientTest.java b/gravitee-am-service/src/test/java/io/gravitee/am/service/model/PatchClientTest.java
index 091db6d8cd..ccc06d2c9e 100644
--- a/gravitee-am-service/src/test/java/io/gravitee/am/service/model/PatchClientTest.java
+++ b/gravitee-am-service/src/test/java/io/gravitee/am/service/model/PatchClientTest.java
@@ -53,7 +53,7 @@ public class PatchClientTest {
         patcher.setRefreshTokenValiditySeconds(Optional.empty());
 
         //Apply patch
-        Client result = patcher.patch(toPatch);
+        Client result = patcher.patch(toPatch, false);
 
         //Checks
         assertNotNull(result);