Merge pull request #1581 from patriot1burke/master
client session required actions
This commit is contained in:
commit
e8b3b3acf3
15 changed files with 209 additions and 69 deletions
|
@ -304,7 +304,9 @@ module.controller('UserTabCtrl', function($scope, $location, Dialog, Notificatio
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser, User, UserExecuteActionsEmail, UserFederationInstances, UserImpersonation, RequiredActions, $location, Dialog, Notifications) {
|
module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser, User,
|
||||||
|
UserFederationInstances, UserImpersonation, RequiredActions,
|
||||||
|
$location, Dialog, Notifications) {
|
||||||
$scope.realm = realm;
|
$scope.realm = realm;
|
||||||
$scope.create = !user.id;
|
$scope.create = !user.id;
|
||||||
$scope.editUsername = $scope.create || $scope.realm.editUsernameAllowed;
|
$scope.editUsername = $scope.create || $scope.realm.editUsernameAllowed;
|
||||||
|
@ -374,36 +376,12 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*[
|
|
||||||
{id: "VERIFY_EMAIL", text: "Verify Email"},
|
|
||||||
{id: "UPDATE_PROFILE", text: "Update Profile"},
|
|
||||||
{id: "CONFIGURE_TOTP", text: "Configure Totp"},
|
|
||||||
{id: "UPDATE_PASSWORD", text: "Update Password"}
|
|
||||||
];
|
|
||||||
*/
|
|
||||||
|
|
||||||
$scope.$watch('user', function() {
|
$scope.$watch('user', function() {
|
||||||
if (!angular.equals($scope.user, user)) {
|
if (!angular.equals($scope.user, user)) {
|
||||||
$scope.changed = true;
|
$scope.changed = true;
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
$scope.sendExecuteActionsEmail = function() {
|
|
||||||
if ($scope.changed) {
|
|
||||||
Dialog.message("Cannot send email", "You must save your current changes before you can send an email");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Dialog.confirm('Send Email', 'Are you sure you want to send email to user?', function() {
|
|
||||||
UserExecuteActionsEmail.update({ realm: realm.realm, userId: user.id }, { }, function() {
|
|
||||||
Notifications.success("Email sent to user");
|
|
||||||
}, function() {
|
|
||||||
Notifications.error("Failed to send email to user");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
$scope.save = function() {
|
$scope.save = function() {
|
||||||
convertAttributeValuesToLists();
|
convertAttributeValuesToLists();
|
||||||
|
|
||||||
|
@ -476,7 +454,7 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.controller('UserCredentialsCtrl', function($scope, realm, user, User, UserCredentials, Notifications, Dialog) {
|
module.controller('UserCredentialsCtrl', function($scope, realm, user, RequiredActions, User, UserExecuteActionsEmail, UserCredentials, Notifications, Dialog) {
|
||||||
console.log('UserCredentialsCtrl');
|
console.log('UserCredentialsCtrl');
|
||||||
|
|
||||||
$scope.realm = realm;
|
$scope.realm = realm;
|
||||||
|
@ -487,6 +465,18 @@ module.controller('UserCredentialsCtrl', function($scope, realm, user, User, Use
|
||||||
if(!!user.totp){
|
if(!!user.totp){
|
||||||
$scope.isTotp = user.totp;
|
$scope.isTotp = user.totp;
|
||||||
}
|
}
|
||||||
|
// ID - Name map for required actions. IDs are enum names.
|
||||||
|
RequiredActions.query({realm: realm.realm}, function(data) {
|
||||||
|
$scope.userReqActionList = [];
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
console.log("listed required action: " + data[i].name);
|
||||||
|
if (data[i].enabled) {
|
||||||
|
var item = data[i];
|
||||||
|
$scope.userReqActionList.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
$scope.resetPassword = function() {
|
$scope.resetPassword = function() {
|
||||||
if ($scope.pwdChange) {
|
if ($scope.pwdChange) {
|
||||||
|
@ -528,6 +518,24 @@ module.controller('UserCredentialsCtrl', function($scope, realm, user, User, Use
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.emailActions = [];
|
||||||
|
|
||||||
|
$scope.sendExecuteActionsEmail = function() {
|
||||||
|
if ($scope.changed) {
|
||||||
|
Dialog.message("Cannot send email", "You must save your current changes before you can send an email");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Dialog.confirm('Send Email', 'Are you sure you want to send email to user?', function() {
|
||||||
|
UserExecuteActionsEmail.update({ realm: realm.realm, userId: user.id }, $scope.emailActions, function() {
|
||||||
|
Notifications.success("Email sent to user");
|
||||||
|
$scope.emailActions = [];
|
||||||
|
}, function() {
|
||||||
|
Notifications.error("Failed to send email to user");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$scope.$watch('user', function() {
|
$scope.$watch('user', function() {
|
||||||
if (!angular.equals($scope.user, user)) {
|
if (!angular.equals($scope.user, user)) {
|
||||||
|
|
20
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html
Normal file → Executable file
20
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html
Normal file → Executable file
|
@ -44,6 +44,26 @@
|
||||||
<button class="btn btn-danger" type="submit" data-ng-click="removeTotp()" tooltip-trigger="mouseover mouseout" tooltip="Remove one time password generator for user." tooltip-placement="right">Remove TOTP</button>
|
<button class="btn btn-danger" type="submit" data-ng-click="removeTotp()" tooltip-trigger="mouseover mouseout" tooltip="Remove one time password generator for user." tooltip-placement="right">Remove TOTP</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</fieldset >
|
||||||
|
<fieldset class="border-top" data-ng-show="user.email">
|
||||||
|
<div class="form-group clearfix">
|
||||||
|
<label class="col-md-2 control-label" for="reqActions">Reset Actions</label>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<select ui-select2 id="reqActions" ng-model="emailActions" data-placeholder="Select an action..." multiple>
|
||||||
|
<option ng-repeat="action in userReqActionList" value="{{action.alias}}">{{action.name}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>Set of actions to execute when sending the user a Reset Actions Email. 'Verify email' sends an email to the user to verify their email address. 'Update profile' requires user to enter in new personal information. 'Update password' requires user to enter in a new password. 'Configure TOTP' requires setup of a mobile password generator.</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="form-group clearfix">
|
||||||
|
<label class="col-md-2 control-label" for="reqActionsEmail">Reset Actions Email</label>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<button id="reqActionsEmail" class="btn btn-default" data-ng-click="sendExecuteActionsEmail()">Send Email</button>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>Sends an email to user with an embedded link. Clicking on link will allow the user to execute the reset actions. They will not have to login prior to this. For example, set the action to update password, click this button, and the user will be able to change their password without logging in.</kc-tooltip>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -99,14 +99,6 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>Require an action when the user logs in. 'Verify email' sends an email to the user to verify their email address. 'Update profile' requires user to enter in new personal information. 'Update password' requires user to enter in a new password. 'Configure TOTP' requires setup of a mobile password generator.</kc-tooltip>
|
<kc-tooltip>Require an action when the user logs in. 'Verify email' sends an email to the user to verify their email address. 'Update profile' requires user to enter in new personal information. 'Update password' requires user to enter in a new password. 'Configure TOTP' requires setup of a mobile password generator.</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group clearfix" data-ng-show="!create && user.email">
|
|
||||||
<label class="col-md-2 control-label" for="reqActionsEmail">Required Actions Email</label>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
<button id="reqActionsEmail" class="btn btn-default" data-ng-click="sendExecuteActionsEmail()">Send Email</button>
|
|
||||||
</div>
|
|
||||||
<kc-tooltip>Sends an email to user with an embedded link. Clicking on link will allow the user to execute all their required actions. They will not have to login prior to this. For example, set the required action to update password, click this button, and the user will be able to change their password without logging in.</kc-tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group clearfix" data-ng-if="realm.internationalizationEnabled">
|
<div class="form-group clearfix" data-ng-if="realm.internationalizationEnabled">
|
||||||
<label class="col-md-2 control-label" for="locale">Locale</label>
|
<label class="col-md-2 control-label" for="locale">Locale</label>
|
||||||
|
|
|
@ -48,11 +48,11 @@ public interface UserResource {
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@Path("execute-actions-email")
|
@Path("execute-actions-email")
|
||||||
public void executeActionsEmail();
|
public void executeActionsEmail(List<String> actions);
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@Path("execute-actions-email")
|
@Path("execute-actions-email")
|
||||||
public void executeActionsEmail(@QueryParam("client_id") String clientId);
|
public void executeActionsEmail(@QueryParam("client_id") String clientId, List<String> actions);
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@Path("send-verify-email")
|
@Path("send-verify-email")
|
||||||
|
|
|
@ -53,6 +53,22 @@ public interface ClientSessionModel {
|
||||||
public void removeNote(String name);
|
public void removeNote(String name);
|
||||||
public Map<String, String> getNotes();
|
public Map<String, String> getNotes();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required actions that are attached to this client session.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Set<String> getRequiredActions();
|
||||||
|
|
||||||
|
void addRequiredAction(String action);
|
||||||
|
|
||||||
|
void removeRequiredAction(String action);
|
||||||
|
|
||||||
|
void addRequiredAction(UserModel.RequiredAction action);
|
||||||
|
|
||||||
|
void removeRequiredAction(UserModel.RequiredAction action);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These are notes you want applied to the UserSessionModel when the client session is attached to it.
|
* These are notes you want applied to the UserSessionModel when the client session is attached to it.
|
||||||
*
|
*
|
||||||
|
|
|
@ -12,6 +12,7 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -200,6 +201,38 @@ public class ClientSessionAdapter implements ClientSessionModel {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getRequiredActions() {
|
||||||
|
Set<String> copy = new HashSet<>();
|
||||||
|
copy.addAll(entity.getRequiredActions());
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addRequiredAction(String action) {
|
||||||
|
entity.getRequiredActions().add(action);
|
||||||
|
update();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeRequiredAction(String action) {
|
||||||
|
entity.getRequiredActions().remove(action);
|
||||||
|
update();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addRequiredAction(UserModel.RequiredAction action) {
|
||||||
|
addRequiredAction(action.name());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeRequiredAction(UserModel.RequiredAction action) {
|
||||||
|
removeRequiredAction(action.name());
|
||||||
|
}
|
||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
provider.getTx().replace(cache, entity.getId(), entity);
|
provider.getTx().replace(cache, entity.getId(), entity);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -197,4 +198,35 @@ public class ClientSessionAdapter implements ClientSessionModel {
|
||||||
else entity.setAuthUserId(user.getId());
|
else entity.setAuthUserId(user.getId());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getRequiredActions() {
|
||||||
|
Set<String> copy = new HashSet<>();
|
||||||
|
copy.addAll(entity.getRequiredActions());
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addRequiredAction(String action) {
|
||||||
|
entity.getRequiredActions().add(action);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeRequiredAction(String action) {
|
||||||
|
entity.getRequiredActions().remove(action);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addRequiredAction(UserModel.RequiredAction action) {
|
||||||
|
addRequiredAction(action.name());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeRequiredAction(UserModel.RequiredAction action) {
|
||||||
|
removeRequiredAction(action.name());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.keycloak.models.sessions.infinispan.compat.entities;
|
||||||
import org.keycloak.models.ClientSessionModel;
|
import org.keycloak.models.ClientSessionModel;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -28,6 +29,8 @@ public class ClientSessionEntity {
|
||||||
private Set<String> protocolMappers;
|
private Set<String> protocolMappers;
|
||||||
private Map<String, String> notes = new HashMap<>();
|
private Map<String, String> notes = new HashMap<>();
|
||||||
private Map<String, String> userSessionNotes = new HashMap<>();
|
private Map<String, String> userSessionNotes = new HashMap<>();
|
||||||
|
private Set<String> requiredActions = new HashSet<>();
|
||||||
|
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
|
@ -133,6 +136,10 @@ public class ClientSessionEntity {
|
||||||
return userSessionNotes;
|
return userSessionNotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<String> getRequiredActions() {
|
||||||
|
return requiredActions;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
|
|
|
@ -3,6 +3,9 @@ package org.keycloak.models.sessions.infinispan.entities;
|
||||||
import org.keycloak.models.ClientSessionModel;
|
import org.keycloak.models.ClientSessionModel;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -31,6 +34,8 @@ public class ClientSessionEntity extends SessionEntity {
|
||||||
private Map<String, String> userSessionNotes;
|
private Map<String, String> userSessionNotes;
|
||||||
private Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus = new HashMap<>();
|
private Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus = new HashMap<>();
|
||||||
private String authUserId;
|
private String authUserId;
|
||||||
|
private Set<String> requiredActions = new HashSet<>();
|
||||||
|
|
||||||
|
|
||||||
public String getClient() {
|
public String getClient() {
|
||||||
return client;
|
return client;
|
||||||
|
@ -136,5 +141,7 @@ public class ClientSessionEntity extends SessionEntity {
|
||||||
this.userSessionNotes = userSessionNotes;
|
this.userSessionNotes = userSessionNotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<String> getRequiredActions() {
|
||||||
|
return requiredActions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ public class ResetOTP extends AbstractSetRequiredActionAuthenticator {
|
||||||
if (context.getExecution().isRequired() ||
|
if (context.getExecution().isRequired() ||
|
||||||
(context.getExecution().isOptional() &&
|
(context.getExecution().isOptional() &&
|
||||||
configuredFor(context))) {
|
configuredFor(context))) {
|
||||||
context.getUser().addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
|
context.getClientSession().addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
|
||||||
}
|
}
|
||||||
context.success();
|
context.success();
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ public class ResetPassword extends AbstractSetRequiredActionAuthenticator {
|
||||||
if (context.getExecution().isRequired() ||
|
if (context.getExecution().isRequired() ||
|
||||||
(context.getExecution().isOptional() &&
|
(context.getExecution().isOptional() &&
|
||||||
configuredFor(context))) {
|
configuredFor(context))) {
|
||||||
context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
|
context.getClientSession().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
|
||||||
}
|
}
|
||||||
context.success();
|
context.success();
|
||||||
}
|
}
|
||||||
|
|
|
@ -441,32 +441,14 @@ public class AuthenticationManager {
|
||||||
event.detail(Details.CODE_ID, clientSession.getId());
|
event.detail(Details.CODE_ID, clientSession.getId());
|
||||||
|
|
||||||
Set<String> requiredActions = user.getRequiredActions();
|
Set<String> requiredActions = user.getRequiredActions();
|
||||||
for (String action : requiredActions) {
|
Response action = executionActions(session, userSession, clientSession, request, event, realm, user, requiredActions);
|
||||||
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action);
|
if (action != null) return action;
|
||||||
RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, model.getProviderId());
|
|
||||||
if (factory == null) {
|
// executionActions() method should remove any duplicate actions that might be in the clientSession
|
||||||
throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?");
|
requiredActions = clientSession.getRequiredActions();
|
||||||
}
|
action = executionActions(session, userSession, clientSession, request, event, realm, user, requiredActions);
|
||||||
RequiredActionProvider actionProvider = factory.create(session);
|
if (action != null) return action;
|
||||||
RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, factory);
|
|
||||||
actionProvider.requiredActionChallenge(context);
|
|
||||||
|
|
||||||
if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
|
|
||||||
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
|
|
||||||
protocol.setRealm(context.getRealm())
|
|
||||||
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
|
|
||||||
.setUriInfo(context.getUriInfo());
|
|
||||||
event.error(Errors.REJECTED_BY_USER);
|
|
||||||
return protocol.consentDenied(context.getClientSession());
|
|
||||||
}
|
|
||||||
else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
|
|
||||||
return context.getChallenge();
|
|
||||||
}
|
|
||||||
else if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {
|
|
||||||
event.clone().event(EventType.CUSTOM_REQUIRED_ACTION).detail(Details.CUSTOM_REQUIRED_ACTION, factory.getId()).success();
|
|
||||||
clientSession.getUserSession().getUser().removeRequiredAction(factory.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (client.isConsentRequired()) {
|
if (client.isConsentRequired()) {
|
||||||
|
|
||||||
UserConsentModel grantedConsent = user.getConsentByClient(client.getId());
|
UserConsentModel grantedConsent = user.getConsentByClient(client.getId());
|
||||||
|
@ -516,6 +498,40 @@ public class AuthenticationManager {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static Response executionActions(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession,
|
||||||
|
HttpRequest request, EventBuilder event, RealmModel realm, UserModel user,
|
||||||
|
Set<String> requiredActions) {
|
||||||
|
for (String action : requiredActions) {
|
||||||
|
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action);
|
||||||
|
RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, model.getProviderId());
|
||||||
|
if (factory == null) {
|
||||||
|
throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?");
|
||||||
|
}
|
||||||
|
RequiredActionProvider actionProvider = factory.create(session);
|
||||||
|
RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, factory);
|
||||||
|
actionProvider.requiredActionChallenge(context);
|
||||||
|
|
||||||
|
if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
|
||||||
|
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
|
||||||
|
protocol.setRealm(context.getRealm())
|
||||||
|
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
|
||||||
|
.setUriInfo(context.getUriInfo());
|
||||||
|
event.error(Errors.REJECTED_BY_USER);
|
||||||
|
return protocol.consentDenied(context.getClientSession());
|
||||||
|
}
|
||||||
|
else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
|
||||||
|
return context.getChallenge();
|
||||||
|
}
|
||||||
|
else if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {
|
||||||
|
event.clone().event(EventType.CUSTOM_REQUIRED_ACTION).detail(Details.CUSTOM_REQUIRED_ACTION, factory.getId()).success();
|
||||||
|
// don't have to perform the same action twice, so remove it from both the user and session required actions
|
||||||
|
clientSession.getUserSession().getUser().removeRequiredAction(factory.getId());
|
||||||
|
clientSession.removeRequiredAction(factory.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static void evaluateRequiredActionTriggers(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession, final ClientConnection clientConnection, final HttpRequest request, final UriInfo uriInfo, final EventBuilder event, final RealmModel realm, final UserModel user) {
|
public static void evaluateRequiredActionTriggers(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession, final ClientConnection clientConnection, final HttpRequest request, final UriInfo uriInfo, final EventBuilder event, final RealmModel realm, final UserModel user) {
|
||||||
|
|
||||||
// see if any required actions need triggering, i.e. an expired password
|
// see if any required actions need triggering, i.e. an expired password
|
||||||
|
|
|
@ -684,6 +684,8 @@ public class LoginActionsService {
|
||||||
provider.processAction(context);
|
provider.processAction(context);
|
||||||
if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {
|
if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {
|
||||||
event.clone().success();
|
event.clone().success();
|
||||||
|
// do both
|
||||||
|
clientSession.removeRequiredAction(factory.getId());
|
||||||
clientSession.getUserSession().getUser().removeRequiredAction(factory.getId());
|
clientSession.getUserSession().getUser().removeRequiredAction(factory.getId());
|
||||||
event.event(EventType.LOGIN);
|
event.event(EventType.LOGIN);
|
||||||
return AuthenticationManager.nextActionAfterAuthentication(session, clientSession.getUserSession(), clientSession, clientConnection, request, uriInfo, event);
|
return AuthenticationManager.nextActionAfterAuthentication(session, clientSession.getUserSession(), clientSession, clientConnection, request, uriInfo, event);
|
||||||
|
|
|
@ -837,7 +837,10 @@ public class UsersResource {
|
||||||
@Path("{id}/execute-actions-email")
|
@Path("{id}/execute-actions-email")
|
||||||
@PUT
|
@PUT
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public Response executeActionsEmail(@PathParam("id") String id, @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) {
|
public Response executeActionsEmail(@PathParam("id") String id,
|
||||||
|
@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri,
|
||||||
|
@QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId,
|
||||||
|
List<String> actions) {
|
||||||
auth.requireManage();
|
auth.requireManage();
|
||||||
|
|
||||||
UserModel user = session.users().getUserById(id, realm);
|
UserModel user = session.users().getUserById(id, realm);
|
||||||
|
@ -850,6 +853,9 @@ public class UsersResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientSessionModel clientSession = createClientSession(user, redirectUri, clientId);
|
ClientSessionModel clientSession = createClientSession(user, redirectUri, clientId);
|
||||||
|
for (String action : actions) {
|
||||||
|
clientSession.addRequiredAction(action);
|
||||||
|
}
|
||||||
ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession);
|
ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession);
|
||||||
accessCode.setAction(ClientSessionModel.Action.EXECUTE_ACTIONS.name());
|
accessCode.setAction(ClientSessionModel.Action.EXECUTE_ACTIONS.name());
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import javax.ws.rs.ClientErrorException;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
@ -359,9 +360,9 @@ public class UserTest extends AbstractClientTest {
|
||||||
String id = ApiUtil.getCreatedId(response);
|
String id = ApiUtil.getCreatedId(response);
|
||||||
response.close();
|
response.close();
|
||||||
UserResource user = realm.users().get(id);
|
UserResource user = realm.users().get(id);
|
||||||
|
List<String> actions = new LinkedList<>();
|
||||||
try {
|
try {
|
||||||
user.executeActionsEmail();
|
user.executeActionsEmail(actions);
|
||||||
fail("Expected failure");
|
fail("Expected failure");
|
||||||
} catch (ClientErrorException e) {
|
} catch (ClientErrorException e) {
|
||||||
assertEquals(400, e.getResponse().getStatus());
|
assertEquals(400, e.getResponse().getStatus());
|
||||||
|
@ -374,7 +375,7 @@ public class UserTest extends AbstractClientTest {
|
||||||
userRep.setEmail("user1@localhost");
|
userRep.setEmail("user1@localhost");
|
||||||
userRep.setEnabled(false);
|
userRep.setEnabled(false);
|
||||||
user.update(userRep);
|
user.update(userRep);
|
||||||
user.executeActionsEmail();
|
user.executeActionsEmail(actions);
|
||||||
fail("Expected failure");
|
fail("Expected failure");
|
||||||
} catch (ClientErrorException e) {
|
} catch (ClientErrorException e) {
|
||||||
assertEquals(400, e.getResponse().getStatus());
|
assertEquals(400, e.getResponse().getStatus());
|
||||||
|
@ -385,7 +386,7 @@ public class UserTest extends AbstractClientTest {
|
||||||
try {
|
try {
|
||||||
userRep.setEnabled(true);
|
userRep.setEnabled(true);
|
||||||
user.update(userRep);
|
user.update(userRep);
|
||||||
user.executeActionsEmail("invalidClientId");
|
user.executeActionsEmail("invalidClientId", actions);
|
||||||
fail("Expected failure");
|
fail("Expected failure");
|
||||||
} catch (ClientErrorException e) {
|
} catch (ClientErrorException e) {
|
||||||
assertEquals(400, e.getResponse().getStatus());
|
assertEquals(400, e.getResponse().getStatus());
|
||||||
|
|
Loading…
Reference in a new issue