KEYCLOAK-272 Improved user credential management, including option to send user password reset email from admin console
This commit is contained in:
parent
457853aa28
commit
722f7c8840
8 changed files with 184 additions and 76 deletions
|
@ -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;
|
||||
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("Error while resetting user password. Be aware that the update password required action" +
|
||||
" was already set.");
|
||||
Notifications.error("Failed to reset user password");
|
||||
});
|
||||
} else {
|
||||
Notifications.success("User settings was updated.");
|
||||
$scope.isTotp = user.totp;
|
||||
$scope.userChange = false;
|
||||
}
|
||||
|
||||
}, function() {
|
||||
Notifications.error("Error while updating user settings.");
|
||||
$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");
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$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");
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<CredentialRepresentation> 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;
|
||||
if (pass == null || pass.getValue() == null || !CredentialRepresentation.PASSWORD.equals(pass.getType())) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
for (CredentialRepresentation rep : credentials) {
|
||||
UserCredentialModel cred = RealmManager.fromRepresentation(rep);
|
||||
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<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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue