keycloak-uncached

Merge pull request #1581 from patriot1burke/master client

9/2/2015 6:10:20 PM

Changes

Details

diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index 12c0eb4..b2a4870 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -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.create = !user.id;
     $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() {
         if (!angular.equals($scope.user, user)) {
             $scope.changed = 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() {
         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');
 
     $scope.realm = realm;
@@ -487,6 +465,18 @@ module.controller('UserCredentialsCtrl', function($scope, realm, user, User, Use
     if(!!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() {
         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() {
         if (!angular.equals($scope.user, user)) {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html
old mode 100644
new mode 100755
index e47376a..13c079d
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html
@@ -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>
                     </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>
         </form>
 </div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
index e00d928..db17683 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
@@ -99,14 +99,6 @@
                 </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>
             </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">
                 <label class="col-md-2 control-label" for="locale">Locale</label>
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java
index 1cbf888..e21ee79 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java
@@ -48,11 +48,11 @@ public interface UserResource {
 
     @PUT
     @Path("execute-actions-email")
-    public void executeActionsEmail();
+    public void executeActionsEmail(List<String> actions);
 
     @PUT
     @Path("execute-actions-email")
-    public void executeActionsEmail(@QueryParam("client_id") String clientId);
+    public void executeActionsEmail(@QueryParam("client_id") String clientId, List<String> actions);
 
     @PUT
     @Path("send-verify-email")
diff --git a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java
index c878927..b4f638d 100755
--- a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java
+++ b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java
@@ -54,6 +54,22 @@ public interface ClientSessionModel {
     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.
      *
      * @param name
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java
index 0b804b4..179a1f0 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java
@@ -12,6 +12,7 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
 
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 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() {
         provider.getTx().replace(cache, entity.getId(), entity);
     }
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/ClientSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/ClientSessionAdapter.java
index 1d806c1..dc6d0be 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/ClientSessionAdapter.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/ClientSessionAdapter.java
@@ -11,6 +11,7 @@ import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity
 
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
@@ -197,4 +198,35 @@ public class ClientSessionAdapter implements ClientSessionModel {
         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());
+    }
+
 }
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/entities/ClientSessionEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/entities/ClientSessionEntity.java
index 73336b7..e178a35 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/entities/ClientSessionEntity.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/entities/ClientSessionEntity.java
@@ -3,6 +3,7 @@ package org.keycloak.models.sessions.infinispan.compat.entities;
 import org.keycloak.models.ClientSessionModel;
 
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
@@ -28,6 +29,8 @@ public class ClientSessionEntity {
     private Set<String> protocolMappers;
     private Map<String, String> notes = new HashMap<>();
     private Map<String, String> userSessionNotes = new HashMap<>();
+    private Set<String> requiredActions = new HashSet<>();
+
 
     public String getId() {
         return id;
@@ -133,6 +136,10 @@ public class ClientSessionEntity {
         return userSessionNotes;
     }
 
+    public Set<String> getRequiredActions() {
+        return requiredActions;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java
index a00e805..5ddea31 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java
@@ -3,6 +3,9 @@ package org.keycloak.models.sessions.infinispan.entities;
 import org.keycloak.models.ClientSessionModel;
 
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -31,6 +34,8 @@ public class ClientSessionEntity extends SessionEntity {
     private Map<String, String> userSessionNotes;
     private Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus = new HashMap<>();
     private String authUserId;
+    private Set<String> requiredActions = new HashSet<>();
+
 
     public String getClient() {
         return client;
@@ -136,5 +141,7 @@ public class ClientSessionEntity extends SessionEntity {
         this.userSessionNotes = userSessionNotes;
     }
 
-
+    public Set<String> getRequiredActions() {
+        return requiredActions;
+    }
 }
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java
index d99dd9f..9ee834c 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java
@@ -19,7 +19,7 @@ public class ResetOTP extends AbstractSetRequiredActionAuthenticator {
         if (context.getExecution().isRequired() ||
                 (context.getExecution().isOptional() &&
                         configuredFor(context))) {
-            context.getUser().addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
+            context.getClientSession().addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
         }
         context.success();
     }
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java
index f41e5bd..468f4a0 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java
@@ -27,7 +27,7 @@ public class ResetPassword extends AbstractSetRequiredActionAuthenticator {
         if (context.getExecution().isRequired() ||
                 (context.getExecution().isOptional() &&
                         configuredFor(context))) {
-            context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+            context.getClientSession().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
         }
         context.success();
     }
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 70f10e7..3790d5a 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -441,32 +441,14 @@ public class AuthenticationManager {
         event.detail(Details.CODE_ID, clientSession.getId());
 
         Set<String> requiredActions = user.getRequiredActions();
-        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);
+        Response action = executionActions(session, userSession, clientSession, request, event, realm, user, requiredActions);
+        if (action != null) return action;
+
+        // executionActions() method should remove any duplicate actions that might be in the clientSession
+        requiredActions = clientSession.getRequiredActions();
+        action = executionActions(session, userSession, clientSession, request, event, realm, user, requiredActions);
+        if (action != null) return action;
 
-            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()) {
 
             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) {
 
         // see if any required actions need triggering, i.e. an expired password
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 3158b69..7aeabc2 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
@@ -837,7 +837,10 @@ public class UsersResource {
     @Path("{id}/execute-actions-email")
     @PUT
     @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();
 
         UserModel user = session.users().getUserById(id, realm);
@@ -850,6 +853,9 @@ public class UsersResource {
         }
 
         ClientSessionModel clientSession = createClientSession(user, redirectUri, clientId);
+        for (String action : actions) {
+            clientSession.addRequiredAction(action);
+        }
         ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession);
         accessCode.setAction(ClientSessionModel.Action.EXECUTE_ACTIONS.name());
 
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index 86adcdc..6d9fee9 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -684,6 +684,8 @@ public class LoginActionsService {
         provider.processAction(context);
         if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {
             event.clone().success();
+            // do both
+            clientSession.removeRequiredAction(factory.getId());
             clientSession.getUserSession().getUser().removeRequiredAction(factory.getId());
             event.event(EventType.LOGIN);
             return AuthenticationManager.nextActionAfterAuthentication(session, clientSession.getUserSession(), clientSession, clientConnection, request, uriInfo, event);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 25a98a0..de1b2ca 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -14,6 +14,7 @@ import javax.ws.rs.ClientErrorException;
 import javax.ws.rs.core.Response;
 
 import java.util.ArrayList;
+import java.util.LinkedList;
 import java.util.List;
 
 import static org.junit.Assert.assertEquals;
@@ -359,9 +360,9 @@ public class UserTest extends AbstractClientTest {
         String id = ApiUtil.getCreatedId(response);
         response.close();
         UserResource user = realm.users().get(id);
-
+        List<String> actions = new LinkedList<>();
         try {
-            user.executeActionsEmail();
+            user.executeActionsEmail(actions);
             fail("Expected failure");
         } catch (ClientErrorException e) {
             assertEquals(400, e.getResponse().getStatus());
@@ -374,7 +375,7 @@ public class UserTest extends AbstractClientTest {
             userRep.setEmail("user1@localhost");
             userRep.setEnabled(false);
             user.update(userRep);
-            user.executeActionsEmail();
+            user.executeActionsEmail(actions);
             fail("Expected failure");
         } catch (ClientErrorException e) {
             assertEquals(400, e.getResponse().getStatus());
@@ -385,7 +386,7 @@ public class UserTest extends AbstractClientTest {
         try {
             userRep.setEnabled(true);
             user.update(userRep);
-            user.executeActionsEmail("invalidClientId");
+            user.executeActionsEmail("invalidClientId", actions);
             fail("Expected failure");
         } catch (ClientErrorException e) {
             assertEquals(400, e.getResponse().getStatus());