keycloak-memoizeit

review changes

3/28/2018 5:45:52 PM

Changes

server-spi-private/src/main/java/org/keycloak/authentication/DisplayUtils.java 20(+0 -20)

Details

diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index efa9ed8..2ef01cb 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -116,6 +116,7 @@ public interface OAuth2Constants {
     String UMA_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket";
 
 
+    String DISPLAY_CONSOLE = "console";
 }
 
 
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java
index 2f17f77..1d60052 100755
--- a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java
@@ -43,5 +43,6 @@ public enum AuthenticationFlowError {
 
     IDENTITY_PROVIDER_NOT_FOUND,
     IDENTITY_PROVIDER_DISABLED,
-    IDENTITY_PROVIDER_ERROR
+    IDENTITY_PROVIDER_ERROR,
+    DISPLAY_NOT_SUPPORTED
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowException.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowException.java
index bf8fbcf..e15386a 100755
--- a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowException.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowException.java
@@ -17,6 +17,8 @@
 
 package org.keycloak.authentication;
 
+import javax.ws.rs.core.Response;
+
 /**
  * Throw this exception from an Authenticator, FormAuthenticator, or FormAction if you want to completely abort the flow.
  *
@@ -25,11 +27,17 @@ package org.keycloak.authentication;
  */
 public class AuthenticationFlowException extends RuntimeException {
     private AuthenticationFlowError error;
+    private Response response;
 
     public AuthenticationFlowException(AuthenticationFlowError error) {
         this.error = error;
     }
 
+    public AuthenticationFlowException(AuthenticationFlowError error, Response response) {
+        this.error = error;
+        this.response = response;
+    }
+
     public AuthenticationFlowException(String message, AuthenticationFlowError error) {
         super(message);
         this.error = error;
@@ -53,4 +61,8 @@ public class AuthenticationFlowException extends RuntimeException {
     public AuthenticationFlowError getError() {
         return error;
     }
+
+    public Response getResponse() {
+        return response;
+    }
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeAuthenticatorFactory.java b/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeAuthenticatorFactory.java
new file mode 100644
index 0000000..0754383
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeAuthenticatorFactory.java
@@ -0,0 +1,21 @@
+package org.keycloak.authentication;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * Implement this interface when declaring your authenticator factory
+ * if your provider has support for multiple oidc display query parameter parameter types
+ * if the display query parameter is set and your factory implements this interface, this method
+ * will be called.
+ *
+ */
+public interface DisplayTypeAuthenticatorFactory {
+    /**
+     *
+     *
+     * @param session
+     * @param displayType i.e. "console", "wap", "popup" are examples
+     * @return null if display type isn't support.
+     */
+    Authenticator createDisplay(KeycloakSession session, String displayType);
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeRequiredActionFactory.java b/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeRequiredActionFactory.java
new file mode 100644
index 0000000..e22e9ff
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeRequiredActionFactory.java
@@ -0,0 +1,13 @@
+package org.keycloak.authentication;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * Implement this interface when declaring your required action factory
+ * has support for multiple oidc display query parameter parameter types
+ * if the display query parameter is set and your factory implements this interface, this method
+ * will be called.
+ */
+public interface DisplayTypeRequiredActionFactory {
+    RequiredActionProvider createDisplay(KeycloakSession session, String displayType);
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/TextChallenge.java b/server-spi-private/src/main/java/org/keycloak/authentication/TextChallenge.java
index 46c0707..b1dc9a2 100644
--- a/server-spi-private/src/main/java/org/keycloak/authentication/TextChallenge.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/TextChallenge.java
@@ -1,6 +1,7 @@
 package org.keycloak.authentication;
 
 import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.KeycloakSession;
 
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
@@ -37,6 +38,20 @@ import javax.ws.rs.core.Response;
 public class TextChallenge {
 
     /**
+     * Browser is required to login.  This will abort client from doing a console login.
+     *
+     * @param session
+     * @return
+     */
+    public static Response browserRequired(KeycloakSession session) {
+        return Response.status(Response.Status.UNAUTHORIZED)
+                .header("WWW-Authenticate", "X-Text-Form-Challenge browserRequired")
+                .type(MediaType.TEXT_PLAIN)
+                .entity("\n" + session.getProvider(LoginFormsProvider.class).getMessage("browserRequired") + "\n").build();
+    }
+
+
+    /**
      * Build challenge response for required actions
      *
      * @param context
diff --git a/server-spi-private/src/main/java/org/keycloak/events/Errors.java b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
index 632c21c..95bcd81 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/Errors.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
@@ -90,5 +90,6 @@ public interface Errors {
     String NOT_LOGGED_IN = "not_logged_in";
     String UNKNOWN_IDENTITY_PROVIDER = "unknown_identity_provider";
     String ILLEGAL_ORIGIN = "illegal_origin";
+    String DISPLAY_UNSUPPORTED = "display_unsupported";
 
 }
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index 537581a..db96f11 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -653,27 +653,33 @@ public class AuthenticationProcessor {
     public Response handleBrowserException(Exception failure) {
         if (failure instanceof AuthenticationFlowException) {
             AuthenticationFlowException e = (AuthenticationFlowException) failure;
+
             if (e.getError() == AuthenticationFlowError.INVALID_USER) {
                 ServicesLogger.LOGGER.failedAuthentication(e);
                 event.error(Errors.USER_NOT_FOUND);
+                if (e.getResponse() != null) return e.getResponse();
                 return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
             } else if (e.getError() == AuthenticationFlowError.USER_DISABLED) {
                 ServicesLogger.LOGGER.failedAuthentication(e);
                 event.error(Errors.USER_DISABLED);
+                if (e.getResponse() != null) return e.getResponse();
                 return ErrorPage.error(session,authenticationSession, Response.Status.BAD_REQUEST, Messages.ACCOUNT_DISABLED);
             } else if (e.getError() == AuthenticationFlowError.USER_TEMPORARILY_DISABLED) {
                 ServicesLogger.LOGGER.failedAuthentication(e);
                 event.error(Errors.USER_TEMPORARILY_DISABLED);
+                if (e.getResponse() != null) return e.getResponse();
                 return ErrorPage.error(session,authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
 
             } else if (e.getError() == AuthenticationFlowError.INVALID_CLIENT_SESSION) {
                 ServicesLogger.LOGGER.failedAuthentication(e);
                 event.error(Errors.INVALID_CODE);
+                if (e.getResponse() != null) return e.getResponse();
                 return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_CODE);
 
             } else if (e.getError() == AuthenticationFlowError.EXPIRED_CODE) {
                 ServicesLogger.LOGGER.failedAuthentication(e);
                 event.error(Errors.EXPIRED_CODE);
+                if (e.getResponse() != null) return e.getResponse();
                 return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.EXPIRED_CODE);
 
             } else if (e.getError() == AuthenticationFlowError.FORK_FLOW) {
@@ -701,9 +707,15 @@ public class AuthenticationProcessor {
                 CacheControlUtil.noBackButtonCacheControlHeader();
                 return processor.authenticate();
 
+            } else if (e.getError() == AuthenticationFlowError.DISPLAY_NOT_SUPPORTED) {
+                ServicesLogger.LOGGER.failedAuthentication(e);
+                event.error(Errors.DISPLAY_UNSUPPORTED);
+                if (e.getResponse() != null) return e.getResponse();
+                return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.DISPLAY_UNSUPPORTED);
             } else {
                 ServicesLogger.LOGGER.failedAuthentication(e);
                 event.error(Errors.INVALID_USER_CREDENTIALS);
+                if (e.getResponse() != null) return e.getResponse();
                 return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
             }
 
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/AttemptedAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/AttemptedAuthenticator.java
new file mode 100644
index 0000000..fc866fd
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/AttemptedAuthenticator.java
@@ -0,0 +1,46 @@
+package org.keycloak.authentication.authenticators;
+
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+
+/**
+ * Pass-thru atheneticator that just sets the context to attempted.
+ */
+public class AttemptedAuthenticator implements Authenticator {
+
+    public static final AttemptedAuthenticator SINGLETON = new AttemptedAuthenticator();
+    @Override
+    public void authenticate(AuthenticationFlowContext context) {
+        context.attempted();
+
+    }
+
+    @Override
+    public void action(AuthenticationFlowContext context) {
+        throw new RuntimeException("Unreachable!");
+
+    }
+
+    @Override
+    public boolean requiresUser() {
+        return false;
+    }
+
+    @Override
+    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+        return true;
+    }
+
+    @Override
+    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+
+    }
+
+    @Override
+    public void close() {
+
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticatorFactory.java
index 7e40298..b87dbe9 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticatorFactory.java
@@ -18,8 +18,11 @@
 package org.keycloak.authentication.authenticators.browser;
 
 import org.keycloak.Config;
+import org.keycloak.OAuth2Constants;
 import org.keycloak.authentication.Authenticator;
 import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
+import org.keycloak.authentication.authenticators.AttemptedAuthenticator;
 import org.keycloak.models.AuthenticationExecutionModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
@@ -31,7 +34,7 @@ import java.util.List;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class CookieAuthenticatorFactory implements AuthenticatorFactory {
+public class CookieAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
     public static final String PROVIDER_ID = "auth-cookie";
     static CookieAuthenticator SINGLETON = new CookieAuthenticator();
 
@@ -41,6 +44,13 @@ public class CookieAuthenticatorFactory implements AuthenticatorFactory {
     }
 
     @Override
+    public Authenticator createDisplay(KeycloakSession session, String displayType) {
+        if (displayType == null) return SINGLETON;
+        if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+        return AttemptedAuthenticator.SINGLETON;  // ignore this authenticator
+    }
+
+    @Override
     public void init(Config.Scope config) {
 
     }
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticatorFactory.java
index 635c95e..b136d33 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticatorFactory.java
@@ -18,8 +18,11 @@
 package org.keycloak.authentication.authenticators.browser;
 
 import org.keycloak.Config;
+import org.keycloak.OAuth2Constants;
 import org.keycloak.authentication.Authenticator;
 import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
+import org.keycloak.authentication.authenticators.AttemptedAuthenticator;
 import org.keycloak.models.AuthenticationExecutionModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
@@ -33,7 +36,7 @@ import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactory {
+public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
 
     protected static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
             AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED
@@ -83,6 +86,13 @@ public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactor
     }
 
     @Override
+    public Authenticator createDisplay(KeycloakSession session, String displayType) {
+        if (displayType == null) return new IdentityProviderAuthenticator();
+        if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+        return AttemptedAuthenticator.SINGLETON;  // ignore this authenticator
+    }
+
+    @Override
     public void init(Config.Scope config) {
     }
 
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java
index 955048b..9126689 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java
@@ -20,8 +20,6 @@ package org.keycloak.authentication.authenticators.browser;
 import org.keycloak.authentication.AuthenticationFlowContext;
 import org.keycloak.authentication.AuthenticationFlowError;
 import org.keycloak.authentication.Authenticator;
-import org.keycloak.authentication.DisplayUtils;
-import org.keycloak.authentication.authenticators.console.ConsoleOTPFormAuthenticator;
 import org.keycloak.events.Errors;
 import org.keycloak.forms.login.LoginFormsProvider;
 import org.keycloak.models.KeycloakSession;
@@ -41,19 +39,11 @@ import javax.ws.rs.core.Response;
 public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {
     @Override
     public void action(AuthenticationFlowContext context) {
-        if (DisplayUtils.isConsole(context)) {
-            ConsoleOTPFormAuthenticator.SINGLETON.action(context);
-            return;
-        }
         validateOTP(context);
     }
 
     @Override
     public void authenticate(AuthenticationFlowContext context) {
-        if (DisplayUtils.isConsole(context)) {
-            ConsoleOTPFormAuthenticator.SINGLETON.authenticate(context);
-            return;
-        }
         Response challengeResponse = challenge(context, null);
         context.challenge(challengeResponse);
     }
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticatorFactory.java
index f443d28..d71659c 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticatorFactory.java
@@ -18,8 +18,11 @@
 package org.keycloak.authentication.authenticators.browser;
 
 import org.keycloak.Config;
+import org.keycloak.OAuth2Constants;
 import org.keycloak.authentication.Authenticator;
 import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
+import org.keycloak.authentication.authenticators.console.ConsoleOTPFormAuthenticator;
 import org.keycloak.models.AuthenticationExecutionModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
@@ -32,7 +35,7 @@ import java.util.List;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class OTPFormAuthenticatorFactory implements AuthenticatorFactory {
+public class OTPFormAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
 
     public static final String PROVIDER_ID = "auth-otp-form";
     public static final OTPFormAuthenticator SINGLETON = new OTPFormAuthenticator();
@@ -43,6 +46,13 @@ public class OTPFormAuthenticatorFactory implements AuthenticatorFactory {
     }
 
     @Override
+    public Authenticator createDisplay(KeycloakSession session, String displayType) {
+        if (displayType == null) return SINGLETON;
+        if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+        return ConsoleOTPFormAuthenticator.SINGLETON;
+    }
+
+    @Override
     public void init(Config.Scope config) {
 
     }
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticatorFactory.java
index 9d837c6..ae5dd0c 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticatorFactory.java
@@ -18,8 +18,10 @@
 package org.keycloak.authentication.authenticators.browser;
 
 import org.keycloak.Config;
+import org.keycloak.OAuth2Constants;
 import org.keycloak.authentication.Authenticator;
 import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
 import org.keycloak.models.AuthenticationExecutionModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
@@ -32,7 +34,7 @@ import java.util.List;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class SpnegoAuthenticatorFactory implements AuthenticatorFactory {
+public class SpnegoAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
 
     public static final String PROVIDER_ID = "auth-spnego";
     public static final SpnegoAuthenticator SINGLETON = new SpnegoAuthenticator();
@@ -43,6 +45,13 @@ public class SpnegoAuthenticatorFactory implements AuthenticatorFactory {
     }
 
     @Override
+    public Authenticator createDisplay(KeycloakSession session, String displayType) {
+        if (displayType == null) return SINGLETON;
+        if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+        return SINGLETON;
+    }
+
+    @Override
     public void init(Config.Scope config) {
 
     }
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
index 1dc966f..43383a0 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
@@ -20,8 +20,6 @@ package org.keycloak.authentication.authenticators.browser;
 import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
 import org.keycloak.authentication.AuthenticationFlowContext;
 import org.keycloak.authentication.Authenticator;
-import org.keycloak.authentication.DisplayUtils;
-import org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticator;
 import org.keycloak.forms.login.LoginFormsProvider;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
@@ -42,10 +40,6 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl
 
     @Override
     public void action(AuthenticationFlowContext context) {
-        if (DisplayUtils.isConsole(context)) {
-            ConsoleUsernamePasswordAuthenticator.SINGLETON.action(context);
-            return;
-        }
         MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
         if (formData.containsKey("cancel")) {
             context.cancelLogin();
@@ -63,10 +57,6 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl
 
     @Override
     public void authenticate(AuthenticationFlowContext context) {
-        if (DisplayUtils.isConsole(context)) {
-            ConsoleUsernamePasswordAuthenticator.SINGLETON.authenticate(context);
-            return;
-        }
         MultivaluedMap<String, String> formData = new MultivaluedMapImpl<>();
         String loginHint = context.getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM);
 
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordFormFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordFormFactory.java
index ef0c9b1..fe42f48 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordFormFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordFormFactory.java
@@ -18,8 +18,11 @@
 package org.keycloak.authentication.authenticators.browser;
 
 import org.keycloak.Config;
+import org.keycloak.OAuth2Constants;
 import org.keycloak.authentication.Authenticator;
 import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
+import org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticator;
 import org.keycloak.models.AuthenticationExecutionModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
@@ -32,7 +35,7 @@ import java.util.List;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class UsernamePasswordFormFactory implements AuthenticatorFactory {
+public class UsernamePasswordFormFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
 
     public static final String PROVIDER_ID = "auth-username-password-form";
     public static final UsernamePasswordForm SINGLETON = new UsernamePasswordForm();
@@ -43,6 +46,13 @@ public class UsernamePasswordFormFactory implements AuthenticatorFactory {
     }
 
     @Override
+    public Authenticator createDisplay(KeycloakSession session, String displayType) {
+        if (displayType == null) return SINGLETON;
+        if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+        return ConsoleUsernamePasswordAuthenticator.SINGLETON;
+    }
+
+    @Override
     public void init(Config.Scope config) {
 
     }
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java
index 8fa34e9..fff2c80 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java
@@ -19,13 +19,10 @@ package org.keycloak.authentication.authenticators.console;
 
 import org.keycloak.authentication.AuthenticationFlowContext;
 import org.keycloak.authentication.Authenticator;
-import org.keycloak.authentication.DisplayUtils;
 import org.keycloak.authentication.TextChallenge;
 import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator;
 import org.keycloak.representations.idm.CredentialRepresentation;
 
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import java.net.URI;
 
diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
index 89471e7..ac0c5e1 100755
--- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
@@ -18,6 +18,7 @@
 package org.keycloak.authentication;
 
 import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
 import org.keycloak.models.AuthenticationExecutionModel;
 import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.UserModel;
@@ -58,6 +59,24 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
                 || status == AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED;
     }
 
+    protected Authenticator createAuthenticator(AuthenticatorFactory factory) {
+        String display = processor.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY);
+        if (display == null) return factory.create(processor.getSession());
+
+
+        if (factory instanceof DisplayTypeAuthenticatorFactory) {
+            Authenticator authenticator = ((DisplayTypeAuthenticatorFactory)factory).createDisplay(processor.getSession(), display);
+            if (authenticator != null) return authenticator;
+        }
+        // todo create a provider for handling lack of display support
+        if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
+            throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, TextChallenge.browserRequired(processor.getSession()));
+
+        } else {
+            return factory.create(processor.getSession());
+        }
+    }
+
 
     @Override
     public Response processAction(String actionExecution) {
@@ -86,7 +105,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
                 if (factory == null) {
                     throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?");
                 }
-                Authenticator authenticator = factory.create(processor.getSession());
+                Authenticator authenticator = createAuthenticator(factory);
                 AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions);
                 logger.debugv("action: {0}", model.getAuthenticator());
                 authenticator.action(result);
@@ -161,7 +180,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
             if (factory == null) {
                 throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?");
             }
-            Authenticator authenticator = factory.create(processor.getSession());
+            Authenticator authenticator = createAuthenticator(factory);
             logger.debugv("authenticator: {0}", factory.getId());
             UserModel authUser = processor.getAuthenticationSession().getAuthenticatedUser();
 
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java
index 41e07d9..24c6938 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java
@@ -33,31 +33,9 @@ import java.util.Arrays;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class ConsoleTermsAndConditions implements RequiredActionProvider, RequiredActionFactory {
+public class ConsoleTermsAndConditions implements RequiredActionProvider {
     public static final ConsoleTermsAndConditions SINGLETON = new ConsoleTermsAndConditions();
-    public static final String PROVIDER_ID = "terms_and_conditions";
-    public static final String USER_ATTRIBUTE = PROVIDER_ID;
-
-    @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 PROVIDER_ID;
-    }
-
+    public static final String USER_ATTRIBUTE = TermsAndConditions.PROVIDER_ID;
 
     @Override
     public void evaluateTriggers(RequiredActionContext context) {
@@ -93,11 +71,6 @@ public class ConsoleTermsAndConditions implements RequiredActionProvider, Requir
     }
 
     @Override
-    public String getDisplayText() {
-        return "Terms and Conditions";
-    }
-
-    @Override
     public void close() {
 
     }
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdatePassword.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdatePassword.java
index 5a9aaf9..d499ead 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdatePassword.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdatePassword.java
@@ -38,7 +38,7 @@ import java.net.URI;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class ConsoleUpdatePassword extends UpdatePassword implements RequiredActionProvider, RequiredActionFactory {
+public class ConsoleUpdatePassword extends UpdatePassword implements RequiredActionProvider {
     public static final ConsoleUpdatePassword SINGLETON = new ConsoleUpdatePassword();
 
     private static final Logger logger = Logger.getLogger(ConsoleUpdatePassword.class);
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateProfile.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateProfile.java
index a5fb335..0b66bef 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateProfile.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateProfile.java
@@ -41,7 +41,7 @@ import java.util.List;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class ConsoleUpdateProfile implements RequiredActionProvider, RequiredActionFactory {
+public class ConsoleUpdateProfile implements RequiredActionProvider {
     public static final ConsoleUpdateProfile SINGLETON = new ConsoleUpdateProfile();
 
     @Override
@@ -65,30 +65,4 @@ public class ConsoleUpdateProfile implements RequiredActionProvider, RequiredAct
     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 getDisplayText() {
-        return "Update Profile";
-    }
-
-
-    @Override
-    public String getId() {
-        return UserModel.RequiredAction.UPDATE_PROFILE.name();
-    }
 }
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateTotp.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateTotp.java
index ec14bfa..89ef89b 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateTotp.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateTotp.java
@@ -44,7 +44,7 @@ import java.net.URI;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class ConsoleUpdateTotp implements RequiredActionProvider, RequiredActionFactory {
+public class ConsoleUpdateTotp implements RequiredActionProvider {
     public static final ConsoleUpdateTotp SINGLETON = new ConsoleUpdateTotp();
 
     @Override
@@ -110,35 +110,4 @@ public class ConsoleUpdateTotp implements RequiredActionProvider, RequiredAction
     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 getDisplayText() {
-        return "Configure OTP";
-    }
-
-
-    @Override
-    public String getId() {
-        return UserModel.RequiredAction.CONFIGURE_TOTP.name();
-    }
-
-    @Override
-    public boolean isOneTimeAction() {
-        return true;
-    }
 }
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleVerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleVerifyEmail.java
index 8eddf37..e136ceb 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleVerifyEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleVerifyEmail.java
@@ -52,7 +52,7 @@ import java.util.concurrent.TimeUnit;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class ConsoleVerifyEmail implements RequiredActionProvider, RequiredActionFactory {
+public class ConsoleVerifyEmail implements RequiredActionProvider {
     public static final ConsoleVerifyEmail SINGLETON = new ConsoleVerifyEmail();
     private static final Logger logger = Logger.getLogger(ConsoleVerifyEmail.class);
     @Override
@@ -113,33 +113,7 @@ public class ConsoleVerifyEmail implements RequiredActionProvider, RequiredActio
 
     }
 
-    @Override
-    public RequiredActionProvider create(KeycloakSession session) {
-        return this;
-    }
-
-    @Override
-    public void init(Config.Scope config) {
-
-    }
-
-    @Override
-    public void postInit(KeycloakSessionFactory factory) {
-
-    }
-
-    @Override
-    public String getDisplayText() {
-        return "Verify Email";
-    }
-
-
     public static String EMAIL_CODE="email_code";
-    @Override
-    public String getId() {
-        return UserModel.RequiredAction.VERIFY_EMAIL.name();
-    }
-
     protected TextChallenge challenge(RequiredActionContext context) {
         return TextChallenge.challenge(context)
                 .header()
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
index 961095c..2f68782 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
@@ -18,10 +18,8 @@
 package org.keycloak.authentication.requiredactions;
 
 import org.keycloak.Config;
-import org.keycloak.authentication.DisplayUtils;
-import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionFactory;
-import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.*;
 import org.keycloak.common.util.Time;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
@@ -33,7 +31,7 @@ import java.util.Arrays;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory {
+public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
     public static final String PROVIDER_ID = "terms_and_conditions";
     public static final String USER_ATTRIBUTE = PROVIDER_ID;
 
@@ -43,6 +41,15 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio
     }
 
     @Override
+    public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
+        if (displayType == null) return this;
+        if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+        return ConsoleTermsAndConditions.SINGLETON;
+    }
+
+
+
+    @Override
     public void init(Config.Scope config) {
 
     }
@@ -66,20 +73,12 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio
 
     @Override
     public void requiredActionChallenge(RequiredActionContext context) {
-        if (DisplayUtils.isConsole(context)) {
-            ConsoleTermsAndConditions.SINGLETON.requiredActionChallenge(context);
-            return;
-        }
         Response challenge = context.form().createForm("terms.ftl");
         context.challenge(challenge);
     }
 
     @Override
     public void processAction(RequiredActionContext context) {
-        if (DisplayUtils.isConsole(context)) {
-            ConsoleTermsAndConditions.SINGLETON.processAction(context);
-            return;
-        }
         if (context.getHttpRequest().getDecodedFormParameters().containsKey("cancel")) {
             context.getUser().removeAttribute(USER_ATTRIBUTE);
             context.failure();
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
index 65ddc9e..45fb05f 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
@@ -19,10 +19,8 @@ package org.keycloak.authentication.requiredactions;
 
 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.authentication.DisplayUtils;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.*;
 import org.keycloak.common.util.Time;
 import org.keycloak.credential.CredentialModel;
 import org.keycloak.credential.CredentialProvider;
@@ -48,7 +46,7 @@ import java.util.concurrent.TimeUnit;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class UpdatePassword implements RequiredActionProvider, RequiredActionFactory {
+public class UpdatePassword implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
     private static final Logger logger = Logger.getLogger(UpdatePassword.class);
     @Override
     public void evaluateTriggers(RequiredActionContext context) {
@@ -75,10 +73,6 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
 
     @Override
     public void requiredActionChallenge(RequiredActionContext context) {
-        if (DisplayUtils.isConsole(context)) {
-            ConsoleUpdatePassword.SINGLETON.requiredActionChallenge(context);
-            return;
-        }
         Response challenge = context.form()
                 .setAttribute("username", context.getAuthenticationSession().getAuthenticatedUser().getUsername())
                 .createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
@@ -87,10 +81,6 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
 
     @Override
     public void processAction(RequiredActionContext context) {
-        if (DisplayUtils.isConsole(context)) {
-            ConsoleUpdatePassword.SINGLETON.processAction(context);
-            return;
-        }
         EventBuilder event = context.getEvent();
         MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
         event.event(EventType.UPDATE_PASSWORD);
@@ -151,6 +141,15 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
         return this;
     }
 
+
+    @Override
+    public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
+        if (displayType == null) return this;
+        if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+        return ConsoleUpdatePassword.SINGLETON;
+    }
+
+
     @Override
     public void init(Config.Scope config) {
 
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java
index f790626..ccaf729 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java
@@ -18,10 +18,8 @@
 package org.keycloak.authentication.requiredactions;
 
 import org.keycloak.Config;
-import org.keycloak.authentication.DisplayUtils;
-import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionFactory;
-import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.*;
 import org.keycloak.events.Details;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.events.EventType;
@@ -42,17 +40,13 @@ import java.util.List;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class UpdateProfile implements RequiredActionProvider, RequiredActionFactory {
+public class UpdateProfile implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
     @Override
     public void evaluateTriggers(RequiredActionContext context) {
     }
 
     @Override
     public void requiredActionChallenge(RequiredActionContext context) {
-        if (DisplayUtils.isConsole(context)) {
-            ConsoleUpdateProfile.SINGLETON.requiredActionChallenge(context);
-            return;
-        }
         Response challenge = context.form()
                 .createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
         context.challenge(challenge);
@@ -60,10 +54,6 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
 
     @Override
     public void processAction(RequiredActionContext context) {
-        if (DisplayUtils.isConsole(context)) {
-            ConsoleUpdateProfile.SINGLETON.processAction(context);
-            return;
-        }
         EventBuilder event = context.getEvent();
         event.event(EventType.UPDATE_PROFILE);
         MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
@@ -151,6 +141,16 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
         return this;
     }
 
+
+    @Override
+    public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
+        if (displayType == null) return this;
+        if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+        return ConsoleUpdateProfile.SINGLETON;
+    }
+
+
+
     @Override
     public void init(Config.Scope config) {
 
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
index e715933..188ba2a 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
@@ -18,10 +18,8 @@
 package org.keycloak.authentication.requiredactions;
 
 import org.keycloak.Config;
-import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionFactory;
-import org.keycloak.authentication.RequiredActionProvider;
-import org.keycloak.authentication.DisplayUtils;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.*;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.events.EventType;
 import org.keycloak.models.KeycloakSession;
@@ -39,17 +37,13 @@ import javax.ws.rs.core.Response;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory {
+public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
     @Override
     public void evaluateTriggers(RequiredActionContext context) {
     }
 
     @Override
     public void requiredActionChallenge(RequiredActionContext context) {
-        if (DisplayUtils.isConsole(context)) {
-            ConsoleUpdateTotp.SINGLETON.requiredActionChallenge(context);
-            return;
-        }
         Response challenge = context.form()
                 .setAttribute("mode", getMode(context))
                 .createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
@@ -62,10 +56,6 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
 
     @Override
     public void processAction(RequiredActionContext context) {
-        if (DisplayUtils.isConsole(context)) {
-            ConsoleUpdateTotp.SINGLETON.processAction(context);
-            return;
-        }
         EventBuilder event = context.getEvent();
         event.event(EventType.UPDATE_TOTP);
         MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
@@ -114,6 +104,15 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
         return this;
     }
 
+
+    @Override
+    public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
+        if (displayType == null) return this;
+        if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+        return ConsoleUpdateTotp.SINGLETON;
+    }
+
+
     @Override
     public void init(Config.Scope config) {
 
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
index 9333905..c29c616 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
@@ -19,11 +19,9 @@ package org.keycloak.authentication.requiredactions;
 
 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.OAuth2Constants;
+import org.keycloak.authentication.*;
 import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
-import org.keycloak.authentication.DisplayUtils;
 import org.keycloak.common.util.Time;
 import org.keycloak.email.EmailException;
 import org.keycloak.email.EmailTemplateProvider;
@@ -46,7 +44,7 @@ import javax.ws.rs.core.*;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class VerifyEmail implements RequiredActionProvider, RequiredActionFactory {
+public class VerifyEmail implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
     private static final Logger logger = Logger.getLogger(VerifyEmail.class);
     @Override
     public void evaluateTriggers(RequiredActionContext context) {
@@ -57,10 +55,6 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
     }
     @Override
     public void requiredActionChallenge(RequiredActionContext context) {
-        if (DisplayUtils.isConsole(context)) {
-            ConsoleVerifyEmail.SINGLETON.requiredActionChallenge(context);
-            return;
-        }
         AuthenticationSessionModel authSession = context.getAuthenticationSession();
 
         if (context.getUser().isEmailVerified()) {
@@ -93,10 +87,6 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
 
     @Override
     public void processAction(RequiredActionContext context) {
-        if (DisplayUtils.isConsole(context)) {
-            ConsoleVerifyEmail.SINGLETON.processAction(context);
-            return;
-        }
         logger.debugf("Re-sending email requested for user: %s", context.getUser().getUsername());
 
         // This will allow user to re-send email again
@@ -116,6 +106,14 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
         return this;
     }
 
+
+    @Override
+    public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
+        if (displayType == null) return this;
+        if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+        return ConsoleVerifyEmail.SINGLETON;
+    }
+
     @Override
     public void init(Config.Scope config) {
 
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 a426bc2..edb9b51 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -21,11 +21,7 @@ import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
 import org.jboss.resteasy.spi.HttpRequest;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.TokenVerifier;
-import org.keycloak.authentication.AuthenticationProcessor;
-import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionContextResult;
-import org.keycloak.authentication.RequiredActionFactory;
-import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.authentication.*;
 import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
 import org.keycloak.broker.provider.IdentityProvider;
 import org.keycloak.common.ClientConnection;
@@ -970,6 +966,25 @@ public class AuthenticationManager {
         authSession.setProtocolMappers(requestedProtocolMappers);
     }
 
+    public static RequiredActionProvider createRequiredAction(KeycloakSession session, RequiredActionFactory factory, AuthenticationSessionModel authSession) {
+        String display = authSession.getClientNote(OAuth2Constants.DISPLAY);
+        if (display == null) return factory.create(session);
+
+
+        if (factory instanceof DisplayTypeRequiredActionFactory) {
+            RequiredActionProvider provider = ((DisplayTypeRequiredActionFactory)factory).createDisplay(session, display);
+            if (provider != null) return provider;
+        }
+        // todo create a provider for handling lack of display support
+        if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
+            throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, TextChallenge.browserRequired(session));
+
+        } else {
+            return factory.create(session);
+        }
+    }
+
+
     protected static Response executionActions(KeycloakSession session, AuthenticationSessionModel authSession,
                                                HttpRequest request, EventBuilder event, RealmModel realm, UserModel user,
                                                Set<String> requiredActions) {
@@ -987,7 +1002,15 @@ public class AuthenticationManager {
             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);
+            RequiredActionProvider actionProvider = null;
+            try {
+                actionProvider = createRequiredAction(session, factory, authSession);
+            } catch (AuthenticationFlowException e) {
+                if (e.getResponse() != null) {
+                    return e.getResponse();
+                }
+                throw e;
+            }
             RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, user, factory);
             actionProvider.requiredActionChallenge(context);
 
diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java
index c6fb6c8..425a889 100755
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -21,6 +21,7 @@ package org.keycloak.services.messages;
  */
 public class Messages {
 
+    public static final String DISPLAY_UNSUPPORTED = "displayUnsupported";
     public static final String LOGIN_TIMEOUT = "loginTimeout";
 
     public static final String INVALID_USER = "invalidUserMessage";
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 7c38ba8..ef9525a 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -16,17 +16,12 @@
  */
 package org.keycloak.services.resources;
 
+import org.keycloak.authentication.*;
 import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
 import org.jboss.logging.Logger;
 import org.jboss.resteasy.spi.HttpRequest;
 import org.keycloak.OAuth2Constants;
-import org.keycloak.authentication.AuthenticationProcessor;
-import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionContextResult;
-import org.keycloak.authentication.RequiredActionFactory;
-import org.keycloak.authentication.RequiredActionProvider;
 import org.keycloak.TokenVerifier;
-import org.keycloak.authentication.ExplainedVerificationException;
 import org.keycloak.authentication.actiontoken.*;
 import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler;
 import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
@@ -934,7 +929,15 @@ public class LoginActionsService {
             event.error(Errors.INVALID_CODE);
             throw new WebApplicationException(ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_CODE));
         }
-        RequiredActionProvider provider = factory.create(session);
+        RequiredActionProvider provider = null;
+        try {
+            provider = AuthenticationManager.createRequiredAction(session, factory, authSession);
+        }  catch (AuthenticationFlowException e) {
+            if (e.getResponse() != null) {
+                return e.getResponse();
+            }
+            throw new WebApplicationException(ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.DISPLAY_UNSUPPORTED));
+        }
 
         RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, authSession.getAuthenticatedUser(), factory) {
             @Override
diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index b00140f..1e5d755 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -251,6 +251,7 @@
                             <packages>
                                 <package>github.com/inconshreveable/mousetrap</package>
                             </packages>
+                            <goPath>${project.build.directory}/gopath</goPath>
                         </configuration>
                     </execution>
                     <execution>
@@ -262,23 +263,11 @@
                             <packages>
                                 <package>github.com/keycloak/kcinit</package>
                             </packages>
-                            <branch>0.1</branch>
+                            <goPath>${project.build.directory}/gopath</goPath>
+                            <tag>0.3</tag>
                         </configuration>
                     </execution>
-                    <execution>
-                        <id>build-kcinit</id>
-                        <goals>
-                            <goal>build</goal>
-                        </goals>
-                        <configuration>
-                            <resultName>kcinit</resultName>
-                            <packages>
-                                <package>github.com/keycloak/kcinit</package>
-                            </packages>
-                            <branch>0.1</branch>
-                        </configuration>
-                    </execution>
-                 </executions>
+                  </executions>
             </plugin>
 
         </plugins>
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/KcinitTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/KcinitTest.java
index 0884d7a..f23a63c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/KcinitTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/KcinitTest.java
@@ -39,6 +39,7 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissionManageme
 import org.keycloak.services.resources.admin.permissions.AdminPermissions;
 import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
 import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.forms.PassThroughAuthenticator;
 import org.keycloak.testsuite.pages.AppPage;
 import org.keycloak.testsuite.pages.ErrorPage;
 import org.keycloak.testsuite.pages.LoginPage;
@@ -139,6 +140,23 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
             session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
             user.setEnabled(true);
             user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+
+            // Parent flow
+            AuthenticationFlowModel browser = new AuthenticationFlowModel();
+            browser.setAlias("no-console-flow");
+            browser.setDescription("browser based authentication");
+            browser.setProviderId("basic-flow");
+            browser.setTopLevel(true);
+            browser.setBuiltIn(true);
+            browser = realm.addAuthenticationFlow(browser);
+
+            AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
+            execution.setParentFlow(browser.getId());
+            execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+            execution.setPriority(20);
+            execution.setAuthenticator(PassThroughAuthenticator.PROVIDER_ID);
+            realm.addAuthenticatorExecution(execution);
+
         });
     }
 
@@ -165,6 +183,40 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
     }
 
     @Test
+    public void testBrowserRequired() throws Exception {
+        // that that a browser require challenge is sent back if authentication flow doesn't support console display mode
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ClientModel kcinit = realm.getClientByClientId(KCINIT_CLIENT);
+            AuthenticationFlowModel flow = realm.getFlowByAlias("no-console-flow");
+            kcinit.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, flow.getId());
+
+
+        });
+
+        testInstall();
+        // login
+        //System.out.println("login....");
+        KcinitExec exe = KcinitExec.newBuilder()
+                .argsLine("login")
+                .executeAsync();
+        exe.waitCompletion();
+        Assert.assertEquals(1, exe.exitCode());
+        Assert.assertTrue(exe.stderrString().contains("Browser required to login"));
+        //Assert.assertEquals("stderr first line", "Browser required to login", exe.stderrLines().get(1));
+
+
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ClientModel kcinit = realm.getClientByClientId(KCINIT_CLIENT);
+            kcinit.removeAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING);
+
+
+        });
+    }
+
+
+    @Test
     public void testBadCommand() throws Exception {
         KcinitExec exe = KcinitExec.execute("covfefe");
         Assert.assertEquals(1, exe.exitCode());
diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
index 3b1eded..5f4c6a2 100755
--- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -34,6 +34,8 @@ emailForgotTitle=Forgot Your Password?
 updatePasswordTitle=Update password
 codeSuccessTitle=Success code
 codeErrorTitle=Error code\: {0}
+displayUnsupported=Requested display type unsupported
+browserRequired=Browser required to login
 
 termsTitle=Terms and Conditions
 termsText=<p>Terms and conditions to be defined</p>