keycloak-memoizeit

kcinit

3/16/2018 1:11:57 PM

Changes

adapters/oidc/cli-sso/login.sh 10(+0 -10)

adapters/oidc/cli-sso/logout.sh 9(+0 -9)

adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java 281(+0 -281)

pom.xml 11(+11 -0)

Details

diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KcinitDriver.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KcinitDriver.java
new file mode 100644
index 0000000..790cbcd
--- /dev/null
+++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KcinitDriver.java
@@ -0,0 +1,707 @@
+/*
+ * 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.adapters.installed;
+
+import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.adapters.ServerRequest;
+import org.keycloak.common.util.Base64;
+import org.keycloak.common.util.Time;
+import org.keycloak.jose.jwe.*;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Form;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.*;
+import java.nio.file.Paths;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.util.*;
+
+/**
+ * All kcinit commands that take input ask for
+ * <p>
+ * 1. . kcinit
+ * - setup and export KC_SESSION_KEY env var if not set.
+ * - checks to see if master token valid, refresh is possible, exit if token valid
+ * - performs command line login
+ * - stores master token for master client
+ * 2. app.sh is a wrapper for app cli.
+ * - token=`kcinit token app`
+ * - checks to see if token for app client has been fetched, refresh if valid, output token to sys.out if exists
+ * - if no token, login.  Prompts go to stderr.
+ * - pass token as cmd line param to app or as environment variable.
+ * <p>
+ * 3. kcinit password {password}
+ * - outputs password key that is used for encryption.
+ * - can be used in .bashrc as export KC_SESSSION_KEY=`kcinit password {password}` or just set it in .bat file
+ * <p>
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class KcinitDriver {
+
+    public static final String KC_SESSION_KEY = "KC_SESSION_KEY";
+    public static final String KC_LOGIN_CONFIG_PATH = "KC_LOGIN_CONFIG_PATH";
+    protected Map<String, String> config;
+    protected boolean debug = true;
+
+    protected static byte[] salt = new byte[]{-4, 88, 66, -101, 78, -94, 21, 105};
+
+    String[] args = null;
+
+    protected boolean forceLogin;
+    protected boolean browserLogin;
+
+    public void mainCmd(String[] args) throws Exception {
+
+        this.args = Arrays.copyOf(args, args.length);
+        for (String arg : args) {
+            if (!arg.startsWith("-")) break;
+            if (arg.equals("-f") || arg.equals("-force")) {
+                forceLogin = true;
+                this.args = Arrays.copyOfRange(this.args, 1, this.args.length);
+            } else if (arg.equals("-browser") || arg.equals("-b")) {
+                browserLogin = true;
+                this.args = Arrays.copyOfRange(this.args, 1, this.args.length);
+            } else {
+                System.err.println("Illegal argument: " + arg);
+                printHelp();
+                System.exit(1);
+            }
+        }
+
+        this.args = args;
+
+
+        if (args.length == 0) {
+            login();
+            return;
+        }
+
+        if (args[0].startsWith("-")) {
+            login();
+            return;
+        }
+
+        if (args[0].equalsIgnoreCase("token")) {
+            //System.err.println("executing token");
+            token();
+        } else if (args[0].equalsIgnoreCase("logout")) {
+            logout();
+        } else if (args[0].equalsIgnoreCase("env")) {
+            System.out.println(System.getenv().toString());
+        } else if (args[0].equalsIgnoreCase("install")) {
+            install();
+        } else if (args[0].equalsIgnoreCase("uninstall")) {
+            uninstall();
+        } else if (args[0].equalsIgnoreCase("password")) {
+            passwordKey();
+        } else {
+            KeycloakInstalled.console().writer().println("Unknown command: " + args[0]);
+            KeycloakInstalled.console().writer().println();
+            printHelp();
+        }
+    }
+
+    public String getHome() {
+        String home = System.getenv("HOME");
+        if (home == null) {
+            home = System.getProperty("HOME");
+            if (home == null) {
+                home = Paths.get("").toAbsolutePath().normalize().toString();
+            }
+        }
+        return home;
+    }
+
+    public void passwordKey() {
+        if (args.length < 2) {
+            printHelp();
+            System.exit(1);
+        }
+        String password = args[1];
+        try {
+            String encodedKey = generateEncryptionKey(password);
+            System.out.printf(encodedKey);
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.exit(1);
+        }
+    }
+
+    protected String generateEncryptionKey(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {
+        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
+        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 100, 128);
+        SecretKey tmp = factory.generateSecret(spec);
+        byte[] aeskey = tmp.getEncoded();
+        return Base64.encodeBytes(aeskey);
+    }
+
+    public JWE createJWE() {
+        String key = getEncryptionKey();
+        if (key == null) {
+            throw new RuntimeException(KC_SESSION_KEY + " env var not set");
+        }
+        byte[] aesKey = null;
+        try {
+            aesKey = Base64.decode(key.getBytes("UTF-8"));
+        } catch (IOException e) {
+            throw new RuntimeException("invalid " + KC_SESSION_KEY + "env var");
+        }
+
+        JWE jwe = new JWE();
+        final SecretKey aesSecret = new SecretKeySpec(aesKey, "AES");
+        jwe.getKeyStorage()
+                .setEncryptionKey(aesSecret);
+        return jwe;
+    }
+
+    protected String encryptionKey;
+
+    protected String getEncryptionKey() {
+        if (encryptionKey != null) return encryptionKey;
+        return System.getenv(KC_SESSION_KEY);
+    }
+
+    public String encrypt(String payload) {
+        JWE jwe = createJWE();
+        JWEHeader jweHeader = new JWEHeader(JWEConstants.A128KW, JWEConstants.A128CBC_HS256, null);
+        try {
+            jwe.header(jweHeader).content(payload.getBytes("UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("cannot encode payload as UTF-8");
+        }
+        try {
+            return jwe.encodeJwe();
+        } catch (JWEException e) {
+            throw new RuntimeException("cannot encrypt payload", e);
+        }
+    }
+
+    public String decrypt(String encoded) {
+        JWE jwe = createJWE();
+        try {
+            jwe.verifyAndDecodeJwe(encoded);
+            byte[] content = jwe.getContent();
+            if (content == null) return null;
+            return new String(content, "UTF-8");
+        } catch (Exception ex) {
+            throw new RuntimeException("cannot decrypt payload", ex);
+
+        }
+
+    }
+
+    public static String getenv(String name, String defaultValue) {
+        String val = System.getenv(name);
+        return val == null ? defaultValue : val;
+    }
+
+    public File getConfigDirectory() {
+        return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit").toFile();
+    }
+
+
+    public File getConfigFile() {
+        return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit", "config.json").toFile();
+    }
+
+    public File getTokenFilePath(String client) {
+        return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit", "tokens", client).toFile();
+    }
+
+    public File getTokenDirectory() {
+        return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit", "tokens").toFile();
+    }
+
+    protected boolean encrypted = false;
+
+    protected void checkEnv() {
+        File configFile = getConfigFile();
+        if (!configFile.exists()) {
+            KeycloakInstalled.console().writer().println("You have not configured kcinit.  Please run 'kcinit install' to configure.");
+            System.exit(1);
+        }
+        byte[] data = new byte[0];
+        try {
+            data = readFileRaw(configFile);
+        } catch (IOException e) {
+
+        }
+        if (data == null) {
+            KeycloakInstalled.console().writer().println("Config file unreadable.  Please run 'kcinit install' to configure.");
+            System.exit(1);
+
+        }
+        String encodedJwe = null;
+        try {
+            encodedJwe = new String(data, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+        }
+
+        if (encodedJwe.contains("realm")) {
+            encrypted = false;
+            return;
+        } else {
+            encrypted = true;
+        }
+
+        if (System.getenv(KC_SESSION_KEY) == null) {
+            promptLocalPassword();
+        }
+    }
+
+    protected void promptLocalPassword() {
+        String password = KeycloakInstalled.console().passwordPrompt("Enter password to unlock kcinit config files: ");
+        try {
+            encryptionKey = generateEncryptionKey(password);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+
+    protected String readFile(File fp) {
+        try {
+            byte[] data = readFileRaw(fp);
+            if (data == null) return null;
+            String file = new String(data, "UTF-8");
+            if (!encrypted) {
+                return file;
+            }
+            String decrypted = decrypt(file);
+            if (decrypted == null)
+                throw new RuntimeException("Unable to decrypt file.  Did you set your local password correctly?");
+            return decrypted;
+        } catch (IOException e) {
+            throw new RuntimeException("failed to decrypt file: " + fp.getAbsolutePath() + " Did you set your local password correctly?", e);
+        }
+
+
+    }
+
+    protected byte[] readFileRaw(File fp) throws IOException {
+        if (!fp.exists()) return null;
+        FileInputStream fis = new FileInputStream(fp);
+        byte[] data = new byte[(int) fp.length()];
+        fis.read(data);
+        fis.close();
+        return data;
+    }
+
+    protected void writeFile(File fp, String payload) {
+        try {
+            String data = payload;
+            if (encrypted) data = encrypt(payload);
+            FileOutputStream fos = new FileOutputStream(fp);
+            fos.write(data.getBytes("UTF-8"));
+            fos.flush();
+            fos.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+
+    public void install() {
+        if (getEncryptionKey() == null) {
+            if (KeycloakInstalled.console().confirm("Do you want to protect tokens stored locally with a password? (y/n): ")) {
+                String password = "p";
+                String confirm = "c";
+                do {
+                    password = KeycloakInstalled.console().passwordPrompt("Enter local password: ");
+                    confirm = KeycloakInstalled.console().passwordPrompt("Confirm local password: ");
+                    if (!password.equals(confirm)) {
+                        KeycloakInstalled.console().writer().println();
+                        KeycloakInstalled.console().writer().println("Confirmation does not match.  Try again.");
+                        KeycloakInstalled.console().writer().println();
+                    }
+                } while (!password.equals(confirm));
+                try {
+                    this.encrypted = true;
+                    this.encryptionKey = generateEncryptionKey(password);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                    System.exit(1);
+                }
+            }
+        } else {
+            if (!KeycloakInstalled.console().confirm("KC_SESSION_KEY env var already set.  Do you want to use this as your local encryption key? (y/n): ")) {
+                KeycloakInstalled.console().writer().println("Unset KC_SESSION_KEY env var and run again");
+                System.exit(1);
+            }
+            this.encrypted = true;
+            this.encryptionKey = getEncryptionKey();
+        }
+        String server = KeycloakInstalled.console().readLine("Authentication server URL [http://localhost:8080/auth]: ").trim();
+        String realm = KeycloakInstalled.console().readLine("Name of realm [master]: ").trim();
+        String client = KeycloakInstalled.console().readLine("CLI client id [kcinit]: ").trim();
+        String secret = KeycloakInstalled.console().readLine("CLI client secret [none]: ").trim();
+        if (server.equals("")) {
+            server = "http://localhost:8080/auth";
+        }
+        if (realm.equals("")) {
+            realm = "master";
+        }
+        if (client.equals("")) {
+            client = "kcinit";
+        }
+        File configDir = getTokenDirectory();
+        configDir.mkdirs();
+
+        File configFile = getConfigFile();
+        Map<String, String> props = new HashMap<>();
+        props.put("server", server);
+        props.put("realm", realm);
+        props.put("client", client);
+        props.put("secret", secret);
+
+        try {
+            String json = JsonSerialization.writeValueAsString(props);
+            writeFile(configFile, json);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        KeycloakInstalled.console().writer().println();
+        KeycloakInstalled.console().writer().println("Installation complete!");
+        KeycloakInstalled.console().writer().println();
+    }
+
+
+    public void printHelp() {
+        KeycloakInstalled.console().writer().println("Commands:");
+        KeycloakInstalled.console().writer().println("  no arguments is a login");
+        KeycloakInstalled.console().writer().println("  no argument with -f forces login");
+        KeycloakInstalled.console().writer().println("  logout");
+        KeycloakInstalled.console().writer().println("  token [client] - print access token of desired client.  Defaults to default master client.  Will print either 'error', 'not-allowed',  or 'login-required' on error.");
+        KeycloakInstalled.console().writer().println("  install - Install this utility.  Will store in $HOME/.keycloak/kcinit unless " + KC_LOGIN_CONFIG_PATH + " env var is set");
+        System.exit(1);
+    }
+
+
+    public AdapterConfig getConfig() {
+        File configFile = getConfigFile();
+        if (!configFile.exists()) {
+            KeycloakInstalled.console().writer().println("You have not configured kcinit.  Please run 'kcinit install' to configure.");
+            System.exit(1);
+            return null;
+        }
+
+        AdapterConfig config = new AdapterConfig();
+        config.setAuthServerUrl((String) getConfigProperties().get("server"));
+        config.setRealm((String) getConfigProperties().get("realm"));
+        config.setResource((String) getConfigProperties().get("client"));
+        config.setSslRequired("external");
+        String secret = (String) getConfigProperties().get("secret");
+        if (secret != null && !secret.trim().equals("")) {
+            Map<String, Object> creds = new HashMap<>();
+            creds.put("secret", secret);
+            config.setCredentials(creds);
+        } else {
+            config.setPublicClient(true);
+        }
+        return config;
+    }
+
+    private Map<String, String> getConfigProperties() {
+        if (this.config != null) return this.config;
+        if (!getConfigFile().exists()) {
+            KeycloakInstalled.console().writer().println();
+            KeycloakInstalled.console().writer().println(("Config file does not exist.  Run kcinit install to set it up."));
+            System.exit(1);
+        }
+        String json = readFile(getConfigFile());
+        try {
+            Map map = JsonSerialization.readValue(json, Map.class);
+            config = (Map<String, String>) map;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        return this.config;
+    }
+
+    public String readToken(String client) throws Exception {
+        String json = getTokenResponse(client);
+        if (json == null) return null;
+
+
+        if (json != null) {
+            try {
+                AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
+                if (Time.currentTime() < tokenResponse.getExpiresIn()) {
+                    return tokenResponse.getToken();
+                }
+                AdapterConfig config = getConfig();
+                KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
+                installed.refreshToken(tokenResponse.getRefreshToken());
+                processResponse(installed, client);
+                return tokenResponse.getToken();
+            } catch (Exception e) {
+                File tokenFile = getTokenFilePath(client);
+                if (tokenFile.exists()) {
+                    tokenFile.delete();
+                }
+
+                return null;
+            }
+        }
+        return null;
+
+    }
+
+    public String readRefreshToken(String client) throws Exception {
+        String json = getTokenResponse(client);
+        if (json == null) return null;
+
+
+        if (json != null) {
+            try {
+                AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
+                return tokenResponse.getRefreshToken();
+            } catch (Exception e) {
+                if (debug) {
+                    e.printStackTrace();
+                }
+                File tokenFile = getTokenFilePath(client);
+                if (tokenFile.exists()) {
+                    tokenFile.delete();
+                }
+
+                return null;
+            }
+        }
+        return null;
+
+    }
+
+
+    private String getTokenResponse(String client) throws IOException {
+        File tokenFile = getTokenFilePath(client);
+        try {
+            return readFile(tokenFile);
+        } catch (Exception e) {
+            if (debug) {
+                System.err.println("Failed to read encrypted file");
+                e.printStackTrace();
+            }
+            if (tokenFile.exists()) tokenFile.delete();
+            return null;
+        }
+    }
+
+
+    public void token() throws Exception {
+        KeycloakInstalled.console().stderrOutput();
+
+        checkEnv();
+        String masterClient = getMasterClient();
+        String client = masterClient;
+        if (args.length > 1) {
+            client = args[1];
+        }
+        //System.err.println("readToken: " + client);
+        String token = readToken(client);
+        if (token != null) {
+            System.out.print(token);
+            return;
+        }
+        if (token == null && client.equals(masterClient)) {
+            //System.err.println("not logged in, logging in.");
+            doConsoleLogin();
+            token = readToken(client);
+            if (token != null) {
+                System.out.print(token);
+                return;
+            }
+
+        }
+        String masterToken = readToken(masterClient);
+        if (masterToken == null) {
+            //System.err.println("not logged in, logging in.");
+            doConsoleLogin();
+            masterToken = readToken(masterClient);
+            if (masterToken == null) {
+                System.err.println("Login failed.  Cannot retrieve token");
+                System.exit(1);
+            }
+        }
+
+        //System.err.println("exchange: " + client);
+        Client httpClient = getHttpClient();
+
+        WebTarget exchangeUrl = httpClient.target(getServer())
+                .path("/realms")
+                .path(getRealm())
+                .path("protocol/openid-connect/token");
+
+        Form form = new Form()
+                .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                .param(OAuth2Constants.CLIENT_ID, masterClient)
+                .param(OAuth2Constants.SUBJECT_TOKEN, masterToken)
+                .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
+                .param(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.REFRESH_TOKEN_TYPE)
+                .param(OAuth2Constants.AUDIENCE, client);
+        if (getMasterClientSecret() != null) {
+            form.param(OAuth2Constants.CLIENT_SECRET, getMasterClientSecret());
+        }
+        Response response = exchangeUrl.request().post(Entity.form(
+                form
+        ));
+
+        if (response.getStatus() == 401 || response.getStatus() == 403) {
+            response.close();
+            System.err.println("Not allowed to exchange for client token");
+            System.exit(1);
+        }
+
+        if (response.getStatus() != 200) {
+            if (response.getMediaType() != null && response.getMediaType().equals(MediaType.APPLICATION_JSON_TYPE)) {
+                try {
+                    String json = response.readEntity(String.class);
+                    OAuth2ErrorRepresentation error = JsonSerialization.readValue(json, OAuth2ErrorRepresentation.class);
+                    System.err.println("Failed to exchange token: " + error.getError() + ". " + error.getErrorDescription());
+                    System.exit(1);
+                } catch (Exception ignore) {
+                    ignore.printStackTrace();
+
+                }
+            }
+
+            response.close();
+            System.err.println("Unknown error exchanging for client token: " + response.getStatus());
+            System.exit(1);
+        }
+
+        String json = response.readEntity(String.class);
+        response.close();
+        AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
+        if (tokenResponse.getToken() != null) {
+            getTokenDirectory().mkdirs();
+            tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
+            tokenResponse.setIdToken(null);
+            json = JsonSerialization.writeValueAsString(tokenResponse);
+            writeFile(getTokenFilePath(client), json);
+            System.out.printf(tokenResponse.getToken());
+        } else {
+            System.err.println("Error processing token");
+            System.exit(1);
+        }
+    }
+
+    protected String getMasterClientSecret() {
+        return getProperty("secret");
+    }
+
+    protected String getServer() {
+        return getProperty("server");
+    }
+
+    protected String getRealm() {
+        return getProperty("realm");
+    }
+
+    public String getProperty(String name) {
+        return (String) getConfigProperties().get(name);
+    }
+
+    protected boolean forceLogin() {
+        return args.length > 0 && args[0].equals("-f");
+
+    }
+
+    public Client getHttpClient() {
+        return new ResteasyClientBuilder().disableTrustManager().build();
+    }
+
+    public void login() throws Exception {
+        checkEnv();
+        String masterClient = getMasterClient();
+        if (!forceLogin() && readToken(masterClient) != null) {
+            KeycloakInstalled.console().writer().println("Already logged in.  `kcinit -f` to force relogin");
+            return;
+        }
+        doConsoleLogin();
+        KeycloakInstalled.console().writer().println("Login successful!");
+    }
+
+    public void doConsoleLogin() throws Exception {
+        String masterClient = getMasterClient();
+        AdapterConfig config = getConfig();
+        KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
+        //System.err.println("calling loginCommandLine");
+        if (!installed.loginCommandLine()) {
+            System.exit(1);
+        }
+        processResponse(installed, masterClient);
+    }
+
+    private String getMasterClient() {
+        return getProperty("client");
+    }
+
+    private void processResponse(KeycloakInstalled installed, String client) throws IOException {
+        AccessTokenResponse tokenResponse = installed.getTokenResponse();
+        tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
+        tokenResponse.setIdToken(null);
+        String json = JsonSerialization.writeValueAsString(tokenResponse);
+        getTokenDirectory().mkdirs();
+        writeFile(getTokenFilePath(client), json);
+    }
+
+    public void logout() throws Exception {
+        String token = readRefreshToken(getMasterClient());
+        if (token != null) {
+            try {
+                KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getConfig());
+                ServerRequest.invokeLogout(deployment, token);
+            } catch (Exception e) {
+                if (debug) {
+                    e.printStackTrace();
+                }
+            }
+
+        }
+        if (getTokenDirectory().exists()) {
+            for (File fp : getTokenDirectory().listFiles()) fp.delete();
+        }
+    }
+    public void uninstall() throws Exception {
+        File configFile = getConfigFile();
+        if (configFile.exists()) configFile.delete();
+        if (getTokenDirectory().exists()) {
+            for (File fp : getTokenDirectory().listFiles()) fp.delete();
+        }
+    }
+}
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 f1fee42..b0b8d0e 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
@@ -39,15 +39,7 @@ 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;
-import java.io.InputStream;
-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.io.*;
 import java.net.ServerSocket;
 import java.net.Socket;
 import java.net.URI;
@@ -65,6 +57,7 @@ public class KeycloakInstalled {
 
     public interface HttpResponseWriter {
         void success(PrintWriter pw, KeycloakInstalled ki);
+
         void failure(PrintWriter pw, KeycloakInstalled ki);
     }
 
@@ -86,12 +79,12 @@ public class KeycloakInstalled {
     private Locale locale;
     private HttpResponseWriter loginResponseWriter;
     private HttpResponseWriter logoutResponseWriter;
+    private ResteasyClient resteasyClient;
     Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\"");
     Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"\\s+mask=(\\S+)");
     Pattern codePattern = Pattern.compile("code=([^&]+)");
 
 
-
     public KeycloakInstalled() {
         InputStream config = Thread.currentThread().getContextClassLoader().getResourceAsStream(KEYCLOAK_JSON);
         deployment = KeycloakDeploymentBuilder.build(config);
@@ -179,6 +172,10 @@ public class KeycloakInstalled {
         this.logoutResponseWriter = logoutResponseWriter;
     }
 
+    public void setResteasyClient(ResteasyClient resteasyClient) {
+        this.resteasyClient = resteasyClient;
+    }
+
     public Locale getLocale() {
         return locale;
     }
@@ -302,6 +299,139 @@ public class KeycloakInstalled {
         status = Status.LOGGED_MANUAL;
     }
 
+    public static class Console {
+        protected java.io.Console console = System.console();
+        protected PrintWriter writer;
+        protected BufferedReader reader;
+
+        static Console SINGLETON = new Console();
+
+        private Console() {
+        }
+
+
+        public PrintWriter writer() {
+            if (console == null) {
+                if (writer == null) {
+                    writer = new PrintWriter(System.err, true);
+                }
+                return writer;
+            }
+            return console.writer();
+        }
+
+        public Reader reader() {
+            if (console == null) {
+                return getReader();
+            }
+            return console.reader();
+        }
+
+        protected BufferedReader getReader() {
+            if (reader != null) return reader;
+            reader = new BufferedReader(new BufferedReader(new InputStreamReader(System.in)));
+            return reader;
+        }
+
+        public Console format(String fmt, Object... args) {
+            if (console == null) {
+                writer().format(fmt, args);
+                return this;
+            }
+            console.format(fmt, args);
+            return this;
+        }
+
+        public Console printf(String format, Object... args) {
+            if (console == null) {
+                writer().printf(format, args);
+                return this;
+            }
+            console.printf(format, args);
+            return this;
+        }
+
+        public String readLine(String fmt, Object... args) {
+            if (console == null) {
+                format(fmt, args);
+                return readLine();
+            }
+            return console.readLine(fmt, args);
+        }
+
+        public boolean confirm(String fmt, Object... args) {
+            String prompt = "";
+            while (!"y".equals(prompt) && !"n".equals(prompt)) {
+                prompt = readLine(fmt, args);
+            }
+            return "y".equals(prompt);
+
+        }
+
+        public String prompt(String fmt, Object... args) {
+            String prompt = "";
+            while (prompt.equals("")) {
+                prompt = readLine(fmt, args).trim();
+            }
+            return prompt;
+
+        }
+
+        public String passwordPrompt(String fmt, Object... args) {
+            String prompt = "";
+            while (prompt.equals("")) {
+                char[] val = readPassword(fmt, args);
+                prompt = new String(val);
+                prompt = prompt.trim();
+            }
+            return prompt;
+
+        }
+
+        public String readLine() {
+            if (console == null) {
+                try {
+                    return getReader().readLine();
+                } catch (IOException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+            return console.readLine();
+        }
+
+        public char[] readPassword(String fmt, Object... args) {
+            if (console == null) {
+                return readLine(fmt, args).toCharArray();
+
+            }
+            return console.readPassword(fmt, args);
+        }
+
+        public char[] readPassword() {
+            if (console == null) {
+                return readLine().toCharArray();
+            }
+            return console.readPassword();
+        }
+
+        public void flush() {
+            if (console == null) {
+                System.err.flush();
+                return;
+            }
+            console.flush();
+        }
+
+        public void stderrOutput() {
+            //System.err.println("not using System.console()");
+            console = null;
+        }
+    }
+
+    public static Console console() {
+        return Console.SINGLETON;
+    }
+
     public boolean loginCommandLine() throws IOException, ServerRequest.HttpFailure, VerificationException {
         String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
 
@@ -309,7 +439,6 @@ public class KeycloakInstalled {
     }
 
 
-
     /**
      * Experimental proprietary WWW-Authentication challenge protocol.
      * WWW-Authentication: X-Text-Form-Challenge callback="{url}" param="{param-name}" label="{param-display-label}"
@@ -325,62 +454,117 @@ public class KeycloakInstalled {
                 .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
                 .queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
                 .queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
+                .queryParam("display", "console")
                 .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
                 .build().toString();
-        ResteasyClient client = new ResteasyClientBuilder().disableTrustManager().build();
+        ResteasyClient client = createResteasyClient();
         try {
+            //System.err.println("initial request");
             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);
+                System.err.println("looping");
+
+                if (response.getStatus() == 403) {
+                    if (response.getMediaType() != null) {
+                        String splash = response.readEntity(String.class);
+                        console().writer().println(splash);
                     } else {
-                        value = System.console().readLine(label);
+                        System.err.println("Forbidden to login");
+                    }
+                } else if (response.getStatus() == 401) {
+                    String authenticationHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
+                    if (authenticationHeader == null) {
+                        System.err.println("Failure:  Invalid protocol.  No WWW-Authenticate header");
+                        return false;
+                    }
+                    //System.err.println("got header: " + authenticationHeader);
+                    if (!authenticationHeader.contains("X-Text-Form-Challenge")) {
+                        System.err.println("Failure:  Invalid WWW-Authenticate header.");
+                        return false;
                     }
-                    form.param(param, value);
+                    if (response.getMediaType() != null) {
+                        String splash = response.readEntity(String.class);
+                        console().writer().println(splash);
+                    } else {
+                        response.close();
+                    }
+                    Matcher m = callbackPattern.matcher(authenticationHeader);
+                    if (!m.find()) {
+                        System.err.println("Failure: Invalid WWW-Authenticate header.");
+                        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 = console().readPassword(label);
+                            value = new String(txt);
+                        } else {
+                            value = console().readLine(label);
+                        }
+                        form.param(param, value);
+                    }
+                    response.close();
+                    client.close();
+                    client = createResteasyClient();
+                    response = client.target(callback).request().post(Entity.form(form));
+                } else if (response.getStatus() == 302) {
+                    int redirectCount = 0;
+                    do {
+                        String location = response.getLocation().toString();
+                        Matcher m = codePattern.matcher(location);
+                        if (!m.find()) {
+                            response.close();
+                            client.close();
+                            client = createResteasyClient();
+                            response = client.target(location).request().get();
+                        } else {
+                            response.close();
+                            client.close();
+                            String code = m.group(1);
+                            processCode(code, redirectUri);
+                            return true;
+                        }
+                        if (response.getStatus() == 302 && redirectCount++ > 4) {
+                            System.err.println("Too many redirects.  Aborting");
+                            return false;
+                        }
+                    } while (response.getStatus() == 302);
+                } else {
+                    System.err.println("Unknown response from server: " + response.getStatus());
+                    return false;
                 }
-                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;
             }
+        } catch (Exception ex) {
+            throw ex;
         } finally {
             client.close();
 
         }
     }
 
+    protected ResteasyClient getResteasyClient() {
+        if (this.resteasyClient == null) {
+            this.resteasyClient = createResteasyClient();
+        }
+        return this.resteasyClient;
+    }
+
+    protected ResteasyClient createResteasyClient() {
+        return new ResteasyClientBuilder()
+                .connectionCheckoutTimeout(1, TimeUnit.HOURS)
+                .connectionTTL(1, TimeUnit.HOURS)
+                .socketTimeout(1, TimeUnit.HOURS)
+                .disableTrustManager().build();
+    }
+
 
     public String getTokenString() throws VerificationException, IOException, ServerRequest.HttpFailure {
         return tokenString;
@@ -400,7 +584,7 @@ public class KeycloakInstalled {
         parseAccessToken(tokenResponse);
     }
 
-    public void refreshToken(String refreshToken)  throws IOException, ServerRequest.HttpFailure, VerificationException {
+    public void refreshToken(String refreshToken) throws IOException, ServerRequest.HttpFailure, VerificationException {
         AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
         parseAccessToken(tokenResponse);
 
@@ -452,7 +636,6 @@ public class KeycloakInstalled {
     }
 
 
-
     private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
         AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null);
         parseAccessToken(tokenResponse);
@@ -474,86 +657,6 @@ 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/adapters/oidc/kcinit/src/main/bin/kcinit b/adapters/oidc/kcinit/src/main/bin/kcinit
new file mode 100755
index 0000000..4f5c2c6
--- /dev/null
+++ b/adapters/oidc/kcinit/src/main/bin/kcinit
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+case "`uname`" in
+    CYGWIN*)
+        CFILE = `cygpath "$0"`
+        RESOLVED_NAME=`readlink -f "$CFILE"`
+        ;;
+    Darwin*)
+        RESOLVED_NAME=`readlink "$0"`
+        ;;
+    FreeBSD)
+        RESOLVED_NAME=`readlink -f "$0"`
+        ;;
+    Linux)
+        RESOLVED_NAME=`readlink -f "$0"`
+        ;;
+esac
+
+if [ "x$RESOLVED_NAME" = "x" ]; then
+    RESOLVED_NAME="$0"
+fi
+
+SCRIPTPATH=`dirname "$RESOLVED_NAME"`
+JAR=$SCRIPTPATH/kcinit-${project.version}.jar
+
+java -jar $JAR $@
diff --git a/adapters/oidc/kcinit/src/main/bin/kcinit.bat b/adapters/oidc/kcinit/src/main/bin/kcinit.bat
new file mode 100755
index 0000000..9055309
--- /dev/null
+++ b/adapters/oidc/kcinit/src/main/bin/kcinit.bat
@@ -0,0 +1,8 @@
+@echo off
+
+if "%OS%" == "Windows_NT" (
+  set "DIRNAME=%~dp0%"
+) else (
+  set DIRNAME=.\
+)
+java -jar %DIRNAME%\kcinit-${project.version}.jar %*
diff --git a/adapters/oidc/kcinit-dist/assembly.xml b/adapters/oidc/kcinit-dist/assembly.xml
new file mode 100755
index 0000000..f48c714
--- /dev/null
+++ b/adapters/oidc/kcinit-dist/assembly.xml
@@ -0,0 +1,49 @@
+<!--
+  ~ 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.
+  -->
+
+<assembly>
+    <id>kcinit-dist</id>
+
+    <formats>
+        <format>zip</format>
+    </formats>
+
+    <includeBaseDirectory>false</includeBaseDirectory>
+
+    <files>
+        <file>
+            <source>../kcinit/src/main/bin/kcinit</source>
+            <outputDirectory>kcinit</outputDirectory>
+            <fileMode>0755</fileMode>
+            <filtered>true</filtered>
+        </file>
+        <file>
+            <source>../kcinit/src/main/bin/kcinit.bat</source>
+            <outputDirectory>kcinit</outputDirectory>
+            <filtered>true</filtered>
+        </file>
+    </files>
+    <dependencySets>
+        <dependencySet>
+            <includes>
+                <include>org.keycloak:kcinit</include>
+            </includes>
+            <outputDirectory>kcinit</outputDirectory>
+        </dependencySet>
+    </dependencySets>
+
+</assembly>
diff --git a/adapters/oidc/kcinit-dist/pom.xml b/adapters/oidc/kcinit-dist/pom.xml
new file mode 100755
index 0000000..effdbb8
--- /dev/null
+++ b/adapters/oidc/kcinit-dist/pom.xml
@@ -0,0 +1,69 @@
+<!--
+  ~ 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>keycloak-client-cli-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>4.0.0.CR1-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>kcinit-dist</artifactId>
+    <packaging>pom</packaging>
+    <name>Kcinit Distribution</name>
+    <description/>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>kcinit</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>kcinit-${project.version}</finalName>
+        <plugins>
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>assemble</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <configuration>
+                            <descriptors>
+                                <descriptor>assembly.xml</descriptor>
+                            </descriptors>
+                            <outputDirectory>
+                                target
+                            </outputDirectory>
+                            <workDirectory>
+                                target/assembly/work
+                            </workDirectory>
+                            <appendAssemblyId>false</appendAssemblyId>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/adapters/oidc/pom.xml b/adapters/oidc/pom.xml
index 4252253..228cbde 100755
--- a/adapters/oidc/pom.xml
+++ b/adapters/oidc/pom.xml
@@ -34,7 +34,8 @@
         <module>adapter-core</module>
         <module>as7-eap6</module>
         <module>installed</module>
-        <module>cli-sso</module>
+        <module>kcinit</module>
+        <module>kcinit-dist</module>
         <module>jaxrs-oauth-client</module>
         <module>jetty</module>
         <module>js</module>
diff --git a/common/src/main/java/org/keycloak/common/util/RandomString.java b/common/src/main/java/org/keycloak/common/util/RandomString.java
new file mode 100644
index 0000000..70ce02d
--- /dev/null
+++ b/common/src/main/java/org/keycloak/common/util/RandomString.java
@@ -0,0 +1,66 @@
+package org.keycloak.common.util;
+
+import java.security.SecureRandom;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Random;
+
+public class RandomString {
+
+    /**
+     * Generate a random string.
+     */
+    public String nextString() {
+        for (int idx = 0; idx < buf.length; ++idx)
+            buf[idx] = symbols[random.nextInt(symbols.length)];
+        return new String(buf);
+    }
+
+    public static final String upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+    public static final String lower = upper.toLowerCase(Locale.ROOT);
+
+    public static final String digits = "0123456789";
+
+    public static final String alphanum = upper + lower + digits;
+
+    private final Random random;
+
+    private final char[] symbols;
+
+    private final char[] buf;
+
+    public RandomString(int length, Random random, String symbols) {
+        if (length < 1) throw new IllegalArgumentException();
+        if (symbols.length() < 2) throw new IllegalArgumentException();
+        this.random = Objects.requireNonNull(random);
+        this.symbols = symbols.toCharArray();
+        this.buf = new char[length];
+    }
+
+    /**
+     * Create an alphanumeric string generator.
+     */
+    public RandomString(int length, Random random) {
+        this(length, random, alphanum);
+    }
+
+    /**
+     * Create an alphanumeric strings from a secure generator.
+     */
+    public RandomString(int length) {
+        this(length, new SecureRandom());
+    }
+
+    /**
+     * Create session identifiers.
+     */
+    public RandomString() {
+        this(21);
+    }
+
+    public static String randomCode(int length) {
+        return new RandomString(length).nextString();
+    }
+
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWE.java b/core/src/main/java/org/keycloak/jose/jwe/JWE.java
index 75759dd..8d954ea 100644
--- a/core/src/main/java/org/keycloak/jose/jwe/JWE.java
+++ b/core/src/main/java/org/keycloak/jose/jwe/JWE.java
@@ -18,13 +18,23 @@
 package org.keycloak.jose.jwe;
 
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
 
