You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by gi...@apache.org on 2016/01/12 16:35:10 UTC

syncope git commit: [SYNCOPE-720] Password reset functionality implemented

Repository: syncope
Updated Branches:
  refs/heads/master 03d7de338 -> 4ed984ef6


[SYNCOPE-720] Password reset functionality implemented


Project: http://git-wip-us.apache.org/repos/asf/syncope/repo
Commit: http://git-wip-us.apache.org/repos/asf/syncope/commit/4ed984ef
Tree: http://git-wip-us.apache.org/repos/asf/syncope/tree/4ed984ef
Diff: http://git-wip-us.apache.org/repos/asf/syncope/diff/4ed984ef

Branch: refs/heads/master
Commit: 4ed984ef6c7def62159e94b939ab3bfc6c83fe1d
Parents: 03d7de3
Author: giacomolm <gi...@hotmail.it>
Authored: Tue Jan 12 16:34:20 2016 +0100
Committer: giacomolm <gi...@hotmail.it>
Committed: Tue Jan 12 16:34:33 2016 +0100

----------------------------------------------------------------------
 .../enduser/SyncopeEnduserApplication.java      | 23 ++++-
 .../resources/SecurityQuestionResource.java     | 35 +++++--
 .../resources/UserSelfPasswordReset.java        | 93 ++++++++++++++++++
 .../META-INF/resources/app/css/app.css          | 20 ++++
 .../resources/META-INF/resources/app/index.html |  6 +-
 .../resources/META-INF/resources/app/js/app.js  | 99 +++++++++++++++-----
 .../app/js/controllers/LoginController.js       |  3 +-
 .../app/js/controllers/UserController.js        | 60 +++++++++++-
 .../resources/app/js/directives/captcha.js      |  2 +
 .../app/js/services/securityQuestionService.js  | 12 ++-
 .../app/js/services/userSelfService.js          | 24 +++--
 .../META-INF/resources/app/views/captcha.html   |  2 +-
 .../resources/app/views/passwordreset.html      | 75 +++++++++++++++
 13 files changed, 403 insertions(+), 51 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/syncope/blob/4ed984ef/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java
index d1ab35c..9fe4cf7 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java
@@ -36,6 +36,7 @@ import org.apache.syncope.client.enduser.resources.SchemaResource;
 import org.apache.syncope.client.enduser.resources.SecurityQuestionResource;
 import org.apache.syncope.client.enduser.resources.SyncopeResourceResource;
 import org.apache.syncope.client.enduser.resources.UserSelfCreateResource;
+import org.apache.syncope.client.enduser.resources.UserSelfPasswordReset;
 import org.apache.syncope.client.enduser.resources.UserSelfReadResource;
 import org.apache.syncope.client.enduser.resources.UserSelfUpdateResource;
 import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
@@ -173,6 +174,16 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
             }
         });
 
+        mountResource("/api/self/requestPasswordReset", new ResourceReference("userSelfPasswordReset") {
+
+            private static final long serialVersionUID = -128426276529456602L;
+
+            @Override
+            public IResource getResource() {
+                return new UserSelfPasswordReset();
+            }
+        });
+
         mountResource("/api/schemas", new ResourceReference("schemas") {
 
             private static final long serialVersionUID = -128426276529456602L;
@@ -192,7 +203,7 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
                 return new SyncopeResourceResource();
             }
         });
-        
+
         mountResource("/api/securityQuestions", new ResourceReference("securityQuestions") {
 
             private static final long serialVersionUID = -128426276529456602L;
@@ -203,6 +214,16 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
             }
         });
 
