keycloak-aplcache

pluggable required actions backend

6/10/2015 12:38:01 PM

Details

diff --git a/services/src/main/java/org/keycloak/authentication/actions/UpdatePassword.java b/services/src/main/java/org/keycloak/authentication/actions/UpdatePassword.java
new file mode 100755
index 0000000..23d5a93
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actions/UpdatePassword.java
@@ -0,0 +1,93 @@
+package org.keycloak.authentication.actions;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionFactory;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.login.LoginFormsProvider;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserCredentialValueModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.util.Time;
+
+import javax.ws.rs.core.Response;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class UpdatePassword implements RequiredActionProvider, RequiredActionFactory {
+    protected static Logger logger = Logger.getLogger(UpdatePassword.class);
+    @Override
+    public void evaluateTriggers(RequiredActionContext context) {
+        int daysToExpirePassword = context.getRealm().getPasswordPolicy().getDaysToExpirePassword();
+        if(daysToExpirePassword != -1) {
+            for (UserCredentialValueModel entity : context.getUser().getCredentialsDirectly()) {
+                if (entity.getType().equals(UserCredentialModel.PASSWORD)) {
+
+                    if(entity.getCreatedDate() == null) {
+                        context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+                        logger.debug("User is required to update password");
+                    } else {
+                        long timeElapsed = Time.toMillis(Time.currentTime()) - entity.getCreatedDate();
+                        long timeToExpire = TimeUnit.DAYS.toMillis(daysToExpirePassword);
+
+                        if(timeElapsed > timeToExpire) {
+                            context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+                            logger.debug("User is required to update password");
+                        }
+                    }
+                    break;
+                }
+            }
+        }
+    }
+
+    @Override
+    public Response invokeRequiredAction(RequiredActionContext context) {
+        ClientSessionCode accessCode = new ClientSessionCode(context.getRealm(), context.getClientSession());
+        accessCode.setAction(ClientSessionModel.Action.UPDATE_PASSWORD.name());
+
+        LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class).setClientSessionCode(accessCode.getCode())
+                .setUser(context.getUser());
+        return loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
+    }
+
+    @Override
+    public Object jaxrsService(RequiredActionContext context) {
+        // this is handled by LoginActionsService at the moment
+        return null;
+    }
+
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public RequiredActionProvider create(KeycloakSession session) {
+        return this;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public String getId() {
+        return UserModel.RequiredAction.UPDATE_PASSWORD.name();
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actions/UpdateProfile.java b/services/src/main/java/org/keycloak/authentication/actions/UpdateProfile.java
new file mode 100755
index 0000000..8ee36e9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actions/UpdateProfile.java
@@ -0,0 +1,75 @@
+package org.keycloak.authentication.actions;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionFactory;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.login.LoginFormsProvider;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RequiredCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.managers.ClientSessionCode;
+
+import javax.ws.rs.core.Response;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class UpdateProfile implements RequiredActionProvider, RequiredActionFactory {
+    protected static Logger logger = Logger.getLogger(UpdateProfile.class);
+    @Override
+    public void evaluateTriggers(RequiredActionContext context) {
+        if (context.getRealm().isVerifyEmail() && !context.getUser().isEmailVerified()) {
+            context.getUser().addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
+            logger.debug("User is required to verify email");
+        }
+    }
+
+    @Override
+    public Response invokeRequiredAction(RequiredActionContext context) {
+        ClientSessionCode accessCode = new ClientSessionCode(context.getRealm(), context.getClientSession());
+        accessCode.setAction(ClientSessionModel.Action.UPDATE_PROFILE.name());
+
+        LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class).setClientSessionCode(accessCode.getCode())
+                .setUser(context.getUser());
+        return loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
+    }
+
+    @Override
+    public Object jaxrsService(RequiredActionContext context) {
+        // this is handled by LoginActionsService at the moment
+        // todo should be refactored to contain it here
+        return null;
+    }
+
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public RequiredActionProvider create(KeycloakSession session) {
+        return this;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public String getId() {
+        return UserModel.RequiredAction.UPDATE_PROFILE.name();
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actions/UpdateTotp.java b/services/src/main/java/org/keycloak/authentication/actions/UpdateTotp.java
new file mode 100755
index 0000000..7f2228a
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actions/UpdateTotp.java
@@ -0,0 +1,84 @@
+package org.keycloak.authentication.actions;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionFactory;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.login.LoginFormsProvider;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RequiredCredentialModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserCredentialValueModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.util.Time;
+
+import javax.ws.rs.core.Response;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory {
+    protected static Logger logger = Logger.getLogger(UpdateTotp.class);
+    @Override
+    public void evaluateTriggers(RequiredActionContext context) {
+        // I don't think we need this check here.  AuthenticationProcessor should be setting the required action
+        // if OTP changes from required from optional or disabled
+        for (RequiredCredentialModel c : context.getRealm().getRequiredCredentials()) {
+            if (c.getType().equals(CredentialRepresentation.TOTP) && !context.getUser().isTotp()) {
+                context.getUser().addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
+                logger.debug("User is required to configure totp");
+            }
+        }
+    }
+
+    @Override
+    public Response invokeRequiredAction(RequiredActionContext context) {
+        ClientSessionCode accessCode = new ClientSessionCode(context.getRealm(), context.getClientSession());
+        accessCode.setAction(ClientSessionModel.Action.CONFIGURE_TOTP.name());
+
+        LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class).setClientSessionCode(accessCode.getCode())
+                .setUser(context.getUser());
+        return loginFormsProvider.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
+    }
+
+    @Override
+    public Object jaxrsService(RequiredActionContext context) {
+        // this is handled by LoginActionsService at the moment
+        // todo should be refactored to contain it here
+        return null;
+    }
+
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public RequiredActionProvider create(KeycloakSession session) {
+        return this;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public String getId() {
+        return UserModel.RequiredAction.CONFIGURE_TOTP.name();
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/actions/VerifyEmail.java
new file mode 100755
index 0000000..1be48a0
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actions/VerifyEmail.java
@@ -0,0 +1,104 @@
+package org.keycloak.authentication.actions;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionFactory;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.EventType;
+import org.keycloak.login.LoginFormsProvider;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserCredentialValueModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.resources.LoginActionsService;
+import org.keycloak.services.validation.Validation;
+import org.keycloak.util.Time;
+
+import javax.ws.rs.core.Response;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class VerifyEmail implements RequiredActionProvider, RequiredActionFactory {
+    protected static Logger logger = Logger.getLogger(VerifyEmail.class);
+    @Override
+    public void evaluateTriggers(RequiredActionContext context) {
+        int daysToExpirePassword = context.getRealm().getPasswordPolicy().getDaysToExpirePassword();
+        if(daysToExpirePassword != -1) {
+            for (UserCredentialValueModel entity : context.getUser().getCredentialsDirectly()) {
+                if (entity.getType().equals(UserCredentialModel.PASSWORD)) {
+
+                    if(entity.getCreatedDate() == null) {
+                        context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+                        logger.debug("User is required to update password");
+                    } else {
+                        long timeElapsed = Time.toMillis(Time.currentTime()) - entity.getCreatedDate();
+                        long timeToExpire = TimeUnit.DAYS.toMillis(daysToExpirePassword);
+
+                        if(timeElapsed > timeToExpire) {
+                            context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+                            logger.debug("User is required to update password");
+                        }
+                    }
+                    break;
+                }
+            }
+        }
+    }
+
+    @Override
+    public Response invokeRequiredAction(RequiredActionContext context) {
+        if (Validation.isBlank(context.getUser().getEmail())) {
+            return null;
+        }
+
+        ClientSessionCode accessCode = new ClientSessionCode(context.getRealm(), context.getClientSession());
+        accessCode.setAction(ClientSessionModel.Action.VERIFY_EMAIL.name());
+        context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, context.getUser().getEmail()).success();
+        LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getUserSession().getId());
+
+        LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class).setClientSessionCode(accessCode.getCode())
+                .setUser(context.getUser());
+        return loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
+    }
+
+    @Override
+    public Object jaxrsService(RequiredActionContext context) {
+        // this is handled by LoginActionsService at the moment
+        return null;
+    }
+
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public RequiredActionProvider create(KeycloakSession session) {
+        return this;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public String getId() {
+        return UserModel.RequiredAction.VERIFY_EMAIL.name();
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionFactory.java b/services/src/main/java/org/keycloak/authentication/RequiredActionFactory.java
new file mode 100755
index 0000000..9acfcd1
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionFactory.java
@@ -0,0 +1,10 @@
+package org.keycloak.authentication;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface RequiredActionFactory extends ProviderFactory<RequiredActionProvider> {
+}
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionProvider.java b/services/src/main/java/org/keycloak/authentication/RequiredActionProvider.java
index b5cc21a..e6ff488 100755
--- a/services/src/main/java/org/keycloak/authentication/RequiredActionProvider.java
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionProvider.java
@@ -9,6 +9,7 @@ import javax.ws.rs.core.Response;
  * @version $Revision: 1 $
  */
 public interface RequiredActionProvider extends Provider {
+    void evaluateTriggers(RequiredActionContext context);
     Response invokeRequiredAction(RequiredActionContext context);
-    Object jaxrsService();
+    Object jaxrsService(RequiredActionContext context);
 }
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionSpi.java b/services/src/main/java/org/keycloak/authentication/RequiredActionSpi.java
new file mode 100755
index 0000000..65370e5
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionSpi.java
@@ -0,0 +1,32 @@
+package org.keycloak.authentication;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class RequiredActionSpi implements Spi {
+
+    @Override
+    public boolean isInternal() {
+        return false;
+    }
+
+    @Override
+    public String getName() {
+        return "required-action";
+    }
+
+    @Override
+    public Class<? extends Provider> getProviderClass() {
+        return RequiredActionProvider.class;
+    }
+
+    @Override
+    public Class<? extends ProviderFactory> getProviderFactoryClass() {
+        return RequiredActionFactory.class;
+    }
+
+}
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 f445bfe..8d19950 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -6,6 +6,9 @@ import org.jboss.resteasy.spi.HttpRequest;
 import org.keycloak.ClientConnection;
 import org.keycloak.RSATokenVerifier;
 import org.keycloak.VerificationException;
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionFactory;
+import org.keycloak.authentication.RequiredActionProvider;
 import org.keycloak.broker.provider.IdentityProvider;
 import org.keycloak.events.Details;
 import org.keycloak.events.EventBuilder;
@@ -28,6 +31,7 @@ import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.protocol.LoginProtocol;
 import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.provider.ProviderFactory;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.services.resources.IdentityBrokerService;
@@ -433,15 +437,70 @@ public class AuthenticationManager {
 
     }
 
-    public static Response actionRequired(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession,
-                                                         ClientConnection clientConnection,
-                                                         HttpRequest request, UriInfo uriInfo, EventBuilder event) {
-        RealmModel realm = clientSession.getRealm();
-        UserModel user = userSession.getUser();
+    public static Response actionRequired(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession,
+                                                         final ClientConnection clientConnection,
+                                                         final HttpRequest request, final UriInfo uriInfo, final EventBuilder event) {
+        final RealmModel realm = clientSession.getRealm();
+        final UserModel user = userSession.getUser();
+        /*
         isForcePasswordUpdateRequired(realm, user);
         isTotpConfigurationRequired(realm, user);
         isEmailVerificationRequired(realm, user);
-        ClientModel client = clientSession.getClient();
+        */
+        final ClientModel client = clientSession.getClient();
+
+        RequiredActionContext context = new RequiredActionContext() {
+            @Override
+            public EventBuilder getEvent() {
+                return event;
+            }
+
+            @Override
+            public UserModel getUser() {
+                return user;
+            }
+
+            @Override
+            public RealmModel getRealm() {
+                return realm;
+            }
+
+            @Override
+            public ClientSessionModel getClientSession() {
+                return clientSession;
+            }
+
+            @Override
+            public UserSessionModel getUserSession() {
+                return userSession;
+            }
+
+            @Override
+            public ClientConnection getConnection() {
+                return clientConnection;
+            }
+
+            @Override
+            public UriInfo getUriInfo() {
+                return uriInfo;
+            }
+
+            @Override
+            public KeycloakSession getSession() {
+                return session;
+            }
+
+            @Override
+            public HttpRequest getHttpRequest() {
+                return request;
+            }
+        };
+
+        // see if any required actions need triggering, i.e. an expired password
+        for (ProviderFactory factory : session.getKeycloakSessionFactory().getProviderFactories(RequiredActionProvider.class)) {
+            RequiredActionProvider provider = ((RequiredActionFactory)factory).create(session);
+            provider.evaluateTriggers(context);
+        }
 
         ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession);
 
@@ -450,38 +509,19 @@ public class AuthenticationManager {
         event.detail(Details.CODE_ID, clientSession.getId());
 
         Set<String> requiredActions = user.getRequiredActions();
-        if (!requiredActions.isEmpty()) {
-            Iterator<String> i = user.getRequiredActions().iterator();
-            String action = i.next();
-            
-            if (action.equals(UserModel.RequiredAction.VERIFY_EMAIL.name()) && Validation.isBlank(user.getEmail())) {
-                if (i.hasNext())
-                    action = i.next();
-                else
-                    action = null;
-            }
-
-            if (action != null) {
-                accessCode.setRequiredAction(RequiredAction.valueOf(action));
-
-                LoginFormsProvider loginFormsProvider = session.getProvider(LoginFormsProvider.class).setClientSessionCode(accessCode.getCode())
-                        .setUser(user);
-                if (action.equals(UserModel.RequiredAction.VERIFY_EMAIL.name())) {
-                    event.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success();
-                    LoginActionsService.createActionCookie(realm, uriInfo, clientConnection, userSession.getId());
-                }
+        for (String action : requiredActions) {
+            RequiredActionProvider actionProvider = session.getProvider(RequiredActionProvider.class, action);
+            Response challenge = actionProvider.invokeRequiredAction(context);
+            if (challenge != null) return challenge;
 
-                return loginFormsProvider.createResponse(RequiredAction.valueOf(action));
-            }
         }
-
         if (client.isConsentRequired()) {
             accessCode.setAction(ClientSessionModel.Action.OAUTH_GRANT.name());
 
             UserConsentModel grantedConsent = user.getConsentByClient(client.getId());
 
-            List<RoleModel> realmRoles = new LinkedList<RoleModel>();
-            MultivaluedMap<String, RoleModel> resourceRoles = new MultivaluedMapImpl<String, RoleModel>();
+            List<RoleModel> realmRoles = new LinkedList<>();
+            MultivaluedMap<String, RoleModel> resourceRoles = new MultivaluedMapImpl<>();
             for (RoleModel r : accessCode.getRequestedRoles()) {
 
                 // Consent already granted by user
@@ -496,7 +536,7 @@ public class AuthenticationManager {
                 }
             }
 
-            List<ProtocolMapperModel> protocolMappers = new LinkedList<ProtocolMapperModel>();
+            List<ProtocolMapperModel> protocolMappers = new LinkedList<>();
             for (ProtocolMapperModel protocolMapper : accessCode.getRequestedProtocolMappers()) {
                 if (protocolMapper.isConsentRequired() && protocolMapper.getConsentText() != null) {
                     if (grantedConsent == null || !grantedConsent.isProtocolMapperGranted(protocolMapper)) {
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 a81327e..08327b9 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -773,7 +773,7 @@ public class LoginActionsService {
             event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success();
         }
 
-        return redirectOauth(user, accessCode, clientSession, userSession);
+        return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event);
     }
 
     @Path("totp")
@@ -818,7 +818,7 @@ public class LoginActionsService {
 
         event.clone().event(EventType.UPDATE_TOTP).success();
 
-        return redirectOauth(user, accessCode, clientSession, userSession);
+        return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event);
     }
 
     @Path("password")
@@ -880,7 +880,7 @@ public class LoginActionsService {
 
         event = event.clone().event(EventType.LOGIN);
 
-        return redirectOauth(user, accessCode, clientSession, userSession);
+        return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event);
     }
 
 
@@ -913,7 +913,7 @@ public class LoginActionsService {
 
             event = event.clone().removeDetail(Details.EMAIL).event(EventType.LOGIN);
 
-            return redirectOauth(user, accessCode, clientSession, userSession);
+            return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event);
         } else {
             Checks checks = new Checks();
             if (!checks.check(code, ClientSessionModel.Action.VERIFY_EMAIL.name())) {
@@ -1057,10 +1057,6 @@ public class LoginActionsService {
         CookieHelper.addCookie(ACTION_COOKIE, sessionId, AuthenticationManager.getRealmCookiePath(realm, uriInfo), null, null, -1, realm.getSslRequired().isRequired(clientConnection), true);
     }
 
-    private Response redirectOauth(UserModel user, ClientSessionCode accessCode, ClientSessionModel clientSession, UserSessionModel userSession) {
-        return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event);
-    }
-
     private void initEvent(ClientSessionModel clientSession) {
         event.event(EventType.LOGIN).client(clientSession.getClient())
                 .user(clientSession.getUserSession().getUser())
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory
new file mode 100755
index 0000000..fc74174
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory
@@ -0,0 +1,4 @@
+org.keycloak.authentication.actions.UpdatePassword
+org.keycloak.authentication.actions.UpdateProfile
+org.keycloak.authentication.actions.UpdateTotp
+org.keycloak.authentication.actions.VerifyEmail
\ No newline at end of file
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index d2b5ca7..050fef2 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -3,4 +3,5 @@ org.keycloak.protocol.ProtocolMapperSpi
 org.keycloak.exportimport.ClientImportSpi
 org.keycloak.wellknown.WellKnownSpi
 org.keycloak.messages.MessagesSpi
-org.keycloak.authentication.AuthenticatorSpi
\ No newline at end of file
+org.keycloak.authentication.AuthenticatorSpi
+org.keycloak.authentication.RequiredActionSpi
\ No newline at end of file