keycloak-aplcache

Changes

Details

diff --git a/adapters/oidc/installed/pom.xml b/adapters/oidc/installed/pom.xml
index d740fc1..e1d8279 100755
--- a/adapters/oidc/installed/pom.xml
+++ b/adapters/oidc/installed/pom.xml
@@ -63,6 +63,15 @@
             <groupId>org.jboss.logging</groupId>
             <artifactId>jboss-logging</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.spec.javax.ws.rs</groupId>
+            <artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
+        </dependency>
+
     </dependencies>
 
     <build>
diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java
index 3c1d365..f2b1bdc 100644
--- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java
+++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java
@@ -53,7 +53,13 @@ public class KeycloakCliSso {
             login();
         } else if (args[0].equalsIgnoreCase("login-manual")) {
             loginManual();
-        } else if (args[0].equalsIgnoreCase("token")) {
+        }
+        /*
+        else if (args[0].equalsIgnoreCase("login-cli")) {
+            loginCli();
+        }
+        */
+        else if (args[0].equalsIgnoreCase("token")) {
             token();
         } else if (args[0].equalsIgnoreCase("logout")) {
             logout();
@@ -69,6 +75,7 @@ public class KeycloakCliSso {
         System.err.println("Commands:");
         System.err.println("  login - login with desktop browser if available, otherwise do manual login.  Output is access token.");
         System.err.println("  login-manual - manual login");
+        //System.err.println("  login-cli - attempt Keycloak proprietary cli protocol.  Otherwise do normal login");
         System.err.println("  token - print access token if logged in");
         System.err.println("  logout - logout.");
         System.exit(1);
@@ -110,7 +117,7 @@ public class KeycloakCliSso {
         return config;
     }
 
-    public boolean checkToken() throws Exception {
+    public boolean checkToken(boolean outputToken) throws Exception {
         String token = getTokenResponse();
         if (token == null) return false;
 
@@ -127,7 +134,7 @@ public class KeycloakCliSso {
                     AdapterConfig config = getConfig();
                     KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
                     installed.refreshToken(tokenResponse.getRefreshToken());
-                    processResponse(installed);
+                    processResponse(installed, outputToken);
                     return true;
                 } catch (Exception e) {
                     System.err.println("Error processing existing token");
@@ -184,11 +191,19 @@ public class KeycloakCliSso {
     }
 
     public void login() throws Exception {
-        if (checkToken()) return;
+        if (checkToken(true)) return;
         AdapterConfig config = getConfig();
         KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
         installed.login();
-        processResponse(installed);
+        processResponse(installed, true);
+    }
+
+    public void loginCli() throws Exception {
+        if (checkToken(false)) return;
+        AdapterConfig config = getConfig();
+        KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
+        if (!installed.loginCommandLine()) installed.login();
+        processResponse(installed, false);
     }
 
     public String getHome() {
@@ -210,7 +225,7 @@ public class KeycloakCliSso {
         return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM"), System.getProperty("KEYCLOAK_CLIENT") + ".json").toFile();
     }
 
-    private void processResponse(KeycloakInstalled installed) throws IOException {
+    private void processResponse(KeycloakInstalled installed, boolean outputToken) throws IOException {
         AccessTokenResponse tokenResponse = installed.getTokenResponse();
         tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
         tokenResponse.setIdToken(null);
@@ -220,16 +235,16 @@ public class KeycloakCliSso {
         fos.write(output.getBytes("UTF-8"));
         fos.flush();
         fos.close();
-        System.out.println(tokenResponse.getToken());
+        if (outputToken) System.out.println(tokenResponse.getToken());
     }
 
     public void loginManual() throws Exception {
-        if (checkToken()) return;
+        if (checkToken(true)) return;
         AdapterConfig config = getConfig();
         KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
         KeycloakInstalled installed = new KeycloakInstalled(deployment);
         installed.loginManual();
-        processResponse(installed);
+        processResponse(installed, true);
     }
 
     public void logout() throws Exception {
diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
index a060685..f1fee42 100644
--- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
+++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
@@ -17,6 +17,8 @@
 
 package org.keycloak.adapters.installed;
 
+import org.jboss.resteasy.client.jaxrs.ResteasyClient;
+import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.OAuthErrorException;
 import org.keycloak.adapters.KeycloakDeployment;
@@ -31,6 +33,11 @@ import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.IDToken;
 
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Form;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
 import java.awt.*;
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -39,6 +46,7 @@ import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.io.PrintStream;
 import java.io.PrintWriter;
+import java.io.PushbackInputStream;
 import java.io.Reader;
 import java.net.ServerSocket;
 import java.net.Socket;
@@ -47,6 +55,8 @@ import java.net.URISyntaxException;
 import java.util.Locale;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -76,6 +86,9 @@ public class KeycloakInstalled {
     private Locale locale;
     private HttpResponseWriter loginResponseWriter;
     private HttpResponseWriter logoutResponseWriter;
+    Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\"");
+    Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"\\s+mask=(\\S+)");
+    Pattern codePattern = Pattern.compile("code=([^&]+)");
 
 
 
@@ -289,6 +302,86 @@ public class KeycloakInstalled {
         status = Status.LOGGED_MANUAL;
     }
 
+    public boolean loginCommandLine() throws IOException, ServerRequest.HttpFailure, VerificationException {
+        String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
+
+        return loginCommandLine(redirectUri);
+    }
+
+
+
+    /**
+     * Experimental proprietary WWW-Authentication challenge protocol.
+     * WWW-Authentication: X-Text-Form-Challenge callback="{url}" param="{param-name}" label="{param-display-label}"
+     *
+     * @param redirectUri
+     * @return
+     * @throws IOException
+     * @throws ServerRequest.HttpFailure
+     * @throws VerificationException
+     */
+    public boolean loginCommandLine(String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
+        String authUrl = deployment.getAuthUrl().clone()
+                .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
+                .queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
+                .queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
+                .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
+                .build().toString();
+        ResteasyClient client = new ResteasyClientBuilder().disableTrustManager().build();
+        try {
+            Response response = client.target(authUrl).request().get();
+            if (response.getStatus() != 401) {
+                return false;
+            }
+            while (true) {
+                String authenticationHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
+                if (authenticationHeader == null) {
+                    return false;
+                }
+                if (!authenticationHeader.contains("X-Text-Form-Challenge")) {
+                    return false;
+                }
+                if (response.getMediaType() != null) {
+                    String splash = response.readEntity(String.class);
+                    System.console().writer().println(splash);
+                }
+                Matcher m = callbackPattern.matcher(authenticationHeader);
+                if (!m.find()) return false;
+                String callback = m.group(1);
+                //System.err.println("callback: " + callback);
+                m = paramPattern.matcher(authenticationHeader);
+                Form form = new Form();
+                while (m.find()) {
+                    String param = m.group(1);
+                    String label = m.group(2);
+                    String mask = m.group(3).trim();
+                    boolean maskInput = mask.equals("true");
+                    String value = null;
+                    if (maskInput) {
+                        char[] txt = System.console().readPassword(label);
+                        value = new String(txt);
+                    } else {
+                        value = System.console().readLine(label);
+                    }
+                    form.param(param, value);
+                }
+                response = client.target(callback).request().post(Entity.form(form));
+                if (response.getStatus() == 401) continue;
+                if (response.getStatus() != 302) return false;
+                String location = response.getLocation().toString();
+                m = codePattern.matcher(location);
+                if (!m.find()) return false;
+                String code = m.group(1);
+                processCode(code, redirectUri);
+                return true;
+            }
+        } finally {
+            client.close();
+
+        }
+    }
+
+
     public String getTokenString() throws VerificationException, IOException, ServerRequest.HttpFailure {
         return tokenString;
     }
@@ -381,6 +474,86 @@ public class KeycloakInstalled {
         return sb.toString();
     }
 
+    public static class MaskingThread extends Thread {
+        private volatile boolean stop;
+        private char echochar = '*';
+
+        public MaskingThread() {
+        }
+
+        /**
+         * Begin masking until asked to stop.
+         */
+        public void run() {
+
+            int priority = Thread.currentThread().getPriority();
+            Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
+
+            try {
+                stop = true;
+                while(stop) {
+                    System.out.print("\010" + echochar);
+                    try {
+                        // attempt masking at this rate
+                        Thread.currentThread().sleep(1);
+                    }catch (InterruptedException iex) {
+                        Thread.currentThread().interrupt();
+                        return;
+                    }
+                }
+            } finally { // restore the original priority
+                Thread.currentThread().setPriority(priority);
+            }
+        }
+
+        /**
+         * Instruct the thread to stop masking.
+         */
+        public void stopMasking() {
+            this.stop = false;
+        }
+    }
+
+    public static String readMasked(Reader reader) {
+        MaskingThread et = new MaskingThread();
+        Thread mask = new Thread(et);
+        mask.start();
+
+        BufferedReader in = new BufferedReader(reader);
+        String password = "";
+
+        try {
+            password = in.readLine();
+        } catch (IOException ioe) {
+            ioe.printStackTrace();
+        }
+        // stop masking
+        et.stopMasking();
+        // return the password entered by the user
+        return password;
+    }
+
+    private String readLine(Reader reader, boolean mask) throws IOException {
+        if (mask) {
+            System.out.print(" ");
+            return readMasked(reader);
+        }
+
+        StringBuilder sb = new StringBuilder();
+
+        char cb[] = new char[1];
+        while (reader.read(cb) != -1) {
+            char c = cb[0];
+            if ((c == '\n') || (c == '\r')) {
+                break;
+            } else {
+                sb.append(c);
+            }
+        }
+
+        return sb.toString();
+    }
+
 
     public class CallbackListener extends Thread {
 
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
index 2f5576e..f9b49c3 100755
--- a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
@@ -80,6 +80,15 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
     URI getActionUrl(String code);
 
     /**
+     * Get the action URL for the required action.
+     *
+     * @param code authentication session access code
+     * @param authSessionIdParam will include auth_session query param for clients that don't process cookies
+     * @return
+     */
+    URI getActionUrl(String code, boolean authSessionIdParam);
+
+    /**
      * Get the action URL for the action token executor.
      *
      * @param tokenString String representation (JWT) of action token
@@ -95,6 +104,14 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
     URI getRefreshExecutionUrl();
 
     /**
+     * Get the refresh URL for the flow.
+     *
+     * @param authSessionIdParam will include auth_session query param for clients that don't process cookies
+     * @return
+     */
+    URI getRefreshUrl(boolean authSessionIdParam);
+
+    /**
      * End the flow and redirect browser based on protocol specific respones.  This should only be executed
      * in browser-based flows.
      *
diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
index af20f5b..256b87f 100755
--- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
@@ -52,6 +52,8 @@ public interface LoginFormsProvider extends Provider {
 
     Response createForm(String form);
 
+    String getMessage(String message);
+
     Response createLogin();
 
     Response createPasswordReset();
@@ -122,6 +124,8 @@ public interface LoginFormsProvider extends Provider {
 
     LoginFormsProvider setStatus(Response.Status status);
 
+    LoginFormsProvider setMediaType(javax.ws.rs.core.MediaType type);
+
     LoginFormsProvider setActionUri(URI requestUri);
 
     LoginFormsProvider setExecution(String execution);
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
index cb2ce3b..f5b8464 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
@@ -46,7 +46,7 @@ public class ActionTokenContext<T extends JsonWebToken> {
 
     @FunctionalInterface
     public interface ProcessBrokerFlow {
-        Response brokerLoginFlow(String code, String execution, String clientId, String tabId, String flowPath);
+        Response brokerLoginFlow(String authSessionId, String code, String execution, String clientId, String tabId, String flowPath);
     };
 
     private final KeycloakSession session;
@@ -160,8 +160,8 @@ public class ActionTokenContext<T extends JsonWebToken> {
         return processAuthenticateFlow.processFlow(action, getExecutionId(), getAuthenticationSession(), flowPath, flow, errorMessage, processor);
     }
 
-    public Response brokerFlow(String code, String flowPath) {
+    public Response brokerFlow(String authSessionId, String code, String flowPath) {
         ClientModel client = authenticationSession.getClient();
-        return processBrokerFlow.brokerLoginFlow(code, getExecutionId(), client.getClientId(), authenticationSession.getTabId(), flowPath);
+        return processBrokerFlow.brokerLoginFlow(authSessionId, code, getExecutionId(), client.getClientId(), authenticationSession.getTabId(), flowPath);
     }
 }
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
index ebd4700..306dd05 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
@@ -122,7 +122,7 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
 
         authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
 
-        return tokenContext.brokerFlow(null, authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH));
+        return tokenContext.brokerFlow(null, null, authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH));
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index 4464ff7..537581a 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -60,6 +60,7 @@ import org.keycloak.sessions.CommonClientSessionModel;
 import org.keycloak.sessions.RootAuthenticationSessionModel;
 
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
 import java.net.URI;
 import java.util.HashMap;
@@ -487,32 +488,72 @@ public class AuthenticationProcessor {
 
         @Override
         public URI getActionUrl(String code) {
-            return LoginActionsService.loginActionsBaseUrl(getUriInfo())
+            UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
                     .path(AuthenticationProcessor.this.flowPath)
-                    .queryParam(OAuth2Constants.CODE, code)
+                    .queryParam(LoginActionsService.SESSION_CODE, code)
                     .queryParam(Constants.EXECUTION, getExecution().getId())
                     .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
-                    .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
+                    .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
+            if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
+                uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
+            }
+            return uriBuilder
                     .build(getRealm().getName());
         }
 
         @Override
         public URI getActionTokenUrl(String tokenString) {
-            return LoginActionsService.actionTokenProcessor(getUriInfo())
+            UriBuilder uriBuilder = LoginActionsService.actionTokenProcessor(getUriInfo())
                     .queryParam(Constants.KEY, tokenString)
                     .queryParam(Constants.EXECUTION, getExecution().getId())
                     .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
-                    .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
+                    .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
+            if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
+                uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
+            }
+            return uriBuilder
+                    .build(getRealm().getName());
+        }
+
+        @Override
+        public URI getActionUrl(String code, boolean authSessionIdParam) {
+            UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
+                    .path(AuthenticationProcessor.this.flowPath)
+                    .queryParam(LoginActionsService.SESSION_CODE, code)
+                    .queryParam(Constants.EXECUTION, getExecution().getId())
+                    .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
+                    .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
+            if (authSessionIdParam) {
+                uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
+            }
+            return uriBuilder
                     .build(getRealm().getName());
         }
 
         @Override
         public URI getRefreshExecutionUrl() {
-            return LoginActionsService.loginActionsBaseUrl(getUriInfo())
+            UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
                     .path(AuthenticationProcessor.this.flowPath)
                     .queryParam(Constants.EXECUTION, getExecution().getId())
                     .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
-                    .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
+                    .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
+            if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
+                uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
+            }
+            return uriBuilder
+                    .build(getRealm().getName());
+        }
+
+        @Override
+        public URI getRefreshUrl(boolean authSessionIdParam) {
+            UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
+                    .path(AuthenticationProcessor.this.flowPath)
+                    .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
+                    .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
+            if (authSessionIdParam) {
+                uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
+            }
+            return uriBuilder
                     .build(getRealm().getName());
         }
 
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/cli/CliUsernamePasswordAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/cli/CliUsernamePasswordAuthenticator.java
new file mode 100755
index 0000000..0989d94
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/cli/CliUsernamePasswordAuthenticator.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authentication.authenticators.cli;
+
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.messages.Messages;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class CliUsernamePasswordAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {
+
+    @Override
+    public boolean requiresUser() {
+        return false;
+    }
+
+    @Override
+    public void authenticate(AuthenticationFlowContext context) {
+        String header = getHeader(context);
+        Response response  = context.form()
+                .setStatus(Response.Status.UNAUTHORIZED)
+                .setMediaType(MediaType.TEXT_PLAIN_TYPE)
+                .setResponseHeader(HttpHeaders.WWW_AUTHENTICATE, header)
+                .createForm("cli_splash.ftl");
+        context.challenge(response);
+
+
+    }
+
+    private String getHeader(AuthenticationFlowContext context) {
+        URI callback = getCallbackUrl(context);
+        return "X-Text-Form-Challenge callback=\"" + callback + "\" param=\"username\" label=\"Username: \" mask=false param=\"password\" label=\"Password: \" mask=true";
+    }
+
+    private URI getCallbackUrl(AuthenticationFlowContext context) {
+        return context.getActionUrl(context.generateAccessCode(), true);
+    }
+
+    @Override
+    protected Response invalidUser(AuthenticationFlowContext context) {
+        String header = getHeader(context);
+        Response response  = Response.status(401)
+                .type(MediaType.TEXT_PLAIN_TYPE)
+                .header(HttpHeaders.WWW_AUTHENTICATE, header)
+                .entity("\n" + context.form().getMessage(Messages.INVALID_USER) + "\n")
+                .build();
+        return response;
+    }
+
+    @Override
+    protected Response disabledUser(AuthenticationFlowContext context) {
+        String header = getHeader(context);
+        Response response  = Response.status(401)
+                .type(MediaType.TEXT_PLAIN_TYPE)
+                .header(HttpHeaders.WWW_AUTHENTICATE, header)
+                .entity("\n" + context.form().getMessage(Messages.ACCOUNT_DISABLED) + "\n")
+                .build();
+        return response;
+    }
+
+    @Override
+    protected Response temporarilyDisabledUser(AuthenticationFlowContext context) {
+        String header = getHeader(context);
+        Response response  = Response.status(401)
+                .type(MediaType.TEXT_PLAIN_TYPE)
+                .header(HttpHeaders.WWW_AUTHENTICATE, header)
+                .entity("\n" + context.form().getMessage(Messages.INVALID_USER) + "\n")
+                .build();
+        return response;
+    }
+
+    @Override
+    protected Response invalidCredentials(AuthenticationFlowContext context) {
+        String header = getHeader(context);
+        Response response  = Response.status(401)
+                .type(MediaType.TEXT_PLAIN_TYPE)
+                .header(HttpHeaders.WWW_AUTHENTICATE, header)
+                .entity("\n" + context.form().getMessage(Messages.INVALID_USER) + "\n")
+                .build();
+        return response;
+    }
+
+    @Override
+    protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
+        context.getEvent().error(eventError);
+        String header = getHeader(context);
+        Response challengeResponse  = Response.status(401)
+                .type(MediaType.TEXT_PLAIN_TYPE)
+                .header(HttpHeaders.WWW_AUTHENTICATE, header)
+                .entity("\n" + context.form().getMessage(loginFormError) + "\n")
+                .build();
+
+        context.failureChallenge(authenticatorError, challengeResponse);
+        return challengeResponse;
+    }
+
+    @Override
+    public void action(AuthenticationFlowContext context) {
+        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+        if (!validateUserAndPassword(context, formData)) {
+            return;
+        }
+
+        context.success();
+    }
+
+    @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/cli/CliUsernamePasswordAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/cli/CliUsernamePasswordAuthenticatorFactory.java
new file mode 100755
index 0000000..cce13c0
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/cli/CliUsernamePasswordAuthenticatorFactory.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authentication.authenticators.cli;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.authenticators.browser.UsernamePasswordForm;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class CliUsernamePasswordAuthenticatorFactory implements AuthenticatorFactory {
+
+    public static final String PROVIDER_ID = "cli-username-password";
+    public static final CliUsernamePasswordAuthenticator SINGLETON = new CliUsernamePasswordAuthenticator();
+
+    @Override
+    public Authenticator create(KeycloakSession session) {
+        return SINGLETON;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public String getReferenceCategory() {
+        return UserCredentialModel.PASSWORD;
+    }
+
+    @Override
+    public boolean isConfigurable() {
+        return false;
+    }
+    public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+            AuthenticationExecutionModel.Requirement.REQUIRED
+    };
+
+    @Override
+    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+        return REQUIREMENT_CHOICES;
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Username Password Challenge";
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Proprietary challenge protocol for CLI clients that queries for username password";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return null;
+    }
+
+    @Override
+    public boolean isUserSetupAllowed() {
+        return false;
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
index 19fb6b4..f1cdcda 100755
--- a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
@@ -266,7 +266,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
     public URI getActionUrl(String executionId, String code) {
         ClientModel client = processor.getAuthenticationSession().getClient();
         return LoginActionsService.registrationFormProcessor(processor.getUriInfo())
-                .queryParam(OAuth2Constants.CODE, code)
+                .queryParam(LoginActionsService.SESSION_CODE, code)
                 .queryParam(Constants.EXECUTION, executionId)
                 .queryParam(Constants.CLIENT_ID, client.getClientId())
                 .queryParam(Constants.TAB_ID, processor.getAuthenticationSession().getTabId())
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
index 4d3b65e..5e9a546 100755
--- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
@@ -136,7 +136,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
     public URI getActionUrl(String code) {
         ClientModel client = authenticationSession.getClient();
         return LoginActionsService.requiredActionProcessor(getUriInfo())
-                .queryParam(OAuth2Constants.CODE, code)
+                .queryParam(LoginActionsService.SESSION_CODE, code)
                 .queryParam(Constants.EXECUTION, getExecution())
                 .queryParam(Constants.CLIENT_ID, client.getClientId())
                 .queryParam(Constants.TAB_ID, authenticationSession.getTabId())
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
index d78b517..cf480fc 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -39,6 +39,7 @@ import org.keycloak.models.*;
 import org.keycloak.models.utils.FormMessage;
 import org.keycloak.services.Urls;
 import org.keycloak.services.messages.Messages;
+import org.keycloak.services.resources.LoginActionsService;
 import org.keycloak.sessions.AuthenticationSessionModel;
 import org.keycloak.theme.BrowserSecurityHeaderSetup;
 import org.keycloak.theme.FreeMarkerException;
@@ -73,6 +74,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
 
     protected String accessCode;
     protected Response.Status status;
+    protected javax.ws.rs.core.MediaType contentType;
     protected List<RoleModel> realmRolesRequested;
     protected MultivaluedMap<String, RoleModel> resourceRolesRequested;
     protected List<ProtocolMapperModel> protocolMappersRequested;
@@ -315,6 +317,23 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
         }
         attributes.put("messagesPerField", messagesPerField);
     }
+
+    @Override
+    public String getMessage(String message) {
+        Theme theme;
+        try {
+            theme = getTheme();
+        } catch (IOException e) {
+            logger.error("Failed to create theme", e);
+            throw new RuntimeException("Failed to create theme");
+        }
+
+        Locale locale = session.getContext().resolveLocale(user);
+        Properties messagesBundle = handleThemeResources(theme, locale);
+        FormMessage msg = new FormMessage(null, message);
+        return formatMessage(msg, messagesBundle, locale);
+
+    }
     
     /**
      * Create common attributes used in all templates.
@@ -329,7 +348,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
     protected void createCommonAttributes(Theme theme, Locale locale, Properties messagesBundle, UriBuilder baseUriBuilder, LoginFormsPages page) {
         URI baseUri = baseUriBuilder.build();
         if (accessCode != null) {
-            baseUriBuilder.queryParam(OAuth2Constants.CODE, accessCode);
+            baseUriBuilder.queryParam(LoginActionsService.SESSION_CODE, accessCode);
         }
         URI baseUriWithCodeAndClientId = baseUriBuilder.build();
 
@@ -389,7 +408,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
     protected Response processTemplate(Theme theme, String templateName, Locale locale) {
         try {
             String result = freeMarker.processTemplate(attributes, templateName, theme);
-            Response.ResponseBuilder builder = Response.status(status == null ? Response.Status.OK : status).type(MediaType.TEXT_HTML_UTF_8_TYPE).language(locale).entity(result);
+            javax.ws.rs.core.MediaType mediaType = contentType == null ? MediaType.TEXT_HTML_UTF_8_TYPE : contentType;
+            Response.ResponseBuilder builder = Response.status(status == null ? Response.Status.OK : status).type(mediaType).language(locale).entity(result);
             BrowserSecurityHeaderSetup.headers(builder, realm);
             for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) {
                 builder.header(entry.getKey(), entry.getValue());
@@ -602,6 +622,14 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
         this.status = status;
         return this;
     }
+    @Override
+    public LoginFormsProvider setMediaType(javax.ws.rs.core.MediaType type) {
+        this.contentType = type;
+        return this;
+    }
+
+
+
 
     @Override
     public LoginFormsProvider setActionUri(URI actionUri) {
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java
index 0a7936d..83b23cf 100644
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java
@@ -106,7 +106,7 @@ public class AuthenticationSessionManager {
     }
 
 
-    private String getAuthSessionCookieDecoded(RealmModel realm) {
+    public String getAuthSessionCookieDecoded(RealmModel realm) {
         String cookieVal = CookieHelper.getCookieValue(AUTH_SESSION_ID);
 
         if (cookieVal != null) {
diff --git a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
index 7a8479c..46a4508 100755
--- a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
+++ b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
@@ -91,29 +91,51 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel> 
         try {
             CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser(sessionClass);
             result.clientSession = getClientSession(code, tabId, session, realm, client, event, clientSessionParser);
-            if (result.clientSession == null) {
-                result.authSessionNotFound = true;
-                return result;
-            }
-
-            if (!clientSessionParser.verifyCode(session, code, result.clientSession)) {
-                result.illegalHash = true;
-                return result;
-            }
-
-            if (clientSessionParser.isExpired(session, code, result.clientSession)) {
-                result.expiredToken = true;
-                return result;
-            }
+            return parseResult(code, session, realm, result, clientSessionParser);
+        } catch (RuntimeException e) {
+            result.illegalHash = true;
+            return result;
+        }
+    }
 
-            result.code = new ClientSessionCode<CLIENT_SESSION>(session, realm, result.clientSession);
+    public static <CLIENT_SESSION extends CommonClientSessionModel> ParseResult<CLIENT_SESSION> parseResult(String code, String tabId,
+                                                                                                            KeycloakSession session, RealmModel realm, ClientModel client,
+                                                                                                            EventBuilder event, CLIENT_SESSION clientSession) {
+        ParseResult<CLIENT_SESSION> result = new ParseResult<>();
+        result.clientSession = clientSession;
+        if (code == null) {
+            result.illegalHash = true;
             return result;
+        }
+        try {
+            CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser((Class<CLIENT_SESSION>)clientSession.getClass());
+            return parseResult(code, session, realm, result, clientSessionParser);
         } catch (RuntimeException e) {
             result.illegalHash = true;
             return result;
         }
     }
 
+    private static <CLIENT_SESSION extends CommonClientSessionModel> ParseResult<CLIENT_SESSION> parseResult(String code, KeycloakSession session, RealmModel realm, ParseResult<CLIENT_SESSION> result, CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser) {
+        if (result.clientSession == null) {
+            result.authSessionNotFound = true;
+            return result;
+        }
+
+        if (!clientSessionParser.verifyCode(session, code, result.clientSession)) {
+            result.illegalHash = true;
+            return result;
+        }
+
+        if (clientSessionParser.isExpired(session, code, result.clientSession)) {
+            result.expiredToken = true;
+            return result;
+        }
+
+        result.code = new ClientSessionCode<CLIENT_SESSION>(session, realm, result.clientSession);
+        return result;
+    }
+
 
     public static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, String tabId, KeycloakSession session, RealmModel realm, ClientModel client,
                                                                                                     EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index c8e3fda..07db430 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -341,7 +341,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
     @POST
     @Path("/{provider_id}/login")
     public Response performPostLogin(@PathParam("provider_id") String providerId,
-                                     @QueryParam("code") String code,
+                                     @QueryParam(LoginActionsService.SESSION_CODE) String code,
                                      @QueryParam("client_id") String clientId,
                                      @QueryParam(Constants.TAB_ID) String tabId) {
         return performLogin(providerId, code, clientId, tabId);
@@ -351,7 +351,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
     @NoCache
     @Path("/{provider_id}/login")
     public Response performLogin(@PathParam("provider_id") String providerId,
-                                 @QueryParam("code") String code,
+                                 @QueryParam(LoginActionsService.SESSION_CODE) String code,
                                  @QueryParam("client_id") String clientId,
                                  @QueryParam(Constants.TAB_ID) String tabId) {
         this.event.detail(Details.IDENTITY_PROVIDER, providerId);
@@ -594,7 +594,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
     @GET
     @NoCache
     @Path("/after-first-broker-login")
-    public Response afterFirstBrokerLogin(@QueryParam("code") String code,
+    public Response afterFirstBrokerLogin(@QueryParam(LoginActionsService.SESSION_CODE) String code,
                                           @QueryParam("client_id") String clientId,
                                           @QueryParam(Constants.TAB_ID) String tabId) {
         ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
@@ -725,7 +725,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
     @GET
     @NoCache
     @Path("/after-post-broker-login")
-    public Response afterPostBrokerLoginFlow(@QueryParam("code") String code,
+    public Response afterPostBrokerLoginFlow(@QueryParam(LoginActionsService.SESSION_CODE) String code,
                                              @QueryParam("client_id") String clientId,
                                              @QueryParam(Constants.TAB_ID) String tabId) {
         ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
@@ -991,7 +991,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
             return ParsedCodeContext.response(staleCodeError);
         }
 
-        SessionCodeChecks checks = new SessionCodeChecks(realmModel, uriInfo, request, clientConnection, session, event, code, null, clientId, tabId, LoginActionsService.AUTHENTICATE_PATH);
+        SessionCodeChecks checks = new SessionCodeChecks(realmModel, uriInfo, request, clientConnection, session, event, null, code, null, clientId, tabId, LoginActionsService.AUTHENTICATE_PATH);
         checks.initialVerify();
         if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
 
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 fd171ee..7c38ba8 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -115,6 +115,9 @@ public class LoginActionsService {
 
     public static final String FORWARDED_ERROR_MESSAGE_NOTE = "forwardedErrorMessage";
 
+    public static final String SESSION_CODE = "session_code";
+    public static final String AUTH_SESSION_ID = "auth_session_id";
+
     private RealmModel realm;
 
     @Context
@@ -184,8 +187,8 @@ public class LoginActionsService {
         }
     }
 
-    private SessionCodeChecks checksForCode(String code, String execution, String clientId, String tabId, String flowPath) {
-        SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, code, execution, clientId, tabId, flowPath);
+    private SessionCodeChecks checksForCode(String authSessionId, String code, String execution, String clientId, String tabId, String flowPath) {
+        SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, authSessionId, code, execution, clientId, tabId, flowPath);
         res.initialVerify();
         return res;
     }
@@ -204,10 +207,11 @@ public class LoginActionsService {
      */
     @Path(RESTART_PATH)
     @GET
-    public Response restartSession(@QueryParam("client_id") String clientId,
+    public Response restartSession(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+                                   @QueryParam("client_id") String clientId,
                                    @QueryParam(Constants.TAB_ID) String tabId) {
         event.event(EventType.RESTART_AUTHENTICATION);
-        SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, null, null, clientId,  tabId, null);
+        SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, authSessionId, null, null, clientId,  tabId, null);
 
         AuthenticationSessionModel authSession = checks.initialVerifyAuthSession();
         if (authSession == null) {
@@ -235,13 +239,14 @@ public class LoginActionsService {
      */
     @Path(AUTHENTICATE_PATH)
     @GET
-    public Response authenticate(@QueryParam("code") String code,
+    public Response authenticate(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+                                 @QueryParam(SESSION_CODE) String code,
                                  @QueryParam("execution") String execution,
                                  @QueryParam("client_id") String clientId,
                                  @QueryParam(Constants.TAB_ID) String tabId) {
         event.event(EventType.LOGIN);
 
-        SessionCodeChecks checks = checksForCode(code, execution, clientId, tabId, AUTHENTICATE_PATH);
+        SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, AUTHENTICATE_PATH);
         if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
             return checks.getResponse();
         }
@@ -305,27 +310,29 @@ public class LoginActionsService {
      */
     @Path(AUTHENTICATE_PATH)
     @POST
-    public Response authenticateForm(@QueryParam("code") String code,
+    public Response authenticateForm(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+                                     @QueryParam(SESSION_CODE) String code,
                                      @QueryParam("execution") String execution,
                                      @QueryParam("client_id") String clientId,
                                      @QueryParam(Constants.TAB_ID) String tabId) {
-        return authenticate(code, execution, clientId, tabId);
+        return authenticate(authSessionId, code, execution, clientId, tabId);
     }
 
     @Path(RESET_CREDENTIALS_PATH)
     @POST
-    public Response resetCredentialsPOST(@QueryParam("code") String code,
+    public Response resetCredentialsPOST(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+                                         @QueryParam(SESSION_CODE) String code,
                                          @QueryParam("execution") String execution,
                                          @QueryParam("client_id") String clientId,
                                          @QueryParam(Constants.TAB_ID) String tabId,
                                          @QueryParam(Constants.KEY) String key) {
         if (key != null) {
-            return handleActionToken(key, execution, clientId, tabId);
+            return handleActionToken(authSessionId, key, execution, clientId, tabId);
         }
 
         event.event(EventType.RESET_PASSWORD);
 
-        return resetCredentials(code, execution, clientId, tabId);
+        return resetCredentials(authSessionId, code, execution, clientId, tabId);
     }
 
     /**
@@ -338,7 +345,8 @@ public class LoginActionsService {
      */
     @Path(RESET_CREDENTIALS_PATH)
     @GET
-    public Response resetCredentialsGET(@QueryParam("code") String code,
+    public Response resetCredentialsGET(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+                                        @QueryParam(SESSION_CODE) String code,
                                         @QueryParam("execution") String execution,
                                         @QueryParam("client_id") String clientId,
                                         @QueryParam(Constants.TAB_ID) String tabId) {
@@ -358,7 +366,7 @@ public class LoginActionsService {
         }
 
         event.event(EventType.RESET_PASSWORD);
-        return resetCredentials(code, execution, clientId, tabId);
+        return resetCredentials(authSessionId, code, execution, clientId, tabId);
     }
 
     AuthenticationSessionModel createAuthenticationSessionForClient()
@@ -388,8 +396,8 @@ public class LoginActionsService {
      * @param execution
      * @return
      */
-    protected Response resetCredentials(String code, String execution, String clientId, String tabId) {
-        SessionCodeChecks checks = checksForCode(code, execution, clientId, tabId, RESET_CREDENTIALS_PATH);
+    protected Response resetCredentials(String authSessionId, String code, String execution, String clientId, String tabId) {
+        SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, RESET_CREDENTIALS_PATH);
         if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
             return checks.getResponse();
         }
@@ -414,14 +422,15 @@ public class LoginActionsService {
      */
     @Path("action-token")
     @GET
-    public Response executeActionToken(@QueryParam("key") String key,
+    public Response executeActionToken(@QueryParam(AUTH_SESSION_ID) String authSessionId,
+                                       @QueryParam("key") String key,
                                        @QueryParam("execution") String execution,
                                        @QueryParam("client_id") String clientId,
                                        @QueryParam(Constants.TAB_ID) String tabId) {
-        return handleActionToken(key, execution, clientId, tabId);
+        return handleActionToken(authSessionId, key, execution, clientId, tabId);
     }
 
-    protected <T extends JsonWebToken & ActionTokenKeyModel> Response handleActionToken(String tokenString, String execution, String clientId, String tabId) {
+    protected <T extends JsonWebToken & ActionTokenKeyModel> Response handleActionToken(String authSessionId, String tokenString, String execution, String clientId, String tabId) {
         T token;
         ActionTokenHandler<T> handler;
         ActionTokenContext<T> tokenContext;
@@ -435,9 +444,11 @@ public class LoginActionsService {
         if (clientId != null) {
             client = realm.getClientByClientId(clientId);
         }
+        AuthenticationSessionManager authenticationSessionManager = new AuthenticationSessionManager(session);
         if (client != null) {
             session.getContext().setClient(client);
-            authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client, tabId);
+            authSessionId = authSessionId == null ? authenticationSessionManager.getAuthSessionCookieDecoded(realm) : authSessionId;
+            authSession = authenticationSessionManager.getCurrentAuthenticationSession(realm, client, tabId);
         }
 
         event.event(EventType.EXECUTE_ACTION_TOKEN);
@@ -520,7 +531,7 @@ public class LoginActionsService {
               ! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, authSession, tokenAuthSessionCompoundId)) {
                 // There exists an authentication session but no auth session ID was received in the action token
                 logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId());
-                new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false);
+                authenticationSessionManager.removeAuthenticationSession(realm, authSession, false);
 
                 authSession = handler.startFreshAuthenticationSession(token, tokenContext);
                 tokenContext.setAuthenticationSession(authSession, true);
@@ -617,11 +628,12 @@ public class LoginActionsService {
      */
     @Path(REGISTRATION_PATH)
     @GET
-    public Response registerPage(@QueryParam("code") String code,
+    public Response registerPage(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+                                 @QueryParam(SESSION_CODE) String code,
                                  @QueryParam("execution") String execution,
                                  @QueryParam("client_id") String clientId,
                                  @QueryParam(Constants.TAB_ID) String tabId) {
-        return registerRequest(code, execution, clientId,  tabId,false);
+        return registerRequest(authSessionId, code, execution, clientId,  tabId,false);
     }
 
 
@@ -633,22 +645,23 @@ public class LoginActionsService {
      */
     @Path(REGISTRATION_PATH)
     @POST
-    public Response processRegister(@QueryParam("code") String code,
+    public Response processRegister(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+                                    @QueryParam(SESSION_CODE) String code,
                                     @QueryParam("execution") String execution,
                                     @QueryParam("client_id") String clientId,
                                     @QueryParam(Constants.TAB_ID) String tabId) {
-        return registerRequest(code, execution, clientId,  tabId,true);
+        return registerRequest(authSessionId, code, execution, clientId,  tabId,true);
     }
 
 
-    private Response registerRequest(String code, String execution, String clientId, String tabId, boolean isPostRequest) {
+    private Response registerRequest(String authSessionId, String code, String execution, String clientId, String tabId, boolean isPostRequest) {
         event.event(EventType.REGISTER);
         if (!realm.isRegistrationAllowed()) {
             event.error(Errors.REGISTRATION_DISABLED);
             return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REGISTRATION_NOT_ALLOWED);
         }
 
-        SessionCodeChecks checks = checksForCode(code, execution, clientId, tabId, REGISTRATION_PATH);
+        SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, REGISTRATION_PATH);
         if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
             return checks.getResponse();
         }
@@ -663,48 +676,52 @@ public class LoginActionsService {
 
     @Path(FIRST_BROKER_LOGIN_PATH)
     @GET
-    public Response firstBrokerLoginGet(@QueryParam("code") String code,
+    public Response firstBrokerLoginGet(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+                                        @QueryParam(SESSION_CODE) String code,
                                         @QueryParam("execution") String execution,
                                         @QueryParam("client_id") String clientId,
                                         @QueryParam(Constants.TAB_ID) String tabId) {
-        return brokerLoginFlow(code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH);
+        return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH);
     }
 
     @Path(FIRST_BROKER_LOGIN_PATH)
     @POST
-    public Response firstBrokerLoginPost(@QueryParam("code") String code,
+    public Response firstBrokerLoginPost(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+                                         @QueryParam(SESSION_CODE) String code,
                                          @QueryParam("execution") String execution,
                                          @QueryParam("client_id") String clientId,
                                          @QueryParam(Constants.TAB_ID) String tabId) {
-        return brokerLoginFlow(code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH);
+        return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH);
     }
 
     @Path(POST_BROKER_LOGIN_PATH)
     @GET
-    public Response postBrokerLoginGet(@QueryParam("code") String code,
+    public Response postBrokerLoginGet(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+                                       @QueryParam(SESSION_CODE) String code,
                                        @QueryParam("execution") String execution,
                                        @QueryParam("client_id") String clientId,
                                        @QueryParam(Constants.TAB_ID) String tabId) {
-        return brokerLoginFlow(code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH);
+        return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH);
     }
 
     @Path(POST_BROKER_LOGIN_PATH)
     @POST
-    public Response postBrokerLoginPost(@QueryParam("code") String code,
+    public Response postBrokerLoginPost(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+                                        @QueryParam(SESSION_CODE) String code,
                                         @QueryParam("execution") String execution,
                                         @QueryParam("client_id") String clientId,
                                         @QueryParam(Constants.TAB_ID) String tabId) {
-        return brokerLoginFlow(code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH);
+        return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH);
     }
 
 
-    protected Response brokerLoginFlow(String code, String execution, String clientId, String tabId, String flowPath) {
+    protected Response brokerLoginFlow(String authSessionId, String code, String execution, String clientId, String tabId, String flowPath) {
         boolean firstBrokerLogin = flowPath.equals(FIRST_BROKER_LOGIN_PATH);
 
         EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
         event.event(eventType);
 
-        SessionCodeChecks checks = checksForCode(code, execution, clientId, tabId, flowPath);
+        SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, flowPath);
         if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
             return checks.getResponse();
         }
@@ -783,10 +800,10 @@ public class LoginActionsService {
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
     public Response processConsent(final MultivaluedMap<String, String> formData) {
         event.event(EventType.LOGIN);
-        String code = formData.getFirst("code");
+        String code = formData.getFirst(SESSION_CODE);
         String clientId = uriInfo.getQueryParameters().getFirst(Constants.CLIENT_ID);
         String tabId = uriInfo.getQueryParameters().getFirst(Constants.TAB_ID);
-        SessionCodeChecks checks = checksForCode(code, null, clientId, tabId, REQUIRED_ACTION);
+        SessionCodeChecks checks = checksForCode(null, code, null, clientId, tabId, REQUIRED_ACTION);
         if (!checks.verifyRequiredAction(AuthenticationSessionModel.Action.OAUTH_GRANT.name())) {
             return checks.getResponse();
         }
@@ -874,26 +891,28 @@ public class LoginActionsService {
 
     @Path(REQUIRED_ACTION)
     @POST
-    public Response requiredActionPOST(@QueryParam("code") final String code,
+    public Response requiredActionPOST(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+                                       @QueryParam(SESSION_CODE) final String code,
                                        @QueryParam("execution") String action,
                                        @QueryParam("client_id") String clientId,
                                        @QueryParam(Constants.TAB_ID) String tabId) {
-        return processRequireAction(code, action, clientId, tabId);
+        return processRequireAction(authSessionId, code, action, clientId, tabId);
     }
 
     @Path(REQUIRED_ACTION)
     @GET
-    public Response requiredActionGET(@QueryParam("code") final String code,
+    public Response requiredActionGET(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+                                      @QueryParam(SESSION_CODE) final String code,
                                       @QueryParam("execution") String action,
                                       @QueryParam("client_id") String clientId,
                                       @QueryParam(Constants.TAB_ID) String tabId) {
-        return processRequireAction(code, action, clientId, tabId);
+        return processRequireAction(authSessionId, code, action, clientId, tabId);
     }
 
-    private Response processRequireAction(final String code, String action, String clientId, String tabId) {
+    private Response processRequireAction(final String authSessionId, final String code, String action, String clientId, String tabId) {
         event.event(EventType.CUSTOM_REQUIRED_ACTION);
 
-        SessionCodeChecks checks = checksForCode(code, action, clientId, tabId, REQUIRED_ACTION);
+        SessionCodeChecks checks = checksForCode(authSessionId, code, action, clientId, tabId, REQUIRED_ACTION);
         if (!checks.verifyRequiredAction(action)) {
             return checks.getResponse();
         }
diff --git a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
index 2f04bf4..bb4097f 100644
--- a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
+++ b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
@@ -71,10 +71,11 @@ public class SessionCodeChecks {
     private final String clientId;
     private final String tabId;
     private final String flowPath;
+    private final String authSessionId;
 
 
     public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event,
-                             String code, String execution, String clientId, String tabId, String flowPath) {
+                             String authSessionId, String code, String execution, String clientId, String tabId, String flowPath) {
         this.realm = realm;
         this.uriInfo = uriInfo;
         this.request = request;
@@ -87,6 +88,7 @@ public class SessionCodeChecks {
         this.clientId = clientId;
         this.tabId = tabId;
         this.flowPath = flowPath;
+        this.authSessionId = authSessionId;
     }
 
 
@@ -146,14 +148,32 @@ public class SessionCodeChecks {
             session.getContext().setClient(client);
         }
 
+
         // object retrieve
         AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(session);
-        AuthenticationSessionModel authSession = authSessionManager.getCurrentAuthenticationSession(realm, client, tabId);
+        AuthenticationSessionModel authSession = null;
+        if (authSessionId != null) authSession = authSessionManager.getAuthenticationSessionByIdAndClient(realm, authSessionId, client, tabId);
+        AuthenticationSessionModel authSessionCookie = authSessionManager.getCurrentAuthenticationSession(realm, client, tabId);
+
+        if (authSession != null && authSessionCookie != null && !authSession.getParentSession().getId().equals(authSessionCookie.getParentSession().getId())) {
+            event.detail(Details.REASON, "cookie does not match auth_session query parameter");
+            event.error(Errors.INVALID_CODE);
+            response = ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_CODE);
+            return null;
+
+        }
+
         if (authSession != null) {
             session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession);
             return authSession;
         }
 
+        if (authSessionCookie != null) {
+            session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSessionCookie);
+            return authSessionCookie;
+
+        }
+
         // See if we are already authenticated and userSession with same ID exists.
         String sessionId = authSessionManager.getCurrentAuthenticationSessionId(realm);
         RootAuthenticationSessionModel existingRootAuthSession = null;
@@ -250,7 +270,7 @@ public class SessionCodeChecks {
                 return false;
             }
         } else {
-            ClientSessionCode.ParseResult<AuthenticationSessionModel> result = ClientSessionCode.parseResult(code, tabId, session, realm, client, event, AuthenticationSessionModel.class);
+            ClientSessionCode.ParseResult<AuthenticationSessionModel> result = ClientSessionCode.parseResult(code, tabId, session, realm, client, event, authSession);
             clientCode = result.getCode();
             if (clientCode == null) {
 
diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java
index 5530b60..c2d4929 100755
--- a/services/src/main/java/org/keycloak/services/Urls.java
+++ b/services/src/main/java/org/keycloak/services/Urls.java
@@ -79,7 +79,7 @@ public class Urls {
                 .path(IdentityBrokerService.class, "performLogin");
 
         if (accessCode != null) {
-            uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode);
+            uriBuilder.replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode);
         }
         if (clientId != null) {
             uriBuilder.replaceQueryParam(Constants.CLIENT_ID, clientId);
@@ -112,7 +112,7 @@ public class Urls {
     public static URI identityProviderAfterFirstBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId) {
         return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
                 .path(IdentityBrokerService.class, "afterFirstBrokerLogin")
-                .replaceQueryParam(OAuth2Constants.CODE, accessCode)
+                .replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode)
                 .replaceQueryParam(Constants.CLIENT_ID, clientId)
                 .replaceQueryParam(Constants.TAB_ID, tabId)
                 .build(realmName);
@@ -121,7 +121,7 @@ public class Urls {
     public static URI identityProviderAfterPostBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId) {
         return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
                 .path(IdentityBrokerService.class, "afterPostBrokerLoginFlow")
-                .replaceQueryParam(OAuth2Constants.CODE, accessCode)
+                .replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode)
                 .replaceQueryParam(Constants.CLIENT_ID, clientId)
                 .replaceQueryParam(Constants.TAB_ID, tabId)
                 .build(realmName);
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
index 2b11382..76b7507 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
@@ -38,3 +38,4 @@ org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFacto
 org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
 org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory
 org.keycloak.protocol.docker.DockerAuthenticatorFactory
+org.keycloak.authentication.authenticators.cli.CliUsernamePasswordAuthenticatorFactory
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
index 8c83294..2642a11 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
@@ -146,6 +146,8 @@ public class ProvidersTest extends AbstractAuthenticationTest {
                 "Validates a username and password from login form.");
         addProviderInfo(result, "auth-x509-client-username-form", "X509/Validate Username Form",
                 "Validates username and password from X509 client certificate received as a part of mutual SSL handshake.");
+        addProviderInfo(result, "cli-username-password", "Username Password Challenge",
+                "Proprietary challenge protocol for CLI clients that queries for username password");
         addProviderInfo(result, "direct-grant-auth-x509-username", "X509/Validate Username",
                 "Validates username and password from X509 client certificate received as a part of mutual SSL handshake.");
         addProviderInfo(result, "direct-grant-validate-otp", "OTP", "Validates the one time password supplied as a 'totp' form parameter in direct grant request");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ChallengeFlowTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ChallengeFlowTest.java
new file mode 100644
index 0000000..72024f6
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ChallengeFlowTest.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.forms;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
+import org.keycloak.authentication.authenticators.cli.CliUsernamePasswordAuthenticatorFactory;
+import org.keycloak.events.Details;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.AuthenticationFlowBindings;
+import org.keycloak.models.AuthenticationFlowModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.ErrorPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.util.BasicAuthHelper;
+import org.openqa.selenium.By;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Form;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
+import java.net.URL;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test that clients can override auth flows
+ *
+ * @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
+ */
+public class ChallengeFlowTest extends AbstractTestRealmKeycloakTest {
+
+    public static final String TEST_APP_DIRECT_OVERRIDE = "test-app-direct-override";
+    public static final String TEST_APP_FLOW = "test-app-flow";
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+
+    @Page
+    protected AppPage appPage;
+
+    @Page
+    protected LoginPage loginPage;
+
+    @Page
+    protected ErrorPage errorPage;
+
+    @Override
+    public void configureTestRealm(RealmRepresentation testRealm) {
+    }
+
+    @Deployment
+    public static WebArchive deploy() {
+        return RunOnServerDeployment.create(UserResource.class)
+                .addPackages(true, "org.keycloak.testsuite");
+    }
+
+
+    @Before
+    public void setupFlows() {
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+
+            ClientModel client = session.realms().getClientByClientId("test-app-flow", realm);
+            if (client != null) {
+                return;
+            }
+
+            // Parent flow
+            AuthenticationFlowModel browser = new AuthenticationFlowModel();
+            browser.setAlias("cli-challenge");
+            browser.setDescription("challenge based authentication");
+            browser.setProviderId("basic-flow");
+            browser.setTopLevel(true);
+            browser.setBuiltIn(true);
+            browser = realm.addAuthenticationFlow(browser);
+
+            // Subflow2 - push the button
+            AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
+            execution.setParentFlow(browser.getId());
+            execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+            execution.setAuthenticator(CliUsernamePasswordAuthenticatorFactory.PROVIDER_ID);
+            execution.setPriority(10);
+            execution.setAuthenticatorFlow(false);
+            realm.addAuthenticatorExecution(execution);
+
+            client = realm.addClient(TEST_APP_FLOW);
+            client.setSecret("password");
+            client.setBaseUrl("http://localhost:8180/auth/realms/master/app/auth");
+            client.setManagementUrl("http://localhost:8180/auth/realms/master/app/admin");
+            client.setEnabled(true);
+            client.addRedirectUri("http://localhost:8180/auth/realms/master/app/auth/*");
+            client.addRedirectUri("urn:ietf:wg:oauth:2.0:oob");
+            client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, browser.getId());
+            client.setPublicClient(false);
+        });
+    }
+
+    //@Test
+    public void testRunConsole() throws Exception {
+        Thread.sleep(10000000);
+    }
+
+
+    @Test
+    public void testChallengeFlow() throws Exception {
+        oauth.clientId(TEST_APP_FLOW);
+        String loginFormUrl = oauth.getLoginFormUrl();
+        Client client = ClientBuilder.newClient();
+        WebTarget loginTarget = client.target(loginFormUrl);
+        Response response = loginTarget.request().get();
+        Assert.assertEquals(401, response.getStatus());
+        String authenticateHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
+        Assert.assertNotNull(authenticateHeader);
+        //System.out.println(authenticateHeader);
+        String splash = response.readEntity(String.class);
+        //System.out.println(splash);
+        response.close();
+
+        // respin Client to make absolutely sure no cookie caching.  need to test that it works with null auth_session_id cookie.
+        client.close();
+        client = ClientBuilder.newClient();
+
+
+        authenticateHeader = authenticateHeader.trim();
+        Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\"");
+        Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"");
+        Matcher m = callbackPattern.matcher(authenticateHeader);
+        String callback = null;
+        if (m.find()) {
+            callback = m.group(1);
+            //System.out.println("------");
+            //System.out.println("callback:");
+            //System.out.println("    " + callback);
+        }
+        m = paramPattern.matcher(authenticateHeader);
+        List<String> params = new LinkedList<>();
+        List<String> labels = new LinkedList<>();
+        while (m.find()) {
+            String param = m.group(1);
+            String label = m.group(2);
+            params.add(param);
+            labels.add(label);
+            //System.out.println("------");
+            //System.out.println("param:" + param);
+            //System.out.println("label:" + label);
+        }
+        Assert.assertEquals("username", params.get(0));
+        Assert.assertEquals("Username:", labels.get(0).trim());
+        Assert.assertEquals("password", params.get(1));
+        Assert.assertEquals("Password:", labels.get(1).trim());
+
+        Form form = new Form();
+        form.param("username", "test-user@localhost");
+        form.param("password", "password");
+        response = client.target(callback)
+                .request()
+                .post(Entity.form(form));
+        Assert.assertEquals(302, response.getStatus());
+        String redirect = response.getHeaderString(HttpHeaders.LOCATION);
+        System.out.println("------");
+        System.out.println(redirect);
+        Pattern codePattern = Pattern.compile("code=([^&]+)");
+        m = codePattern.matcher(redirect);
+        Assert.assertTrue(m.find());
+        String code = m.group(1);
+        OAuthClient.AccessTokenResponse oauthResponse = oauth.doAccessTokenRequest(code, "password");
+        Assert.assertNotNull(oauthResponse.getAccessToken());
+        client.close();
+
+
+    }
+
+
+}
diff --git a/themes/src/main/resources/theme/base/login/cli_splash.ftl b/themes/src/main/resources/theme/base/login/cli_splash.ftl
new file mode 100644
index 0000000..cd9ebbb
--- /dev/null
+++ b/themes/src/main/resources/theme/base/login/cli_splash.ftl
@@ -0,0 +1,7 @@
+ _  __               _             _
+| |/ /___ _   _  ___| | ___   __ _| | __
+| ' // _ \ | | |/ __| |/ _ \ / _` | |/ /
+| . \  __/ |_| | (__| | (_) | (_| |   <
+|_|\_\___|\__, |\___|_|\___/ \__,_|_|\_\
+          |___/
+