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();
+ }
+
}