diff --git a/adapters/oidc/cli-sso/login.sh b/adapters/oidc/cli-sso/login.sh
deleted file mode 100755
index ff33a015e9..0000000000
--- a/adapters/oidc/cli-sso/login.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/sh
-export KC_AUTH_SERVER=http://localhost:8080/auth
-export KC_REALM=master
-export KC_CLIENT=cli
-
-export KC_ACCESS_TOKEN=`java -DKEYCLOAK_AUTH_SERVER=$KC_AUTH_SERVER -DKEYCLOAK_REALM=$KC_REALM -DKEYCLOAK_CLIENT=$KC_CLIENT -jar target/keycloak-cli-sso-3.3.0.CR1-SNAPSHOT.jar login`
-
-
-
-
diff --git a/adapters/oidc/cli-sso/logout.sh b/adapters/oidc/cli-sso/logout.sh
deleted file mode 100644
index ca99f88b76..0000000000
--- a/adapters/oidc/cli-sso/logout.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/sh
-
-java -DKEYCLOAK_AUTH_SERVER=$KC_AUTH_SERVER -DKEYCLOAK_REALM=$KC_REALM -DKEYCLOAK_CLIENT=$KC_CLIENT -jar target/keycloak-cli-sso-3.3.0.CR1-SNAPSHOT.jar logout
-
-unset KC_ACCESS_TOKEN
-
-
-
-
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 0000000000..4693f62512
--- /dev/null
+++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KcinitDriver.java
@@ -0,0 +1,702 @@
+/*
+ * 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
+ *
+ * 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.
+ *
+ * 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
+ *
+ *
+ * @author Bill Burke
+ * @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 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 = args;
+
+
+ if (args.length == 0) {
+ printHelp();
+ return;
+ }
+
+ if (args[0].equalsIgnoreCase("token")) {
+ //System.err.println("executing token");
+ token();
+ } else if (args[0].equalsIgnoreCase("login")) {
+ login();
+ } 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 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(" login [-f] -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 creds = new HashMap<>();
+ creds.put("secret", secret);
+ config.setCredentials(creds);
+ } else {
+ config.setPublicClient(true);
+ }
+ return config;
+ }
+
+ private Map 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) 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();
+ this.args = Arrays.copyOfRange(this.args, 1, this.args.length);
+ for (String arg : args) {
+ 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);
+ }
+ }
+
+ 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/KeycloakCliSso.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java
deleted file mode 100644
index f2b1bdc121..0000000000
--- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * 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.keycloak.adapters.KeycloakDeployment;
-import org.keycloak.adapters.KeycloakDeploymentBuilder;
-import org.keycloak.adapters.ServerRequest;
-import org.keycloak.common.util.Time;
-import org.keycloak.representations.AccessTokenResponse;
-import org.keycloak.representations.adapters.config.AdapterConfig;
-import org.keycloak.util.JsonSerialization;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.file.Paths;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- *
- *
- *
- * @author Bill Burke
- * @version $Revision: 1 $
- */
-public class KeycloakCliSso {
-
- public void mainCmd(String[] args) throws Exception {
- if (args.length != 1) {
- printHelp();
- return;
- }
-
- if (args[0].equalsIgnoreCase("login")) {
- login();
- } else if (args[0].equalsIgnoreCase("login-manual")) {
- loginManual();
- }
- /*
- else if (args[0].equalsIgnoreCase("login-cli")) {
- loginCli();
- }
- */
- else if (args[0].equalsIgnoreCase("token")) {
- token();
- } else if (args[0].equalsIgnoreCase("logout")) {
- logout();
- } else if (args[0].equalsIgnoreCase("env")) {
- System.out.println(System.getenv().toString());
- } else {
- printHelp();
- }
- }
-
-
- public void printHelp() {
- System.err.println("Commands:");
- System.err.println(" login - login with desktop browser if available, otherwise do manual login. Output is access token.");
- System.err.println(" login-manual - manual login");
- //System.err.println(" login-cli - attempt Keycloak proprietary cli protocol. Otherwise do normal login");
- System.err.println(" token - print access token if logged in");
- System.err.println(" logout - logout.");
- System.exit(1);
- }
-
- public AdapterConfig getConfig() {
- String url = System.getProperty("KEYCLOAK_AUTH_SERVER");
- if (url == null) {
- System.err.println("KEYCLOAK_AUTH_SERVER property not set");
- System.exit(1);
- }
- String realm = System.getProperty("KEYCLOAK_REALM");
- if (realm == null) {
- System.err.println("KEYCLOAK_REALM property not set");
- System.exit(1);
-
- }
- String client = System.getProperty("KEYCLOAK_CLIENT");
- if (client == null) {
- System.err.println("KEYCLOAK_CLIENT property not set");
- System.exit(1);
- }
- String secret = System.getProperty("KEYCLOAK_CLIENT_SECRET");
-
-
-
- AdapterConfig config = new AdapterConfig();
- config.setAuthServerUrl(url);
- config.setRealm(realm);
- config.setResource(client);
- config.setSslRequired("external");
- if (secret != null) {
- Map creds = new HashMap<>();
- creds.put("secret", secret);
- config.setCredentials(creds);
- } else {
- config.setPublicClient(true);
- }
- return config;
- }
-
- public boolean checkToken(boolean outputToken) throws Exception {
- String token = getTokenResponse();
- if (token == null) return false;
-
-
- if (token != null) {
- Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
- if (m.find()) {
- String json = m.group(0);
- try {
- AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
- if (Time.currentTime() < tokenResponse.getExpiresIn()) {
- return true;
- }
- AdapterConfig config = getConfig();
- KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
- installed.refreshToken(tokenResponse.getRefreshToken());
- processResponse(installed, outputToken);
- return true;
- } catch (Exception e) {
- System.err.println("Error processing existing token");
- e.printStackTrace();
- }
-
- }
- }
- return false;
-
- }
-
- private String getTokenResponse() throws IOException {
- String token = null;
- File tokenFile = getTokenFilePath();
- if (tokenFile.exists()) {
- FileInputStream fis = new FileInputStream(tokenFile);
- byte[] data = new byte[(int) tokenFile.length()];
- fis.read(data);
- fis.close();
- token = new String(data, "UTF-8");
- }
- return token;
- }
-
- public void token() throws Exception {
- String token = getTokenResponse();
- if (token == null) {
- System.err.println("There is no token for client");
- System.exit(1);
- } else {
- Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
- if (m.find()) {
- String json = m.group(0);
- try {
- AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
- if (Time.currentTime() < tokenResponse.getExpiresIn()) {
- System.out.println(tokenResponse.getToken());
- return;
- } else {
- System.err.println("token in response file is expired");
- System.exit(1);
- }
- } catch (Exception e) {
- System.err.println("Failure processing token response file");
- e.printStackTrace();
- System.exit(1);
- }
- } else {
- System.err.println("Could not find json within token response file");
- System.exit(1);
- }
- }
- }
-
- public void login() throws Exception {
- if (checkToken(true)) return;
- AdapterConfig config = getConfig();
- KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
- installed.login();
- processResponse(installed, true);
- }
-
- public void loginCli() throws Exception {
- if (checkToken(false)) return;
- AdapterConfig config = getConfig();
- KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
- if (!installed.loginCommandLine()) installed.login();
- processResponse(installed, false);
- }
-
- public String getHome() {
- String home = System.getenv("HOME");
- if (home == null) {
- home = System.getProperty("HOME");
- if (home == null) {
- home = Paths.get("").toAbsolutePath().normalize().toString();
- }
- }
- return home;
- }
-
- public File getTokenDirectory() {
- return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM")).toFile();
- }
-
- public File getTokenFilePath() {
- return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM"), System.getProperty("KEYCLOAK_CLIENT") + ".json").toFile();
- }
-
- private void processResponse(KeycloakInstalled installed, boolean outputToken) throws IOException {
- AccessTokenResponse tokenResponse = installed.getTokenResponse();
- tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
- tokenResponse.setIdToken(null);
- String output = JsonSerialization.writeValueAsString(tokenResponse);
- getTokenDirectory().mkdirs();
- FileOutputStream fos = new FileOutputStream(getTokenFilePath());
- fos.write(output.getBytes("UTF-8"));
- fos.flush();
- fos.close();
- if (outputToken) System.out.println(tokenResponse.getToken());
- }
-
- public void loginManual() throws Exception {
- if (checkToken(true)) return;
- AdapterConfig config = getConfig();
- KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
- KeycloakInstalled installed = new KeycloakInstalled(deployment);
- installed.loginManual();
- processResponse(installed, true);
- }
-
- public void logout() throws Exception {
- String token = getTokenResponse();
- if (token != null) {
- Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
- if (m.find()) {
- String json = m.group(0);
- try {
- AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
- if (Time.currentTime() > tokenResponse.getExpiresIn()) {
- System.err.println("Login is expired");
- System.exit(1);
- }
- AdapterConfig config = getConfig();
- KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
- ServerRequest.invokeLogout(deployment, tokenResponse.getRefreshToken());
- for (File fp : getTokenDirectory().listFiles()) fp.delete();
- System.out.println("logout complete");
- } catch (Exception e) {
- System.err.println("Failure processing token response file");
- e.printStackTrace();
- System.exit(1);
- }
- } else {
- System.err.println("Could not find json within token response file");
- System.exit(1);
- }
- } else {
- System.err.println("Not logged in");
- System.exit(1);
- }
- }
-}
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 f1fee42161..4f311c223f 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,116 @@ 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);
+ 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");
}
- form.param(param, value);
+ return false;
+ } 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;
+ }
+ 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 +583,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 +635,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 +656,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/cli-sso/README.md b/adapters/oidc/kcinit/README.md
similarity index 100%
rename from adapters/oidc/cli-sso/README.md
rename to adapters/oidc/kcinit/README.md
diff --git a/adapters/oidc/cli-sso/pom.xml b/adapters/oidc/kcinit/pom.xml
similarity index 95%
rename from adapters/oidc/cli-sso/pom.xml
rename to adapters/oidc/kcinit/pom.xml
index c8d6016dc2..75dcf4d387 100755
--- a/adapters/oidc/cli-sso/pom.xml
+++ b/adapters/oidc/kcinit/pom.xml
@@ -26,7 +26,7 @@
4.0.0
- keycloak-cli-sso
+ kcinitKeycloak CLI SSO Framework
@@ -54,7 +54,7 @@
- org.keycloak.adapters.KeycloakCliSsoMain
+ org.keycloak.adapters.KcinitMain
diff --git a/adapters/oidc/kcinit/src/main/bin/kcinit b/adapters/oidc/kcinit/src/main/bin/kcinit
new file mode 100755
index 0000000000..4f5c2c6a72
--- /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 0000000000..90553091cc
--- /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/cli-sso/src/main/java/org/keycloak/adapters/KeycloakCliSsoMain.java b/adapters/oidc/kcinit/src/main/java/org/keycloak/adapters/KcinitMain.java
similarity index 56%
rename from adapters/oidc/cli-sso/src/main/java/org/keycloak/adapters/KeycloakCliSsoMain.java
rename to adapters/oidc/kcinit/src/main/java/org/keycloak/adapters/KcinitMain.java
index 3aaeb9b18a..275956f01f 100644
--- a/adapters/oidc/cli-sso/src/main/java/org/keycloak/adapters/KeycloakCliSsoMain.java
+++ b/adapters/oidc/kcinit/src/main/java/org/keycloak/adapters/KcinitMain.java
@@ -16,30 +16,15 @@
*/
package org.keycloak.adapters;
-import org.keycloak.adapters.installed.KeycloakCliSso;
-import org.keycloak.adapters.installed.KeycloakInstalled;
-import org.keycloak.common.util.Time;
-import org.keycloak.representations.AccessTokenResponse;
-import org.keycloak.representations.adapters.config.AdapterConfig;
-import org.keycloak.util.JsonSerialization;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.file.Paths;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import org.keycloak.adapters.installed.KcinitDriver;
/**
* @author Bill Burke
* @version $Revision: 1 $
*/
-public class KeycloakCliSsoMain extends KeycloakCliSso {
+public class KcinitMain extends KcinitDriver {
public static void main(String[] args) throws Exception {
- new KeycloakCliSsoMain().mainCmd(args);
+ new KcinitMain().mainCmd(args);
}
}
diff --git a/adapters/oidc/pom.xml b/adapters/oidc/pom.xml
index c380735326..6bc847388a 100755
--- a/adapters/oidc/pom.xml
+++ b/adapters/oidc/pom.xml
@@ -34,7 +34,7 @@
adapter-coreas7-eap6installed
- cli-sso
+ kcinitjaxrs-oauth-clientjettyjs
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 0000000000..70ce02d794
--- /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/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index df5411257d..2ef01cbb1f 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";
@@ -114,6 +116,7 @@ public interface OAuth2Constants {
String UMA_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket";
+ String DISPLAY_CONSOLE = "console";
}
diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWE.java b/core/src/main/java/org/keycloak/jose/jwe/JWE.java
index 75759dd556..8d954eaba8 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 Marek Posolda
*/
@@ -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/test/java/org/keycloak/jose/JWETest.java b/core/src/test/java/org/keycloak/jose/JWETest.java
index 31d8a8ac13..cc179bf988 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 Marek Posolda
@@ -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 {
@@ -118,10 +117,25 @@ public class JWETest {
System.out.println("Iterations: " + iterations + ", took: " + took);
}
+ @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 {
diff --git a/pom.xml b/pom.xml
index eab1938cf8..ddc8d25bc6 100755
--- a/pom.xml
+++ b/pom.xml
@@ -1414,6 +1414,17 @@
${project.version}zip
+
+ org.keycloak
+ kcinit
+ ${project.version}
+
+
+ org.keycloak
+ kcinit-dist
+ ${project.version}
+ zip
+
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java
index 2f17f77df1..1d600525e2 100755
--- a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java
@@ -43,5 +43,6 @@ public enum AuthenticationFlowError {
IDENTITY_PROVIDER_NOT_FOUND,
IDENTITY_PROVIDER_DISABLED,
- IDENTITY_PROVIDER_ERROR
+ IDENTITY_PROVIDER_ERROR,
+ DISPLAY_NOT_SUPPORTED
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowException.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowException.java
index bf8fbcf81b..e15386a74f 100755
--- a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowException.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowException.java
@@ -17,6 +17,8 @@
package org.keycloak.authentication;
+import javax.ws.rs.core.Response;
+
/**
* Throw this exception from an Authenticator, FormAuthenticator, or FormAction if you want to completely abort the flow.
*
@@ -25,11 +27,17 @@ package org.keycloak.authentication;
*/
public class AuthenticationFlowException extends RuntimeException {
private AuthenticationFlowError error;
+ private Response response;
public AuthenticationFlowException(AuthenticationFlowError error) {
this.error = error;
}
+ public AuthenticationFlowException(AuthenticationFlowError error, Response response) {
+ this.error = error;
+ this.response = response;
+ }
+
public AuthenticationFlowException(String message, AuthenticationFlowError error) {
super(message);
this.error = error;
@@ -53,4 +61,8 @@ public class AuthenticationFlowException extends RuntimeException {
public AuthenticationFlowError getError() {
return error;
}
+
+ public Response getResponse() {
+ return response;
+ }
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeAuthenticatorFactory.java b/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeAuthenticatorFactory.java
new file mode 100644
index 0000000000..07543834f0
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeAuthenticatorFactory.java
@@ -0,0 +1,21 @@
+package org.keycloak.authentication;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * Implement this interface when declaring your authenticator factory
+ * if your provider has support for multiple oidc display query parameter parameter types
+ * if the display query parameter is set and your factory implements this interface, this method
+ * will be called.
+ *
+ */
+public interface DisplayTypeAuthenticatorFactory {
+ /**
+ *
+ *
+ * @param session
+ * @param displayType i.e. "console", "wap", "popup" are examples
+ * @return null if display type isn't support.
+ */
+ Authenticator createDisplay(KeycloakSession session, String displayType);
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeRequiredActionFactory.java b/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeRequiredActionFactory.java
new file mode 100644
index 0000000000..e22e9fff7e
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeRequiredActionFactory.java
@@ -0,0 +1,13 @@
+package org.keycloak.authentication;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * Implement this interface when declaring your required action factory
+ * has support for multiple oidc display query parameter parameter types
+ * if the display query parameter is set and your factory implements this interface, this method
+ * will be called.
+ */
+public interface DisplayTypeRequiredActionFactory {
+ RequiredActionProvider createDisplay(KeycloakSession session, String displayType);
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java
index caaa14e59c..1289842868 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
@@ -59,6 +59,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
*
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 0000000000..b1dc9a2d15
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/TextChallenge.java
@@ -0,0 +1,303 @@
+package org.keycloak.authentication;
+
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.KeycloakSession;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+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 {
+
+ /**
+ * Browser is required to login. This will abort client from doing a console login.
+ *
+ * @param session
+ * @return
+ */
+ public static Response browserRequired(KeycloakSession session) {
+ return Response.status(Response.Status.UNAUTHORIZED)
+ .header("WWW-Authenticate", "X-Text-Form-Challenge browserRequired")
+ .type(MediaType.TEXT_PLAIN)
+ .entity("\n" + session.getProvider(LoginFormsProvider.class).getMessage("browserRequired") + "\n").build();
+ }
+
+
+ /**
+ * Build challenge response for required actions
+ *
+ * @param context
+ * @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 a60ebc0677..425ec523f9 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 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
+
+ com.igormaznitsa
+ mvn-golang-wrapper
+ 2.1.6
+ true
+
+ 1.9.2
+
+
+
+ get-mousetrap
+
+ get
+
+
+
+ github.com/inconshreveable/mousetrap
+
+ ${project.build.directory}/gopath
+
+
+
+ get-kcinit
+
+ get
+
+
+
+ github.com/keycloak/kcinit
+
+ ${project.build.directory}/gopath
+ 0.3
+
+
+
+
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 0000000000..513eb54d27
--- /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 Marko Strukelj
+ */
+public class KcinitExec extends AbstractExec {
+
+ public static final String WORK_DIR = System.getProperty("user.dir") + "/target";
+
+ public static final String CMD = OS_ARCH.isWindows() ? "kcinit" : "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 {
+
+ @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/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 b5476b5aa4..ddfe91dd73 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;
@@ -33,7 +34,7 @@ public abstract class AbstractExec {
private boolean logStreams = Boolean.valueOf(System.getProperty("cli.log.output", "true"));
- protected boolean dumpStreams;
+ protected boolean dumpStreams = true;
protected String workDir = WORK_DIR;
@@ -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/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
index 2642a11ef7..e5d9471406 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
@@ -146,7 +146,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Validates a username and password from login form.");
addProviderInfo(result, "auth-x509-client-username-form", "X509/Validate Username Form",
"Validates username and password from X509 client certificate received as a part of mutual SSL handshake.");
- addProviderInfo(result, "cli-username-password", "Username Password Challenge",
+ addProviderInfo(result, "console-username-password", "Username Password Challenge",
"Proprietary challenge protocol for CLI clients that queries for username password");
addProviderInfo(result, "direct-grant-auth-x509-username", "X509/Validate Username",
"Validates username and password from X509 client certificate received as a part of mutual SSL handshake.");
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 0000000000..f23a63c4b2
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/KcinitTest.java
@@ -0,0 +1,564 @@
+/*
+ * 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.forms.PassThroughAuthenticator;
+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 Bill Burke
+ */
+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("p@p.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);
+ user = session.users().addUser(realm, "tbrady");
+ session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
+ user.setEnabled(true);
+ user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+
+ // Parent flow
+ AuthenticationFlowModel browser = new AuthenticationFlowModel();
+ browser.setAlias("no-console-flow");
+ browser.setDescription("browser based authentication");
+ browser.setProviderId("basic-flow");
+ browser.setTopLevel(true);
+ browser.setBuiltIn(true);
+ browser = realm.addAuthenticationFlow(browser);
+
+ AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
+ execution.setParentFlow(browser.getId());
+ execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+ execution.setPriority(20);
+ execution.setAuthenticator(PassThroughAuthenticator.PROVIDER_ID);
+ realm.addAuthenticatorExecution(execution);
+
+ });
+ }
+
+ //@Test
+ public void testDemo() throws Exception {
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+ Map 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(100000000);
+ }
+
+ @Test
+ public void testBrowserRequired() throws Exception {
+ // that that a browser require challenge is sent back if authentication flow doesn't support console display mode
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+ ClientModel kcinit = realm.getClientByClientId(KCINIT_CLIENT);
+ AuthenticationFlowModel flow = realm.getFlowByAlias("no-console-flow");
+ kcinit.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, flow.getId());
+
+
+ });
+
+ testInstall();
+ // login
+ //System.out.println("login....");
+ KcinitExec exe = KcinitExec.newBuilder()
+ .argsLine("login")
+ .executeAsync();
+ exe.waitCompletion();
+ Assert.assertEquals(1, exe.exitCode());
+ Assert.assertTrue(exe.stderrString().contains("Browser required to login"));
+ //Assert.assertEquals("stderr first line", "Browser required to login", exe.stderrLines().get(1));
+
+
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+ ClientModel kcinit = realm.getClientByClientId(KCINIT_CLIENT);
+ kcinit.removeAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING);
+
+
+ });
+ }
+
+
+ @Test
+ public void testBadCommand() throws Exception {
+ KcinitExec exe = KcinitExec.execute("covfefe");
+ Assert.assertEquals(1, exe.exitCode());
+ Assert.assertEquals("stderr first line", "Error: unknown command \"covfefe\" for \"kcinit\"", 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();
+ //System.out.println(exe.stderrString());
+ //exe.waitForStderr("(y/n):");
+ //exe.sendLine("n");
+ exe.waitForStderr("Authentication server URL [http://localhost:8080/auth]:");
+ exe.sendLine(OAuthClient.AUTH_SERVER_ROOT);
+ //System.out.println(exe.stderrString());
+ exe.waitForStderr("Name of realm [master]:");
+ exe.sendLine("test");
+ //System.out.println(exe.stderrString());
+ exe.waitForStderr("client id [kcinit]:");
+ exe.sendLine("");
+ //System.out.println(exe.stderrString());
+ exe.waitForStderr("Client secret [none]:");
+ exe.sendLine("password");
+ //System.out.println(exe.stderrString());
+ exe.waitCompletion();
+ Assert.assertEquals(0, exe.exitCode());
+ }
+
+ @Test
+ public void testBasic() throws Exception {
+ testInstall();
+ // login
+ //System.out.println("login....");
+ KcinitExec exe = KcinitExec.newBuilder()
+ .argsLine("login")
+ .executeAsync();
+ //System.out.println(exe.stderrString());
+ exe.waitForStderr("Username:");
+ exe.sendLine("wburke");
+ //System.out.println(exe.stderrString());
+ exe.waitForStderr("Password:");
+ exe.sendLine("password");
+ //System.out.println(exe.stderrString());
+ 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), 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("login")
+ .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("login")
+ .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("login")
+ .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("login -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("login")
+ .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("login")
+ .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("login")
+ .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 72024f63f1..5855979eb2 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 0000000000..b4a01c9eee
--- /dev/null
+++ b/themes/src/main/resources/theme/base/email/html/email-verification-with-code.ftl
@@ -0,0 +1,5 @@
+
+
+${msg("emailVerificationBodyCodeHtml",code)?no_esc}
+
+
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 e04e947a44..b2fd0c04c4 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=
Please verify your email address by entering in the following code.
{0}
+
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 0000000000..4ffb7d8798
--- /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 0000000000..d609182edf
--- /dev/null
+++ b/themes/src/main/resources/theme/base/login/login-config-totp-text.ftl
@@ -0,0 +1,31 @@
+<#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>
+
+Enter in your one time password so we can verify you have installed it correctly.
+
+
+
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 0000000000..87abcd7864
--- /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 d4c12e47be..5f4c6a2f6d 100755
--- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -34,9 +34,12 @@ emailForgotTitle=Forgot Your Password?
updatePasswordTitle=Update password
codeSuccessTitle=Success code
codeErrorTitle=Error code\: {0}
+displayUnsupported=Requested display type unsupported
+browserRequired=Browser required to login
termsTitle=Terms and Conditions
termsText=
Terms and conditions to be defined
+termsPlainText=Terms and conditions to be defined.
recaptchaFailed=Invalid Recaptcha
recaptchaNotConfigured=Recaptcha is required, but not configured
@@ -66,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
@@ -278,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