+        mountResource("/api/securityQuestions/byUser/${username}", new ResourceReference("securityQuestions") {
+
+            private static final long serialVersionUID = -128426276529456602L;
+
+            @Override
+            public IResource getResource() {
+                return new SecurityQuestionResource();
+            }
+        });
+
         mountResource("/api/info", new ResourceReference("info") {
 
             private static final long serialVersionUID = -128426276529456602L;

http://git-wip-us.apache.org/repos/asf/syncope/blob/4ed984ef/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SecurityQuestionResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SecurityQuestionResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SecurityQuestionResource.java
index 3f6c0ea..7e79b1b 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SecurityQuestionResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SecurityQuestionResource.java
@@ -26,8 +26,10 @@ import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.common.lib.to.SecurityQuestionTO;
 import org.apache.syncope.common.rest.api.service.SecurityQuestionService;
 import org.apache.syncope.core.misc.serialization.POJOHelper;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.request.resource.AbstractResource;
 import org.apache.wicket.request.resource.IResource;
+import org.apache.wicket.util.string.StringValue;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -60,15 +62,30 @@ public class SecurityQuestionResource extends AbstractBaseResource {
                 return response;
             }
 
-            final List<SecurityQuestionTO> securityQuestionTOs = securityQuestionService.list();
-
-            response.setWriteCallback(new AbstractResource.WriteCallback() {
-
-                @Override
-                public void writeData(final IResource.Attributes attributes) throws IOException {
-                    attributes.getResponse().write(POJOHelper.serialize(securityQuestionTOs));
-                }
-            });
+            PageParameters parameters = attributes.getParameters();
+            StringValue username = parameters.get("username");
+            //if the username is defined then retrieve its security questions, otherwise retrieve all security questions
+            if (!username.isEmpty()) {
+                final SecurityQuestionTO securityQuestionTO = securityQuestionService.readByUser(username.toString());
+
+                response.setWriteCallback(new AbstractResource.WriteCallback() {
+
+                    @Override
+                    public void writeData(final IResource.Attributes attributes) throws IOException {
+                        attributes.getResponse().write(POJOHelper.serialize(securityQuestionTO));
+                    }
+                });
+            } else {
+                final List<SecurityQuestionTO> securityQuestionTOs = securityQuestionService.list();
+
+                response.setWriteCallback(new AbstractResource.WriteCallback() {
+
+                    @Override
+                    public void writeData(final IResource.Attributes attributes) throws IOException {
+                        attributes.getResponse().write(POJOHelper.serialize(securityQuestionTOs));
+                    }
+                });
+            }
 
             response.setStatusCode(Response.Status.OK.getStatusCode());
         } catch (Exception e) {

http://git-wip-us.apache.org/repos/asf/syncope/blob/4ed984ef/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfPasswordReset.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfPasswordReset.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfPasswordReset.java
new file mode 100644
index 0000000..5198ba7
--- /dev/null
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfPasswordReset.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.resources;
+
+import java.io.IOException;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.Response;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.common.rest.api.service.UserSelfService;
+import org.apache.wicket.request.resource.AbstractResource;
+import org.apache.wicket.request.resource.IResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class UserSelfPasswordReset extends AbstractBaseResource {
+
+    private static final long serialVersionUID = -2721621682300247583L;
+
+    private static final Logger LOG = LoggerFactory.getLogger(UserSelfPasswordReset.class);
+
+    private final UserSelfService userSelfService;
+
+    public UserSelfPasswordReset() {
+        userSelfService = SyncopeEnduserSession.get().getService(UserSelfService.class);
+    }
+
+    @Override
+    protected ResourceResponse newResourceResponse(final IResource.Attributes attributes) {
+
+        AbstractResource.ResourceResponse response = new AbstractResource.ResourceResponse();
+
+        try {
+            HttpServletRequest request = (HttpServletRequest) attributes.getRequest().getContainerRequest();
+            if (!xsrfCheck(request)) {
+                LOG.error("XSRF TOKEN does not match");
+                response.setError(Response.Status.BAD_REQUEST.getStatusCode(), "XSRF TOKEN does not match");
+                return response;
+            }
+            Map<String, String[]> parameters = request.getParameterMap();
+            if (parameters.get("username") == null || parameters.get("username").length == 0) {
+                throw new Exception("A valid username should be provided");
+            }
+            if (SyncopeEnduserSession.get().getSyncopeTO().isPwdResetRequiringSecurityQuestions()) {
+                if (parameters.get("securityanswer") == null || parameters.get("securityanswer").length == 0) {
+                    throw new Exception("A correct security answer should be provided");
+                }
+                userSelfService.requestPasswordReset(parameters.get("username")[0],
+                        parameters.get("securityanswer")[0]);
+            } else {
+                userSelfService.requestPasswordReset(parameters.get("username")[0], null);
+            }
+            final String responseMessage = new StringBuilder().append("Password reset request sent for user ").append(
+                    parameters.get("username")[0]).toString();
+
+            response.setWriteCallback(new WriteCallback() {
+
+                @Override
+                public void writeData(final Attributes attributes) throws IOException {
+                    attributes.getResponse().write(responseMessage);
+                }
+            });
+
+            response.setStatusCode(Response.Status.OK.getStatusCode());
+
+        } catch (final Exception e) {
+            LOG.error("Error while updating user", e);
+            response.setError(Response.Status.BAD_REQUEST.getStatusCode(), new StringBuilder()
+                    .append("ErrorMessage{{ ")
+                    .append(e.getMessage())
+                    .append(" }}")
+                    .toString());
+        }
+        return response;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/4ed984ef/client/enduser/src/main/resources/META-INF/resources/app/css/app.css
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/css/app.css b/client/enduser/src/main/resources/META-INF/resources/app/css/app.css
index a4ae15d..2f18d47 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/css/app.css
+++ b/client/enduser/src/main/resources/META-INF/resources/app/css/app.css
@@ -33,4 +33,24 @@ under the License.
 
 .k-notification{
   width : 320px;
+}
+
+.suggestions{
+  font-size: 10px;
+  display: inline-block;
+  margin-bottom: 5px;
+}
+
+#resetpassword{
+  background: -moz-linear-gradient(top, #a9db80 0%, #96c56f 100%); /* FF3.6+ */
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#a9db80), color-stop(100%,#96c56f)); /* Chrome,Safari4+ */
+  background: -webkit-linear-gradient(top, #a9db80 0%,#96c56f 100%); /* Chrome10+,Safari5.1+ */
+  background: -o-linear-gradient(top, #a9db80 0%,#96c56f 100%); /* Opera 11.10+ */
+  background: -ms-linear-gradient(top, #a9db80 0%,#96c56f 100%); /* IE10+ */
+  color: black;
+  margin-left: 5px;
+  //width: 15%;
+}
+#resetpassword:hover {
+  background: #658D5D;
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/syncope/blob/4ed984ef/client/enduser/src/main/resources/META-INF/resources/app/index.html
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/index.html b/client/enduser/src/main/resources/META-INF/resources/app/index.html
index eb9b50a..f83defa 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/index.html
+++ b/client/enduser/src/main/resources/META-INF/resources/app/index.html
@@ -40,11 +40,11 @@ under the License.
         <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
     <![endif]-->
 
+    <span id="notifications" kendo-notification="notifications"></span>
+    
     <!--<div ng-view ng-cloak ng-controller="ApplicationController"></div>-->
     <div ui-view ng-cloak ng-controller="ApplicationController" ng-init="initApplication()">      
-    </div>
-    
-    <span id="notification" kendo-notification="notification"></span>
+    </div>    
 
     <!--    <footer id="footer" class="hidden-print">
           <ul class="nav pull-right">

http://git-wip-us.apache.org/repos/asf/syncope/blob/4ed984ef/client/enduser/src/main/resources/META-INF/resources/app/js/app.js
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/app.js b/client/enduser/src/main/resources/META-INF/resources/app/js/app.js
index 4d791dc..113fc86 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/js/app.js
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/app.js
@@ -173,6 +173,10 @@ app.config(['$stateProvider', '$urlRouterProvider', '$httpProvider',
                   return AuthenticationHelper.authenticated();
                 }
               }
+            })
+            .state('passwordreset', {
+              url: '/passwordreset',
+              templateUrl: 'views/passwordreset.html'
             });
 
     // catch all other routes
@@ -187,14 +191,18 @@ app.config(['$stateProvider', '$urlRouterProvider', '$httpProvider',
     $httpProvider.interceptors.push(function ($q, $rootScope, $location) {
       var numLoadings = 0;
       return {
-//        'request': function (config) {
-//          numLoadings++;
-//          // Show loader
-//          if (config.url.indexOf("skipLoader=true") == -1) {
-//            $rootScope.$broadcast("loader_show");
-//          }
-//          return config || $q.when(config);
-//        },
+        'request': function (config, a, b) {
+          //if the url is an html, we're changing page
+          if (config.url.indexOf('.html', config.url.length - 5) == -1) {
+            $rootScope.$broadcast("xhrStarted");
+          }
+          /*numLoadings++;
+           // Show loader
+           if (config.url.indexOf("skipLoader=true") == -1) {
+           $rootScope.$broadcast("loader_show");
+           }*/
+          return config || $q.when(config);
+        },
 //        'response': function (response) {
 //          if ((--numLoadings) === 0) {
 //            // Hide loader
@@ -274,12 +282,14 @@ app.controller('ApplicationController', ['$scope', '$rootScope', 'InfoService',
       $rootScope.selfRegAllowed = false;
       $rootScope.pwdResetAllowed = false;
       $rootScope.version = "";
+      $rootScope.pwdResetRequiringSecurityQuestions = false;
       //info settings are initialized every time an user open the login page
       InfoService.getInfo().then(
               function (response) {
                 $rootScope.pwdResetAllowed = response.pwdResetAllowed;
                 $rootScope.selfRegAllowed = response.selfRegAllowed;
                 $rootScope.version = response.version;
+                $rootScope.pwdResetRequiringSecurityQuestions = response.pwdResetRequiringSecurityQuestions;
               },
               function (response) {
                 console.log("Something went wrong while accessing info resource", response);
@@ -295,30 +305,71 @@ app.controller('ApplicationController', ['$scope', '$rootScope', 'InfoService',
         return $rootScope.version;
       };
 
-      //Intercepting location change event
-      $rootScope.$on("$locationChangeStart", function (event, next, current) {
-        //When a location changes, old notifications should be removed
+      //Notification management           
+      $scope.notification = $('#notifications').kendoNotification().data("kendoNotification");
+      $scope.notification.setOptions({stacking: "down"});
+      $scope.notification.options.position["top"] = 20;
+      $scope.showSuccess = function (message, component) {
+        if (!$scope.notificationExists(message)) {
+          component.options.autoHideAfter = 3000;
+          component.show(message, "success");
+        }
+      }
+      $scope.showError = function (message, component) {        
+        if (!$scope.notificationExists(message)) {
+          component.options.autoHideAfter = 0;
+          component.show(message, "error");
+        }
+      }
+      $scope.notificationExists = function (message) {
+        var result = false;
         if ($scope.notification != null) {
-          var pendingNotifications = $scope.notification.getNotifications();          
-          setTimeout(function () {
+          var pendingNotifications = $scope.notification.getNotifications();
+          pendingNotifications.each(function (idx, element) {
+            var popup = $(element).data("kendoPopup");
+            if (!popup.hide && popup.wrapper.html().indexOf(message) > -1) {
+              result = true;
+              return false; //breaking the each and storing the real result
+            }
+          });
+        }
+        return result;
+      }
+      $scope.hideNotifications = function (timer) {
+        if ($scope.notification != null) {
+          var pendingNotifications = $scope.notification.getNotifications();
+          if (timer && timer > 0) {
+            setTimeout(function () {
+              pendingNotifications.each(function (idx, element) {
+                var popup = $(element).data("kendoPopup");
+                if (popup) {
+                  popup.hide = true;
+                  popup.close();
+                }
+              });
+            }, timer);
+          }
+          else {
             pendingNotifications.each(function (idx, element) {
               var popup = $(element).data("kendoPopup");
               if (popup) {
-                popup.close();
+                popup.hide = true;
+                //we should destroy the message immediately
+                popup.destroy();
               }
             });
-          }, 3000);
+          }
         }
-      });
-
-      $scope.showSuccess = function (message, component) {
-        component.options.autoHideAfter = 3000;
-        component.show(message, "success");
-      }
-      $scope.showError = function (message, component) {
-        component.options.autoHideAfter = 0;
-        component.show(message, "error");
       }
+      //Intercepting location change event
+      $rootScope.$on("$locationChangeStart", function (event, next, current) {
+        //When a location changes, old notifications should be removed
+        $scope.hideNotifications(3000)
+      });
+      //Intercepting xhr start event
+      $scope.$on('xhrStarted', function (event, next, current) {
+        $scope.hideNotifications(0);
+      });
     }
   }]);
 app.factory('AuthenticationHelper', ['$q', '$rootScope',

http://git-wip-us.apache.org/repos/asf/syncope/blob/4ed984ef/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/LoginController.js
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/LoginController.js b/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/LoginController.js
index 40794a3..c1c563c 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/LoginController.js
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/LoginController.js
@@ -66,8 +66,7 @@ angular.module("login").controller("LoginController", ['$scope', '$rootScope', '
     };
 
     $scope.passwordReset = function () {
-      // TODO
-      console.log("NOT YET IMPLEMENTED")
+       $location.path("/passwordreset");
     };
 
     $scope.errorAPI = function () {

http://git-wip-us.apache.org/repos/asf/syncope/blob/4ed984ef/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/UserController.js
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/UserController.js b/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/UserController.js
index 89eccae..7521553 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/UserController.js
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/UserController.js
@@ -226,7 +226,7 @@ angular.module("self").controller("UserController", ['$scope', '$rootScope', '$l
         if ($scope.createMode) {
 
           UserSelfService.create(user).then(function (response) {
-            console.log("Created user: ", response);            
+            console.log("Created user: ", response);
             $scope.showSuccess("User " + $scope.user.username + " successfully created", $scope.notification);
             $location.path('/self');
           }, function (response) {
@@ -273,5 +273,63 @@ angular.module("self").controller("UserController", ['$scope', '$rootScope', '$l
         $scope.showError("Error: " + (errorMessage || response), $scope.notification);
         return;
       });
+    },
+    $scope.retrieveSecurityQuestion = function (user) {
+      if ($rootScope.pwdResetRequiringSecurityQuestions){
+        if(user && user.username && user.username.length) {
+        return SecurityQuestionService.
+                getSecurityQuestionByUser(user.username).then(function (data) {
+                  $scope.userSecurityQuestion = data.content;          
+                }, function (response) {
+                  var errorMessage;
+                  // parse error response 
+                  if (response !== undefined) {
+                    errorMessage = response.split("ErrorMessage{{")[1];
+                    errorMessage = errorMessage.split("}}")[0];
+                    $scope.userSecurityQuestion = "";
+                  }
+                  $scope.showError("Error retrieving user security question: " + errorMessage, $scope.notification);
+                });
+        }
+        else{
+           $scope.userSecurityQuestion = "";
+        }    
+      }
+    },
+    $scope.resetPassword = function (user) {
+      if (user && user.username) {
+        $scope.retrieveSecurityQuestion(user);
+        CaptchaService.validate($scope.captchaInput).then(function (response) {
+          if (!(response === 'true')) {
+            $scope.showError("Captcha inserted is not valid, please digit the correct captcha", $scope.notification);
+            return;
+          }
+          UserSelfService.passwordReset(user).then(function (data) {
+            $scope.showSuccess(data, $scope.notification);
+            $location.path('/self');
+          }, function (response) {
+            var errorMessage;
+            // parse error response 
+            if (response !== undefined) {
+              errorMessage = response.split("ErrorMessage{{")[1];
+              errorMessage = errorMessage.split("}}")[0];
+              $scope.showError("An error occured during password reset: " + errorMessage, $scope.notification);
+              //we need to refresh captcha after a valid request
+              $scope.$broadcast("refreshCaptcha");
+            }
+          });
+        }, function (response) {          
+          var errorMessage;
+          // parse error response 
+          if (response !== undefined) {
+            errorMessage = response.split("ErrorMessage{{")[1];
+            errorMessage = errorMessage.split("}}")[0];
+          }
+          $scope.showError("Error: " + (errorMessage || response), $scope.notification);
+          return;
+        });
+      } else {
+        $scope.showError("You should use a valid and non-empty username", $scope.notification);
+      }
     };
   }]);

http://git-wip-us.apache.org/repos/asf/syncope/blob/4ed984ef/client/enduser/src/main/resources/META-INF/resources/app/js/directives/captcha.js
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/directives/captcha.js b/client/enduser/src/main/resources/META-INF/resources/app/js/directives/captcha.js
index c00a4f2..00720fa 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/js/directives/captcha.js
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/directives/captcha.js
@@ -37,6 +37,8 @@ angular.module('self')
 
               // initialize captcha
               $scope.refreshCaptcha();
+              
+              $scope.$on("refreshCaptcha", function(){$scope.refreshCaptcha()});
             }
           };
         });

http://git-wip-us.apache.org/repos/asf/syncope/blob/4ed984ef/client/enduser/src/main/resources/META-INF/resources/app/js/services/securityQuestionService.js
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/services/securityQuestionService.js b/client/enduser/src/main/resources/META-INF/resources/app/js/services/securityQuestionService.js
index ff91f18..417468c 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/js/services/securityQuestionService.js
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/services/securityQuestionService.js
@@ -28,11 +28,17 @@ angular.module('self')
             securityQuestionService.getAvailableSecurityQuestions = function () {
               return  $http.get("/syncope-enduser/api/securityQuestions")
                       .then(function (response) {
-                        console.log("security questions response: ", response);
                         return response.data;
                       }, function (response) {
-                        console.log("Something went wrong during security questions retrieval, exit with status: ",
-                                response);
+                        return $q.reject(response.data || response.statusText);
+                      });
+            };
+
+            securityQuestionService.getSecurityQuestionByUser = function (username) {
+              return  $http.get("/syncope-enduser/api/securityQuestions/byUser/"+username)
+                      .then(function (response) {
+                        return response.data;
+                      }, function (response) {
                         return $q.reject(response.data || response.statusText);
                       });
             };

http://git-wip-us.apache.org/repos/asf/syncope/blob/4ed984ef/client/enduser/src/main/resources/META-INF/resources/app/js/services/userSelfService.js
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/services/userSelfService.js b/client/enduser/src/main/resources/META-INF/resources/app/js/services/userSelfService.js
index 2f713a7..fc60c10 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/js/services/userSelfService.js
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/services/userSelfService.js
@@ -18,13 +18,11 @@
  */
 
 'use strict';
-
 angular.module('login')
         .factory('UserSelfService', ['$resource', '$q', '$http',
           function ($resource, $q, $http) {
 
             var userSelfService = {};
-
             userSelfService.read = function () {
               return $http
                       .get('/syncope-enduser/api/self/read')
@@ -36,7 +34,6 @@ angular.module('login')
                         return $q.reject(response.data || response.statusText);
                       });
             };
-
             userSelfService.create = function (user) {
               return $http
                       .post('/syncope-enduser/api/self/create', user)
@@ -48,7 +45,6 @@ angular.module('login')
                         return $q.reject(response.data || response.statusText);
                       });
             };
-
             userSelfService.update = function (user) {
               return $http
                       .post('/syncope-enduser/api/self/update', user)
@@ -59,10 +55,24 @@ angular.module('login')
                         return $q.reject(response.data || response.statusText);
                       });
             };
-
-            userSelfService.passwordReset = function () {
+            userSelfService.passwordReset = function (user) {
+              return $http
+                      .post('/syncope-enduser/api/self/requestPasswordReset', user,
+                              {
+                                headers: {'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'},
+                                transformRequest: function (obj) {
+                                  var str = [];
+                                  for (var p in obj)
+                                    str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
+                                  return str.join("&");
+                                }
+                              })
+                      .then(function (response) {
+                        return response.data || response.statusText;
+                      }, function (response) {
+                        return $q.reject(response.data || response.statusText);
+                      });
             };
-
             return userSelfService;
           }]);
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/4ed984ef/client/enduser/src/main/resources/META-INF/resources/app/views/captcha.html
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/views/captcha.html b/client/enduser/src/main/resources/META-INF/resources/app/views/captcha.html
index 06582dc..c40e8dc 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/views/captcha.html
+++ b/client/enduser/src/main/resources/META-INF/resources/app/views/captcha.html
@@ -2,7 +2,7 @@
   <nav class="navbar">
     <div class="container-fluid">
       <div class="navbar-header">
-        <img alt="captcha" ng-src="{{captchaUrl}}'"/>
+        <img id="captchaImg" alt="captcha" ng-src="{{captchaUrl}}'"/>
         <div style="margin-top: 5%">
           <button id="refresh" type="button" class="btn btn-default btn-xs glyphicon glyphicon-refresh" 
                   ng-click="refreshCaptcha()" title="Refresh Captcha"></button>

http://git-wip-us.apache.org/repos/asf/syncope/blob/4ed984ef/client/enduser/src/main/resources/META-INF/resources/app/views/passwordreset.html
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/views/passwordreset.html b/client/enduser/src/main/resources/META-INF/resources/app/views/passwordreset.html
new file mode 100644
index 0000000..f4fdc57
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/views/passwordreset.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+-->
+<div ng-cloak class="container">
+  <div ng-controller="UserController" style="box-sizing: border-box; ">
+
+    <div id="form-container" class="col-md-6 col-md-offset-3" style="box-sizing: border-box; background-color: #F7F7F7;">
+
+      <div>
+        <div class="page-header" style="text-align: left; font-weight: 700;">
+          <span>Password reset</span>
+        </div>
+        <div class="breadcrumb-header text-center">
+
+          <div class="row">            
+            <div id="status-buttons" class="btn-group btn-breadcrumb">
+              <a href="#/self" class="btn btn-default"><i class="glyphicon glyphicon-home"></i></a>
+              <!--add class breadcrumb-disabled-link to buttons to prevent click-->
+              <a ui-sref-active="active" class="btn btn-default">User Details</a>
+            </div>
+          </div>
+        </div>
+        <form class="signup-form" name="passwordResetForm" ng-submit="resetPassword(user)" novalidate>
+
+          <div id="form-views" ui-view>
+            <div id="attribute" class="form-group">
+              <label for="user.username">User</label>
+              <input name="username" type="text" class="form-control" ng-model="user.username" required 
+                     placeholder="Username" ng-blur="retrieveSecurityQuestion(user)">
+              <p ng-show="(userForm.username.$error.required && !userForm.username.$pristine)" 
+                 class="text-validation-error">Username is required</p>
+            </div>
+            <div id="attribute" class="form-group" ng-show="$root.pwdResetRequiringSecurityQuestions">
+              <label for="user.securityquestion">Security Question</label> 
+              <div class="suggestions">(Not Loading? <a href ng-click="retrieveSecurityQuestion(user)">Reload</a>)</div>
+              <input name="securityquestion" type="text" class="form-control" ng-model="userSecurityQuestion" 
+                     disabled="disabled">              
+            </div>
+            <div id="attribute" class="form-group" ng-show="$root.pwdResetRequiringSecurityQuestions">
+              <label for="user.securityanswer">Security Answer</label>
+              <input name="securityanswer" type="text" class="form-control" ng-model="user.securityanswer" 
+                     placeholder="Security Answer" >              
+            </div>
+            <div id="attribute" class="form-group row">
+              <!--captcha-->
+              <div class="form-group row">
+                <captcha input="captchaInput"></captcha>
+              </div>
+              <button id="resetpassword" type="submit" class="btn btn-default pull-right">Submit</button>
+              <div class="pull-left">
+                <a id="cancel" href="#/self" class="btn btn-danger">Cancel</a>
+              </div>
+            </div>
+          </div>
+        </form>
+      </div>
+    </div>
+  </div>
+</div>