+import org.keycloak.common.util.Base64;
 import org.keycloak.common.util.Base64Url;
 import org.keycloak.common.util.BouncyIntegration;
 import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
 import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
 import org.keycloak.util.JsonSerialization;
 
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
@@ -193,4 +203,66 @@ public class JWE {
         }
     }
 
+    public static String encryptUTF8(String password, String saltString, String payload) {
+        byte[] bytes = null;
+        try {
+            bytes = payload.getBytes("UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+        return encrypt(password, saltString, bytes);
+
+    }
+
+
+    public static String encrypt(String password, String saltString, byte[] payload) {
+        try {
+            byte[] salt = Base64.decode(saltString);
+            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
+            KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 100, 128);
+            SecretKey tmp = factory.generateSecret(spec);
+            SecretKey aesKey = new SecretKeySpec(tmp.getEncoded(), "AES");
+
+            JWEHeader jweHeader = new JWEHeader(JWEConstants.A128KW, JWEConstants.A128CBC_HS256, null);
+            JWE jwe = new JWE()
+                    .header(jweHeader)
+                    .content(payload);
+
+            jwe.getKeyStorage()
+                    .setEncryptionKey(aesKey);
+
+            return jwe.encodeJwe();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static byte[] decrypt(String password, String saltString, String encodedJwe) {
+        try {
+            byte[] salt = Base64.decode(saltString);
+            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
+            KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 100, 128);
+            SecretKey tmp = factory.generateSecret(spec);
+            SecretKey aesKey = new SecretKeySpec(tmp.getEncoded(), "AES");
+
+            JWE jwe = new JWE();
+            jwe.getKeyStorage()
+                    .setEncryptionKey(aesKey);
+
+            jwe.verifyAndDecodeJwe(encodedJwe);
+            return jwe.getContent();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static String decryptUTF8(String password, String saltString, String encodedJwe) {
+        byte[] payload = decrypt(password, saltString, encodedJwe);
+        try {
+            return new String(payload, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
 }
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index df54112..efa9ed8 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -34,6 +34,8 @@ public interface OAuth2Constants {
 
     String REDIRECT_URI = "redirect_uri";
 
+    String DISPLAY = "display";
+
     String SCOPE = "scope";
 
     String STATE = "state";
diff --git a/core/src/test/java/org/keycloak/jose/JWETest.java b/core/src/test/java/org/keycloak/jose/JWETest.java
index 31d8a8a..cc179bf 100644
--- a/core/src/test/java/org/keycloak/jose/JWETest.java
+++ b/core/src/test/java/org/keycloak/jose/JWETest.java
@@ -19,18 +19,18 @@ package org.keycloak.jose;
 
 import java.io.UnsupportedEncodingException;
 import java.security.Key;
+import java.security.spec.KeySpec;
 
 import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
 import javax.crypto.spec.SecretKeySpec;
 
 import org.junit.Assert;
 import org.junit.Test;
+import org.keycloak.common.util.Base64;
 import org.keycloak.common.util.Base64Url;
-import org.keycloak.jose.jwe.JWE;
-import org.keycloak.jose.jwe.JWEConstants;
-import org.keycloak.jose.jwe.JWEException;
-import org.keycloak.jose.jwe.JWEHeader;
-import org.keycloak.jose.jwe.JWEKeyStorage;
+import org.keycloak.jose.jwe.*;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -53,7 +53,6 @@ public class JWETest {
         testDirectEncryptAndDecrypt(aesKey, hmacKey, JWEConstants.A128CBC_HS256, PAYLOAD, true);
     }
 
-
     // Works just on OpenJDK 8. Other JDKs (IBM, Oracle) have restrictions on maximum key size of AES to be 128
     // @Test
     public void testDirect_Aes256CbcHmacSha512() throws Exception {
@@ -119,9 +118,24 @@ public class JWETest {
     }
 
     @Test
+    public void testPassword() throws Exception {
+        byte[] salt = JWEUtils.generateSecret(8);
+        String encodedSalt = Base64.encodeBytes(salt);
+        String jwe = JWE.encryptUTF8("geheim", encodedSalt, PAYLOAD);
+        String decodedContent = JWE.decryptUTF8("geheim", encodedSalt, jwe);
+        Assert.assertEquals(PAYLOAD, decodedContent);
+    }
+
+
+
+    @Test
     public void testAesKW_Aes128CbcHmacSha256() throws Exception {
         SecretKey aesKey = new SecretKeySpec(AES_128_KEY, "AES");
 
+        testAesKW_Aes128CbcHmacSha256(aesKey);
+    }
+
+    private void testAesKW_Aes128CbcHmacSha256(SecretKey aesKey) throws UnsupportedEncodingException, JWEException {
         JWEHeader jweHeader = new JWEHeader(JWEConstants.A128KW, JWEConstants.A128CBC_HS256, null);
         JWE jwe = new JWE()
                 .header(jweHeader)
@@ -146,6 +160,15 @@ public class JWETest {
         Assert.assertEquals(PAYLOAD, decodedContent);
     }
 
+    @Test
+    public void testSalt() {
+        byte[] random = JWEUtils.generateSecret(8);
+        System.out.print("new byte[] = {");
+        for (byte b : random) {
+            System.out.print(""+Byte.toString(b)+",");
+        }
+    }
+
 
     @Test
     public void externalJweAes128CbcHmacSha256Test() throws UnsupportedEncodingException, JWEException {

pom.xml 11(+11 -0)

diff --git a/pom.xml b/pom.xml
index a82b1a2..207bb36 100755
--- a/pom.xml
+++ b/pom.xml
@@ -1404,6 +1404,17 @@
                 <version>${project.version}</version>
                 <type>zip</type>
             </dependency>
+            <dependency>
+                <groupId>org.keycloak</groupId>
+                <artifactId>kcinit</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.keycloak</groupId>
+                <artifactId>kcinit-dist</artifactId>
+                <version>${project.version}</version>
+                <type>zip</type>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/DisplayUtils.java b/server-spi-private/src/main/java/org/keycloak/authentication/DisplayUtils.java
new file mode 100644
index 0000000..6a393a3
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/DisplayUtils.java
@@ -0,0 +1,20 @@
+package org.keycloak.authentication;
+
+import org.keycloak.OAuth2Constants;
+
+/**
+ * Determine OIDC display parameter type.
+ *
+ */
+public class DisplayUtils {
+
+    public static boolean isConsole(AuthenticationFlowContext context) {
+        String displayParam = context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY);
+        return displayParam != null && displayParam.equals("console");
+    }
+    public static boolean isConsole(RequiredActionContext context) {
+        String displayParam = context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY);
+        return displayParam != null && displayParam.equals("console");
+    }
+
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java
index caaa14e..1289842 100755
--- a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java
@@ -60,6 +60,15 @@ public interface RequiredActionContext {
     URI getActionUrl();
 
     /**
+     * Get the action URL for the required action.  This auto-generates the access code.
+     *
+     * @param authSessionIdParam if true, will embed session id as query param.  Useful for clients that don't support cookies (i.e. console)
+     *
+     * @return
+     */
+    URI getActionUrl(boolean authSessionIdParam);
+
+    /**
      * Create a Freemarker form builder that presets the user, action URI, and a generated access code
      *
      * @return
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
new file mode 100644
index 0000000..46c0707
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/TextChallenge.java
@@ -0,0 +1,288 @@
+package org.keycloak.authentication;
+
+import org.keycloak.forms.login.LoginFormsProvider;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+/**
+ * This class encapsulates a proprietary HTTP challenge protocol designed by keycloak team which is used by text-based console
+ * clients to dynamically render and prompt for information in a textual manner.  The class is a builder which can
+ * build the challenge response (the header and response body).
+ *
+ * When doing code to token flow in OAuth, server could respond with
+ *
+ * 401
+ * WWW-Authenticate: X-Text-Form-Challenge callback="http://localhost/..."
+ *                                         param="username" label="Username: " mask=false
+ *                                         param="password" label="Password: " mask=true
+ * Content-Type: text/plain
+ *
+ * Please login with your username and password
+ *
+ *
+ * The client receives this challenge.  It first outputs whatever the text body of the message contains.  It will
+ * then prompt for username and password using the label values as prompt messages for each parameter.
+ *
+ * After the input has been entered by the user, the client does a form POST to the callback url with the values of the
+ * input parameters entered.
+ *
+ * The server can challenge with 401 as many times as it wants.  The client will look for 302 responses.  It will will
+ * follow all redirects unless the Location url has an OAuth "code" parameter.  If there is a code parameter, then the
+ * client will stop and finish the OAuth flow to obtain a token.  Any other response code other than 401 or 302 the client
+ * should abort with an error message.
+ *
+ */
+public class TextChallenge {
+
+    /**
+     * Build challenge response for required actions
+     *
+     * @param context
+     * @return
+     */
+    public static TextChallenge challenge(RequiredActionContext context) {
+        return new TextChallenge(context);
+
+    }
+
+    /**
+     * Build challenge response for authentication flows
+     *
+     * @param context
+     * @return
+     */
+    public static TextChallenge challenge(AuthenticationFlowContext context) {
+        return new TextChallenge(context);
+
+    }
+    /**
+     * Build challenge response header only for required actions
+     *
+     * @param context
+     * @return
+     */
+    public static HeaderBuilder header(RequiredActionContext context) {
+        return new TextChallenge(context).header();
+
+    }
+
+    /**
+     * Build challenge response header only for authentication flows
+     *
+     * @param context
+     * @return
+     */
+    public static HeaderBuilder header(AuthenticationFlowContext context) {
+        return new TextChallenge(context).header();
+
+    }
+    TextChallenge(RequiredActionContext requiredActionContext) {
+        this.requiredActionContext = requiredActionContext;
+    }
+
+    TextChallenge(AuthenticationFlowContext flowContext) {
+        this.flowContext = flowContext;
+    }
+
+
+    protected RequiredActionContext requiredActionContext;
+    protected AuthenticationFlowContext flowContext;
+    protected HeaderBuilder header;
+
+    /**
+     * Create a theme form pre-populated with challenge
+     *
+     * @return
+     */
+    public LoginFormsProvider form() {
+        if (header == null) throw new RuntimeException("Header Not Set");
+        return formInternal()
+                .setStatus(Response.Status.UNAUTHORIZED)
+                .setMediaType(MediaType.TEXT_PLAIN_TYPE)
+                .setResponseHeader(HttpHeaders.WWW_AUTHENTICATE, header.build());
+    }
+
+    /**
+     * Create challenge response with a  body generated from localized
+     * message.properties of your theme
+     *
+     * @param msg message id
+     * @param params parameters to use to format the message
+     *
+     * @return
+     */
+    public Response message(String msg, String... params) {
+        if (header == null) throw new RuntimeException("Header Not Set");
+        Response response = Response.status(401)
+                .header(HttpHeaders.WWW_AUTHENTICATE, header.build())
+                .type(MediaType.TEXT_PLAIN)
+                .entity("\n" + formInternal().getMessage(msg, params) + "\n").build();
+        return response;
+    }
+
+    /**
+     * Create challenge response with a text message body
+     *
+     * @param text plain text of http response body
+     *
+     * @return
+     */
+    public Response text(String text) {
+        if (header == null) throw new RuntimeException("Header Not Set");
+        Response response = Response.status(401)
+                .header(HttpHeaders.WWW_AUTHENTICATE, header.build())
+                .type(MediaType.TEXT_PLAIN)
+                .entity("\n" + text + "\n").build();
+        return response;
+
+    }
+
+
+    /**
+     * Generate response with empty http response body
+     *
+     * @return
+     */
+    public Response response() {
+        if (header == null) throw new RuntimeException("Header Not Set");
+        Response response = Response.status(401)
+                .header(HttpHeaders.WWW_AUTHENTICATE, header.build()).build();
+        return response;
+
+    }
+
+
+
+    protected LoginFormsProvider formInternal() {
+        if (requiredActionContext != null) {
+            return requiredActionContext.form();
+        } else {
+            return flowContext.form();
+
+        }
+    }
+
+    /**
+     * Start building the header
+     *
+     * @return
+     */
+    public HeaderBuilder header() {
+        String callback;
+        if (requiredActionContext != null) {
+            callback = requiredActionContext.getActionUrl(true).toString();
+        } else {
+            callback = flowContext.getActionUrl(flowContext.generateAccessCode(), true).toString();
+
+        }
+        header = new HeaderBuilder(callback);
+        return header;
+    }
+
+    public class HeaderBuilder {
+        protected StringBuilder builder = new StringBuilder();
+
+        protected HeaderBuilder(String callback) {
+            builder.append("X-Text-Form-Challenge callback=\"").append(callback).append("\" ");
+        }
+
+        protected ParamBuilder param;
+
+        protected void checkParam() {
+            if (param != null) {
+                param.buildInternal();
+                param = null;
+            }
+        }
+
+        /**
+         * Build header string
+         *
+         * @return
+         */
+        public String build() {
+            checkParam();
+            return builder.toString();
+        }
+
+        /**
+         * Define a param
+         *
+         * @param name
+         * @return
+         */
+        public ParamBuilder param(String name) {
+            checkParam();
+            builder.append("param=\"").append(name).append("\" ");
+            param = new ParamBuilder(name);
+            return param;
+        }
+
+        public class ParamBuilder {
+            protected boolean mask;
+            protected String label;
+
+            protected ParamBuilder(String name) {
+                this.label = name;
+            }
+
+            public ParamBuilder label(String msg) {
+                this.label = formInternal().getMessage(msg);
+                return this;
+            }
+
+            public ParamBuilder labelText(String txt) {
+                this.label = txt;
+                return this;
+            }
+
+            /**
+             * Should input be masked by the client.  For example, when entering password, you don't want to show password on console.
+             *
+             * @param mask
+             * @return
+             */
+            public ParamBuilder mask(boolean mask) {
+                this.mask = mask;
+                return this;
+            }
+
+            public void buildInternal() {
+                builder.append("label=\"").append(label).append(" \" ");
+                builder.append("mask=").append(mask).append(" ");
+            }
+
+            /**
+             * Build header string
+             *
+             * @return
+             */
+            public String build() {
+                return HeaderBuilder.this.build();
+            }
+
+            public TextChallenge challenge() {
+                return TextChallenge.this;
+            }
+
+            public LoginFormsProvider form() {
+                return TextChallenge.this.form();
+            }
+
+            public Response message(String msg, String... params) {
+                return TextChallenge.this.message(msg, params);
+            }
+
+            public Response text(String text) {
+                return TextChallenge.this.text(text);
+
+            }
+
+            public ParamBuilder param(String name) {
+                return HeaderBuilder.this.param(name);
+            }
+        }
+    }
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
index a60ebc0..425ec52 100755
--- a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
@@ -23,6 +23,7 @@ import org.keycloak.models.UserModel;
 import org.keycloak.provider.Provider;
 import org.keycloak.sessions.AuthenticationSessionModel;
 
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -76,4 +77,24 @@ public interface EmailTemplateProvider extends Provider {
 
     public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException;
 
+    /**
+     * Send formatted email
+     *
+     * @param subjectFormatKey message property that will be used to format email subject
+     * @param bodyTemplate freemarker template file
+     * @param bodyAttributes attributes used to fill template
+     * @throws EmailException
+     */
+    void send(String subjectFormatKey, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException;
+
+    /**
+     * Send formatted email
+     *
+     * @param subjectFormatKey message property that will be used to format email subject
+     * @param subjectAttributes attributes used to fill subject format message
+     * @param bodyTemplate freemarker template file
+     * @param bodyAttributes attributes used to fill template
+     * @throws EmailException
+     */
+    void send(String subjectFormatKey, List<Object> subjectAttributes, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException;
 }
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 256b87f..31f430d 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
@@ -54,6 +54,8 @@ public interface LoginFormsProvider extends Provider {
 
     String getMessage(String message);
 
+    String getMessage(String message, String... parameters);
+
     Response createLogin();
 
     Response createPasswordReset();
diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java
index 5cb1ec5..ae2f4c7 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java
@@ -52,6 +52,7 @@ public interface Constants {
     int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
 
     String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
+    String VERIFY_EMAIL_CODE = "VERIFY_EMAIL_CODE";
     String EXECUTION = "execution";
     String CLIENT_ID = "client_id";
     String TAB_ID = "tab_id";
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java
index 5218347..170f9d7 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java
@@ -18,6 +18,7 @@
 package org.keycloak.authentication.authenticators.browser;
 
 import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
 import org.keycloak.authentication.AuthenticationFlowContext;
 import org.keycloak.authentication.Authenticator;
 import org.keycloak.constants.AdapterConstants;
@@ -25,10 +26,13 @@ import org.keycloak.models.IdentityProviderModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.services.Urls;
 import org.keycloak.services.managers.ClientSessionCode;
 
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
 import java.util.List;
 
 /**
@@ -66,8 +70,11 @@ public class IdentityProviderAuthenticator implements Authenticator {
                 String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode();
                 String clientId = context.getAuthenticationSession().getClient().getClientId();
                 String tabId = context.getAuthenticationSession().getTabId();
-                Response response = Response.seeOther(
-                        Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId))
+                URI location = Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId);
+                if (context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY) != null) {
+                    location = UriBuilder.fromUri(location).queryParam(OAuth2Constants.DISPLAY, context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY)).build();
+                }
+                Response response = Response.seeOther(location)
                         .build();
 
                 LOG.debugf("Redirecting to %s", providerId);
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 9126689..955048b 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,6 +20,8 @@ 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;
@@ -39,11 +41,19 @@ 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/UsernamePasswordForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
index bd81263..1dc966f 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
@@ -19,8 +19,9 @@ package org.keycloak.authentication.authenticators.browser;
 
 import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
 import org.keycloak.authentication.AuthenticationFlowContext;
-import org.keycloak.authentication.AuthenticationProcessor;
 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;
@@ -41,6 +42,10 @@ 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();
@@ -58,6 +63,10 @@ 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/console/ConsoleOTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java
new file mode 100755
index 0000000..8fa34e9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java
@@ -0,0 +1,76 @@
+/*
+ * 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.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;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ConsoleOTPFormAuthenticator extends OTPFormAuthenticator implements Authenticator {
+    public static final ConsoleOTPFormAuthenticator SINGLETON = new ConsoleOTPFormAuthenticator();
+
+    public static URI getCallbackUrl(AuthenticationFlowContext context) {
+        return context.getActionUrl(context.generateAccessCode(), true);
+    }
+
+    protected TextChallenge challenge(AuthenticationFlowContext context) {
+        return TextChallenge.challenge(context)
+                .header()
+                .param(CredentialRepresentation.TOTP)
+                .label("console-otp")
+                 .challenge();
+    }
+
+    @Override
+    public void action(AuthenticationFlowContext context) {
+        validateOTP(context);
+    }
+
+
+
+    @Override
+    public void authenticate(AuthenticationFlowContext context) {
+        Response challengeResponse = challenge(context, null);
+        context.challenge(challengeResponse);
+    }
+
+    @Override
+    protected Response challenge(AuthenticationFlowContext context, String msg) {
+        if (msg == null) {
+            return challenge(context).response();
+        }
+        return challenge(context).message(msg);
+    }
+
+    @Override
+    public void close() {
+
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticator.java
new file mode 100755
index 0000000..4595df5
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticator.java
@@ -0,0 +1,122 @@
+/*
+ * 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.console;
+
+import org.keycloak.authentication.*;
+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 ConsoleUsernamePasswordAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {
+
+    public static final ConsoleUsernamePasswordAuthenticator SINGLETON = new ConsoleUsernamePasswordAuthenticator();
+
+    @Override
+    public boolean requiresUser() {
+        return false;
+    }
+
+    protected TextChallenge challenge(AuthenticationFlowContext context) {
+        return TextChallenge.challenge(context)
+                .header()
+                .param("username")
+                .label("console-username")
+                .param("password")
+                .label("console-password")
+                .mask(true)
+                .challenge();
+    }
+
+
+    @Override
+    public void authenticate(AuthenticationFlowContext context) {
+        Response response = challenge(context).form().createForm("cli_splash.ftl");
+        context.challenge(response);
+
+
+    }
+
+    @Override
+    protected Response invalidUser(AuthenticationFlowContext context) {
+        Response response = challenge(context).message(Messages.INVALID_USER);
+        return response;
+    }
+
+    @Override
+    protected Response disabledUser(AuthenticationFlowContext context) {
+        Response response = challenge(context).message(Messages.ACCOUNT_DISABLED);
+        return response;
+    }
+
+    @Override
+    protected Response temporarilyDisabledUser(AuthenticationFlowContext context) {
+        Response response = challenge(context).message(Messages.INVALID_USER);
+        return response;
+    }
+
+    @Override
+    protected Response invalidCredentials(AuthenticationFlowContext context) {
+        Response response = challenge(context).message(Messages.INVALID_USER);
+        return response;
+    }
+
+    @Override
+    protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
+        context.getEvent().error(eventError);
+        Response response = challenge(context).message(loginFormError);
+
+        context.failureChallenge(authenticatorError, response);
+        return response;
+    }
+
+    @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/console/ConsoleUsernamePasswordAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticatorFactory.java
new file mode 100755
index 0000000..05aa235
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticatorFactory.java
@@ -0,0 +1,102 @@
+/*
+ * 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.console;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+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 ConsoleUsernamePasswordAuthenticatorFactory implements AuthenticatorFactory {
+
+    public static final String PROVIDER_ID = "console-username-password";
+
+    @Override
+    public Authenticator create(KeycloakSession session) {
+        return ConsoleUsernamePasswordAuthenticator.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/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
index 5e9a546..38b9c2f 100755
--- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
@@ -33,6 +33,7 @@ import org.keycloak.services.resources.LoginActionsService;
 import org.keycloak.sessions.AuthenticationSessionModel;
 
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
 import java.net.URI;
 
@@ -163,6 +164,16 @@ public class RequiredActionContextResult implements RequiredActionContext {
     }
 
     @Override
+    public URI getActionUrl(boolean authSessionIdParam) {
+        URI uri = getActionUrl();
+        if (authSessionIdParam) {
+            uri = UriBuilder.fromUri(uri).queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()).build();
+        }
+        return uri;
+
+    }
+
+    @Override
     public LoginFormsProvider form() {
         String accessCode = generateCode();
         URI action = getActionUrl(accessCode);
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java
new file mode 100755
index 0000000..41e07d9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.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.requiredactions;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionFactory;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.authentication.TextChallenge;
+import org.keycloak.common.util.Time;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+import javax.ws.rs.core.Response;
+import java.util.Arrays;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ConsoleTermsAndConditions implements RequiredActionProvider, RequiredActionFactory {
+    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;
+    }
+
+
+    @Override
+    public void evaluateTriggers(RequiredActionContext context) {
+
+    }
+
+
+    @Override
+    public void requiredActionChallenge(RequiredActionContext context) {
+        Response challenge = TextChallenge.challenge(context)
+                .header()
+                .param("accept")
+                .label("console-accept-terms")
+                .message("termsPlainText");
+        context.challenge(challenge);
+    }
+
+    @Override
+    public void processAction(RequiredActionContext context) {
+        String accept = context.getHttpRequest().getDecodedFormParameters().getFirst("accept");
+
+        String yes = context.form().getMessage("console-accept");
+
+        if (!accept.equals(yes)) {
+            context.getUser().removeAttribute(USER_ATTRIBUTE);
+            requiredActionChallenge(context);
+            return;
+        }
+
+        context.getUser().setAttribute(USER_ATTRIBUTE, Arrays.asList(Integer.toString(Time.currentTime())));
+
+        context.success();
+    }
+
+    @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
new file mode 100755
index 0000000..5a9aaf9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdatePassword.java
@@ -0,0 +1,103 @@
+/*
+ * 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.requiredactions;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.authentication.*;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.*;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.validation.Validation;
+
+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 ConsoleUpdatePassword extends UpdatePassword implements RequiredActionProvider, RequiredActionFactory {
+    public static final ConsoleUpdatePassword SINGLETON = new ConsoleUpdatePassword();
+
+    private static final Logger logger = Logger.getLogger(ConsoleUpdatePassword.class);
+    public static final String PASSWORD_NEW = "password-new";
+    public static final String PASSWORD_CONFIRM = "password-confirm";
+
+     protected TextChallenge challenge(RequiredActionContext context) {
+        return TextChallenge.challenge(context)
+                .header()
+                .param(PASSWORD_NEW)
+                .label("console-new-password")
+                .mask(true)
+                .param(PASSWORD_CONFIRM)
+                .label("console-confirm-password")
+                .mask(true)
+                .challenge();
+    }
+
+
+
+    @Override
+    public void requiredActionChallenge(RequiredActionContext context) {
+        context.challenge(
+                challenge(context).message("console-update-password"));
+    }
+
+    @Override
+    public void processAction(RequiredActionContext context) {
+        EventBuilder event = context.getEvent();
+        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+        event.event(EventType.UPDATE_PASSWORD);
+        String passwordNew = formData.getFirst(PASSWORD_NEW);
+        String passwordConfirm = formData.getFirst(PASSWORD_CONFIRM);
+
+        EventBuilder errorEvent = event.clone().event(EventType.UPDATE_PASSWORD_ERROR)
+                .client(context.getAuthenticationSession().getClient())
+                .user(context.getAuthenticationSession().getAuthenticatedUser());
+
+        if (Validation.isBlank(passwordNew)) {
+            context.challenge(challenge(context).message(Messages.MISSING_PASSWORD));
+            errorEvent.error(Errors.PASSWORD_MISSING);
+            return;
+        } else if (!passwordNew.equals(passwordConfirm)) {
+            context.challenge(challenge(context).message(Messages.NOTMATCH_PASSWORD));
+            errorEvent.error(Errors.PASSWORD_CONFIRM_ERROR);
+            return;
+        }
+
+        try {
+            context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), UserCredentialModel.password(passwordNew, false));
+            context.success();
+        } catch (ModelException me) {
+            errorEvent.detail(Details.REASON, me.getMessage()).error(Errors.PASSWORD_REJECTED);
+            context.challenge(challenge(context).text(me.getMessage()));
+            return;
+        } catch (Exception ape) {
+            errorEvent.detail(Details.REASON, ape.getMessage()).error(Errors.PASSWORD_REJECTED);
+            context.challenge(challenge(context).text(ape.getMessage()));
+            return;
+        }
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateProfile.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateProfile.java
new file mode 100644
index 0000000..a5fb335
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateProfile.java
@@ -0,0 +1,94 @@
+/*
+ * 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.requiredactions;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionFactory;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.FormMessage;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.resources.AttributeFormDataProcessor;
+import org.keycloak.services.validation.Validation;
+
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ConsoleUpdateProfile implements RequiredActionProvider, RequiredActionFactory {
+    public static final ConsoleUpdateProfile SINGLETON = new ConsoleUpdateProfile();
+
+    @Override
+    public void evaluateTriggers(RequiredActionContext context) {
+    }
+
+    @Override
+    public void requiredActionChallenge(RequiredActionContext context) {
+        // do nothing right now.  I think this behavior is ok.  We just defer this action until a browser login happens.
+        context.ignore();
+    }
+
+    @Override
+    public void processAction(RequiredActionContext context) {
+        throw new RuntimeException("Should be unreachable");
+
+    }
+
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public RequiredActionProvider create(KeycloakSession session) {
+        return this;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public String 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
new file mode 100644
index 0000000..ec14bfa
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateTotp.java
@@ -0,0 +1,144 @@
+/*
+ * 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.requiredactions;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionFactory;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.authentication.TextChallenge;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.forms.login.freemarker.model.TotpBean;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.CredentialValidation;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.validation.Validation;
+
+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 ConsoleUpdateTotp implements RequiredActionProvider, RequiredActionFactory {
+    public static final ConsoleUpdateTotp SINGLETON = new ConsoleUpdateTotp();
+
+    @Override
+    public void evaluateTriggers(RequiredActionContext context) {
+    }
+    @Override
+    public void requiredActionChallenge(RequiredActionContext context) {
+        TotpBean totpBean = new TotpBean(context.getSession(), context.getRealm(), context.getUser(), context.getUriInfo().getRequestUriBuilder());
+        String totpSecret = totpBean.getTotpSecret();
+        context.getAuthenticationSession().setAuthNote("totpSecret", totpSecret);
+        Response challenge = challenge(context).form()
+                .setAttribute("totp", totpBean)
+                .createForm("login-config-totp-text.ftl");
+        context.challenge(challenge);
+    }
+
+    protected TextChallenge challenge(RequiredActionContext context) {
+        return TextChallenge.challenge(context)
+                .header()
+                .param("totp")
+                .label("console-otp")
+                .challenge();
+    }
+
+    @Override
+    public void processAction(RequiredActionContext context) {
+        EventBuilder event = context.getEvent();
+        event.event(EventType.UPDATE_TOTP);
+        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+        String totp = formData.getFirst("totp");
+        String totpSecret = context.getAuthenticationSession().getAuthNote("totpSecret");
+
+        if (Validation.isBlank(totp)) {
+            context.challenge(
+                    challenge(context).message(Messages.MISSING_TOTP)
+            );
+            return;
+        } else if (!CredentialValidation.validOTP(context.getRealm(), totp, totpSecret)) {
+            context.challenge(
+                    challenge(context).message(Messages.INVALID_TOTP)
+            );
+            return;
+        }
+
+        UserCredentialModel credentials = new UserCredentialModel();
+        credentials.setType(context.getRealm().getOTPPolicy().getType());
+        credentials.setValue(totpSecret);
+        context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), credentials);
+
+
+        // if type is HOTP, to update counter we execute validation based on supplied token
+        UserCredentialModel cred = new UserCredentialModel();
+        cred.setType(context.getRealm().getOTPPolicy().getType());
+        cred.setValue(totp);
+        context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(), cred);
+
+        context.getAuthenticationSession().removeAuthNote("totpSecret");
+        context.success();
+    }
+
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public RequiredActionProvider create(KeycloakSession session) {
+        return this;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public String 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
new file mode 100755
index 0000000..8eddf37
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleVerifyEmail.java
@@ -0,0 +1,178 @@
+/*
+ * 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.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.TextChallenge;
+import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
+import org.keycloak.common.util.RandomString;
+import org.keycloak.common.util.Time;
+import org.keycloak.email.EmailException;
+import org.keycloak.email.EmailTemplateProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.*;
+import org.keycloak.services.Urls;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.validation.Validation;
+import org.keycloak.sessions.AuthenticationSessionCompoundId;
+import org.keycloak.sessions.AuthenticationSessionModel;
+
+import javax.ws.rs.core.*;
+import java.net.URI;
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+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 static final ConsoleVerifyEmail SINGLETON = new ConsoleVerifyEmail();
+    private static final Logger logger = Logger.getLogger(ConsoleVerifyEmail.class);
+    @Override
+    public void evaluateTriggers(RequiredActionContext context) {
+        if (context.getRealm().isVerifyEmail() && !context.getUser().isEmailVerified()) {
+            context.getUser().addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
+            logger.debug("User is required to verify email");
+        }
+    }
+
+    @Override
+    public void requiredActionChallenge(RequiredActionContext context) {
+        AuthenticationSessionModel authSession = context.getAuthenticationSession();
+
+        if (context.getUser().isEmailVerified()) {
+            context.success();
+            authSession.removeAuthNote(Constants.VERIFY_EMAIL_KEY);
+            return;
+        }
+
+        String email = context.getUser().getEmail();
+        if (Validation.isBlank(email)) {
+            context.ignore();
+            return;
+        }
+
+        Response challenge = sendVerifyEmail(context);
+        context.challenge(challenge);
+    }
+
+
+    @Override
+    public void processAction(RequiredActionContext context) {
+        EventBuilder event = context.getEvent().clone().event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, context.getUser().getEmail());
+        String code = context.getAuthenticationSession().getAuthNote(Constants.VERIFY_EMAIL_CODE);
+        if (code == null) {
+            requiredActionChallenge(context);
+            return;
+        }
+
+        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+        String emailCode = formData.getFirst(EMAIL_CODE);
+
+        if (!code.equals(emailCode)) {
+            context.challenge(
+                    challenge(context).message(Messages.INVALID_CODE)
+            );
+            event.error(Errors.INVALID_CODE);
+            return;
+        }
+        event.success();
+        context.success();
+    }
+
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public RequiredActionProvider create(KeycloakSession session) {
+        return this;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public String 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()
+                .param(EMAIL_CODE)
+                .label("console-email-code")
+                .challenge();
+    }
+
+     private Response sendVerifyEmail(RequiredActionContext context) throws UriBuilderException, IllegalArgumentException {
+        KeycloakSession session = context.getSession();
+        UserModel user = context.getUser();
+        AuthenticationSessionModel authSession = context.getAuthenticationSession();
+        EventBuilder event = context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail());
+        String code = RandomString.randomCode(8);
+        authSession.setAuthNote(Constants.VERIFY_EMAIL_CODE, code);
+        RealmModel realm = session.getContext().getRealm();
+
+        Map<String, Object> attributes = new HashMap<>();
+        attributes.put("code", code);
+
+        try {
+            session
+              .getProvider(EmailTemplateProvider.class)
+              .setAuthenticationSession(authSession)
+              .setRealm(realm)
+              .setUser(user)
+              .send("emailVerificationSubject", "email-verification-with-code.ftl", attributes);
+            event.success();
+        } catch (EmailException e) {
+            logger.error("Failed to send verification email", e);
+            event.error(Errors.EMAIL_SEND_FAILED);
+        }
+
+        return challenge(context).text(context.form().getMessage("console-verify-email", user.getEmail()));
+    }
+}
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 f4a1566..961095c 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
@@ -18,6 +18,7 @@
 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;
@@ -65,12 +66,20 @@ 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 1e9f37a..65ddc9e 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
@@ -22,6 +22,7 @@ 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.common.util.Time;
 import org.keycloak.credential.CredentialModel;
 import org.keycloak.credential.CredentialProvider;
@@ -74,6 +75,10 @@ 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);
@@ -82,6 +87,10 @@ 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);
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 3ed1f12..f790626 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java
@@ -18,6 +18,7 @@
 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;
@@ -48,6 +49,10 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
 
     @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);
@@ -55,6 +60,10 @@ 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();
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 e85ec7e..e715933 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
@@ -21,6 +21,7 @@ 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.events.EventBuilder;
 import org.keycloak.events.EventType;
 import org.keycloak.models.KeycloakSession;
@@ -45,6 +46,10 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
 
     @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);
@@ -57,6 +62,10 @@ 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();
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 969f350..9333905 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
@@ -23,6 +23,7 @@ import org.keycloak.authentication.RequiredActionContext;
 import org.keycloak.authentication.RequiredActionFactory;
 import org.keycloak.authentication.RequiredActionProvider;
 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;
@@ -56,6 +57,10 @@ 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()) {
@@ -88,6 +93,10 @@ 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
diff --git a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
index c55a585..e3e447f 100755
--- a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
+++ b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
@@ -192,8 +192,9 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
         }
     }
 
-    protected void send(String subjectKey, String template, Map<String, Object> attributes) throws EmailException {
-        send(subjectKey, Collections.emptyList(), template, attributes);
+    @Override
+    public void send(String subjectFormatKey, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException {
+        send(subjectFormatKey, Collections.emptyList(), bodyTemplate, bodyAttributes);
     }
 
     protected EmailTemplate processTemplate(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
@@ -229,9 +230,10 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
         return session.theme().getTheme(Theme.Type.EMAIL);
     }
 
-    protected void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
+    @Override
+    public void send(String subjectFormatKey, List<Object> subjectAttributes, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException {
         try {
-            EmailTemplate email = processTemplate(subjectKey, subjectAttributes, template, attributes);
+            EmailTemplate email = processTemplate(subjectFormatKey, subjectAttributes, bodyTemplate, bodyAttributes);
             send(email.getSubject(), email.getTextBody(), email.getHtmlBody());
         } catch (EmailException e) {
             throw e;
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 cf480fc..3d0f52d 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
@@ -332,9 +332,25 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
         Properties messagesBundle = handleThemeResources(theme, locale);
         FormMessage msg = new FormMessage(null, message);
         return formatMessage(msg, messagesBundle, locale);
+    }
 
+    @Override
+    public String getMessage(String message, String... parameters) {
+        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(message, parameters);
+        return formatMessage(msg, messagesBundle, locale);
     }
-    
+
+
     /**
      * Create common attributes used in all templates.
      * 
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index cf0bb4a..65c66e2 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -371,6 +371,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
         if (request.getResponseMode() != null) authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
         if (request.getClaims()!= null) authenticationSession.setClientNote(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims());
         if (request.getAcr() != null) authenticationSession.setClientNote(OIDCLoginProtocol.ACR_PARAM, request.getAcr());
+        if (request.getDisplay() != null) authenticationSession.setClientNote(OAuth2Constants.DISPLAY, request.getDisplay());
 
         // https://tools.ietf.org/html/rfc7636#section-4
         if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java
index 29edb03..083c2c3 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java
@@ -32,6 +32,7 @@ public class AuthorizationEndpointRequest {
     String state;
     String scope;
     String loginHint;
+    String display;
     String prompt;
     String nonce;
     Integer maxAge;
@@ -111,4 +112,7 @@ public class AuthorizationEndpointRequest {
         return codeChallengeMethod;
     }
 
+    public String getDisplay() {
+        return display;
+    }
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java
index 90160ee..d6cb1b7 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java
@@ -18,6 +18,7 @@
 package org.keycloak.protocol.oidc.endpoints.request;
 
 import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
 import org.keycloak.constants.AdapterConstants;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 
@@ -91,6 +92,7 @@ abstract class AuthzEndpointRequestParser {
         request.maxAge = replaceIfNotNull(request.maxAge, getIntParameter(OIDCLoginProtocol.MAX_AGE_PARAM));
         request.claims = replaceIfNotNull(request.claims, getParameter(OIDCLoginProtocol.CLAIMS_PARAM));
         request.acr = replaceIfNotNull(request.acr, getParameter(OIDCLoginProtocol.ACR_PARAM));
+        request.display = replaceIfNotNull(request.display, getParameter(OAuth2Constants.DISPLAY));
 
         // https://tools.ietf.org/html/rfc7636#section-6.1
         request.codeChallenge = replaceIfNotNull(request.codeChallenge, getParameter(OIDCLoginProtocol.CODE_CHALLENGE_PARAM));
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 05f9f4c..38d2489 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -776,8 +776,15 @@ public class TokenEndpoint {
         String audience = formParams.getFirst(OAuth2Constants.AUDIENCE);
         if (audience != null) {
             targetClient = realm.getClientByClientId(audience);
+            if (targetClient == null) {
+                event.detail(Details.REASON, "audience not found");
+                event.error(Errors.CLIENT_NOT_FOUND);
+                throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, "Audience not found", Response.Status.BAD_REQUEST);
+
+            }
         }
 
+
         if (targetClient.isConsentRequired()) {
             event.detail(Details.REASON, "audience requires consent");
             event.error(Errors.CONSENT_DENIED);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
index 36d93fa..4da336c 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
@@ -71,6 +71,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
     public static final String MAX_AGE_PARAM = OAuth2Constants.MAX_AGE;
     public static final String PROMPT_PARAM = OAuth2Constants.PROMPT;
     public static final String LOGIN_HINT_PARAM = "login_hint";
+    public static final String DISPLAY_PARAM = "display";
     public static final String REQUEST_PARAM = "request";
     public static final String REQUEST_URI_PARAM = "request_uri";
     public static final String UI_LOCALES_PARAM = OAuth2Constants.UI_LOCALES_PARAM;
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 803e778..a426bc2 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -761,6 +761,11 @@ public class AuthenticationManager {
         uriBuilder.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId());
         uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId());
 
+        if (uriInfo.getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
+            uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, authSession.getParentSession().getId());
+
+        }
+
         URI redirect = uriBuilder.build(realm.getName());
         return Response.status(302).location(redirect).build();
 
diff --git a/services/src/main/java/org/keycloak/utils/TotpUtils.java b/services/src/main/java/org/keycloak/utils/TotpUtils.java
index 67ff697..d076735 100644
--- a/services/src/main/java/org/keycloak/utils/TotpUtils.java
+++ b/services/src/main/java/org/keycloak/utils/TotpUtils.java
@@ -45,6 +45,12 @@ public class TotpUtils {
         return sb.toString();
     }
 
+    public static String decode(String totpSecretEncoded) {
+        String encoded = totpSecretEncoded.replace(" ", "");
+        byte[] bytes = Base32.decode(encoded);
+        return new String(bytes);
+    }
+
     public static String qrCode(String totpSecret, RealmModel realm, UserModel user) {
         try {
             String keyUri = realm.getOTPPolicy().getKeyURI(realm, user, totpSecret);
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 76b7507..ee29448 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,4 +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
+org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory
diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index 49babb7..404fc82 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -228,6 +228,13 @@
                                     <type>zip</type>
                                     <outputDirectory>${containers.home}</outputDirectory>
                                 </artifactItem>
+                                <artifactItem>
+                                    <groupId>org.keycloak</groupId>
+                                    <artifactId>kcinit-dist</artifactId>
+                                    <version>${project.version}</version>
+                                    <type>zip</type>
+                                    <outputDirectory>${containers.home}</outputDirectory>
+                                </artifactItem>
                             </artifactItems>
                         </configuration>
                     </execution>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java
index b5476b5..408dc35 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java
@@ -1,5 +1,6 @@
 package org.keycloak.testsuite.cli.exec;
 
+import org.keycloak.client.admin.cli.util.OsUtil;
 import org.keycloak.testsuite.cli.OsArch;
 import org.keycloak.testsuite.cli.OsUtils;
 
@@ -177,6 +178,7 @@ public abstract class AbstractExec {
         return new String(stdout.toByteArray());
     }
 
+
     public InputStream stderr() {
         return new ByteArrayInputStream(stderr.toByteArray());
     }
@@ -224,6 +226,22 @@ public abstract class AbstractExec {
         throw new RuntimeException("Timed while waiting for content to appear in stdout");
     }
 
+    public void waitForStderr(String content) {
+        long start = System.currentTimeMillis();
+        while (System.currentTimeMillis() - start < waitTimeout) {
+            if (stderrString().indexOf(content) != -1) {
+                return;
+            }
+            try {
+                Thread.sleep(10);
+            } catch (InterruptedException e) {
+                throw new RuntimeException("Interrupted ...", e);
+            }
+        }
+
+        throw new RuntimeException("Timed while waiting for content to appear in stdout");
+    }
+
     public void sendToStdin(String s) {
         if (stdin instanceof InteractiveInputStream) {
             ((InteractiveInputStream) stdin).pushBytes(s.getBytes());
@@ -232,6 +250,10 @@ public abstract class AbstractExec {
         }
     }
 
+    public void sendLine(String s) {
+        sendToStdin(s + OsUtil.EOL);
+    }
+
 
 
     static void copyStream(InputStream is, OutputStream os) throws IOException {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcinitExec.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcinitExec.java
new file mode 100644
index 0000000..99ae7ef
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcinitExec.java
@@ -0,0 +1,58 @@
+package org.keycloak.testsuite.cli;
+
+import org.keycloak.testsuite.cli.exec.AbstractExec;
+import org.keycloak.testsuite.cli.exec.AbstractExecBuilder;
+
+import java.io.InputStream;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcinitExec extends AbstractExec {
+
+    public static final String WORK_DIR = System.getProperty("user.dir") + "/target/containers/kcinit";
+
+    public static final String CMD = OS_ARCH.isWindows() ? "kcinit.bat" : "kcinit";
+
+    private KcinitExec(String workDir, String argsLine, InputStream stdin) {
+        this(workDir, argsLine, null, stdin);
+    }
+
+    private KcinitExec(String workDir, String argsLine, String env, InputStream stdin) {
+        super(workDir, argsLine, env, stdin);
+    }
+
+    @Override
+    public String getCmd() {
+        return "./" + CMD;
+    }
+
+    public static KcinitExec.Builder newBuilder() {
+        return (KcinitExec.Builder) new KcinitExec.Builder().workDir(WORK_DIR);
+    }
+
+    public static KcinitExec execute(String args) {
+        return newBuilder()
+                .argsLine(args)
+                .execute();
+    }
+
+    public static class Builder extends AbstractExecBuilder<KcinitExec> {
+
+        @Override
+        public KcinitExec execute() {
+            KcinitExec exe = new KcinitExec(workDir, argsLine, env, stdin);
+            exe.dumpStreams = dumpStreams;
+            exe.execute();
+            return exe;
+        }
+
+        @Override
+        public KcinitExec executeAsync() {
+            KcinitExec exe = new KcinitExec(workDir, argsLine, env, stdin);
+            exe.dumpStreams = dumpStreams;
+            exe.executeAsync();
+            return exe;
+        }
+    }
+}
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
new file mode 100644
index 0000000..e54aee7
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/KcinitTest.java
@@ -0,0 +1,499 @@
+/*
+ * 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.cli;
+
+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.admin.client.resource.UserResource;
+import org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory;
+import org.keycloak.authentication.requiredactions.TermsAndConditions;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.credential.CredentialModel;
+import org.keycloak.models.*;
+import org.keycloak.models.utils.TimeBasedOTP;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
+import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
+import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
+import org.keycloak.services.resources.admin.permissions.AdminPermissions;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+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.GreenMailRule;
+import org.keycloak.testsuite.util.MailUtils;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.util.JsonSerialization;
+import org.keycloak.utils.TotpUtils;
+
+import javax.mail.internet.MimeMessage;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Test that clients can override auth flows
+ *
+ * @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
+ */
+public class KcinitTest extends AbstractTestRealmKeycloakTest {
+
+    public static final String KCINIT_CLIENT = "kcinit";
+    public static final String APP = "app";
+    public static final String UNAUTHORIZED_APP = "unauthorized_app";
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+
+    @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() {
+        RequiredActionProviderRepresentation rep = adminClient.realm("test").flows().getRequiredAction("terms_and_conditions");
+        rep.setEnabled(true);
+        adminClient.realm("test").flows().updateRequiredAction("terms_and_conditions", rep);
+
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+
+            ClientModel client = session.realms().getClientByClientId("kcinit", realm);
+            if (client != null) {
+                return;
+            }
+
+            ClientModel kcinit = realm.addClient(KCINIT_CLIENT);
+            kcinit.setSecret("password");
+            kcinit.setEnabled(true);
+            kcinit.addRedirectUri("urn:ietf:wg:oauth:2.0:oob");
+            kcinit.setPublicClient(false);
+
+            ClientModel app = realm.addClient(APP);
+            app.setSecret("password");
+            app.setEnabled(true);
+            app.setPublicClient(false);
+
+            ClientModel unauthorizedApp = realm.addClient(UNAUTHORIZED_APP);
+            unauthorizedApp.setSecret("password");
+            unauthorizedApp.setEnabled(true);
+            unauthorizedApp.setPublicClient(false);
+
+            // permission for client to client exchange to "target" client
+            AdminPermissionManagement management = AdminPermissions.management(session, realm);
+            management.clients().setPermissionsEnabled(app, true);
+            ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
+            clientRep.setName("to");
+            clientRep.addClient(kcinit.getId());
+            ResourceServer server = management.realmResourceServer();
+            Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
+            management.clients().exchangeToPermission(app).addAssociatedPolicy(clientPolicy);
+            PasswordPolicy policy = realm.getPasswordPolicy();
+            policy = PasswordPolicy.parse(session, "hashIterations(1)");
+            realm.setPasswordPolicy(policy);
+
+            UserModel user = session.users().addUser(realm, "bburke");
+            session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
+            user.setEnabled(true);
+            user.setEmail("patriot1burke@gmail.com");
+            user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+            user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
+            user.addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
+            user.addRequiredAction(TermsAndConditions.PROVIDER_ID);
+            user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
+
+            user = session.users().addUser(realm, "wburke");
+            session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
+            user.setEnabled(true);
+        });
+    }
+
+    //@Test
+    public void testDemo() throws Exception {
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            Map<String, String> smtp = new HashMap<>();
+            smtp.put("host", "smtp.gmail.com");
+            smtp.put("port", "465");
+            smtp.put("fromDisplayName", "Keycloak SSO");
+            smtp.put("from", "****");
+            smtp.put("replyToDisplayName", "Keycloak no-reply");
+            smtp.put("replyTo", "reply-to@keycloak.org");
+            smtp.put("ssl", "true");
+            smtp.put("auth", "true");
+            smtp.put("user", "****");
+            smtp.put("password", "****");
+            realm.setSmtpConfig(smtp);
+
+        });
+
+        Thread.sleep(10000000);
+    }
+
+    @Test
+    public void testBadCommand() throws Exception {
+        KcinitExec exe = KcinitExec.execute("covfefe");
+        Assert.assertEquals(1, exe.exitCode());
+        Assert.assertEquals("stderr first line", "Unknown command: covfefe", exe.stderrLines().get(0));
+    }
+
+    //@Test
+    public void testInstall() throws Exception {
+        KcinitExec exe = KcinitExec.execute("uninstall");
+        Assert.assertEquals(0, exe.exitCode());
+
+        exe = KcinitExec.newBuilder()
+                .argsLine("install")
+                .executeAsync();
+        exe.waitForStderr("(y/n):");
+        exe.sendLine("n");
+        exe.waitForStderr("Authentication server URL [http://localhost:8080/auth]:");
+        exe.sendLine(OAuthClient.AUTH_SERVER_ROOT);
+        exe.waitForStderr("Name of realm [master]:");
+        exe.sendLine("test");
+        exe.waitForStderr("client id [kcinit]:");
+        exe.sendLine("");
+        exe.waitForStderr("client secret [none]:");
+        exe.sendLine("password");
+        exe.waitCompletion();
+        Assert.assertEquals(0, exe.exitCode());
+    }
+
+    @Test
+    public void testBasic() throws Exception {
+        testInstall();
+        // login
+        KcinitExec exe = KcinitExec.newBuilder()
+                .argsLine("")
+                .executeAsync();
+        exe.waitForStderr("Username:");
+        exe.sendLine("wburke");
+        exe.waitForStderr("Password:");
+        exe.sendLine("password");
+        exe.waitForStderr("Login successful");
+        exe.waitCompletion();
+        Assert.assertEquals(0, exe.exitCode());
+        Assert.assertEquals(0, exe.stdoutLines().size());
+
+        exe = KcinitExec.execute("token");
+        Assert.assertEquals(0, exe.exitCode());
+        Assert.assertEquals(1, exe.stdoutLines().size());
+        String token = exe.stdoutLines().get(0).trim();
+        //System.out.println("token: " + token);
+        String introspect = oauth.introspectAccessTokenWithClientCredential("kcinit", "password", token);
+        Map json = JsonSerialization.readValue(introspect, Map.class);
+        Assert.assertTrue(json.containsKey("active"));
+        Assert.assertTrue((Boolean)json.get("active"));
+        //System.out.println("introspect");
+        //System.out.println(introspect);
+
+        exe = KcinitExec.execute("token app");
+        Assert.assertEquals(0, exe.exitCode());
+        Assert.assertEquals(1, exe.stdoutLines().size());
+        String appToken = exe.stdoutLines().get(0).trim();
+        Assert.assertFalse(appToken.equals(token));
+        //System.out.println("token: " + token);
+        introspect = oauth.introspectAccessTokenWithClientCredential("kcinit", "password", appToken);
+        json = JsonSerialization.readValue(introspect, Map.class);
+        Assert.assertTrue(json.containsKey("active"));
+        Assert.assertTrue((Boolean)json.get("active"));
+
+
+        exe = KcinitExec.execute("token badapp");
+        Assert.assertEquals(1, exe.exitCode());
+        Assert.assertEquals(0, exe.stdoutLines().size());
+        Assert.assertEquals(1, exe.stderrLines().size());
+        Assert.assertTrue(exe.stderrLines().get(0).contains("Failed to exchange token: invalid_client. Audience not found"));
+
+        exe = KcinitExec.execute("logout");
+        Assert.assertEquals(0, exe.exitCode());
+
+        introspect = oauth.introspectAccessTokenWithClientCredential("kcinit", "password", token);
+        json = JsonSerialization.readValue(introspect, Map.class);
+        Assert.assertTrue(json.containsKey("active"));
+        Assert.assertFalse((Boolean)json.get("active"));
+
+
+
+    }
+
+    @Test
+    public void testTerms() throws Exception {
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            UserModel user = session.users().getUserByUsername("wburke", realm);
+            user.addRequiredAction(TermsAndConditions.PROVIDER_ID);
+        });
+
+        testInstall();
+
+        KcinitExec exe = KcinitExec.newBuilder()
+                .argsLine("")
+                .executeAsync();
+        exe.waitForStderr("Username:");
+        exe.sendLine("wburke");
+        exe.waitForStderr("Password:");
+        exe.sendLine("password");
+        exe.waitForStderr("Accept Terms? [y/n]:");
+        exe.sendLine("y");
+        exe.waitForStderr("Login successful");
+        exe.waitCompletion();
+        Assert.assertEquals(0, exe.exitCode());
+        Assert.assertEquals(0, exe.stdoutLines().size());
+    }
+
+
+    @Test
+    public void testUpdateProfile() throws Exception {
+        // expects that updateProfile is a passthrough
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            UserModel user = session.users().getUserByUsername("wburke", realm);
+            user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
+        });
+
+        try {
+            testInstall();
+
+            //Thread.sleep(100000000);
+
+            KcinitExec exe = KcinitExec.newBuilder()
+                    .argsLine("")
+                    .executeAsync();
+            try {
+                exe.waitForStderr("Username:");
+                exe.sendLine("wburke");
+                exe.waitForStderr("Password:");
+                exe.sendLine("password");
+
+                exe.waitForStderr("Login successful");
+                exe.waitCompletion();
+                Assert.assertEquals(0, exe.exitCode());
+                Assert.assertEquals(0, exe.stdoutLines().size());
+            } catch (Exception ex) {
+                System.out.println(exe.stderrString());
+                throw ex;
+            }
+        } finally {
+
+            testingClient.server().run(session -> {
+                RealmModel realm = session.realms().getRealmByName("test");
+                UserModel user = session.users().getUserByUsername("wburke", realm);
+                user.removeRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
+            });
+        }
+    }
+
+
+    @Test
+    public void testUpdatePassword() throws Exception {
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            UserModel user = session.users().getUserByUsername("wburke", realm);
+            user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+        });
+
+        try {
+            testInstall();
+
+            KcinitExec exe = KcinitExec.newBuilder()
+                    .argsLine("")
+                    .executeAsync();
+            exe.waitForStderr("Username:");
+            exe.sendLine("wburke");
+            exe.waitForStderr("Password:");
+            exe.sendLine("password");
+            exe.waitForStderr("New Password:");
+            exe.sendLine("pw");
+            exe.waitForStderr("Confirm Password:");
+            exe.sendLine("pw");
+            exe.waitForStderr("Login successful");
+            exe.waitCompletion();
+            Assert.assertEquals(0, exe.exitCode());
+            Assert.assertEquals(0, exe.stdoutLines().size());
+
+            exe = KcinitExec.newBuilder()
+                    .argsLine("-f")
+                    .executeAsync();
+            exe.waitForStderr("Username:");
+            exe.sendLine("wburke");
+            exe.waitForStderr("Password:");
+            exe.sendLine("pw");
+            exe.waitForStderr("Login successful");
+            exe.waitCompletion();
+            Assert.assertEquals(0, exe.exitCode());
+            Assert.assertEquals(0, exe.stdoutLines().size());
+
+            exe = KcinitExec.execute("logout");
+            Assert.assertEquals(0, exe.exitCode());
+        } finally {
+
+            testingClient.server().run(session -> {
+                RealmModel realm = session.realms().getRealmByName("test");
+                UserModel user = session.users().getUserByUsername("wburke", realm);
+                session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
+            });
+        }
+
+    }
+
+    protected TimeBasedOTP totp = new TimeBasedOTP();
+
+
+    @Test
+    public void testConfigureTOTP() throws Exception {
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            UserModel user = session.users().getUserByUsername("wburke", realm);
+            user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
+        });
+
+        try {
+
+            testInstall();
+
+            KcinitExec exe = KcinitExec.newBuilder()
+                    .argsLine("")
+                    .executeAsync();
+            exe.waitForStderr("Username:");
+            exe.sendLine("wburke");
+            exe.waitForStderr("Password:");
+            exe.sendLine("password");
+            exe.waitForStderr("One Time Password:");
+
+            Pattern p = Pattern.compile("Open the application and enter the key\\s+(.+)\\s+Use the following configuration values");
+            //Pattern p = Pattern.compile("Open the application and enter the key");
+
+            String stderr = exe.stderrString();
+            //System.out.println("***************");
+            //System.out.println(stderr);
+            //System.out.println("***************");
+            Matcher m = p.matcher(stderr);
+            Assert.assertTrue(m.find());
+            String otpSecret = m.group(1).trim();
+
+            //System.out.println("***************");
+            //System.out.println(otpSecret);
+            //System.out.println("***************");
+
+            otpSecret = TotpUtils.decode(otpSecret);
+            String code = totp.generateTOTP(otpSecret);
+            //System.out.println("***************");
+            //System.out.println("code: " + code);
+            //System.out.println("***************");
+            exe.sendLine(code);
+            Thread.sleep(100);
+            //stderr = exe.stderrString();
+            //System.out.println("***************");
+            //System.out.println(stderr);
+            //System.out.println("***************");
+            exe.waitForStderr("Login successful");
+            exe.waitCompletion();
+            Assert.assertEquals(0, exe.exitCode());
+            Assert.assertEquals(0, exe.stdoutLines().size());
+
+
+            exe = KcinitExec.execute("logout");
+            Assert.assertEquals(0, exe.exitCode());
+
+            exe = KcinitExec.newBuilder()
+                    .argsLine("")
+                    .executeAsync();
+            exe.waitForStderr("Username:");
+            exe.sendLine("wburke");
+            exe.waitForStderr("Password:");
+            exe.sendLine("password");
+            exe.waitForStderr("One Time Password:");
+            exe.sendLine(totp.generateTOTP(otpSecret));
+            exe.waitForStderr("Login successful");
+            exe.waitCompletion();
+
+            exe = KcinitExec.execute("logout");
+            Assert.assertEquals(0, exe.exitCode());
+        } finally {
+            testingClient.server().run(session -> {
+                RealmModel realm = session.realms().getRealmByName("test");
+                UserModel user = session.users().getUserByUsername("wburke", realm);
+                session.userCredentialManager().disableCredentialType(realm, user, CredentialModel.OTP);
+            });
+        }
+
+
+    }
+
+    @Rule
+    public GreenMailRule greenMail = new GreenMailRule();
+
+    @Test
+    public void testVerifyEmail() throws Exception {
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            UserModel user = session.users().getUserByUsername("test-user@localhost", realm);
+            user.addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
+        });
+
+        testInstall();
+
+        KcinitExec exe = KcinitExec.newBuilder()
+                .argsLine("")
+                .executeAsync();
+        exe.waitForStderr("Username:");
+        exe.sendLine("test-user@localhost");
+        exe.waitForStderr("Password:");
+        exe.sendLine("password");
+        exe.waitForStderr("Email Code:");
+
+        Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+
+        MimeMessage message = greenMail.getReceivedMessages()[0];
+
+        String text = MailUtils.getBody(message).getText();
+        Assert.assertTrue(text.contains("Please verify your email address by entering in the following code."));
+        String code = text.substring("Please verify your email address by entering in the following code.".length()).trim();
+
+        exe.sendLine(code);
+
+        exe.waitForStderr("Login successful");
+        exe.waitCompletion();
+        Assert.assertEquals(0, exe.exitCode());
+        Assert.assertEquals(0, exe.stdoutLines().size());
+
+
+        exe = KcinitExec.execute("logout");
+        Assert.assertEquals(0, exe.exitCode());
+    }
+
+
+
+
+}
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
index 72024f6..5855979 100644
--- 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
@@ -24,29 +24,21 @@ 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.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory;
 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;
@@ -55,9 +47,6 @@ 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;
@@ -120,7 +109,7 @@ public class ChallengeFlowTest extends AbstractTestRealmKeycloakTest {
             AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
             execution.setParentFlow(browser.getId());
             execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
-            execution.setAuthenticator(CliUsernamePasswordAuthenticatorFactory.PROVIDER_ID);
+            execution.setAuthenticator(ConsoleUsernamePasswordAuthenticatorFactory.PROVIDER_ID);
             execution.setPriority(10);
             execution.setAuthenticatorFlow(false);
             realm.addAuthenticatorExecution(execution);
diff --git a/themes/src/main/resources/theme/base/email/html/email-verification-with-code.ftl b/themes/src/main/resources/theme/base/email/html/email-verification-with-code.ftl
new file mode 100644
index 0000000..b4a01c9
--- /dev/null
+++ b/themes/src/main/resources/theme/base/email/html/email-verification-with-code.ftl
@@ -0,0 +1,5 @@
+<html>
+<body>
+${msg("emailVerificationBodyCodeHtml",code)?no_esc}
+</body>
+</html>
diff --git a/themes/src/main/resources/theme/base/email/messages/messages_en.properties b/themes/src/main/resources/theme/base/email/messages/messages_en.properties
index e04e947..b2fd0c0 100755
--- a/themes/src/main/resources/theme/base/email/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/email/messages/messages_en.properties
@@ -45,3 +45,7 @@ linkExpirationFormatter.timePeriodUnit.hours=hours
 linkExpirationFormatter.timePeriodUnit.hours.1=hour
 linkExpirationFormatter.timePeriodUnit.days=days
 linkExpirationFormatter.timePeriodUnit.days.1=day
+
+emailVerificationBodyCode=Please verify your email address by entering in the following code.\n\n{0}\n\n.
+emailVerificationBodyCodeHtml=<p>Please verify your email address by entering in the following code.</p><p><b>{0}</b></p>
+
diff --git a/themes/src/main/resources/theme/base/email/text/email-verification-with-code.ftl b/themes/src/main/resources/theme/base/email/text/email-verification-with-code.ftl
new file mode 100644
index 0000000..4ffb7d8
--- /dev/null
+++ b/themes/src/main/resources/theme/base/email/text/email-verification-with-code.ftl
@@ -0,0 +1,2 @@
+<#ftl output_format="plainText">
+${msg("emailVerificationBodyCode",code)}
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/login/login-config-totp-text.ftl b/themes/src/main/resources/theme/base/login/login-config-totp-text.ftl
new file mode 100755
index 0000000..22360f3
--- /dev/null
+++ b/themes/src/main/resources/theme/base/login/login-config-totp-text.ftl
@@ -0,0 +1,27 @@
+<#ftl output_format="plainText">
+${msg("loginTotpIntro")}
+
+${msg("loginTotpStep1")}
+
+<#list totp.policy.supportedApplications as app>
+* ${app}
+</#list>
+
+${msg("loginTotpManualStep2")}
+
+    ${totp.totpSecretEncoded}
+
+
+${msg("loginTotpManualStep3")}
+
+- ${msg("loginTotpType")}: ${msg("loginTotp." + totp.policy.type)}
+- ${msg("loginTotpAlgorithm")}: ${totp.policy.getAlgorithmKey()}
+- ${msg("loginTotpDigits")}: ${totp.policy.digits}
+<#if totp.policy.type = "totp">
+- ${msg("loginTotpInterval")}: ${totp.policy.period}
+
+<#elseif totp.policy.type = "hotp">
+- ${msg("loginTotpCounter")}: ${totp.policy.initialCounter}
+
+</#if>
+
diff --git a/themes/src/main/resources/theme/base/login/login-verify-email-code-text.ftl b/themes/src/main/resources/theme/base/login/login-verify-email-code-text.ftl
new file mode 100644
index 0000000..87abcd7
--- /dev/null
+++ b/themes/src/main/resources/theme/base/login/login-verify-email-code-text.ftl
@@ -0,0 +1,2 @@
+<#ftl output_format="plainText">
+${msg("console-verify-email",email, code)}
\ No newline at end of file
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 3e61043..f53e7c4 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
@@ -39,6 +39,7 @@ codeErrorTitle=Error code\: {0}
 termsTitle=Terms and Conditions
 termsTitleHtml=Terms and Conditions
 termsText=<p>Terms and conditions to be defined</p>
+termsPlainText=Terms and conditions to be defined.
 
 recaptchaFailed=Invalid Recaptcha
 recaptchaNotConfigured=Recaptcha is required, but not configured
@@ -68,6 +69,7 @@ country=Country
 emailVerified=Email verified
 gssDelegationCredential=GSS Delegation Credential
 
+loginTotpIntro=You are required to set up a One Time Password generator to access this account
 loginTotpStep1=Install one of the following applications on your mobile
 loginTotpStep2=Open the application and scan the barcode
 loginTotpStep3=Enter the one-time code provided by the application and click Submit to finish the setup
@@ -280,3 +282,14 @@ noCertificate=[No Certificate]
 
 pageNotFound=Page not found
 internalServerError=An internal server error has occurred
+
+console-username=Username:
+console-password=Password:
+console-otp=One Time Password:
+console-new-password=New Password:
+console-confirm-password=Confirm Password:
+console-update-password=Update of your password is required.
+console-verify-email=You are required to verify your email address.  An email has been sent to {0} that contains a verification code.  Please enter this code into the input below.
+console-email-code=Email Code:
+console-accept-terms=Accept Terms? [y/n]:
+console-accept=y
\ No newline at end of file