commit
8d3dc790df
75 changed files with 3403 additions and 530 deletions
|
@ -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`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
* <p>
|
||||||
|
* 1. . kcinit
|
||||||
|
* - setup and export KC_SESSION_KEY env var if not set.
|
||||||
|
* - checks to see if master token valid, refresh is possible, exit if token valid
|
||||||
|
* - performs command line login
|
||||||
|
* - stores master token for master client
|
||||||
|
* 2. app.sh is a wrapper for app cli.
|
||||||
|
* - token=`kcinit token app`
|
||||||
|
* - checks to see if token for app client has been fetched, refresh if valid, output token to sys.out if exists
|
||||||
|
* - if no token, login. Prompts go to stderr.
|
||||||
|
* - pass token as cmd line param to app or as environment variable.
|
||||||
|
* <p>
|
||||||
|
* 3. kcinit password {password}
|
||||||
|
* - outputs password key that is used for encryption.
|
||||||
|
* - can be used in .bashrc as export KC_SESSSION_KEY=`kcinit password {password}` or just set it in .bat file
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class KcinitDriver {
|
||||||
|
|
||||||
|
public static final String KC_SESSION_KEY = "KC_SESSION_KEY";
|
||||||
|
public static final String KC_LOGIN_CONFIG_PATH = "KC_LOGIN_CONFIG_PATH";
|
||||||
|
protected Map<String, String> config;
|
||||||
|
protected boolean debug = true;
|
||||||
|
|
||||||
|
protected static byte[] salt = new byte[]{-4, 88, 66, -101, 78, -94, 21, 105};
|
||||||
|
|
||||||
|
String[] args = null;
|
||||||
|
|
||||||
|
protected boolean forceLogin;
|
||||||
|
protected boolean browserLogin;
|
||||||
|
|
||||||
|
public void mainCmd(String[] args) throws Exception {
|
||||||
|
|
||||||
|
this.args = 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<String, String> props = new HashMap<>();
|
||||||
|
props.put("server", server);
|
||||||
|
props.put("realm", realm);
|
||||||
|
props.put("client", client);
|
||||||
|
props.put("secret", secret);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String json = JsonSerialization.writeValueAsString(props);
|
||||||
|
writeFile(configFile, json);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
KeycloakInstalled.console().writer().println();
|
||||||
|
KeycloakInstalled.console().writer().println("Installation complete!");
|
||||||
|
KeycloakInstalled.console().writer().println();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void printHelp() {
|
||||||
|
KeycloakInstalled.console().writer().println("Commands:");
|
||||||
|
KeycloakInstalled.console().writer().println(" 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<String, Object> creds = new HashMap<>();
|
||||||
|
creds.put("secret", secret);
|
||||||
|
config.setCredentials(creds);
|
||||||
|
} else {
|
||||||
|
config.setPublicClient(true);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> getConfigProperties() {
|
||||||
|
if (this.config != null) return this.config;
|
||||||
|
if (!getConfigFile().exists()) {
|
||||||
|
KeycloakInstalled.console().writer().println();
|
||||||
|
KeycloakInstalled.console().writer().println(("Config file does not exist. Run kcinit install to set it up."));
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
String json = readFile(getConfigFile());
|
||||||
|
try {
|
||||||
|
Map map = JsonSerialization.readValue(json, Map.class);
|
||||||
|
config = (Map<String, String>) map;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return this.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String readToken(String client) throws Exception {
|
||||||
|
String json = getTokenResponse(client);
|
||||||
|
if (json == null) return null;
|
||||||
|
|
||||||
|
|
||||||
|
if (json != null) {
|
||||||
|
try {
|
||||||
|
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
|
||||||
|
if (Time.currentTime() < tokenResponse.getExpiresIn()) {
|
||||||
|
return tokenResponse.getToken();
|
||||||
|
}
|
||||||
|
AdapterConfig config = getConfig();
|
||||||
|
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
|
||||||
|
installed.refreshToken(tokenResponse.getRefreshToken());
|
||||||
|
processResponse(installed, client);
|
||||||
|
return tokenResponse.getToken();
|
||||||
|
} catch (Exception e) {
|
||||||
|
File tokenFile = getTokenFilePath(client);
|
||||||
|
if (tokenFile.exists()) {
|
||||||
|
tokenFile.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public String readRefreshToken(String client) throws Exception {
|
||||||
|
String json = getTokenResponse(client);
|
||||||
|
if (json == null) return null;
|
||||||
|
|
||||||
|
|
||||||
|
if (json != null) {
|
||||||
|
try {
|
||||||
|
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
|
||||||
|
return tokenResponse.getRefreshToken();
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (debug) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
File tokenFile = getTokenFilePath(client);
|
||||||
|
if (tokenFile.exists()) {
|
||||||
|
tokenFile.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String getTokenResponse(String client) throws IOException {
|
||||||
|
File tokenFile = getTokenFilePath(client);
|
||||||
|
try {
|
||||||
|
return readFile(tokenFile);
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (debug) {
|
||||||
|
System.err.println("Failed to read encrypted file");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
if (tokenFile.exists()) tokenFile.delete();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void token() throws Exception {
|
||||||
|
KeycloakInstalled.console().stderrOutput();
|
||||||
|
|
||||||
|
checkEnv();
|
||||||
|
String masterClient = getMasterClient();
|
||||||
|
String client = masterClient;
|
||||||
|
if (args.length > 1) {
|
||||||
|
client = args[1];
|
||||||
|
}
|
||||||
|
//System.err.println("readToken: " + client);
|
||||||
|
String token = readToken(client);
|
||||||
|
if (token != null) {
|
||||||
|
System.out.print(token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (token == null && client.equals(masterClient)) {
|
||||||
|
//System.err.println("not logged in, logging in.");
|
||||||
|
doConsoleLogin();
|
||||||
|
token = readToken(client);
|
||||||
|
if (token != null) {
|
||||||
|
System.out.print(token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
String masterToken = readToken(masterClient);
|
||||||
|
if (masterToken == null) {
|
||||||
|
//System.err.println("not logged in, logging in.");
|
||||||
|
doConsoleLogin();
|
||||||
|
masterToken = readToken(masterClient);
|
||||||
|
if (masterToken == null) {
|
||||||
|
System.err.println("Login failed. Cannot retrieve token");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//System.err.println("exchange: " + client);
|
||||||
|
Client httpClient = getHttpClient();
|
||||||
|
|
||||||
|
WebTarget exchangeUrl = httpClient.target(getServer())
|
||||||
|
.path("/realms")
|
||||||
|
.path(getRealm())
|
||||||
|
.path("protocol/openid-connect/token");
|
||||||
|
|
||||||
|
Form form = new Form()
|
||||||
|
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
|
||||||
|
.param(OAuth2Constants.CLIENT_ID, masterClient)
|
||||||
|
.param(OAuth2Constants.SUBJECT_TOKEN, masterToken)
|
||||||
|
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
|
||||||
|
.param(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.REFRESH_TOKEN_TYPE)
|
||||||
|
.param(OAuth2Constants.AUDIENCE, client);
|
||||||
|
if (getMasterClientSecret() != null) {
|
||||||
|
form.param(OAuth2Constants.CLIENT_SECRET, getMasterClientSecret());
|
||||||
|
}
|
||||||
|
Response response = exchangeUrl.request().post(Entity.form(
|
||||||
|
form
|
||||||
|
));
|
||||||
|
|
||||||
|
if (response.getStatus() == 401 || response.getStatus() == 403) {
|
||||||
|
response.close();
|
||||||
|
System.err.println("Not allowed to exchange for client token");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.getStatus() != 200) {
|
||||||
|
if (response.getMediaType() != null && response.getMediaType().equals(MediaType.APPLICATION_JSON_TYPE)) {
|
||||||
|
try {
|
||||||
|
String json = response.readEntity(String.class);
|
||||||
|
OAuth2ErrorRepresentation error = JsonSerialization.readValue(json, OAuth2ErrorRepresentation.class);
|
||||||
|
System.err.println("Failed to exchange token: " + error.getError() + ". " + error.getErrorDescription());
|
||||||
|
System.exit(1);
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
ignore.printStackTrace();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.close();
|
||||||
|
System.err.println("Unknown error exchanging for client token: " + response.getStatus());
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String json = response.readEntity(String.class);
|
||||||
|
response.close();
|
||||||
|
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
|
||||||
|
if (tokenResponse.getToken() != null) {
|
||||||
|
getTokenDirectory().mkdirs();
|
||||||
|
tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
|
||||||
|
tokenResponse.setIdToken(null);
|
||||||
|
json = JsonSerialization.writeValueAsString(tokenResponse);
|
||||||
|
writeFile(getTokenFilePath(client), json);
|
||||||
|
System.out.printf(tokenResponse.getToken());
|
||||||
|
} else {
|
||||||
|
System.err.println("Error processing token");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getMasterClientSecret() {
|
||||||
|
return getProperty("secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getServer() {
|
||||||
|
return getProperty("server");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getRealm() {
|
||||||
|
return getProperty("realm");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProperty(String name) {
|
||||||
|
return (String) getConfigProperties().get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean forceLogin() {
|
||||||
|
return args.length > 0 && args[0].equals("-f");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public Client getHttpClient() {
|
||||||
|
return new ResteasyClientBuilder().disableTrustManager().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void login() throws Exception {
|
||||||
|
checkEnv();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
|
||||||
* @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<String, Object> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -39,15 +39,7 @@ import javax.ws.rs.core.Form;
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.BufferedReader;
|
import java.io.*;
|
||||||
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.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -65,6 +57,7 @@ public class KeycloakInstalled {
|
||||||
|
|
||||||
public interface HttpResponseWriter {
|
public interface HttpResponseWriter {
|
||||||
void success(PrintWriter pw, KeycloakInstalled ki);
|
void success(PrintWriter pw, KeycloakInstalled ki);
|
||||||
|
|
||||||
void failure(PrintWriter pw, KeycloakInstalled ki);
|
void failure(PrintWriter pw, KeycloakInstalled ki);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,12 +79,12 @@ public class KeycloakInstalled {
|
||||||
private Locale locale;
|
private Locale locale;
|
||||||
private HttpResponseWriter loginResponseWriter;
|
private HttpResponseWriter loginResponseWriter;
|
||||||
private HttpResponseWriter logoutResponseWriter;
|
private HttpResponseWriter logoutResponseWriter;
|
||||||
|
private ResteasyClient resteasyClient;
|
||||||
Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\"");
|
Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\"");
|
||||||
Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"\\s+mask=(\\S+)");
|
Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"\\s+mask=(\\S+)");
|
||||||
Pattern codePattern = Pattern.compile("code=([^&]+)");
|
Pattern codePattern = Pattern.compile("code=([^&]+)");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public KeycloakInstalled() {
|
public KeycloakInstalled() {
|
||||||
InputStream config = Thread.currentThread().getContextClassLoader().getResourceAsStream(KEYCLOAK_JSON);
|
InputStream config = Thread.currentThread().getContextClassLoader().getResourceAsStream(KEYCLOAK_JSON);
|
||||||
deployment = KeycloakDeploymentBuilder.build(config);
|
deployment = KeycloakDeploymentBuilder.build(config);
|
||||||
|
@ -179,6 +172,10 @@ public class KeycloakInstalled {
|
||||||
this.logoutResponseWriter = logoutResponseWriter;
|
this.logoutResponseWriter = logoutResponseWriter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setResteasyClient(ResteasyClient resteasyClient) {
|
||||||
|
this.resteasyClient = resteasyClient;
|
||||||
|
}
|
||||||
|
|
||||||
public Locale getLocale() {
|
public Locale getLocale() {
|
||||||
return locale;
|
return locale;
|
||||||
}
|
}
|
||||||
|
@ -302,6 +299,139 @@ public class KeycloakInstalled {
|
||||||
status = Status.LOGGED_MANUAL;
|
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 {
|
public boolean loginCommandLine() throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||||
String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
|
String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
|
||||||
|
|
||||||
|
@ -309,7 +439,6 @@ public class KeycloakInstalled {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Experimental proprietary WWW-Authentication challenge protocol.
|
* Experimental proprietary WWW-Authentication challenge protocol.
|
||||||
* WWW-Authentication: X-Text-Form-Challenge callback="{url}" param="{param-name}" label="{param-display-label}"
|
* 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.RESPONSE_TYPE, OAuth2Constants.CODE)
|
||||||
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
||||||
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
|
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
|
||||||
|
.queryParam("display", "console")
|
||||||
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
|
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
|
||||||
.build().toString();
|
.build().toString();
|
||||||
ResteasyClient client = new ResteasyClientBuilder().disableTrustManager().build();
|
ResteasyClient client = createResteasyClient();
|
||||||
try {
|
try {
|
||||||
|
//System.err.println("initial request");
|
||||||
Response response = client.target(authUrl).request().get();
|
Response response = client.target(authUrl).request().get();
|
||||||
if (response.getStatus() != 401) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
while (true) {
|
while (true) {
|
||||||
String authenticationHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
|
if (response.getStatus() == 403) {
|
||||||
if (authenticationHeader == null) {
|
if (response.getMediaType() != null) {
|
||||||
return false;
|
String splash = response.readEntity(String.class);
|
||||||
}
|
console().writer().println(splash);
|
||||||
if (!authenticationHeader.contains("X-Text-Form-Challenge")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (response.getMediaType() != null) {
|
|
||||||
String splash = response.readEntity(String.class);
|
|
||||||
System.console().writer().println(splash);
|
|
||||||
}
|
|
||||||
Matcher m = callbackPattern.matcher(authenticationHeader);
|
|
||||||
if (!m.find()) return false;
|
|
||||||
String callback = m.group(1);
|
|
||||||
//System.err.println("callback: " + callback);
|
|
||||||
m = paramPattern.matcher(authenticationHeader);
|
|
||||||
Form form = new Form();
|
|
||||||
while (m.find()) {
|
|
||||||
String param = m.group(1);
|
|
||||||
String label = m.group(2);
|
|
||||||
String mask = m.group(3).trim();
|
|
||||||
boolean maskInput = mask.equals("true");
|
|
||||||
String value = null;
|
|
||||||
if (maskInput) {
|
|
||||||
char[] txt = System.console().readPassword(label);
|
|
||||||
value = new String(txt);
|
|
||||||
} else {
|
} 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 {
|
} finally {
|
||||||
client.close();
|
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 {
|
public String getTokenString() throws VerificationException, IOException, ServerRequest.HttpFailure {
|
||||||
return tokenString;
|
return tokenString;
|
||||||
|
@ -400,7 +583,7 @@ public class KeycloakInstalled {
|
||||||
parseAccessToken(tokenResponse);
|
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);
|
AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
|
||||||
parseAccessToken(tokenResponse);
|
parseAccessToken(tokenResponse);
|
||||||
|
|
||||||
|
@ -452,7 +635,6 @@ public class KeycloakInstalled {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||||
AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null);
|
AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null);
|
||||||
parseAccessToken(tokenResponse);
|
parseAccessToken(tokenResponse);
|
||||||
|
@ -474,86 +656,6 @@ public class KeycloakInstalled {
|
||||||
return sb.toString();
|
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 {
|
public class CallbackListener extends Thread {
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<artifactId>keycloak-cli-sso</artifactId>
|
<artifactId>kcinit</artifactId>
|
||||||
<name>Keycloak CLI SSO Framework</name>
|
<name>Keycloak CLI SSO Framework</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
<configuration>
|
<configuration>
|
||||||
<transformers>
|
<transformers>
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
<mainClass>org.keycloak.adapters.KeycloakCliSsoMain</mainClass>
|
<mainClass>org.keycloak.adapters.KcinitMain</mainClass>
|
||||||
</transformer>
|
</transformer>
|
||||||
</transformers>
|
</transformers>
|
||||||
|
|
26
adapters/oidc/kcinit/src/main/bin/kcinit
Executable file
26
adapters/oidc/kcinit/src/main/bin/kcinit
Executable file
|
@ -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 $@
|
8
adapters/oidc/kcinit/src/main/bin/kcinit.bat
Executable file
8
adapters/oidc/kcinit/src/main/bin/kcinit.bat
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
if "%OS%" == "Windows_NT" (
|
||||||
|
set "DIRNAME=%~dp0%"
|
||||||
|
) else (
|
||||||
|
set DIRNAME=.\
|
||||||
|
)
|
||||||
|
java -jar %DIRNAME%\kcinit-${project.version}.jar %*
|
|
@ -16,30 +16,15 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.adapters;
|
package org.keycloak.adapters;
|
||||||
|
|
||||||
import org.keycloak.adapters.installed.KeycloakCliSso;
|
import org.keycloak.adapters.installed.KcinitDriver;
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class KeycloakCliSsoMain extends KeycloakCliSso {
|
public class KcinitMain extends KcinitDriver {
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
new KeycloakCliSsoMain().mainCmd(args);
|
new KcinitMain().mainCmd(args);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -34,7 +34,7 @@
|
||||||
<module>adapter-core</module>
|
<module>adapter-core</module>
|
||||||
<module>as7-eap6</module>
|
<module>as7-eap6</module>
|
||||||
<module>installed</module>
|
<module>installed</module>
|
||||||
<module>cli-sso</module>
|
<module>kcinit</module>
|
||||||
<module>jaxrs-oauth-client</module>
|
<module>jaxrs-oauth-client</module>
|
||||||
<module>jetty</module>
|
<module>jetty</module>
|
||||||
<module>js</module>
|
<module>js</module>
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -34,6 +34,8 @@ public interface OAuth2Constants {
|
||||||
|
|
||||||
String REDIRECT_URI = "redirect_uri";
|
String REDIRECT_URI = "redirect_uri";
|
||||||
|
|
||||||
|
String DISPLAY = "display";
|
||||||
|
|
||||||
String SCOPE = "scope";
|
String SCOPE = "scope";
|
||||||
|
|
||||||
String STATE = "state";
|
String STATE = "state";
|
||||||
|
@ -114,6 +116,7 @@ public interface OAuth2Constants {
|
||||||
String UMA_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket";
|
String UMA_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket";
|
||||||
|
|
||||||
|
|
||||||
|
String DISPLAY_CONSOLE = "console";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,13 +18,23 @@
|
||||||
package org.keycloak.jose.jwe;
|
package org.keycloak.jose.jwe;
|
||||||
|
|
||||||
import java.io.IOException;
|
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.Base64Url;
|
||||||
import org.keycloak.common.util.BouncyIntegration;
|
import org.keycloak.common.util.BouncyIntegration;
|
||||||
import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
|
import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
|
||||||
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
|
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.SecretKeyFactory;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
|
@ -193,4 +203,66 @@ public class JWE {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String encryptUTF8(String password, String saltString, String payload) {
|
||||||
|
byte[] bytes = null;
|
||||||
|
try {
|
||||||
|
bytes = payload.getBytes("UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return encrypt(password, saltString, bytes);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static String encrypt(String password, String saltString, byte[] payload) {
|
||||||
|
try {
|
||||||
|
byte[] salt = Base64.decode(saltString);
|
||||||
|
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||||
|
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 100, 128);
|
||||||
|
SecretKey tmp = factory.generateSecret(spec);
|
||||||
|
SecretKey aesKey = new SecretKeySpec(tmp.getEncoded(), "AES");
|
||||||
|
|
||||||
|
JWEHeader jweHeader = new JWEHeader(JWEConstants.A128KW, JWEConstants.A128CBC_HS256, null);
|
||||||
|
JWE jwe = new JWE()
|
||||||
|
.header(jweHeader)
|
||||||
|
.content(payload);
|
||||||
|
|
||||||
|
jwe.getKeyStorage()
|
||||||
|
.setEncryptionKey(aesKey);
|
||||||
|
|
||||||
|
return jwe.encodeJwe();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] decrypt(String password, String saltString, String encodedJwe) {
|
||||||
|
try {
|
||||||
|
byte[] salt = Base64.decode(saltString);
|
||||||
|
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||||
|
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 100, 128);
|
||||||
|
SecretKey tmp = factory.generateSecret(spec);
|
||||||
|
SecretKey aesKey = new SecretKeySpec(tmp.getEncoded(), "AES");
|
||||||
|
|
||||||
|
JWE jwe = new JWE();
|
||||||
|
jwe.getKeyStorage()
|
||||||
|
.setEncryptionKey(aesKey);
|
||||||
|
|
||||||
|
jwe.verifyAndDecodeJwe(encodedJwe);
|
||||||
|
return jwe.getContent();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String decryptUTF8(String password, String saltString, String encodedJwe) {
|
||||||
|
byte[] payload = decrypt(password, saltString, encodedJwe);
|
||||||
|
try {
|
||||||
|
return new String(payload, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,18 +19,18 @@ package org.keycloak.jose;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
import java.security.spec.KeySpec;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.SecretKeyFactory;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.util.Base64;
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.jose.jwe.JWE;
|
import org.keycloak.jose.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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -53,7 +53,6 @@ public class JWETest {
|
||||||
testDirectEncryptAndDecrypt(aesKey, hmacKey, JWEConstants.A128CBC_HS256, PAYLOAD, true);
|
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
|
// Works just on OpenJDK 8. Other JDKs (IBM, Oracle) have restrictions on maximum key size of AES to be 128
|
||||||
// @Test
|
// @Test
|
||||||
public void testDirect_Aes256CbcHmacSha512() throws Exception {
|
public void testDirect_Aes256CbcHmacSha512() throws Exception {
|
||||||
|
@ -118,10 +117,25 @@ public class JWETest {
|
||||||
System.out.println("Iterations: " + iterations + ", took: " + took);
|
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
|
@Test
|
||||||
public void testAesKW_Aes128CbcHmacSha256() throws Exception {
|
public void testAesKW_Aes128CbcHmacSha256() throws Exception {
|
||||||
SecretKey aesKey = new SecretKeySpec(AES_128_KEY, "AES");
|
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);
|
JWEHeader jweHeader = new JWEHeader(JWEConstants.A128KW, JWEConstants.A128CBC_HS256, null);
|
||||||
JWE jwe = new JWE()
|
JWE jwe = new JWE()
|
||||||
.header(jweHeader)
|
.header(jweHeader)
|
||||||
|
@ -146,6 +160,15 @@ public class JWETest {
|
||||||
Assert.assertEquals(PAYLOAD, decodedContent);
|
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
|
@Test
|
||||||
public void externalJweAes128CbcHmacSha256Test() throws UnsupportedEncodingException, JWEException {
|
public void externalJweAes128CbcHmacSha256Test() throws UnsupportedEncodingException, JWEException {
|
||||||
|
|
11
pom.xml
11
pom.xml
|
@ -1414,6 +1414,17 @@
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
<type>zip</type>
|
<type>zip</type>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>kcinit</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>kcinit-dist</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<type>zip</type>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|
|
@ -43,5 +43,6 @@ public enum AuthenticationFlowError {
|
||||||
|
|
||||||
IDENTITY_PROVIDER_NOT_FOUND,
|
IDENTITY_PROVIDER_NOT_FOUND,
|
||||||
IDENTITY_PROVIDER_DISABLED,
|
IDENTITY_PROVIDER_DISABLED,
|
||||||
IDENTITY_PROVIDER_ERROR
|
IDENTITY_PROVIDER_ERROR,
|
||||||
|
DISPLAY_NOT_SUPPORTED
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
package org.keycloak.authentication;
|
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.
|
* 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 {
|
public class AuthenticationFlowException extends RuntimeException {
|
||||||
private AuthenticationFlowError error;
|
private AuthenticationFlowError error;
|
||||||
|
private Response response;
|
||||||
|
|
||||||
public AuthenticationFlowException(AuthenticationFlowError error) {
|
public AuthenticationFlowException(AuthenticationFlowError error) {
|
||||||
this.error = error;
|
this.error = error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AuthenticationFlowException(AuthenticationFlowError error, Response response) {
|
||||||
|
this.error = error;
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
|
||||||
public AuthenticationFlowException(String message, AuthenticationFlowError error) {
|
public AuthenticationFlowException(String message, AuthenticationFlowError error) {
|
||||||
super(message);
|
super(message);
|
||||||
this.error = error;
|
this.error = error;
|
||||||
|
@ -53,4 +61,8 @@ public class AuthenticationFlowException extends RuntimeException {
|
||||||
public AuthenticationFlowError getError() {
|
public AuthenticationFlowError getError() {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Response getResponse() {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -59,6 +59,15 @@ public interface RequiredActionContext {
|
||||||
*/
|
*/
|
||||||
URI getActionUrl();
|
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
|
* Create a Freemarker form builder that presets the user, action URI, and a generated access code
|
||||||
*
|
*
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -76,4 +77,24 @@ public interface EmailTemplateProvider extends Provider {
|
||||||
|
|
||||||
public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException;
|
public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send formatted email
|
||||||
|
*
|
||||||
|
* @param subjectFormatKey message property that will be used to format email subject
|
||||||
|
* @param bodyTemplate freemarker template file
|
||||||
|
* @param bodyAttributes attributes used to fill template
|
||||||
|
* @throws EmailException
|
||||||
|
*/
|
||||||
|
void send(String subjectFormatKey, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send formatted email
|
||||||
|
*
|
||||||
|
* @param subjectFormatKey message property that will be used to format email subject
|
||||||
|
* @param subjectAttributes attributes used to fill subject format message
|
||||||
|
* @param bodyTemplate freemarker template file
|
||||||
|
* @param bodyAttributes attributes used to fill template
|
||||||
|
* @throws EmailException
|
||||||
|
*/
|
||||||
|
void send(String subjectFormatKey, List<Object> subjectAttributes, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException;
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,5 +90,6 @@ public interface Errors {
|
||||||
String NOT_LOGGED_IN = "not_logged_in";
|
String NOT_LOGGED_IN = "not_logged_in";
|
||||||
String UNKNOWN_IDENTITY_PROVIDER = "unknown_identity_provider";
|
String UNKNOWN_IDENTITY_PROVIDER = "unknown_identity_provider";
|
||||||
String ILLEGAL_ORIGIN = "illegal_origin";
|
String ILLEGAL_ORIGIN = "illegal_origin";
|
||||||
|
String DISPLAY_UNSUPPORTED = "display_unsupported";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,8 @@ public interface LoginFormsProvider extends Provider {
|
||||||
|
|
||||||
String getMessage(String message);
|
String getMessage(String message);
|
||||||
|
|
||||||
|
String getMessage(String message, String... parameters);
|
||||||
|
|
||||||
Response createLogin();
|
Response createLogin();
|
||||||
|
|
||||||
Response createPasswordReset();
|
Response createPasswordReset();
|
||||||
|
|
|
@ -52,6 +52,7 @@ public interface Constants {
|
||||||
int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
|
int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
|
||||||
|
|
||||||
String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
|
String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
|
||||||
|
String VERIFY_EMAIL_CODE = "VERIFY_EMAIL_CODE";
|
||||||
String EXECUTION = "execution";
|
String EXECUTION = "execution";
|
||||||
String CLIENT_ID = "client_id";
|
String CLIENT_ID = "client_id";
|
||||||
String TAB_ID = "tab_id";
|
String TAB_ID = "tab_id";
|
||||||
|
|
|
@ -653,27 +653,33 @@ public class AuthenticationProcessor {
|
||||||
public Response handleBrowserException(Exception failure) {
|
public Response handleBrowserException(Exception failure) {
|
||||||
if (failure instanceof AuthenticationFlowException) {
|
if (failure instanceof AuthenticationFlowException) {
|
||||||
AuthenticationFlowException e = (AuthenticationFlowException) failure;
|
AuthenticationFlowException e = (AuthenticationFlowException) failure;
|
||||||
|
|
||||||
if (e.getError() == AuthenticationFlowError.INVALID_USER) {
|
if (e.getError() == AuthenticationFlowError.INVALID_USER) {
|
||||||
ServicesLogger.LOGGER.failedAuthentication(e);
|
ServicesLogger.LOGGER.failedAuthentication(e);
|
||||||
event.error(Errors.USER_NOT_FOUND);
|
event.error(Errors.USER_NOT_FOUND);
|
||||||
|
if (e.getResponse() != null) return e.getResponse();
|
||||||
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
|
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
|
||||||
} else if (e.getError() == AuthenticationFlowError.USER_DISABLED) {
|
} else if (e.getError() == AuthenticationFlowError.USER_DISABLED) {
|
||||||
ServicesLogger.LOGGER.failedAuthentication(e);
|
ServicesLogger.LOGGER.failedAuthentication(e);
|
||||||
event.error(Errors.USER_DISABLED);
|
event.error(Errors.USER_DISABLED);
|
||||||
|
if (e.getResponse() != null) return e.getResponse();
|
||||||
return ErrorPage.error(session,authenticationSession, Response.Status.BAD_REQUEST, Messages.ACCOUNT_DISABLED);
|
return ErrorPage.error(session,authenticationSession, Response.Status.BAD_REQUEST, Messages.ACCOUNT_DISABLED);
|
||||||
} else if (e.getError() == AuthenticationFlowError.USER_TEMPORARILY_DISABLED) {
|
} else if (e.getError() == AuthenticationFlowError.USER_TEMPORARILY_DISABLED) {
|
||||||
ServicesLogger.LOGGER.failedAuthentication(e);
|
ServicesLogger.LOGGER.failedAuthentication(e);
|
||||||
event.error(Errors.USER_TEMPORARILY_DISABLED);
|
event.error(Errors.USER_TEMPORARILY_DISABLED);
|
||||||
|
if (e.getResponse() != null) return e.getResponse();
|
||||||
return ErrorPage.error(session,authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
|
return ErrorPage.error(session,authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
|
||||||
|
|
||||||
} else if (e.getError() == AuthenticationFlowError.INVALID_CLIENT_SESSION) {
|
} else if (e.getError() == AuthenticationFlowError.INVALID_CLIENT_SESSION) {
|
||||||
ServicesLogger.LOGGER.failedAuthentication(e);
|
ServicesLogger.LOGGER.failedAuthentication(e);
|
||||||
event.error(Errors.INVALID_CODE);
|
event.error(Errors.INVALID_CODE);
|
||||||
|
if (e.getResponse() != null) return e.getResponse();
|
||||||
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_CODE);
|
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_CODE);
|
||||||
|
|
||||||
} else if (e.getError() == AuthenticationFlowError.EXPIRED_CODE) {
|
} else if (e.getError() == AuthenticationFlowError.EXPIRED_CODE) {
|
||||||
ServicesLogger.LOGGER.failedAuthentication(e);
|
ServicesLogger.LOGGER.failedAuthentication(e);
|
||||||
event.error(Errors.EXPIRED_CODE);
|
event.error(Errors.EXPIRED_CODE);
|
||||||
|
if (e.getResponse() != null) return e.getResponse();
|
||||||
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.EXPIRED_CODE);
|
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.EXPIRED_CODE);
|
||||||
|
|
||||||
} else if (e.getError() == AuthenticationFlowError.FORK_FLOW) {
|
} else if (e.getError() == AuthenticationFlowError.FORK_FLOW) {
|
||||||
|
@ -701,9 +707,15 @@ public class AuthenticationProcessor {
|
||||||
CacheControlUtil.noBackButtonCacheControlHeader();
|
CacheControlUtil.noBackButtonCacheControlHeader();
|
||||||
return processor.authenticate();
|
return processor.authenticate();
|
||||||
|
|
||||||
|
} else if (e.getError() == AuthenticationFlowError.DISPLAY_NOT_SUPPORTED) {
|
||||||
|
ServicesLogger.LOGGER.failedAuthentication(e);
|
||||||
|
event.error(Errors.DISPLAY_UNSUPPORTED);
|
||||||
|
if (e.getResponse() != null) return e.getResponse();
|
||||||
|
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.DISPLAY_UNSUPPORTED);
|
||||||
} else {
|
} else {
|
||||||
ServicesLogger.LOGGER.failedAuthentication(e);
|
ServicesLogger.LOGGER.failedAuthentication(e);
|
||||||
event.error(Errors.INVALID_USER_CREDENTIALS);
|
event.error(Errors.INVALID_USER_CREDENTIALS);
|
||||||
|
if (e.getResponse() != null) return e.getResponse();
|
||||||
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
|
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.authentication;
|
package org.keycloak.authentication;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.AuthenticationFlowModel;
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
@ -58,6 +59,24 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
||||||
|| status == AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED;
|
|| status == AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Authenticator createAuthenticator(AuthenticatorFactory factory) {
|
||||||
|
String display = processor.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY);
|
||||||
|
if (display == null) return factory.create(processor.getSession());
|
||||||
|
|
||||||
|
|
||||||
|
if (factory instanceof DisplayTypeAuthenticatorFactory) {
|
||||||
|
Authenticator authenticator = ((DisplayTypeAuthenticatorFactory)factory).createDisplay(processor.getSession(), display);
|
||||||
|
if (authenticator != null) return authenticator;
|
||||||
|
}
|
||||||
|
// todo create a provider for handling lack of display support
|
||||||
|
if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
|
||||||
|
throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, TextChallenge.browserRequired(processor.getSession()));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return factory.create(processor.getSession());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response processAction(String actionExecution) {
|
public Response processAction(String actionExecution) {
|
||||||
|
@ -86,7 +105,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
||||||
if (factory == null) {
|
if (factory == null) {
|
||||||
throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?");
|
throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?");
|
||||||
}
|
}
|
||||||
Authenticator authenticator = factory.create(processor.getSession());
|
Authenticator authenticator = createAuthenticator(factory);
|
||||||
AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions);
|
AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions);
|
||||||
logger.debugv("action: {0}", model.getAuthenticator());
|
logger.debugv("action: {0}", model.getAuthenticator());
|
||||||
authenticator.action(result);
|
authenticator.action(result);
|
||||||
|
@ -161,7 +180,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
||||||
if (factory == null) {
|
if (factory == null) {
|
||||||
throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?");
|
throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?");
|
||||||
}
|
}
|
||||||
Authenticator authenticator = factory.create(processor.getSession());
|
Authenticator authenticator = createAuthenticator(factory);
|
||||||
logger.debugv("authenticator: {0}", factory.getId());
|
logger.debugv("authenticator: {0}", factory.getId());
|
||||||
UserModel authUser = processor.getAuthenticationSession().getAuthenticatedUser();
|
UserModel authUser = processor.getAuthenticationSession().getAuthenticatedUser();
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.keycloak.services.resources.LoginActionsService;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
||||||
|
@ -162,6 +163,16 @@ public class RequiredActionContextResult implements RequiredActionContext {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URI getActionUrl(boolean authSessionIdParam) {
|
||||||
|
URI uri = getActionUrl();
|
||||||
|
if (authSessionIdParam) {
|
||||||
|
uri = UriBuilder.fromUri(uri).queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()).build();
|
||||||
|
}
|
||||||
|
return uri;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoginFormsProvider form() {
|
public LoginFormsProvider form() {
|
||||||
String accessCode = generateCode();
|
String accessCode = generateCode();
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
package org.keycloak.authentication.authenticators;
|
||||||
|
|
||||||
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
|
import org.keycloak.authentication.Authenticator;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass-thru atheneticator that just sets the context to attempted.
|
||||||
|
*/
|
||||||
|
public class AttemptedAuthenticator implements Authenticator {
|
||||||
|
|
||||||
|
public static final AttemptedAuthenticator SINGLETON = new AttemptedAuthenticator();
|
||||||
|
@Override
|
||||||
|
public void authenticate(AuthenticationFlowContext context) {
|
||||||
|
context.attempted();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void action(AuthenticationFlowContext context) {
|
||||||
|
throw new RuntimeException("Unreachable!");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean requiresUser() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,8 +18,11 @@
|
||||||
package org.keycloak.authentication.authenticators.browser;
|
package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.authentication.AuthenticatorFactory;
|
import org.keycloak.authentication.AuthenticatorFactory;
|
||||||
|
import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
|
||||||
|
import org.keycloak.authentication.authenticators.AttemptedAuthenticator;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
@ -31,7 +34,7 @@ import java.util.List;
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class CookieAuthenticatorFactory implements AuthenticatorFactory {
|
public class CookieAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
|
||||||
public static final String PROVIDER_ID = "auth-cookie";
|
public static final String PROVIDER_ID = "auth-cookie";
|
||||||
static CookieAuthenticator SINGLETON = new CookieAuthenticator();
|
static CookieAuthenticator SINGLETON = new CookieAuthenticator();
|
||||||
|
|
||||||
|
@ -40,6 +43,13 @@ public class CookieAuthenticatorFactory implements AuthenticatorFactory {
|
||||||
return SINGLETON;
|
return SINGLETON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authenticator createDisplay(KeycloakSession session, String displayType) {
|
||||||
|
if (displayType == null) return SINGLETON;
|
||||||
|
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
|
||||||
|
return AttemptedAuthenticator.SINGLETON; // ignore this authenticator
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.authentication.authenticators.browser;
|
package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.constants.AdapterConstants;
|
import org.keycloak.constants.AdapterConstants;
|
||||||
|
@ -25,10 +26,13 @@ import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.managers.ClientSessionCode;
|
import org.keycloak.services.managers.ClientSessionCode;
|
||||||
|
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import java.net.URI;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,8 +70,11 @@ public class IdentityProviderAuthenticator implements Authenticator {
|
||||||
String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode();
|
String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode();
|
||||||
String clientId = context.getAuthenticationSession().getClient().getClientId();
|
String clientId = context.getAuthenticationSession().getClient().getClientId();
|
||||||
String tabId = context.getAuthenticationSession().getTabId();
|
String tabId = context.getAuthenticationSession().getTabId();
|
||||||
Response response = Response.seeOther(
|
URI location = Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId);
|
||||||
Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId))
|
if (context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY) != null) {
|
||||||
|
location = UriBuilder.fromUri(location).queryParam(OAuth2Constants.DISPLAY, context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY)).build();
|
||||||
|
}
|
||||||
|
Response response = Response.seeOther(location)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
LOG.debugf("Redirecting to %s", providerId);
|
LOG.debugf("Redirecting to %s", providerId);
|
||||||
|
|
|
@ -18,8 +18,11 @@
|
||||||
package org.keycloak.authentication.authenticators.browser;
|
package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.authentication.AuthenticatorFactory;
|
import org.keycloak.authentication.AuthenticatorFactory;
|
||||||
|
import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
|
||||||
|
import org.keycloak.authentication.authenticators.AttemptedAuthenticator;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
@ -33,7 +36,7 @@ import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactory {
|
public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
|
||||||
|
|
||||||
protected static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
protected static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||||
AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED
|
AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED
|
||||||
|
@ -82,6 +85,13 @@ public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactor
|
||||||
return new IdentityProviderAuthenticator();
|
return new IdentityProviderAuthenticator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authenticator createDisplay(KeycloakSession session, String displayType) {
|
||||||
|
if (displayType == null) return new IdentityProviderAuthenticator();
|
||||||
|
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
|
||||||
|
return AttemptedAuthenticator.SINGLETON; // ignore this authenticator
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,11 @@
|
||||||
package org.keycloak.authentication.authenticators.browser;
|
package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.authentication.AuthenticatorFactory;
|
import org.keycloak.authentication.AuthenticatorFactory;
|
||||||
|
import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
|
||||||
|
import org.keycloak.authentication.authenticators.console.ConsoleOTPFormAuthenticator;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
@ -32,7 +35,7 @@ import java.util.List;
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class OTPFormAuthenticatorFactory implements AuthenticatorFactory {
|
public class OTPFormAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "auth-otp-form";
|
public static final String PROVIDER_ID = "auth-otp-form";
|
||||||
public static final OTPFormAuthenticator SINGLETON = new OTPFormAuthenticator();
|
public static final OTPFormAuthenticator SINGLETON = new OTPFormAuthenticator();
|
||||||
|
@ -42,6 +45,13 @@ public class OTPFormAuthenticatorFactory implements AuthenticatorFactory {
|
||||||
return SINGLETON;
|
return SINGLETON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authenticator createDisplay(KeycloakSession session, String displayType) {
|
||||||
|
if (displayType == null) return SINGLETON;
|
||||||
|
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
|
||||||
|
return ConsoleOTPFormAuthenticator.SINGLETON;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,10 @@
|
||||||
package org.keycloak.authentication.authenticators.browser;
|
package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.authentication.AuthenticatorFactory;
|
import org.keycloak.authentication.AuthenticatorFactory;
|
||||||
|
import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
@ -32,7 +34,7 @@ import java.util.List;
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class SpnegoAuthenticatorFactory implements AuthenticatorFactory {
|
public class SpnegoAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "auth-spnego";
|
public static final String PROVIDER_ID = "auth-spnego";
|
||||||
public static final SpnegoAuthenticator SINGLETON = new SpnegoAuthenticator();
|
public static final SpnegoAuthenticator SINGLETON = new SpnegoAuthenticator();
|
||||||
|
@ -42,6 +44,13 @@ public class SpnegoAuthenticatorFactory implements AuthenticatorFactory {
|
||||||
return SINGLETON;
|
return SINGLETON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authenticator createDisplay(KeycloakSession session, String displayType) {
|
||||||
|
if (displayType == null) return SINGLETON;
|
||||||
|
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
|
||||||
|
return SINGLETON;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
|
@ -18,8 +18,11 @@
|
||||||
package org.keycloak.authentication.authenticators.browser;
|
package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.authentication.AuthenticatorFactory;
|
import org.keycloak.authentication.AuthenticatorFactory;
|
||||||
|
import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
|
||||||
|
import org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticator;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
@ -32,7 +35,7 @@ import java.util.List;
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class UsernamePasswordFormFactory implements AuthenticatorFactory {
|
public class UsernamePasswordFormFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "auth-username-password-form";
|
public static final String PROVIDER_ID = "auth-username-password-form";
|
||||||
public static final UsernamePasswordForm SINGLETON = new UsernamePasswordForm();
|
public static final UsernamePasswordForm SINGLETON = new UsernamePasswordForm();
|
||||||
|
@ -42,6 +45,13 @@ public class UsernamePasswordFormFactory implements AuthenticatorFactory {
|
||||||
return SINGLETON;
|
return SINGLETON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authenticator createDisplay(KeycloakSession session, String displayType) {
|
||||||
|
if (displayType == null) return SINGLETON;
|
||||||
|
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
|
||||||
|
return ConsoleUsernamePasswordAuthenticator.SINGLETON;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.authentication.authenticators.console;
|
||||||
|
|
||||||
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
|
import org.keycloak.authentication.Authenticator;
|
||||||
|
import org.keycloak.authentication.TextChallenge;
|
||||||
|
import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator;
|
||||||
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class ConsoleOTPFormAuthenticator extends OTPFormAuthenticator implements Authenticator {
|
||||||
|
public static final ConsoleOTPFormAuthenticator SINGLETON = new ConsoleOTPFormAuthenticator();
|
||||||
|
|
||||||
|
public static URI getCallbackUrl(AuthenticationFlowContext context) {
|
||||||
|
return context.getActionUrl(context.generateAccessCode(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TextChallenge challenge(AuthenticationFlowContext context) {
|
||||||
|
return TextChallenge.challenge(context)
|
||||||
|
.header()
|
||||||
|
.param(CredentialRepresentation.TOTP)
|
||||||
|
.label("console-otp")
|
||||||
|
.challenge();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void action(AuthenticationFlowContext context) {
|
||||||
|
validateOTP(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void authenticate(AuthenticationFlowContext context) {
|
||||||
|
Response challengeResponse = challenge(context, null);
|
||||||
|
context.challenge(challengeResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response challenge(AuthenticationFlowContext context, String msg) {
|
||||||
|
if (msg == null) {
|
||||||
|
return challenge(context).response();
|
||||||
|
}
|
||||||
|
return challenge(context).message(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.authentication.authenticators.console;
|
||||||
|
|
||||||
|
import org.keycloak.authentication.*;
|
||||||
|
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class ConsoleUsernamePasswordAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {
|
||||||
|
|
||||||
|
public static final ConsoleUsernamePasswordAuthenticator SINGLETON = new ConsoleUsernamePasswordAuthenticator();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean requiresUser() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TextChallenge challenge(AuthenticationFlowContext context) {
|
||||||
|
return TextChallenge.challenge(context)
|
||||||
|
.header()
|
||||||
|
.param("username")
|
||||||
|
.label("console-username")
|
||||||
|
.param("password")
|
||||||
|
.label("console-password")
|
||||||
|
.mask(true)
|
||||||
|
.challenge();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void authenticate(AuthenticationFlowContext context) {
|
||||||
|
Response response = challenge(context).form().createForm("cli_splash.ftl");
|
||||||
|
context.challenge(response);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response invalidUser(AuthenticationFlowContext context) {
|
||||||
|
Response response = challenge(context).message(Messages.INVALID_USER);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response disabledUser(AuthenticationFlowContext context) {
|
||||||
|
Response response = challenge(context).message(Messages.ACCOUNT_DISABLED);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response temporarilyDisabledUser(AuthenticationFlowContext context) {
|
||||||
|
Response response = challenge(context).message(Messages.INVALID_USER);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response invalidCredentials(AuthenticationFlowContext context) {
|
||||||
|
Response response = challenge(context).message(Messages.INVALID_USER);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
|
||||||
|
context.getEvent().error(eventError);
|
||||||
|
Response response = challenge(context).message(loginFormError);
|
||||||
|
|
||||||
|
context.failureChallenge(authenticatorError, response);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void action(AuthenticationFlowContext context) {
|
||||||
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
if (!validateUserAndPassword(context, formData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.authentication.authenticators.console;
|
||||||
|
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.authentication.Authenticator;
|
||||||
|
import org.keycloak.authentication.AuthenticatorFactory;
|
||||||
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.UserCredentialModel;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class ConsoleUsernamePasswordAuthenticatorFactory implements AuthenticatorFactory {
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "console-username-password";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authenticator create(KeycloakSession session) {
|
||||||
|
return ConsoleUsernamePasswordAuthenticator.SINGLETON;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getReferenceCategory() {
|
||||||
|
return UserCredentialModel.PASSWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConfigurable() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||||
|
AuthenticationExecutionModel.Requirement.REQUIRED
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||||
|
return REQUIREMENT_CHOICES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayType() {
|
||||||
|
return "Username Password Challenge";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "Proprietary challenge protocol for CLI clients that queries for username password";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUserSetupAllowed() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.authentication.requiredactions;
|
||||||
|
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.authentication.RequiredActionContext;
|
||||||
|
import org.keycloak.authentication.RequiredActionFactory;
|
||||||
|
import org.keycloak.authentication.RequiredActionProvider;
|
||||||
|
import org.keycloak.authentication.TextChallenge;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class ConsoleTermsAndConditions implements RequiredActionProvider {
|
||||||
|
public static final ConsoleTermsAndConditions SINGLETON = new ConsoleTermsAndConditions();
|
||||||
|
public static final String USER_ATTRIBUTE = TermsAndConditions.PROVIDER_ID;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void evaluateTriggers(RequiredActionContext context) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requiredActionChallenge(RequiredActionContext context) {
|
||||||
|
Response challenge = TextChallenge.challenge(context)
|
||||||
|
.header()
|
||||||
|
.param("accept")
|
||||||
|
.label("console-accept-terms")
|
||||||
|
.message("termsPlainText");
|
||||||
|
context.challenge(challenge);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processAction(RequiredActionContext context) {
|
||||||
|
String accept = context.getHttpRequest().getDecodedFormParameters().getFirst("accept");
|
||||||
|
|
||||||
|
String yes = context.form().getMessage("console-accept");
|
||||||
|
|
||||||
|
if (!accept.equals(yes)) {
|
||||||
|
context.getUser().removeAttribute(USER_ATTRIBUTE);
|
||||||
|
requiredActionChallenge(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.getUser().setAttribute(USER_ATTRIBUTE, Arrays.asList(Integer.toString(Time.currentTime())));
|
||||||
|
|
||||||
|
context.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.authentication.requiredactions;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.authentication.*;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
|
import org.keycloak.events.Errors;
|
||||||
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.models.*;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class ConsoleUpdatePassword extends UpdatePassword implements RequiredActionProvider {
|
||||||
|
public static final ConsoleUpdatePassword SINGLETON = new ConsoleUpdatePassword();
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(ConsoleUpdatePassword.class);
|
||||||
|
public static final String PASSWORD_NEW = "password-new";
|
||||||
|
public static final String PASSWORD_CONFIRM = "password-confirm";
|
||||||
|
|
||||||
|
protected TextChallenge challenge(RequiredActionContext context) {
|
||||||
|
return TextChallenge.challenge(context)
|
||||||
|
.header()
|
||||||
|
.param(PASSWORD_NEW)
|
||||||
|
.label("console-new-password")
|
||||||
|
.mask(true)
|
||||||
|
.param(PASSWORD_CONFIRM)
|
||||||
|
.label("console-confirm-password")
|
||||||
|
.mask(true)
|
||||||
|
.challenge();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requiredActionChallenge(RequiredActionContext context) {
|
||||||
|
context.challenge(
|
||||||
|
challenge(context).message("console-update-password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processAction(RequiredActionContext context) {
|
||||||
|
EventBuilder event = context.getEvent();
|
||||||
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
event.event(EventType.UPDATE_PASSWORD);
|
||||||
|
String passwordNew = formData.getFirst(PASSWORD_NEW);
|
||||||
|
String passwordConfirm = formData.getFirst(PASSWORD_CONFIRM);
|
||||||
|
|
||||||
|
EventBuilder errorEvent = event.clone().event(EventType.UPDATE_PASSWORD_ERROR)
|
||||||
|
.client(context.getAuthenticationSession().getClient())
|
||||||
|
.user(context.getAuthenticationSession().getAuthenticatedUser());
|
||||||
|
|
||||||
|
if (Validation.isBlank(passwordNew)) {
|
||||||
|
context.challenge(challenge(context).message(Messages.MISSING_PASSWORD));
|
||||||
|
errorEvent.error(Errors.PASSWORD_MISSING);
|
||||||
|
return;
|
||||||
|
} else if (!passwordNew.equals(passwordConfirm)) {
|
||||||
|
context.challenge(challenge(context).message(Messages.NOTMATCH_PASSWORD));
|
||||||
|
errorEvent.error(Errors.PASSWORD_CONFIRM_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), UserCredentialModel.password(passwordNew, false));
|
||||||
|
context.success();
|
||||||
|
} catch (ModelException me) {
|
||||||
|
errorEvent.detail(Details.REASON, me.getMessage()).error(Errors.PASSWORD_REJECTED);
|
||||||
|
context.challenge(challenge(context).text(me.getMessage()));
|
||||||
|
return;
|
||||||
|
} catch (Exception ape) {
|
||||||
|
errorEvent.detail(Details.REASON, ape.getMessage()).error(Errors.PASSWORD_REJECTED);
|
||||||
|
context.challenge(challenge(context).text(ape.getMessage()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.authentication.requiredactions;
|
||||||
|
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.authentication.RequiredActionContext;
|
||||||
|
import org.keycloak.authentication.RequiredActionFactory;
|
||||||
|
import org.keycloak.authentication.RequiredActionProvider;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.utils.FormMessage;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.services.resources.AttributeFormDataProcessor;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class ConsoleUpdateProfile implements RequiredActionProvider {
|
||||||
|
public static final ConsoleUpdateProfile SINGLETON = new ConsoleUpdateProfile();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void evaluateTriggers(RequiredActionContext context) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requiredActionChallenge(RequiredActionContext context) {
|
||||||
|
// do nothing right now. I think this behavior is ok. We just defer this action until a browser login happens.
|
||||||
|
context.ignore();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processAction(RequiredActionContext context) {
|
||||||
|
throw new RuntimeException("Should be unreachable");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.authentication.requiredactions;
|
||||||
|
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.authentication.RequiredActionContext;
|
||||||
|
import org.keycloak.authentication.RequiredActionFactory;
|
||||||
|
import org.keycloak.authentication.RequiredActionProvider;
|
||||||
|
import org.keycloak.authentication.TextChallenge;
|
||||||
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
|
import org.keycloak.forms.login.freemarker.model.TotpBean;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.UserCredentialModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.utils.CredentialValidation;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class ConsoleUpdateTotp implements RequiredActionProvider {
|
||||||
|
public static final ConsoleUpdateTotp SINGLETON = new ConsoleUpdateTotp();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void evaluateTriggers(RequiredActionContext context) {
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void requiredActionChallenge(RequiredActionContext context) {
|
||||||
|
TotpBean totpBean = new TotpBean(context.getSession(), context.getRealm(), context.getUser(), context.getUriInfo().getRequestUriBuilder());
|
||||||
|
String totpSecret = totpBean.getTotpSecret();
|
||||||
|
context.getAuthenticationSession().setAuthNote("totpSecret", totpSecret);
|
||||||
|
Response challenge = challenge(context).form()
|
||||||
|
.setAttribute("totp", totpBean)
|
||||||
|
.createForm("login-config-totp-text.ftl");
|
||||||
|
context.challenge(challenge);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TextChallenge challenge(RequiredActionContext context) {
|
||||||
|
return TextChallenge.challenge(context)
|
||||||
|
.header()
|
||||||
|
.param("totp")
|
||||||
|
.label("console-otp")
|
||||||
|
.challenge();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processAction(RequiredActionContext context) {
|
||||||
|
EventBuilder event = context.getEvent();
|
||||||
|
event.event(EventType.UPDATE_TOTP);
|
||||||
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
String totp = formData.getFirst("totp");
|
||||||
|
String totpSecret = context.getAuthenticationSession().getAuthNote("totpSecret");
|
||||||
|
|
||||||
|
if (Validation.isBlank(totp)) {
|
||||||
|
context.challenge(
|
||||||
|
challenge(context).message(Messages.MISSING_TOTP)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (!CredentialValidation.validOTP(context.getRealm(), totp, totpSecret)) {
|
||||||
|
context.challenge(
|
||||||
|
challenge(context).message(Messages.INVALID_TOTP)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserCredentialModel credentials = new UserCredentialModel();
|
||||||
|
credentials.setType(context.getRealm().getOTPPolicy().getType());
|
||||||
|
credentials.setValue(totpSecret);
|
||||||
|
context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), credentials);
|
||||||
|
|
||||||
|
|
||||||
|
// if type is HOTP, to update counter we execute validation based on supplied token
|
||||||
|
UserCredentialModel cred = new UserCredentialModel();
|
||||||
|
cred.setType(context.getRealm().getOTPPolicy().getType());
|
||||||
|
cred.setValue(totp);
|
||||||
|
context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(), cred);
|
||||||
|
|
||||||
|
context.getAuthenticationSession().removeAuthNote("totpSecret");
|
||||||
|
context.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.authentication.requiredactions;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.authentication.RequiredActionContext;
|
||||||
|
import org.keycloak.authentication.RequiredActionFactory;
|
||||||
|
import org.keycloak.authentication.RequiredActionProvider;
|
||||||
|
import org.keycloak.authentication.TextChallenge;
|
||||||
|
import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
|
||||||
|
import org.keycloak.common.util.RandomString;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.email.EmailException;
|
||||||
|
import org.keycloak.email.EmailTemplateProvider;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
|
import org.keycloak.events.Errors;
|
||||||
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
|
import org.keycloak.models.*;
|
||||||
|
import org.keycloak.services.Urls;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.*;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class ConsoleVerifyEmail implements RequiredActionProvider {
|
||||||
|
public static final ConsoleVerifyEmail SINGLETON = new ConsoleVerifyEmail();
|
||||||
|
private static final Logger logger = Logger.getLogger(ConsoleVerifyEmail.class);
|
||||||
|
@Override
|
||||||
|
public void evaluateTriggers(RequiredActionContext context) {
|
||||||
|
if (context.getRealm().isVerifyEmail() && !context.getUser().isEmailVerified()) {
|
||||||
|
context.getUser().addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
|
||||||
|
logger.debug("User is required to verify email");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requiredActionChallenge(RequiredActionContext context) {
|
||||||
|
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
||||||
|
|
||||||
|
if (context.getUser().isEmailVerified()) {
|
||||||
|
context.success();
|
||||||
|
authSession.removeAuthNote(Constants.VERIFY_EMAIL_KEY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String email = context.getUser().getEmail();
|
||||||
|
if (Validation.isBlank(email)) {
|
||||||
|
context.ignore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response challenge = sendVerifyEmail(context);
|
||||||
|
context.challenge(challenge);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processAction(RequiredActionContext context) {
|
||||||
|
EventBuilder event = context.getEvent().clone().event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, context.getUser().getEmail());
|
||||||
|
String code = context.getAuthenticationSession().getAuthNote(Constants.VERIFY_EMAIL_CODE);
|
||||||
|
if (code == null) {
|
||||||
|
requiredActionChallenge(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
String emailCode = formData.getFirst(EMAIL_CODE);
|
||||||
|
|
||||||
|
if (!code.equals(emailCode)) {
|
||||||
|
context.challenge(
|
||||||
|
challenge(context).message(Messages.INVALID_CODE)
|
||||||
|
);
|
||||||
|
event.error(Errors.INVALID_CODE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.success();
|
||||||
|
context.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String EMAIL_CODE="email_code";
|
||||||
|
protected TextChallenge challenge(RequiredActionContext context) {
|
||||||
|
return TextChallenge.challenge(context)
|
||||||
|
.header()
|
||||||
|
.param(EMAIL_CODE)
|
||||||
|
.label("console-email-code")
|
||||||
|
.challenge();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response sendVerifyEmail(RequiredActionContext context) throws UriBuilderException, IllegalArgumentException {
|
||||||
|
KeycloakSession session = context.getSession();
|
||||||
|
UserModel user = context.getUser();
|
||||||
|
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
||||||
|
EventBuilder event = context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail());
|
||||||
|
String code = RandomString.randomCode(8);
|
||||||
|
authSession.setAuthNote(Constants.VERIFY_EMAIL_CODE, code);
|
||||||
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
attributes.put("code", code);
|
||||||
|
|
||||||
|
try {
|
||||||
|
session
|
||||||
|
.getProvider(EmailTemplateProvider.class)
|
||||||
|
.setAuthenticationSession(authSession)
|
||||||
|
.setRealm(realm)
|
||||||
|
.setUser(user)
|
||||||
|
.send("emailVerificationSubject", "email-verification-with-code.ftl", attributes);
|
||||||
|
event.success();
|
||||||
|
} catch (EmailException e) {
|
||||||
|
logger.error("Failed to send verification email", e);
|
||||||
|
event.error(Errors.EMAIL_SEND_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return challenge(context).text(context.form().getMessage("console-verify-email", user.getEmail()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,9 +18,8 @@
|
||||||
package org.keycloak.authentication.requiredactions;
|
package org.keycloak.authentication.requiredactions;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.authentication.RequiredActionContext;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.RequiredActionFactory;
|
import org.keycloak.authentication.*;
|
||||||
import org.keycloak.authentication.RequiredActionProvider;
|
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
@ -32,7 +31,7 @@ import java.util.Arrays;
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory {
|
public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
|
||||||
public static final String PROVIDER_ID = "terms_and_conditions";
|
public static final String PROVIDER_ID = "terms_and_conditions";
|
||||||
public static final String USER_ATTRIBUTE = PROVIDER_ID;
|
public static final String USER_ATTRIBUTE = PROVIDER_ID;
|
||||||
|
|
||||||
|
@ -41,6 +40,15 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
|
||||||
|
if (displayType == null) return this;
|
||||||
|
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
|
||||||
|
return ConsoleTermsAndConditions.SINGLETON;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,8 @@ package org.keycloak.authentication.requiredactions;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.authentication.RequiredActionContext;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.RequiredActionFactory;
|
import org.keycloak.authentication.*;
|
||||||
import org.keycloak.authentication.RequiredActionProvider;
|
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.credential.CredentialModel;
|
import org.keycloak.credential.CredentialModel;
|
||||||
import org.keycloak.credential.CredentialProvider;
|
import org.keycloak.credential.CredentialProvider;
|
||||||
|
@ -47,7 +46,7 @@ import java.util.concurrent.TimeUnit;
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class UpdatePassword implements RequiredActionProvider, RequiredActionFactory {
|
public class UpdatePassword implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
|
||||||
private static final Logger logger = Logger.getLogger(UpdatePassword.class);
|
private static final Logger logger = Logger.getLogger(UpdatePassword.class);
|
||||||
@Override
|
@Override
|
||||||
public void evaluateTriggers(RequiredActionContext context) {
|
public void evaluateTriggers(RequiredActionContext context) {
|
||||||
|
@ -142,6 +141,15 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
|
||||||
|
if (displayType == null) return this;
|
||||||
|
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
|
||||||
|
return ConsoleUpdatePassword.SINGLETON;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,8 @@
|
||||||
package org.keycloak.authentication.requiredactions;
|
package org.keycloak.authentication.requiredactions;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.authentication.RequiredActionContext;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.RequiredActionFactory;
|
import org.keycloak.authentication.*;
|
||||||
import org.keycloak.authentication.RequiredActionProvider;
|
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
|
@ -41,7 +40,7 @@ import java.util.List;
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class UpdateProfile implements RequiredActionProvider, RequiredActionFactory {
|
public class UpdateProfile implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
|
||||||
@Override
|
@Override
|
||||||
public void evaluateTriggers(RequiredActionContext context) {
|
public void evaluateTriggers(RequiredActionContext context) {
|
||||||
}
|
}
|
||||||
|
@ -142,6 +141,16 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
|
||||||
|
if (displayType == null) return this;
|
||||||
|
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
|
||||||
|
return ConsoleUpdateProfile.SINGLETON;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,8 @@
|
||||||
package org.keycloak.authentication.requiredactions;
|
package org.keycloak.authentication.requiredactions;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.authentication.RequiredActionContext;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.RequiredActionFactory;
|
import org.keycloak.authentication.*;
|
||||||
import org.keycloak.authentication.RequiredActionProvider;
|
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -38,7 +37,7 @@ import javax.ws.rs.core.Response;
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory {
|
public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
|
||||||
@Override
|
@Override
|
||||||
public void evaluateTriggers(RequiredActionContext context) {
|
public void evaluateTriggers(RequiredActionContext context) {
|
||||||
}
|
}
|
||||||
|
@ -105,6 +104,15 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
|
||||||
|
if (displayType == null) return this;
|
||||||
|
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
|
||||||
|
return ConsoleUpdateTotp.SINGLETON;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,8 @@ package org.keycloak.authentication.requiredactions;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.authentication.RequiredActionContext;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.RequiredActionFactory;
|
import org.keycloak.authentication.*;
|
||||||
import org.keycloak.authentication.RequiredActionProvider;
|
|
||||||
import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
|
import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.email.EmailException;
|
import org.keycloak.email.EmailException;
|
||||||
|
@ -45,7 +44,7 @@ import javax.ws.rs.core.*;
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class VerifyEmail implements RequiredActionProvider, RequiredActionFactory {
|
public class VerifyEmail implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
|
||||||
private static final Logger logger = Logger.getLogger(VerifyEmail.class);
|
private static final Logger logger = Logger.getLogger(VerifyEmail.class);
|
||||||
@Override
|
@Override
|
||||||
public void evaluateTriggers(RequiredActionContext context) {
|
public void evaluateTriggers(RequiredActionContext context) {
|
||||||
|
@ -107,6 +106,14 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
|
||||||
|
if (displayType == null) return this;
|
||||||
|
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
|
||||||
|
return ConsoleVerifyEmail.SINGLETON;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
|
|
@ -192,8 +192,9 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void send(String subjectKey, String template, Map<String, Object> attributes) throws EmailException {
|
@Override
|
||||||
send(subjectKey, Collections.emptyList(), template, attributes);
|
public void send(String subjectFormatKey, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException {
|
||||||
|
send(subjectFormatKey, Collections.emptyList(), bodyTemplate, bodyAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected EmailTemplate processTemplate(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
|
protected EmailTemplate processTemplate(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
|
||||||
|
@ -229,9 +230,10 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
|
||||||
return session.theme().getTheme(Theme.Type.EMAIL);
|
return session.theme().getTheme(Theme.Type.EMAIL);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
|
@Override
|
||||||
|
public void send(String subjectFormatKey, List<Object> subjectAttributes, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException {
|
||||||
try {
|
try {
|
||||||
EmailTemplate email = processTemplate(subjectKey, subjectAttributes, template, attributes);
|
EmailTemplate email = processTemplate(subjectFormatKey, subjectAttributes, bodyTemplate, bodyAttributes);
|
||||||
send(email.getSubject(), email.getTextBody(), email.getHtmlBody());
|
send(email.getSubject(), email.getTextBody(), email.getHtmlBody());
|
||||||
} catch (EmailException e) {
|
} catch (EmailException e) {
|
||||||
throw e;
|
throw e;
|
||||||
|
|
|
@ -332,9 +332,25 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
Properties messagesBundle = handleThemeResources(theme, locale);
|
Properties messagesBundle = handleThemeResources(theme, locale);
|
||||||
FormMessage msg = new FormMessage(null, message);
|
FormMessage msg = new FormMessage(null, message);
|
||||||
return formatMessage(msg, messagesBundle, locale);
|
return formatMessage(msg, messagesBundle, locale);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage(String message, String... parameters) {
|
||||||
|
Theme theme;
|
||||||
|
try {
|
||||||
|
theme = getTheme();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Failed to create theme", e);
|
||||||
|
throw new RuntimeException("Failed to create theme");
|
||||||
|
}
|
||||||
|
|
||||||
|
Locale locale = session.getContext().resolveLocale(user);
|
||||||
|
Properties messagesBundle = handleThemeResources(theme, locale);
|
||||||
|
FormMessage msg = new FormMessage(message, parameters);
|
||||||
|
return formatMessage(msg, messagesBundle, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create common attributes used in all templates.
|
* Create common attributes used in all templates.
|
||||||
*
|
*
|
||||||
|
|
|
@ -71,6 +71,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
||||||
public static final String MAX_AGE_PARAM = OAuth2Constants.MAX_AGE;
|
public static final String MAX_AGE_PARAM = OAuth2Constants.MAX_AGE;
|
||||||
public static final String PROMPT_PARAM = OAuth2Constants.PROMPT;
|
public static final String PROMPT_PARAM = OAuth2Constants.PROMPT;
|
||||||
public static final String LOGIN_HINT_PARAM = "login_hint";
|
public static final String LOGIN_HINT_PARAM = "login_hint";
|
||||||
|
public static final String DISPLAY_PARAM = "display";
|
||||||
public static final String REQUEST_PARAM = "request";
|
public static final String REQUEST_PARAM = "request";
|
||||||
public static final String REQUEST_URI_PARAM = "request_uri";
|
public static final String REQUEST_URI_PARAM = "request_uri";
|
||||||
public static final String UI_LOCALES_PARAM = OAuth2Constants.UI_LOCALES_PARAM;
|
public static final String UI_LOCALES_PARAM = OAuth2Constants.UI_LOCALES_PARAM;
|
||||||
|
|
|
@ -371,6 +371,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||||
if (request.getResponseMode() != null) authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
|
if (request.getResponseMode() != null) authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
|
||||||
if (request.getClaims()!= null) authenticationSession.setClientNote(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims());
|
if (request.getClaims()!= null) authenticationSession.setClientNote(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims());
|
||||||
if (request.getAcr() != null) authenticationSession.setClientNote(OIDCLoginProtocol.ACR_PARAM, request.getAcr());
|
if (request.getAcr() != null) authenticationSession.setClientNote(OIDCLoginProtocol.ACR_PARAM, request.getAcr());
|
||||||
|
if (request.getDisplay() != null) authenticationSession.setClientNote(OAuth2Constants.DISPLAY, request.getDisplay());
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc7636#section-4
|
// https://tools.ietf.org/html/rfc7636#section-4
|
||||||
if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
|
if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
|
||||||
|
|
|
@ -164,12 +164,11 @@ public class LogoutEndpoint {
|
||||||
*
|
*
|
||||||
* returns 204 if successful, 400 if not with a json error response.
|
* returns 204 if successful, 400 if not with a json error response.
|
||||||
*
|
*
|
||||||
* @param authorizationHeader
|
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@POST
|
@POST
|
||||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
public Response logoutToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader) {
|
public Response logoutToken() {
|
||||||
MultivaluedMap<String, String> form = request.getDecodedFormParameters();
|
MultivaluedMap<String, String> form = request.getDecodedFormParameters();
|
||||||
checkSsl();
|
checkSsl();
|
||||||
|
|
||||||
|
|
|
@ -776,8 +776,15 @@ public class TokenEndpoint {
|
||||||
String audience = formParams.getFirst(OAuth2Constants.AUDIENCE);
|
String audience = formParams.getFirst(OAuth2Constants.AUDIENCE);
|
||||||
if (audience != null) {
|
if (audience != null) {
|
||||||
targetClient = realm.getClientByClientId(audience);
|
targetClient = realm.getClientByClientId(audience);
|
||||||
|
if (targetClient == null) {
|
||||||
|
event.detail(Details.REASON, "audience not found");
|
||||||
|
event.error(Errors.CLIENT_NOT_FOUND);
|
||||||
|
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, "Audience not found", Response.Status.BAD_REQUEST);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (targetClient.isConsentRequired()) {
|
if (targetClient.isConsentRequired()) {
|
||||||
event.detail(Details.REASON, "audience requires consent");
|
event.detail(Details.REASON, "audience requires consent");
|
||||||
event.error(Errors.CONSENT_DENIED);
|
event.error(Errors.CONSENT_DENIED);
|
||||||
|
|
|
@ -32,6 +32,7 @@ public class AuthorizationEndpointRequest {
|
||||||
String state;
|
String state;
|
||||||
String scope;
|
String scope;
|
||||||
String loginHint;
|
String loginHint;
|
||||||
|
String display;
|
||||||
String prompt;
|
String prompt;
|
||||||
String nonce;
|
String nonce;
|
||||||
Integer maxAge;
|
Integer maxAge;
|
||||||
|
@ -111,4 +112,7 @@ public class AuthorizationEndpointRequest {
|
||||||
return codeChallengeMethod;
|
return codeChallengeMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDisplay() {
|
||||||
|
return display;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.protocol.oidc.endpoints.request;
|
package org.keycloak.protocol.oidc.endpoints.request;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.constants.AdapterConstants;
|
import org.keycloak.constants.AdapterConstants;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
|
||||||
|
@ -91,6 +92,7 @@ abstract class AuthzEndpointRequestParser {
|
||||||
request.maxAge = replaceIfNotNull(request.maxAge, getIntParameter(OIDCLoginProtocol.MAX_AGE_PARAM));
|
request.maxAge = replaceIfNotNull(request.maxAge, getIntParameter(OIDCLoginProtocol.MAX_AGE_PARAM));
|
||||||
request.claims = replaceIfNotNull(request.claims, getParameter(OIDCLoginProtocol.CLAIMS_PARAM));
|
request.claims = replaceIfNotNull(request.claims, getParameter(OIDCLoginProtocol.CLAIMS_PARAM));
|
||||||
request.acr = replaceIfNotNull(request.acr, getParameter(OIDCLoginProtocol.ACR_PARAM));
|
request.acr = replaceIfNotNull(request.acr, getParameter(OIDCLoginProtocol.ACR_PARAM));
|
||||||
|
request.display = replaceIfNotNull(request.display, getParameter(OAuth2Constants.DISPLAY));
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc7636#section-6.1
|
// https://tools.ietf.org/html/rfc7636#section-6.1
|
||||||
request.codeChallenge = replaceIfNotNull(request.codeChallenge, getParameter(OIDCLoginProtocol.CODE_CHALLENGE_PARAM));
|
request.codeChallenge = replaceIfNotNull(request.codeChallenge, getParameter(OIDCLoginProtocol.CODE_CHALLENGE_PARAM));
|
||||||
|
|
|
@ -21,11 +21,7 @@ import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.TokenVerifier;
|
import org.keycloak.TokenVerifier;
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
import org.keycloak.authentication.*;
|
||||||
import org.keycloak.authentication.RequiredActionContext;
|
|
||||||
import org.keycloak.authentication.RequiredActionContextResult;
|
|
||||||
import org.keycloak.authentication.RequiredActionFactory;
|
|
||||||
import org.keycloak.authentication.RequiredActionProvider;
|
|
||||||
import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
|
import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
|
||||||
import org.keycloak.broker.provider.IdentityProvider;
|
import org.keycloak.broker.provider.IdentityProvider;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
|
@ -761,6 +757,11 @@ public class AuthenticationManager {
|
||||||
uriBuilder.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId());
|
uriBuilder.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId());
|
||||||
uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId());
|
uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId());
|
||||||
|
|
||||||
|
if (uriInfo.getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
|
||||||
|
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, authSession.getParentSession().getId());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
URI redirect = uriBuilder.build(realm.getName());
|
URI redirect = uriBuilder.build(realm.getName());
|
||||||
return Response.status(302).location(redirect).build();
|
return Response.status(302).location(redirect).build();
|
||||||
|
|
||||||
|
@ -965,6 +966,25 @@ public class AuthenticationManager {
|
||||||
authSession.setProtocolMappers(requestedProtocolMappers);
|
authSession.setProtocolMappers(requestedProtocolMappers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static RequiredActionProvider createRequiredAction(KeycloakSession session, RequiredActionFactory factory, AuthenticationSessionModel authSession) {
|
||||||
|
String display = authSession.getClientNote(OAuth2Constants.DISPLAY);
|
||||||
|
if (display == null) return factory.create(session);
|
||||||
|
|
||||||
|
|
||||||
|
if (factory instanceof DisplayTypeRequiredActionFactory) {
|
||||||
|
RequiredActionProvider provider = ((DisplayTypeRequiredActionFactory)factory).createDisplay(session, display);
|
||||||
|
if (provider != null) return provider;
|
||||||
|
}
|
||||||
|
// todo create a provider for handling lack of display support
|
||||||
|
if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
|
||||||
|
throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, TextChallenge.browserRequired(session));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return factory.create(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
protected static Response executionActions(KeycloakSession session, AuthenticationSessionModel authSession,
|
protected static Response executionActions(KeycloakSession session, AuthenticationSessionModel authSession,
|
||||||
HttpRequest request, EventBuilder event, RealmModel realm, UserModel user,
|
HttpRequest request, EventBuilder event, RealmModel realm, UserModel user,
|
||||||
Set<String> requiredActions) {
|
Set<String> requiredActions) {
|
||||||
|
@ -982,7 +1002,15 @@ public class AuthenticationManager {
|
||||||
if (factory == null) {
|
if (factory == null) {
|
||||||
throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?");
|
throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?");
|
||||||
}
|
}
|
||||||
RequiredActionProvider actionProvider = factory.create(session);
|
RequiredActionProvider actionProvider = null;
|
||||||
|
try {
|
||||||
|
actionProvider = createRequiredAction(session, factory, authSession);
|
||||||
|
} catch (AuthenticationFlowException e) {
|
||||||
|
if (e.getResponse() != null) {
|
||||||
|
return e.getResponse();
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, user, factory);
|
RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, user, factory);
|
||||||
actionProvider.requiredActionChallenge(context);
|
actionProvider.requiredActionChallenge(context);
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ package org.keycloak.services.messages;
|
||||||
*/
|
*/
|
||||||
public class Messages {
|
public class Messages {
|
||||||
|
|
||||||
|
public static final String DISPLAY_UNSUPPORTED = "displayUnsupported";
|
||||||
public static final String LOGIN_TIMEOUT = "loginTimeout";
|
public static final String LOGIN_TIMEOUT = "loginTimeout";
|
||||||
|
|
||||||
public static final String INVALID_USER = "invalidUserMessage";
|
public static final String INVALID_USER = "invalidUserMessage";
|
||||||
|
|
|
@ -16,17 +16,12 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.services.resources;
|
package org.keycloak.services.resources;
|
||||||
|
|
||||||
|
import org.keycloak.authentication.*;
|
||||||
import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
|
import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
|
||||||
import org.keycloak.authentication.RequiredActionContext;
|
|
||||||
import org.keycloak.authentication.RequiredActionContextResult;
|
|
||||||
import org.keycloak.authentication.RequiredActionFactory;
|
|
||||||
import org.keycloak.authentication.RequiredActionProvider;
|
|
||||||
import org.keycloak.TokenVerifier;
|
import org.keycloak.TokenVerifier;
|
||||||
import org.keycloak.authentication.ExplainedVerificationException;
|
|
||||||
import org.keycloak.authentication.actiontoken.*;
|
import org.keycloak.authentication.actiontoken.*;
|
||||||
import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler;
|
import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler;
|
||||||
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
||||||
|
@ -934,7 +929,15 @@ public class LoginActionsService {
|
||||||
event.error(Errors.INVALID_CODE);
|
event.error(Errors.INVALID_CODE);
|
||||||
throw new WebApplicationException(ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_CODE));
|
throw new WebApplicationException(ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_CODE));
|
||||||
}
|
}
|
||||||
RequiredActionProvider provider = factory.create(session);
|
RequiredActionProvider provider = null;
|
||||||
|
try {
|
||||||
|
provider = AuthenticationManager.createRequiredAction(session, factory, authSession);
|
||||||
|
} catch (AuthenticationFlowException e) {
|
||||||
|
if (e.getResponse() != null) {
|
||||||
|
return e.getResponse();
|
||||||
|
}
|
||||||
|
throw new WebApplicationException(ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.DISPLAY_UNSUPPORTED));
|
||||||
|
}
|
||||||
|
|
||||||
RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, authSession.getAuthenticatedUser(), factory) {
|
RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, authSession.getAuthenticatedUser(), factory) {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -45,6 +45,12 @@ public class TotpUtils {
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String decode(String totpSecretEncoded) {
|
||||||
|
String encoded = totpSecretEncoded.replace(" ", "");
|
||||||
|
byte[] bytes = Base32.decode(encoded);
|
||||||
|
return new String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
public static String qrCode(String totpSecret, RealmModel realm, UserModel user) {
|
public static String qrCode(String totpSecret, RealmModel realm, UserModel user) {
|
||||||
try {
|
try {
|
||||||
String keyUri = realm.getOTPPolicy().getKeyURI(realm, user, totpSecret);
|
String keyUri = realm.getOTPPolicy().getKeyURI(realm, user, totpSecret);
|
||||||
|
|
|
@ -38,4 +38,4 @@ org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFacto
|
||||||
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
|
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory
|
org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory
|
||||||
org.keycloak.protocol.docker.DockerAuthenticatorFactory
|
org.keycloak.protocol.docker.DockerAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.cli.CliUsernamePasswordAuthenticatorFactory
|
org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory
|
||||||
|
|
|
@ -228,11 +228,47 @@
|
||||||
<type>zip</type>
|
<type>zip</type>
|
||||||
<outputDirectory>${containers.home}</outputDirectory>
|
<outputDirectory>${containers.home}</outputDirectory>
|
||||||
</artifactItem>
|
</artifactItem>
|
||||||
</artifactItems>
|
</artifactItems>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.igormaznitsa</groupId>
|
||||||
|
<artifactId>mvn-golang-wrapper</artifactId>
|
||||||
|
<version>2.1.6</version>
|
||||||
|
<extensions>true</extensions>
|
||||||
|
<configuration>
|
||||||
|
<goVersion>1.9.2</goVersion>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>get-mousetrap</id>
|
||||||
|
<goals>
|
||||||
|
<goal>get</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<packages>
|
||||||
|
<package>github.com/inconshreveable/mousetrap</package>
|
||||||
|
</packages>
|
||||||
|
<goPath>${project.build.directory}/gopath</goPath>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>get-kcinit</id>
|
||||||
|
<goals>
|
||||||
|
<goal>get</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<packages>
|
||||||
|
<package>github.com/keycloak/kcinit</package>
|
||||||
|
</packages>
|
||||||
|
<goPath>${project.build.directory}/gopath</goPath>
|
||||||
|
<tag>0.3</tag>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
</plugins>
|
</plugins>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package org.keycloak.testsuite.cli;
|
||||||
|
|
||||||
|
import org.keycloak.testsuite.cli.exec.AbstractExec;
|
||||||
|
import org.keycloak.testsuite.cli.exec.AbstractExecBuilder;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class KcinitExec extends AbstractExec {
|
||||||
|
|
||||||
|
public static final String WORK_DIR = System.getProperty("user.dir") + "/target";
|
||||||
|
|
||||||
|
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<KcinitExec> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KcinitExec execute() {
|
||||||
|
KcinitExec exe = new KcinitExec(workDir, argsLine, env, stdin);
|
||||||
|
exe.dumpStreams = dumpStreams;
|
||||||
|
exe.execute();
|
||||||
|
return exe;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KcinitExec executeAsync() {
|
||||||
|
KcinitExec exe = new KcinitExec(workDir, argsLine, env, stdin);
|
||||||
|
exe.dumpStreams = dumpStreams;
|
||||||
|
exe.executeAsync();
|
||||||
|
return exe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package org.keycloak.testsuite.cli.exec;
|
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.OsArch;
|
||||||
import org.keycloak.testsuite.cli.OsUtils;
|
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"));
|
private boolean logStreams = Boolean.valueOf(System.getProperty("cli.log.output", "true"));
|
||||||
|
|
||||||
protected boolean dumpStreams;
|
protected boolean dumpStreams = true;
|
||||||
|
|
||||||
protected String workDir = WORK_DIR;
|
protected String workDir = WORK_DIR;
|
||||||
|
|
||||||
|
@ -177,6 +178,7 @@ public abstract class AbstractExec {
|
||||||
return new String(stdout.toByteArray());
|
return new String(stdout.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public InputStream stderr() {
|
public InputStream stderr() {
|
||||||
return new ByteArrayInputStream(stderr.toByteArray());
|
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");
|
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) {
|
public void sendToStdin(String s) {
|
||||||
if (stdin instanceof InteractiveInputStream) {
|
if (stdin instanceof InteractiveInputStream) {
|
||||||
((InteractiveInputStream) stdin).pushBytes(s.getBytes());
|
((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 {
|
static void copyStream(InputStream is, OutputStream os) throws IOException {
|
||||||
|
|
|
@ -146,7 +146,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
|
||||||
"Validates a username and password from login form.");
|
"Validates a username and password from login form.");
|
||||||
addProviderInfo(result, "auth-x509-client-username-form", "X509/Validate Username 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.");
|
"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");
|
"Proprietary challenge protocol for CLI clients that queries for username password");
|
||||||
addProviderInfo(result, "direct-grant-auth-x509-username", "X509/Validate Username",
|
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.");
|
"Validates username and password from X509 client certificate received as a part of mutual SSL handshake.");
|
||||||
|
|
|
@ -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 <a href="mailto:bburke@redhat.com">Bill Burke</a>
|
||||||
|
*/
|
||||||
|
public class KcinitTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
|
public static final String KCINIT_CLIENT = "kcinit";
|
||||||
|
public static final String APP = "app";
|
||||||
|
public static final String UNAUTHORIZED_APP = "unauthorized_app";
|
||||||
|
@Rule
|
||||||
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deployment
|
||||||
|
public static WebArchive deploy() {
|
||||||
|
return RunOnServerDeployment.create(UserResource.class)
|
||||||
|
.addPackages(true, "org.keycloak.testsuite");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setupFlows() {
|
||||||
|
RequiredActionProviderRepresentation rep = adminClient.realm("test").flows().getRequiredAction("terms_and_conditions");
|
||||||
|
rep.setEnabled(true);
|
||||||
|
adminClient.realm("test").flows().updateRequiredAction("terms_and_conditions", rep);
|
||||||
|
|
||||||
|
testingClient.server().run(session -> {
|
||||||
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
|
|
||||||
|
ClientModel client = session.realms().getClientByClientId("kcinit", realm);
|
||||||
|
if (client != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientModel kcinit = realm.addClient(KCINIT_CLIENT);
|
||||||
|
kcinit.setSecret("password");
|
||||||
|
kcinit.setEnabled(true);
|
||||||
|
kcinit.addRedirectUri("urn:ietf:wg:oauth:2.0:oob");
|
||||||
|
kcinit.setPublicClient(false);
|
||||||
|
|
||||||
|
ClientModel app = realm.addClient(APP);
|
||||||
|
app.setSecret("password");
|
||||||
|
app.setEnabled(true);
|
||||||
|
app.setPublicClient(false);
|
||||||
|
|
||||||
|
ClientModel unauthorizedApp = realm.addClient(UNAUTHORIZED_APP);
|
||||||
|
unauthorizedApp.setSecret("password");
|
||||||
|
unauthorizedApp.setEnabled(true);
|
||||||
|
unauthorizedApp.setPublicClient(false);
|
||||||
|
|
||||||
|
// permission for client to client exchange to "target" client
|
||||||
|
AdminPermissionManagement management = AdminPermissions.management(session, realm);
|
||||||
|
management.clients().setPermissionsEnabled(app, true);
|
||||||
|
ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
|
||||||
|
clientRep.setName("to");
|
||||||
|
clientRep.addClient(kcinit.getId());
|
||||||
|
ResourceServer server = management.realmResourceServer();
|
||||||
|
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
|
||||||
|
management.clients().exchangeToPermission(app).addAssociatedPolicy(clientPolicy);
|
||||||
|
PasswordPolicy policy = realm.getPasswordPolicy();
|
||||||
|
policy = PasswordPolicy.parse(session, "hashIterations(1)");
|
||||||
|
realm.setPasswordPolicy(policy);
|
||||||
|
|
||||||
|
UserModel user = session.users().addUser(realm, "bburke");
|
||||||
|
session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
|
||||||
|
user.setEnabled(true);
|
||||||
|
user.setEmail("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<String, String> smtp = new HashMap<>();
|
||||||
|
smtp.put("host", "smtp.gmail.com");
|
||||||
|
smtp.put("port", "465");
|
||||||
|
smtp.put("fromDisplayName", "Keycloak SSO");
|
||||||
|
smtp.put("from", "****");
|
||||||
|
smtp.put("replyToDisplayName", "Keycloak no-reply");
|
||||||
|
smtp.put("replyTo", "reply-to@keycloak.org");
|
||||||
|
smtp.put("ssl", "true");
|
||||||
|
smtp.put("auth", "true");
|
||||||
|
smtp.put("user", "*****");
|
||||||
|
smtp.put("password", "****");
|
||||||
|
realm.setSmtpConfig(smtp);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Thread.sleep(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());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -24,29 +24,21 @@ import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
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.admin.client.resource.UserResource;
|
||||||
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
|
import org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory;
|
||||||
import org.keycloak.authentication.authenticators.cli.CliUsernamePasswordAuthenticatorFactory;
|
|
||||||
import org.keycloak.events.Details;
|
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.AuthenticationFlowBindings;
|
import org.keycloak.models.AuthenticationFlowBindings;
|
||||||
import org.keycloak.models.AuthenticationFlowModel;
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory;
|
|
||||||
import org.keycloak.testsuite.pages.AppPage;
|
import org.keycloak.testsuite.pages.AppPage;
|
||||||
import org.keycloak.testsuite.pages.ErrorPage;
|
import org.keycloak.testsuite.pages.ErrorPage;
|
||||||
import org.keycloak.testsuite.pages.LoginPage;
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
|
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
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.Client;
|
||||||
import javax.ws.rs.client.ClientBuilder;
|
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.Form;
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.Response;
|
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.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
@ -120,7 +109,7 @@ public class ChallengeFlowTest extends AbstractTestRealmKeycloakTest {
|
||||||
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
|
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
|
||||||
execution.setParentFlow(browser.getId());
|
execution.setParentFlow(browser.getId());
|
||||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||||
execution.setAuthenticator(CliUsernamePasswordAuthenticatorFactory.PROVIDER_ID);
|
execution.setAuthenticator(ConsoleUsernamePasswordAuthenticatorFactory.PROVIDER_ID);
|
||||||
execution.setPriority(10);
|
execution.setPriority(10);
|
||||||
execution.setAuthenticatorFlow(false);
|
execution.setAuthenticatorFlow(false);
|
||||||
realm.addAuthenticatorExecution(execution);
|
realm.addAuthenticatorExecution(execution);
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
${msg("emailVerificationBodyCodeHtml",code)?no_esc}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -45,3 +45,7 @@ linkExpirationFormatter.timePeriodUnit.hours=hours
|
||||||
linkExpirationFormatter.timePeriodUnit.hours.1=hour
|
linkExpirationFormatter.timePeriodUnit.hours.1=hour
|
||||||
linkExpirationFormatter.timePeriodUnit.days=days
|
linkExpirationFormatter.timePeriodUnit.days=days
|
||||||
linkExpirationFormatter.timePeriodUnit.days.1=day
|
linkExpirationFormatter.timePeriodUnit.days.1=day
|
||||||
|
|
||||||
|
emailVerificationBodyCode=Please verify your email address by entering in the following code.\n\n{0}\n\n.
|
||||||
|
emailVerificationBodyCodeHtml=<p>Please verify your email address by entering in the following code.</p><p><b>{0}</b></p>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
<#ftl output_format="plainText">
|
||||||
|
${msg("emailVerificationBodyCode",code)}
|
31
themes/src/main/resources/theme/base/login/login-config-totp-text.ftl
Executable file
31
themes/src/main/resources/theme/base/login/login-config-totp-text.ftl
Executable file
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
<#ftl output_format="plainText">
|
||||||
|
${msg("console-verify-email",email, code)}
|
|
@ -34,9 +34,12 @@ emailForgotTitle=Forgot Your Password?
|
||||||
updatePasswordTitle=Update password
|
updatePasswordTitle=Update password
|
||||||
codeSuccessTitle=Success code
|
codeSuccessTitle=Success code
|
||||||
codeErrorTitle=Error code\: {0}
|
codeErrorTitle=Error code\: {0}
|
||||||
|
displayUnsupported=Requested display type unsupported
|
||||||
|
browserRequired=Browser required to login
|
||||||
|
|
||||||
termsTitle=Terms and Conditions
|
termsTitle=Terms and Conditions
|
||||||
termsText=<p>Terms and conditions to be defined</p>
|
termsText=<p>Terms and conditions to be defined</p>
|
||||||
|
termsPlainText=Terms and conditions to be defined.
|
||||||
|
|
||||||
recaptchaFailed=Invalid Recaptcha
|
recaptchaFailed=Invalid Recaptcha
|
||||||
recaptchaNotConfigured=Recaptcha is required, but not configured
|
recaptchaNotConfigured=Recaptcha is required, but not configured
|
||||||
|
@ -66,6 +69,7 @@ country=Country
|
||||||
emailVerified=Email verified
|
emailVerified=Email verified
|
||||||
gssDelegationCredential=GSS Delegation Credential
|
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
|
loginTotpStep1=Install one of the following applications on your mobile
|
||||||
loginTotpStep2=Open the application and scan the barcode
|
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
|
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
|
pageNotFound=Page not found
|
||||||
internalServerError=An internal server error has occurred
|
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
|
Loading…
Reference in a new issue