keycloak-aplcache

KEYCLOAK-272 Improved user credential management, including

2/5/2014 12:34:17 PM

Details

diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/users.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/users.js
index a90c45a..82556c7 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/users.js
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/users.js
@@ -227,7 +227,7 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, $locatio
     };
 });
 
-module.controller('UserCredentialsCtrl', function($scope, realm, user, User, UserCredentials, Notifications) {
+module.controller('UserCredentialsCtrl', function($scope, realm, user, User, UserCredentials, Notifications, Dialog) {
     console.log('UserCredentialsCtrl');
 
     $scope.realm = realm;
@@ -239,56 +239,45 @@ module.controller('UserCredentialsCtrl', function($scope, realm, user, User, Use
     }
 
     $scope.resetPassword = function() {
-
         if ($scope.pwdChange) {
             if ($scope.password != $scope.confirmPassword) {
                 Notifications.error("Password and confirmation does not match.");
-                $scope.password = "";
-                $scope.confirmPassword = "";
                 return;
             }
-
-            if (!$scope.user.hasOwnProperty('requiredActions')){
-                $scope.user.requiredActions = [];
-            }
-            if ($scope.user.requiredActions.indexOf("UPDATE_PASSWORD") < 0){
-                $scope.user.requiredActions.push("UPDATE_PASSWORD");
-            }
         }
 
-        var credentials = [ { type : "password", value : $scope.password } ];
-
-        User.update({
-            realm: realm.realm,
-            userId: $scope.user.username
-        }, $scope.user, function () {
-
-            $scope.isTotp = $scope.user.totp;
-
-            if ($scope.pwdChange){
-                UserCredentials.update({
-                    realm: realm.realm,
-                    userId: $scope.user.username
-                }, credentials, function () {
-                    Notifications.success("The password has been reset. The user is required to change his password on" +
-                        " the next login.");
-                    $scope.password = "";
-                    $scope.confirmPassword = "";
-                    $scope.pwdChange = false;
-                    $scope.isTotp = user.totp;
-                    $scope.userChange = false;
-                }, function () {
-                    Notifications.error("Error while resetting user password. Be aware that the update password required action" +
-                        " was already set.");
-                });
-            } else {
-                Notifications.success("User settings was updated.");
-                $scope.isTotp = user.totp;
-                $scope.userChange = false;
-            }
+        Dialog.confirm('Reset password', 'Are you sure you want to reset the users password?', function() {
+            UserCredentials.resetPassword({ realm: realm.realm, userId: user.username }, { type : "password", value : $scope.password }, function() {
+                Notifications.success("The password has been reset");
+                $scope.password = null;
+                $scope.confirmPassword = null;
+            }, function() {
+                Notifications.error("Failed to reset user password");
+            });
+        }, function() {
+            $scope.password = null;
+            $scope.confirmPassword = null;
+        });
+    };
+
+    $scope.removeTotp = function() {
+        Dialog.confirm('Remove totp', 'Are you sure you want to remove the users totp configuration?', function() {
+            UserCredentials.removeTotp({ realm: realm.realm, userId: user.username }, { }, function() {
+                Notifications.success("The users totp configuration has been removed");
+                $scope.user.totp = false;
+            }, function() {
+                Notifications.error("Failed to remove the users totp configuration");
+            });
+        });
+    };
 
-        }, function () {
-            Notifications.error("Error while updating user settings.");
+    $scope.resetPasswordEmail = function() {
+        Dialog.confirm('Reset password email', 'Are you sure you want to send password reset email to user?', function() {
+            UserCredentials.resetPasswordEmail({ realm: realm.realm, userId: user.username }, { }, function() {
+                Notifications.success("Password reset email sent to user");
+            }, function() {
+                Notifications.error("Failed to send password reset mail to user");
+            });
         });
     };
 
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js
index b572a10..fd1ca4b 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js
@@ -51,6 +51,28 @@ module.service('Dialog', function($dialog) {
         });
     }
 
+    dialog.confirm = function(title, message, success, cancel) {
+        var title = title;
+        var msg = '<span class="primary">' + message + '"</span>' +
+            '<span>This action can\'t be undone.</span>';
+        var btns = [ {
+            result : 'cancel',
+            label : 'Cancel'
+        }, {
+            result : 'ok',
+            label : title,
+            cssClass : 'destructive'
+        } ];
+
+        $dialog.messageBox(title, msg, btns).open().then(function(result) {
+            if (result == "ok") {
+                success();
+            } else {
+                cancel && cancel();
+            }
+        });
+    }
+
 	return dialog
 });
 
@@ -136,15 +158,36 @@ module.factory('User', function($resource) {
 });
 
 module.factory('UserCredentials', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/users/:userId/credentials', {
+    var credentials = {};
+
+    credentials.resetPassword = $resource('/auth/rest/admin/realms/:realm/users/:userId/reset-password', {
         realm : '@realm',
         userId : '@userId'
     }, {
         update : {
-            method : 'PUT',
-            isArray : true
+            method : 'PUT'
         }
-    });
+    }).update;
+
+    credentials.removeTotp = $resource('/auth/rest/admin/realms/:realm/users/:userId/remove-totp', {
+        realm : '@realm',
+        userId : '@userId'
+    }, {
+        update : {
+            method : 'PUT'
+        }
+    }).update;
+
+    credentials.resetPasswordEmail = $resource('/auth/rest/admin/realms/:realm/users/:userId/reset-password-email', {
+        realm : '@realm',
+        userId : '@userId'
+    }, {
+        update : {
+            method : 'PUT'
+        }
+    }).update;
+
+    return credentials;
 });
 
 module.factory('RealmRoleMapping', function($resource) {
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-credentials.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-credentials.html
index 5f67c0e..b5fb987 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-credentials.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-credentials.html
@@ -18,32 +18,30 @@
 
                 <form name="userForm" novalidate>
                     <fieldset class="border-top">
-                        <legend uncollapsed><span class="text">Reset Password</span></legend>
+                        <legend uncollapsed><span class="text">Credential Management</span></legend>
                         <div class="form-group">
-                            <label for="password">New Password</label>
+                            <label for="password">Reset password</label>
                             <div class="controls">
-                                <input type="password" id="password" name="password" data-ng-model="password" autofocus
-                                       required>
+                                <input type="password" id="password" name="password" data-ng-model="password" placeholder="Temporary password" required>
+                                <input type="password" id="confirmPassword" name="confirmPassword" data-ng-model="confirmPassword" placeholder="Password confirmation" required>
+                                <button type="submit" data-ng-click="resetPassword()" class="destructive" data-ng-show="password">Reset Password</button>
                             </div>
                         </div>
-                        <div class="form-group">
-                            <label class="two-lines" for="password">New Password Confirmation</label>
+
+                        <div class="form-group" data-ng-show="user.email">
+                            <label for="password">Reset password email</label>
                             <div class="controls">
-                                <input type="password" id="confirmPassword" name="confirmPassword"
-                                       data-ng-model="confirmPassword" required>
+                                <button type="submit" data-ng-click="resetPasswordEmail()" class="destructive">Send Email</button>
                             </div>
                         </div>
-                        <div class="form-group clearfix block" >
-                            <label for="userTotp" class="control-label">TOTP Enabled</label>
-                            <input ng-model="user.totp" name="userTotp" class="kokosak"  ng-disabled="!isTotp" id="userTotp" onoffswitch/>
+
+                        <div class="form-group" data-ng-show="user.totp">
+                            <label for="password">Remove totp</label>
+                            <div class="controls" data-ng-show="user.totp">
+                                <button type="submit" data-ng-click="removeTotp()" class="destructive">Remove TOTP</button>
+                            </div>
                         </div>
                     </fieldset>
-                    <div class="form-actions">
-                        <button type="submit" data-ng-click="resetPassword()" class="primary" data-ng-show="userChange && !pwdChange">Save</button>
-                        <button type="submit" data-ng-click="resetPassword()" class="primary" data-ng-show="!userChange && pwdChange">Reset Password</button>
-                        <button type="submit" data-ng-click="resetPassword()" class="primary" data-ng-show="userChange && pwdChange">Save and Reset Password</button>
-                        <button type="submit" kc-reset data-ng-show="userChange || pwdChange">Clear changes</button>
-                    </div>
                 </form>
 
             </div>
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java
index c5ded4d..0ae7163 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java
@@ -192,7 +192,7 @@ public class AdminService {
             logger.warn("not a Realm admin");
             throw new NotAuthorizedException("Bearer");
         }
-        RealmsAdminResource adminResource = new RealmsAdminResource(admin);
+        RealmsAdminResource adminResource = new RealmsAdminResource(admin, tokenManager);
         resourceContext.initResource(adminResource);
         return adminResource;
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index deb46fb..2b5e420 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -8,6 +8,7 @@ import org.keycloak.models.UserModel;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.services.managers.ModelToRepresentation;
 import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.managers.TokenManager;
 
 import javax.ws.rs.*;
 import javax.ws.rs.container.ResourceContext;
@@ -21,6 +22,7 @@ public class RealmAdminResource extends RoleContainerResource {
     protected static final Logger logger = Logger.getLogger(RealmAdminResource.class);
     protected UserModel admin;
     protected RealmModel realm;
+    private TokenManager tokenManager;
 
     @Context
     protected ResourceContext resourceContext;
@@ -28,10 +30,11 @@ public class RealmAdminResource extends RoleContainerResource {
     @Context
     protected KeycloakSession session;
 
-    public RealmAdminResource(UserModel admin, RealmModel realm) {
+    public RealmAdminResource(UserModel admin, RealmModel realm, TokenManager tokenManager) {
         super(realm, realm);
         this.admin = admin;
         this.realm = realm;
+        this.tokenManager = tokenManager;
     }
 
     @Path("applications")
@@ -72,7 +75,7 @@ public class RealmAdminResource extends RoleContainerResource {
 
     @Path("users")
     public UsersResource users() {
-        UsersResource users = new UsersResource(realm);
+        UsersResource users = new UsersResource(realm, tokenManager);
         resourceContext.initResource(users);
         return users;
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java
index fe60118..48e91f8 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java
@@ -11,6 +11,7 @@ import org.keycloak.models.UserModel;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.services.managers.ModelToRepresentation;
 import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.managers.TokenManager;
 import org.keycloak.services.resources.flows.Flows;
 
 import javax.ws.rs.*;
@@ -35,9 +36,11 @@ import java.util.Map;
 public class RealmsAdminResource {
     protected static final Logger logger = Logger.getLogger(RealmsAdminResource.class);
     protected UserModel admin;
+    protected TokenManager tokenManager;
 
-    public RealmsAdminResource(UserModel admin) {
+    public RealmsAdminResource(UserModel admin, TokenManager tokenManager) {
         this.admin = admin;
+        this.tokenManager = tokenManager;
     }
 
     public static final CacheControl noCache = new CacheControl();
@@ -110,7 +113,7 @@ public class RealmsAdminResource {
         RealmModel realm = realmManager.getRealmByName(name);
         if (realm == null) throw new NotFoundException("{realm} = " + name);
 
-        RealmAdminResource adminResource = new RealmAdminResource(admin, realm);
+        RealmAdminResource adminResource = new RealmAdminResource(admin, realm, tokenManager);
         resourceContext.initResource(adminResource);
         return adminResource;
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index a95c30c..24cfdc6 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -10,10 +10,16 @@ import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.representations.idm.*;
+import org.keycloak.services.email.EmailException;
+import org.keycloak.services.email.EmailSender;
+import org.keycloak.services.managers.AccessCodeEntry;
 import org.keycloak.services.managers.ModelToRepresentation;
 import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.managers.TokenManager;
 import org.keycloak.services.resources.flows.Flows;
+import org.keycloak.services.resources.flows.Urls;
 
+import javax.ws.rs.BadRequestException;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
@@ -25,12 +31,15 @@ import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.ServerErrorException;
 import javax.ws.rs.container.ResourceContext;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -44,11 +53,17 @@ public class UsersResource {
 
     protected RealmModel realm;
 
-    public UsersResource(RealmModel realm) {
+    private TokenManager tokenManager;
+
+    public UsersResource(RealmModel realm, TokenManager tokenManager) {
         this.realm = realm;
+        this.tokenManager = tokenManager;
     }
 
     @Context
+    protected UriInfo uriInfo;
+
+    @Context
     protected ResourceContext resourceContext;
 
     @Context
@@ -373,21 +388,72 @@ public class UsersResource {
         }
     }
 
-    @Path("{username}/credentials")
+    @Path("{username}/reset-password")
+    @PUT
+    @Consumes("application/json")
+    public void resetPassword(@PathParam("username") String username, CredentialRepresentation pass) {
+        UserModel user = realm.getUser(username);
+        if (user == null) {
+            throw new NotFoundException();
+        }
+        if (pass == null || pass.getValue() == null || !CredentialRepresentation.PASSWORD.equals(pass.getType())) {
+            throw new BadRequestException();
+        }
+
+        UserCredentialModel cred = RealmManager.fromRepresentation(pass);
+        realm.updateCredential(user, cred);
+        user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+    }
+
+    @Path("{username}/remove-totp")
+    @PUT
+    @Consumes("application/json")
+    public void removeTotp(@PathParam("username") String username) {
+        UserModel user = realm.getUser(username);
+        if (user == null) {
+            throw new NotFoundException();
+        }
+
+        user.setTotp(false);
+    }
+
+    @Path("{username}/reset-password-email")
     @PUT
     @Consumes("application/json")
-    public void updateCredentials(@PathParam("username") String username, List<CredentialRepresentation> credentials) {
+    public Response resetPasswordEmail(@PathParam("username") String username) {
         UserModel user = realm.getUser(username);
         if (user == null) {
             throw new NotFoundException();
         }
-        if (credentials == null) return;
 
-        for (CredentialRepresentation rep : credentials) {
-            UserCredentialModel cred = RealmManager.fromRepresentation(rep);
-            realm.updateCredential(user, cred);
+        if (user.getEmail() == null) {
+            return Flows.errors().error("User email missing", Response.Status.BAD_REQUEST);
+        }
+
+        String redirect = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
+        String clientId = Constants.ACCOUNT_APPLICATION;
+        String state = null;
+        String scope = null;
+
+        UserModel client = realm.getUser(clientId);
+        if (client == null || !client.isEnabled()) {
+            return Flows.errors().error("Account management not enabled", Response.Status.INTERNAL_SERVER_ERROR);
         }
 
+        Set<UserModel.RequiredAction> requiredActions = new HashSet<UserModel.RequiredAction>(user.getRequiredActions());
+        requiredActions.add(UserModel.RequiredAction.UPDATE_PASSWORD);
+
+        AccessCodeEntry accessCode = tokenManager.createAccessCode(scope, state, redirect, realm, client, user);
+        accessCode.setRequiredActions(requiredActions);
+        accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction());
+
+        try {
+            new EmailSender(realm.getSmtpConfig()).sendPasswordReset(user, realm, accessCode, uriInfo);
+            return Response.ok().build();
+        } catch (EmailException e) {
+            logger.error("Failed to send password reset email", e);
+            return Flows.errors().error("Failed to send email", Response.Status.INTERNAL_SERVER_ERROR);
+        }
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/ErrorFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/ErrorFlows.java
index 5a96566..7d78352 100644
--- a/services/src/main/java/org/keycloak/services/resources/flows/ErrorFlows.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/ErrorFlows.java
@@ -16,4 +16,10 @@ public class ErrorFlows {
         return Response.status(Response.Status.CONFLICT).entity(error).type(MediaType.APPLICATION_JSON).build();
     }
 
+    public Response error(String message, Response.Status status) {
+        ErrorRepresentation error = new ErrorRepresentation();
+        error.setErrorMessage(message);
+        return Response.status(status).entity(error).type(MediaType.APPLICATION_JSON).build();
+    }
+
 }