From 722f7c88404a0077ebaebd3c5a8c6c41e252017b Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 5 Feb 2014 14:34:17 +0000 Subject: [PATCH] KEYCLOAK-272 Improved user credential management, including option to send user password reset email from admin console --- .../resources/admin/js/controllers/users.js | 73 +++++++---------- .../META-INF/resources/admin/js/services.js | 51 +++++++++++- .../admin/partials/user-credentials.html | 32 ++++---- .../resources/admin/AdminService.java | 2 +- .../resources/admin/RealmAdminResource.java | 7 +- .../resources/admin/RealmsAdminResource.java | 7 +- .../resources/admin/UsersResource.java | 82 +++++++++++++++++-- .../services/resources/flows/ErrorFlows.java | 6 ++ 8 files changed, 184 insertions(+), 76 deletions(-) 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 a90c45a027..82556c79f7 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 } ]; + 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; + }); + }; - User.update({ - realm: realm.realm, - userId: $scope.user.username - }, $scope.user, function () { + $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"); + }); + }); + }; - $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; - } - - }, 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 b572a10519..fd1ca4b347 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 = '' + message + '"' + + 'This action can\'t be undone.'; + 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 5f67c0ea89..b5fb987208 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 @@
- Reset Password + Credential Management
- +
- + + +
-
- + +
+
- +
-
- - + +
+ +
+ +
-
- - - - -
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 c5ded4d16d..0ae7163835 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 deb46fb919..2b5e420e75 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 fe60118219..48e91f8946 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 a95c30c15d..24cfdc6eeb 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,10 +53,16 @@ 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; @@ -373,21 +388,72 @@ public class UsersResource { } } - @Path("{username}/credentials") + @Path("{username}/reset-password") @PUT @Consumes("application/json") - public void updateCredentials(@PathParam("username") String username, List credentials) { + public void resetPassword(@PathParam("username") String username, CredentialRepresentation pass) { 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 (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 Response resetPasswordEmail(@PathParam("username") String username) { + UserModel user = realm.getUser(username); + if (user == null) { + throw new NotFoundException(); + } + + 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 requiredActions = new HashSet(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 5a965662f4..7d78352482 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(); + } + }