Merge pull request #5087 from patriot1burke/kcinit

KEYCLOAK-6813
This commit is contained in:
Bill Burke 2018-03-28 17:35:33 -04:00 committed by GitHub
commit 8d3dc790df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 3403 additions and 530 deletions

View file

@ -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`

View file

@ -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

View file

@ -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();
}
}
}

View file

@ -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);
}
}
}

View file

@ -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 {

View file

@ -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>

View 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 $@

View file

@ -0,0 +1,8 @@
@echo off
if "%OS%" == "Windows_NT" (
set "DIRNAME=%~dp0%"
) else (
set DIRNAME=.\
)
java -jar %DIRNAME%\kcinit-${project.version}.jar %*

View file

@ -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);
} }
} }

View file

@ -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>

View file

@ -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();
}
}

View file

@ -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";
} }

View file

@ -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);
}
}
} }

View file

@ -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
View file

@ -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>

View file

@ -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
} }

View file

@ -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;
}
} }

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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
* *

View file

@ -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);
}
}
}
}

View file

@ -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;
} }

View file

@ -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";
} }

View file

@ -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();

View file

@ -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";

View file

@ -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);
} }

View file

@ -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();

View file

@ -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();

View file

@ -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() {
}
}

View file

@ -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) {

View file

@ -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);

View file

@ -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) {
} }

View file

@ -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) {

View file

@ -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) {

View file

@ -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;

View file

@ -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) {

View file

@ -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() {
}
}

View file

@ -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() {
}
}

View file

@ -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;
}
}

View file

@ -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() {
}
}

View file

@ -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;
}
}
}

View file

@ -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() {
}
}

View file

@ -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() {
}
}

View file

@ -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()));
}
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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;

View file

@ -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.
* *

View file

@ -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;

View file

@ -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());

View file

@ -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();

View file

@ -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);

View file

@ -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;
}
} }

View file

@ -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));

View file

@ -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);

View file

@ -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";

View file

@ -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

View file

@ -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);

View file

@ -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

View file

@ -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>

View file

@ -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;
}
}
}

View file

@ -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 {

View file

@ -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.");

View file

@ -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());
}
}

View file

@ -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);

View file

@ -0,0 +1,5 @@
<html>
<body>
${msg("emailVerificationBodyCodeHtml",code)?no_esc}
</body>
</html>

View file

@ -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>

View file

@ -0,0 +1,2 @@
<#ftl output_format="plainText">
${msg("emailVerificationBodyCode",code)}

View 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.

View file

@ -0,0 +1,2 @@
<#ftl output_format="plainText">
${msg("console-verify-email",email, code)}

View file

@ -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