token exchange
This commit is contained in:
parent
11ff5a05e9
commit
db9b1bcb21
16 changed files with 1007 additions and 15 deletions
35
adapters/oidc/cli-sso/README.md
Executable file
35
adapters/oidc/cli-sso/README.md
Executable file
|
@ -0,0 +1,35 @@
|
|||
CLI Single Sign On
|
||||
===================================
|
||||
|
||||
This java-based utility is meant for providing Keycloak integration to
|
||||
command line applications that are either written in Java or another language. The
|
||||
idea is that the Java app provided by this utility performs a login for a specific
|
||||
client, parses responses, and exports an access token as an environment variable
|
||||
that can be used by the command line utility you are accessing.
|
||||
|
||||
So, the idea is that you create a login script as follows:
|
||||
|
||||
#!/bin/sh
|
||||
export KC_TOKEN_RESPONSE=`java -DKEYCLOAK_TOKEN_RESPONSE=$KC_TOKEN_RESPONSE -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`
|
||||
export KC_ACCESS_TOKEN=`java -DKEYCLOAK_TOKEN_RESPONSE=$KC_TOKEN_RESPONSE -jar target/keycloak-cli-sso-3.3.0.CR1-SNAPSHOT.jar token`
|
||||
|
||||
|
||||
You pass in the parameters for connecting to Keycloak through Java system properties. The command `login` will perform the following steps:
|
||||
1. Look to see if `KEYCLOAK_TOKEN_RESPONSE` is set. If so, it will check expiration information and perform a refresh token call if the token needs refreshing.
|
||||
2. If `KEYCLOAK_TOKEN_RESPONSE` is not set, then it will use open a browser and perform a login to obtain an OAuth token response back.
|
||||
3. The output to STDOUT will be the oauth accesss token response. This will be stored by the script in an environment variable.
|
||||
4. The 2nd line of the script extracts the access token from the response json and stuffs it in an environment variable that will be used by the CLI application.
|
||||
|
||||
So, the idea is that you wrap your CLI application within this script and extract the access token
|
||||
from an environment variable. For example, if our CLI application is a C program named `mycli` the script would look like this:
|
||||
|
||||
#!/bin/sh
|
||||
export KC_TOKEN_RESPONSE=`java -DKEYCLOAK_TOKEN_RESPONSE=$KC_TOKEN_RESPONSE -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`
|
||||
export KC_ACCESS_TOKEN=`java -DKEYCLOAK_TOKEN_RESPONSE=$KC_TOKEN_RESPONSE -jar target/keycloak-cli-sso-3.3.0.CR1-SNAPSHOT.jar token`
|
||||
mycli $@
|
||||
|
||||
You would invoke it as follows:
|
||||
|
||||
$ . mycli.sh
|
||||
|
||||
Using the `.` so that the environment variables get exported.
|
10
adapters/oidc/cli-sso/login.sh
Executable file
10
adapters/oidc/cli-sso/login.sh
Executable file
|
@ -0,0 +1,10 @@
|
|||
#!/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`
|
||||
|
||||
|
||||
|
||||
|
9
adapters/oidc/cli-sso/logout.sh
Normal file
9
adapters/oidc/cli-sso/logout.sh
Normal file
|
@ -0,0 +1,9 @@
|
|||
#!/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
|
||||
|
||||
|
||||
|
||||
|
84
adapters/oidc/cli-sso/pom.xml
Executable file
84
adapters/oidc/cli-sso/pom.xml
Executable file
|
@ -0,0 +1,84 @@
|
|||
<?xml version="1.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.
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>3.3.0.CR1-SNAPSHOT</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-cli-sso</artifactId>
|
||||
<name>Keycloak CLI SSO Framework</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-installed-adapter</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.keycloak.adapters.KeycloakCliSsoMain</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import org.keycloak.adapters.installed.KeycloakCliSso;
|
||||
import org.keycloak.adapters.installed.KeycloakInstalled;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class KeycloakCliSsoMain extends KeycloakCliSso {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new KeycloakCliSsoMain().mainCmd(args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,266 @@
|
|||
/*
|
||||
* 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("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(" 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() 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);
|
||||
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()) return;
|
||||
AdapterConfig config = getConfig();
|
||||
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
|
||||
installed.login();
|
||||
processResponse(installed);
|
||||
}
|
||||
|
||||
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) 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();
|
||||
System.out.println(tokenResponse.getToken());
|
||||
}
|
||||
|
||||
public void loginManual() throws Exception {
|
||||
if (checkToken()) return;
|
||||
AdapterConfig config = getConfig();
|
||||
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
|
||||
KeycloakInstalled installed = new KeycloakInstalled(deployment);
|
||||
installed.loginManual();
|
||||
processResponse(installed);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.keycloak.adapters.installed;
|
||||
|
||||
import org.apache.commons.codec.Charsets;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
|
@ -24,6 +25,7 @@ import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
|||
import org.keycloak.adapters.ServerRequest;
|
||||
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
|
@ -43,6 +45,7 @@ import java.net.ServerSocket;
|
|||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
@ -51,6 +54,11 @@ import java.util.concurrent.TimeUnit;
|
|||
*/
|
||||
public class KeycloakInstalled {
|
||||
|
||||
public interface HttpResponseWriter {
|
||||
void success(PrintWriter pw, KeycloakInstalled ki);
|
||||
void failure(PrintWriter pw, KeycloakInstalled ki);
|
||||
}
|
||||
|
||||
private static final String KEYCLOAK_JSON = "META-INF/keycloak.json";
|
||||
|
||||
private KeycloakDeployment deployment;
|
||||
|
@ -59,12 +67,18 @@ public class KeycloakInstalled {
|
|||
LOGGED_MANUAL, LOGGED_DESKTOP
|
||||
}
|
||||
|
||||
private AccessTokenResponse tokenResponse;
|
||||
private String tokenString;
|
||||
private String idTokenString;
|
||||
private IDToken idToken;
|
||||
private AccessToken token;
|
||||
private String refreshToken;
|
||||
private Status status;
|
||||
private Locale locale;
|
||||
private HttpResponseWriter loginResponseWriter;
|
||||
private HttpResponseWriter logoutResponseWriter;
|
||||
|
||||
|
||||
|
||||
public KeycloakInstalled() {
|
||||
InputStream config = Thread.currentThread().getContextClassLoader().getResourceAsStream(KEYCLOAK_JSON);
|
||||
|
@ -75,6 +89,92 @@ public class KeycloakInstalled {
|
|||
deployment = KeycloakDeploymentBuilder.build(config);
|
||||
}
|
||||
|
||||
public KeycloakInstalled(KeycloakDeployment deployment) {
|
||||
this.deployment = deployment;
|
||||
}
|
||||
|
||||
private static HttpResponseWriter defaultLoginWriter = new HttpResponseWriter() {
|
||||
@Override
|
||||
public void success(PrintWriter pw, KeycloakInstalled ki) {
|
||||
pw.println("HTTP/1.1 200 OK");
|
||||
pw.println("Content-Type: text/html");
|
||||
pw.println();
|
||||
pw.println("<html><h1>Login completed.</h1><div>");
|
||||
pw.println("This browser will remain logged in until you close it, logout, or the session expires.");
|
||||
pw.println("</div></html>");
|
||||
pw.flush();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failure(PrintWriter pw, KeycloakInstalled ki) {
|
||||
pw.println("HTTP/1.1 200 OK");
|
||||
pw.println("Content-Type: text/html");
|
||||
pw.println();
|
||||
pw.println("<html><h1>Login attempt failed.</h1><div>");
|
||||
pw.println("</div></html>");
|
||||
pw.flush();
|
||||
|
||||
}
|
||||
};
|
||||
private static HttpResponseWriter defaultLogoutWriter = new HttpResponseWriter() {
|
||||
@Override
|
||||
public void success(PrintWriter pw, KeycloakInstalled ki) {
|
||||
pw.println("HTTP/1.1 200 OK");
|
||||
pw.println("Content-Type: text/html");
|
||||
pw.println();
|
||||
pw.println("<html><h1>Logout completed.</h1><div>");
|
||||
pw.println("You may close this browser tab.");
|
||||
pw.println("</div></html>");
|
||||
pw.flush();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failure(PrintWriter pw, KeycloakInstalled ki) {
|
||||
pw.println("HTTP/1.1 200 OK");
|
||||
pw.println("Content-Type: text/html");
|
||||
pw.println();
|
||||
pw.println("<html><h1>Logout failed.</h1><div>");
|
||||
pw.println("You may close this browser tab.");
|
||||
pw.println("</div></html>");
|
||||
pw.flush();
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
public HttpResponseWriter getLoginResponseWriter() {
|
||||
if (loginResponseWriter == null) {
|
||||
return defaultLoginWriter;
|
||||
} else {
|
||||
return loginResponseWriter;
|
||||
}
|
||||
}
|
||||
|
||||
public HttpResponseWriter getLogoutResponseWriter() {
|
||||
if (logoutResponseWriter == null) {
|
||||
return defaultLogoutWriter;
|
||||
} else {
|
||||
return logoutResponseWriter;
|
||||
}
|
||||
}
|
||||
|
||||
public void setLoginResponseWriter(HttpResponseWriter loginResponseWriter) {
|
||||
this.loginResponseWriter = loginResponseWriter;
|
||||
}
|
||||
|
||||
public void setLogoutResponseWriter(HttpResponseWriter logoutResponseWriter) {
|
||||
this.logoutResponseWriter = logoutResponseWriter;
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public void setLocale(Locale locale) {
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
public void login() throws IOException, ServerRequest.HttpFailure, VerificationException, InterruptedException, OAuthErrorException, URISyntaxException {
|
||||
if (isDesktopSupported()) {
|
||||
loginDesktop();
|
||||
|
@ -108,19 +208,22 @@ public class KeycloakInstalled {
|
|||
}
|
||||
|
||||
public void loginDesktop() throws IOException, VerificationException, OAuthErrorException, URISyntaxException, ServerRequest.HttpFailure, InterruptedException {
|
||||
CallbackListener callback = new CallbackListener();
|
||||
CallbackListener callback = new CallbackListener(getLoginResponseWriter());
|
||||
callback.start();
|
||||
|
||||
String redirectUri = "http://localhost:" + callback.server.getLocalPort();
|
||||
String state = UUID.randomUUID().toString();
|
||||
|
||||
String authUrl = deployment.getAuthUrl().clone()
|
||||
KeycloakUriBuilder builder = deployment.getAuthUrl().clone()
|
||||
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
||||
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
||||
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
|
||||
.queryParam(OAuth2Constants.STATE, state)
|
||||
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
|
||||
.build().toString();
|
||||
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID);
|
||||
if (locale != null) {
|
||||
builder.queryParam(OAuth2Constants.UI_LOCALES_PARAM, locale.getLanguage());
|
||||
}
|
||||
String authUrl = builder.build().toString();
|
||||
|
||||
Desktop.getDesktop().browse(new URI(authUrl));
|
||||
|
||||
|
@ -144,7 +247,7 @@ public class KeycloakInstalled {
|
|||
}
|
||||
|
||||
private void logoutDesktop() throws IOException, URISyntaxException, InterruptedException {
|
||||
CallbackListener callback = new CallbackListener();
|
||||
CallbackListener callback = new CallbackListener(getLogoutResponseWriter());
|
||||
callback.start();
|
||||
|
||||
String redirectUri = "http://localhost:" + callback.server.getLocalPort();
|
||||
|
@ -167,9 +270,6 @@ public class KeycloakInstalled {
|
|||
}
|
||||
|
||||
public void loginManual(PrintStream printer, Reader reader) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||
CallbackListener callback = new CallbackListener();
|
||||
callback.start();
|
||||
|
||||
String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
|
||||
|
||||
String authUrl = deployment.getAuthUrl().clone()
|
||||
|
@ -208,7 +308,14 @@ public class KeycloakInstalled {
|
|||
parseAccessToken(tokenResponse);
|
||||
}
|
||||
|
||||
public void refreshToken(String refreshToken) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||
AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
|
||||
parseAccessToken(tokenResponse);
|
||||
|
||||
}
|
||||
|
||||
private void parseAccessToken(AccessTokenResponse tokenResponse) throws VerificationException {
|
||||
this.tokenResponse = tokenResponse;
|
||||
tokenString = tokenResponse.getToken();
|
||||
refreshToken = tokenResponse.getRefreshToken();
|
||||
idTokenString = tokenResponse.getIdToken();
|
||||
|
@ -240,6 +347,10 @@ public class KeycloakInstalled {
|
|||
return refreshToken;
|
||||
}
|
||||
|
||||
public AccessTokenResponse getTokenResponse() {
|
||||
return tokenResponse;
|
||||
}
|
||||
|
||||
public boolean isDesktopSupported() {
|
||||
return Desktop.isDesktopSupported();
|
||||
}
|
||||
|
@ -248,6 +359,8 @@ public class KeycloakInstalled {
|
|||
return deployment;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||
AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null);
|
||||
parseAccessToken(tokenResponse);
|
||||
|
@ -269,6 +382,7 @@ public class KeycloakInstalled {
|
|||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
public class CallbackListener extends Thread {
|
||||
|
||||
private ServerSocket server;
|
||||
|
@ -283,14 +397,19 @@ public class KeycloakInstalled {
|
|||
|
||||
private String state;
|
||||
|
||||
public CallbackListener() throws IOException {
|
||||
private Socket socket;
|
||||
|
||||
private HttpResponseWriter writer;
|
||||
|
||||
public CallbackListener(HttpResponseWriter writer) throws IOException {
|
||||
this.writer = writer;
|
||||
server = new ServerSocket(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Socket socket = server.accept();
|
||||
socket = server.accept();
|
||||
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
String request = br.readLine();
|
||||
|
@ -314,10 +433,15 @@ public class KeycloakInstalled {
|
|||
}
|
||||
}
|
||||
|
||||
PrintWriter pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
|
||||
pw.println("Please close window and return to application");
|
||||
pw.flush();
|
||||
OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream());
|
||||
PrintWriter pw = new PrintWriter(out);
|
||||
|
||||
if (error == null) {
|
||||
writer.success(pw, KeycloakInstalled.this);
|
||||
} else {
|
||||
writer.failure(pw, KeycloakInstalled.this);
|
||||
}
|
||||
pw.flush();
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
errorException = e;
|
||||
|
@ -328,6 +452,8 @@ public class KeycloakInstalled {
|
|||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
<module>adapter-core</module>
|
||||
<module>as7-eap6</module>
|
||||
<module>installed</module>
|
||||
<module>cli-sso</module>
|
||||
<module>jaxrs-oauth-client</module>
|
||||
<module>jetty</module>
|
||||
<module>js</module>
|
||||
|
|
|
@ -50,6 +50,7 @@ public interface OAuth2Constants {
|
|||
|
||||
String AUTHORIZATION_CODE = "authorization_code";
|
||||
|
||||
|
||||
String IMPLICIT = "implicit";
|
||||
|
||||
String PASSWORD = "password";
|
||||
|
@ -92,6 +93,17 @@ public interface OAuth2Constants {
|
|||
String PKCE_METHOD_PLAIN = "plain";
|
||||
String PKCE_METHOD_S256 = "S256";
|
||||
|
||||
String TOKEN_EXCHANGE_GRANT_TYPE="urn:ietf:params:oauth:grant-type:token-exchange";
|
||||
String AUDIENCE="audience";
|
||||
String SUBJECT_TOKEN="subject_token";
|
||||
String SUBJECT_TOKEN_TYPE="subject_token_type";
|
||||
String ACCESS_TOKEN_TYPE="urn:ietf:params:oauth:token-type:access_token";
|
||||
String REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token";
|
||||
String JWT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt";
|
||||
String ID_TOKEN_TYPE="urn:ietf:params:oauth:token-type:id_token";
|
||||
String TOKEN_EXCHANGEABLE ="token-exchangeable";
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -123,7 +123,9 @@ public enum EventType {
|
|||
CLIENT_DELETE_ERROR(true),
|
||||
|
||||
CLIENT_INITIATED_ACCOUNT_LINKING(true),
|
||||
CLIENT_INITIATED_ACCOUNT_LINKING_ERROR(true);
|
||||
CLIENT_INITIATED_ACCOUNT_LINKING_ERROR(true),
|
||||
TOKEN_EXCHANGE(true),
|
||||
TOKEN_EXCHANGE_ERROR(true);
|
||||
|
||||
private boolean saveByDefault;
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.keycloak.models.AuthenticatedClientSessionModel;
|
|||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
|
@ -52,6 +53,7 @@ import org.keycloak.services.managers.ClientManager;
|
|||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.resources.Cors;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
|
@ -80,7 +82,7 @@ public class TokenEndpoint {
|
|||
private Map<String, String> clientAuthAttributes;
|
||||
|
||||
private enum Action {
|
||||
AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS
|
||||
AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS, TOKEN_EXCHANGE
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc7636#section-4.2
|
||||
|
@ -134,6 +136,8 @@ public class TokenEndpoint {
|
|||
return buildResourceOwnerPasswordCredentialsGrant();
|
||||
case CLIENT_CREDENTIALS:
|
||||
return buildClientCredentialsGrant();
|
||||
case TOKEN_EXCHANGE:
|
||||
return buildTokenExchange();
|
||||
}
|
||||
|
||||
throw new RuntimeException("Unknown action " + action);
|
||||
|
@ -197,6 +201,10 @@ public class TokenEndpoint {
|
|||
} else if (grantType.equals(OAuth2Constants.CLIENT_CREDENTIALS)) {
|
||||
event.event(EventType.CLIENT_LOGIN);
|
||||
action = Action.CLIENT_CREDENTIALS;
|
||||
} else if (grantType.equals(OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)) {
|
||||
event.event(EventType.TOKEN_EXCHANGE);
|
||||
action = Action.TOKEN_EXCHANGE;
|
||||
|
||||
} else {
|
||||
throw new ErrorResponseException(Errors.INVALID_REQUEST, "Invalid " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
@ -544,6 +552,96 @@ public class TokenEndpoint {
|
|||
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
|
||||
}
|
||||
|
||||
public Response buildTokenExchange() {
|
||||
event.detail(Details.AUTH_METHOD, "oauth_credentials");
|
||||
|
||||
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
|
||||
String subjectToken = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN);
|
||||
String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
|
||||
AuthenticationManager.AuthResult authResult = AuthenticationManager.verifyIdentityToken(session, realm, uriInfo, clientConnection, true, true, false, subjectToken, headers);
|
||||
if (authResult == null) {
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (authResult.getToken().getAudience() == null || authResult.getToken().getAudience().length > 1
|
||||
|| !client.getClientId().equals(authResult.getToken().getAudience()[0]) ) {
|
||||
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Cannot exchange token from different client", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
String audience = formParams.getFirst(OAuth2Constants.AUDIENCE);
|
||||
if (audience == null) {
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
throw new ErrorResponseException("invalid_audience", "No audience specified", Response.Status.BAD_REQUEST);
|
||||
|
||||
}
|
||||
ClientModel targetClient = null;
|
||||
if (audience != null) {
|
||||
targetClient = realm.getClientByClientId(audience);
|
||||
}
|
||||
if (targetClient == null) {
|
||||
event.error(Errors.INVALID_CLIENT);
|
||||
throw new ErrorResponseException("invalid_client", "Client authentication ended, but client is null", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (targetClient.isConsentRequired()) {
|
||||
event.error(Errors.CONSENT_DENIED);
|
||||
throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
boolean allowed = false;
|
||||
UserModel serviceAccount = session.users().getServiceAccount(client);
|
||||
if (serviceAccount != null) {
|
||||
RoleModel exchangeable = targetClient.getRole(OAuth2Constants.TOKEN_EXCHANGEABLE);
|
||||
RoleModel realmExchangeable = AdminPermissions.management(session, realm).getRealmManagementClient().getRole(OAuth2Constants.TOKEN_EXCHANGEABLE);
|
||||
allowed = (exchangeable != null && serviceAccount.hasRole(exchangeable)) || (realmExchangeable != null && serviceAccount.hasRole(realmExchangeable));
|
||||
|
||||
}
|
||||
|
||||
if (!allowed) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
|
||||
|
||||
}
|
||||
|
||||
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, targetClient, false);
|
||||
authSession.setAuthenticatedUser(authResult.getUser());
|
||||
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
|
||||
authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
|
||||
|
||||
UserSessionModel userSession = authResult.getSession();
|
||||
event.session(userSession);
|
||||
|
||||
AuthenticationManager.setRolesAndMappersInSession(authSession);
|
||||
AuthenticatedClientSessionModel clientSession = TokenManager.attachAuthenticationSession(session, userSession, authSession);
|
||||
|
||||
// Notes about client details
|
||||
userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId());
|
||||
userSession.setNote(ServiceAccountConstants.CLIENT_HOST, clientConnection.getRemoteHost());
|
||||
userSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, clientConnection.getRemoteAddr());
|
||||
|
||||
updateUserSessionFromClientAuth(userSession);
|
||||
|
||||
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, targetClient, event, session, userSession, clientSession)
|
||||
.generateAccessToken()
|
||||
.generateRefreshToken();
|
||||
|
||||
String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE);
|
||||
if (TokenUtil.isOIDCRequest(scopeParam)) {
|
||||
responseBuilder.generateIDToken();
|
||||
}
|
||||
|
||||
AccessTokenResponse res = responseBuilder.build();
|
||||
|
||||
event.success();
|
||||
|
||||
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
|
||||
}
|
||||
|
||||
|
||||
// https://tools.ietf.org/html/rfc7636#section-4.1
|
||||
private boolean isValidPkceCodeVerifier(String codeVerifier) {
|
||||
if (codeVerifier.length() < OIDCLoginProtocol.PKCE_CODE_VERIFIER_MIN_LENGTH) {
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.keycloak.services.resources.admin.permissions;
|
|||
|
||||
import org.keycloak.authorization.AuthorizationProvider;
|
||||
import org.keycloak.authorization.model.ResourceServer;
|
||||
import org.keycloak.models.ClientModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -27,6 +28,8 @@ public interface AdminPermissionManagement {
|
|||
public static final String MANAGE_SCOPE = "manage";
|
||||
public static final String VIEW_SCOPE = "view";
|
||||
|
||||
ClientModel getRealmManagementClient();
|
||||
|
||||
AuthorizationProvider authz();
|
||||
|
||||
RolePermissionManagement roles();
|
||||
|
|
|
@ -122,6 +122,7 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
|
|||
this.identity = new UserModelIdentity(realm, admin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel getRealmManagementClient() {
|
||||
ClientModel client = null;
|
||||
if (realm.getName().equals(Config.getAdminRealm())) {
|
||||
|
|
|
@ -389,6 +389,51 @@ public class OAuthClient {
|
|||
}
|
||||
}
|
||||
|
||||
public AccessTokenResponse doTokenExchange(String realm, String token, String targetAudience,
|
||||
String clientId, String clientSecret) throws Exception {
|
||||
CloseableHttpClient client = newCloseableHttpClient();
|
||||
try {
|
||||
HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
|
||||
|
||||
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
|
||||
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE));
|
||||
parameters.add(new BasicNameValuePair(OAuth2Constants.SUBJECT_TOKEN, token));
|
||||
parameters.add(new BasicNameValuePair(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE));
|
||||
parameters.add(new BasicNameValuePair(OAuth2Constants.AUDIENCE, targetAudience));
|
||||
|
||||
if (clientSecret != null) {
|
||||
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
|
||||
post.setHeader("Authorization", authorization);
|
||||
} else {
|
||||
parameters.add(new BasicNameValuePair("client_id", clientId));
|
||||
|
||||
}
|
||||
|
||||
if (clientSessionState != null) {
|
||||
parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, clientSessionState));
|
||||
}
|
||||
if (clientSessionHost != null) {
|
||||
parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, clientSessionHost));
|
||||
}
|
||||
if (scope != null) {
|
||||
parameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, scope));
|
||||
}
|
||||
|
||||
UrlEncodedFormEntity formEntity;
|
||||
try {
|
||||
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
post.setEntity(formEntity);
|
||||
|
||||
return new AccessTokenResponse(client.execute(post));
|
||||
} finally {
|
||||
closeClient(client);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public JSONWebKeySet doCertsRequest(String realm) throws Exception {
|
||||
CloseableHttpClient client = new DefaultHttpClient();
|
||||
try {
|
||||
|
|
|
@ -51,6 +51,7 @@ import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
|
|||
import org.keycloak.testsuite.util.AdminClientUtil;
|
||||
|
||||
import javax.ws.rs.ClientErrorException;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -741,6 +742,81 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
|
|||
testingClient.server().run(FineGrainAdminUnitTest::invokeDelete);
|
||||
}
|
||||
|
||||
// KEYCLOAK-5211
|
||||
@Test
|
||||
public void testCreateRealmCreateClient() throws Exception {
|
||||
ClientRepresentation rep = new ClientRepresentation();
|
||||
rep.setName("fullScopedClient");
|
||||
rep.setClientId("fullScopedClient");
|
||||
rep.setFullScopeAllowed(true);
|
||||
rep.setSecret("618268aa-51e6-4e64-93c4-3c0bc65b8171");
|
||||
rep.setProtocol("openid-connect");
|
||||
rep.setPublicClient(false);
|
||||
rep.setEnabled(true);
|
||||
adminClient.realm("master").clients().create(rep);
|
||||
|
||||
Keycloak realmClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(),
|
||||
"master", "admin", "admin", "fullScopedClient", "618268aa-51e6-4e64-93c4-3c0bc65b8171");
|
||||
|
||||
RealmRepresentation newRealm=new RealmRepresentation();
|
||||
newRealm.setRealm("anotherRealm");
|
||||
newRealm.setId("anotherRealm");
|
||||
newRealm.setEnabled(true);
|
||||
realmClient.realms().create(newRealm);
|
||||
|
||||
ClientRepresentation newClient = new ClientRepresentation();
|
||||
|
||||
newClient.setName("newClient");
|
||||
newClient.setClientId("newClient");
|
||||
newClient.setFullScopeAllowed(true);
|
||||
newClient.setSecret("secret");
|
||||
newClient.setProtocol("openid-connect");
|
||||
newClient.setPublicClient(false);
|
||||
newClient.setEnabled(true);
|
||||
Response response = realmClient.realm("anotherRealm").clients().create(newClient);
|
||||
Assert.assertEquals(403, response.getStatus());
|
||||
|
||||
realmClient.close();
|
||||
realmClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(),
|
||||
"master", "admin", "admin", "fullScopedClient", "618268aa-51e6-4e64-93c4-3c0bc65b8171");
|
||||
response = realmClient.realm("anotherRealm").clients().create(newClient);
|
||||
Assert.assertEquals(201, response.getStatus());
|
||||
|
||||
|
||||
}
|
||||
|
||||
// KEYCLOAK-5211
|
||||
@Test
|
||||
public void testCreateRealmCreateClientWithMaster() throws Exception {
|
||||
ClientRepresentation rep = new ClientRepresentation();
|
||||
rep.setName("fullScopedClient");
|
||||
rep.setClientId("fullScopedClient");
|
||||
rep.setFullScopeAllowed(true);
|
||||
rep.setSecret("618268aa-51e6-4e64-93c4-3c0bc65b8171");
|
||||
rep.setProtocol("openid-connect");
|
||||
rep.setPublicClient(false);
|
||||
rep.setEnabled(true);
|
||||
adminClient.realm("master").clients().create(rep);
|
||||
|
||||
RealmRepresentation newRealm=new RealmRepresentation();
|
||||
newRealm.setRealm("anotherRealm");
|
||||
newRealm.setId("anotherRealm");
|
||||
newRealm.setEnabled(true);
|
||||
adminClient.realms().create(newRealm);
|
||||
|
||||
ClientRepresentation newClient = new ClientRepresentation();
|
||||
|
||||
newClient.setName("newClient");
|
||||
newClient.setClientId("newClient");
|
||||
newClient.setFullScopeAllowed(true);
|
||||
newClient.setSecret("secret");
|
||||
newClient.setProtocol("openid-connect");
|
||||
newClient.setPublicClient(false);
|
||||
newClient.setEnabled(true);
|
||||
Response response = adminClient.realm("anotherRealm").clients().create(newClient);
|
||||
Assert.assertEquals(201, response.getStatus());
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* 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.testsuite.oauth;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.jboss.arquillian.container.test.api.Deployment;
|
||||
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.TimeBasedOTP;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.admin.FineGrainAdminUnitTest;
|
||||
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
|
||||
import org.keycloak.testsuite.util.ClientBuilder;
|
||||
import org.keycloak.testsuite.util.ClientManager;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
import org.keycloak.testsuite.util.RealmManager;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.testsuite.util.UserManager;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class TokenExchangeTest extends AbstractKeycloakTest {
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
@Deployment
|
||||
public static WebArchive deploy() {
|
||||
return RunOnServerDeployment.create(TokenExchangeTest.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
RealmRepresentation testRealmRep = new RealmRepresentation();
|
||||
testRealmRep.setId(TEST);
|
||||
testRealmRep.setRealm(TEST);
|
||||
testRealmRep.setEnabled(true);
|
||||
testRealms.add(testRealmRep);
|
||||
}
|
||||
|
||||
public static void setupRealm(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName(TEST);
|
||||
RoleModel realmExchangeable = AdminPermissions.management(session, realm).getRealmManagementClient().addRole(OAuth2Constants.TOKEN_EXCHANGEABLE);
|
||||
|
||||
RoleModel exampleRole = realm.addRole("example");
|
||||
|
||||
ClientModel target = realm.addClient("target");
|
||||
target.setDirectAccessGrantsEnabled(true);
|
||||
target.setEnabled(true);
|
||||
target.setSecret("secret");
|
||||
target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
target.setFullScopeAllowed(false);
|
||||
target.addScopeMapping(exampleRole);
|
||||
RoleModel targetExchangeable = target.addRole(OAuth2Constants.TOKEN_EXCHANGEABLE);
|
||||
|
||||
target = realm.addClient("realm-exchanger");
|
||||
target.setClientId("realm-exchanger");
|
||||
target.setDirectAccessGrantsEnabled(true);
|
||||
target.setEnabled(true);
|
||||
target.setSecret("secret");
|
||||
target.setServiceAccountsEnabled(true);
|
||||
target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
target.setFullScopeAllowed(false);
|
||||
new org.keycloak.services.managers.ClientManager(new org.keycloak.services.managers.RealmManager(session)).enableServiceAccount(target);
|
||||
session.users().getServiceAccount(target).grantRole(realmExchangeable);
|
||||
|
||||
target = realm.addClient("client-exchanger");
|
||||
target.setClientId("client-exchanger");
|
||||
target.setDirectAccessGrantsEnabled(true);
|
||||
target.setEnabled(true);
|
||||
target.setSecret("secret");
|
||||
target.setServiceAccountsEnabled(true);
|
||||
target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
target.setFullScopeAllowed(false);
|
||||
new org.keycloak.services.managers.ClientManager(new org.keycloak.services.managers.RealmManager(session)).enableServiceAccount(target);
|
||||
session.users().getServiceAccount(target).grantRole(targetExchangeable);
|
||||
|
||||
target = realm.addClient("account-not-allowed");
|
||||
target.setClientId("account-not-allowed");
|
||||
target.setDirectAccessGrantsEnabled(true);
|
||||
target.setEnabled(true);
|
||||
target.setSecret("secret");
|
||||
target.setServiceAccountsEnabled(true);
|
||||
target.setFullScopeAllowed(false);
|
||||
target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
new org.keycloak.services.managers.ClientManager(new org.keycloak.services.managers.RealmManager(session)).enableServiceAccount(target);
|
||||
|
||||
target = realm.addClient("no-account");
|
||||
target.setClientId("no-account");
|
||||
target.setDirectAccessGrantsEnabled(true);
|
||||
target.setEnabled(true);
|
||||
target.setSecret("secret");
|
||||
target.setServiceAccountsEnabled(true);
|
||||
target.setFullScopeAllowed(false);
|
||||
target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
|
||||
UserModel user = session.users().addUser(realm, "user");
|
||||
user.setEnabled(true);
|
||||
session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
|
||||
user.grantRole(exampleRole);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isImportAfterEachMethod() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testExchange() throws Exception {
|
||||
testingClient.server().run(TokenExchangeTest::setupRealm);
|
||||
|
||||
oauth.realm(TEST);
|
||||
oauth.clientId("realm-exchanger");
|
||||
|
||||
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password");
|
||||
String accessToken = response.getAccessToken();
|
||||
|
||||
response = oauth.doTokenExchange(TEST,accessToken, "target", "realm-exchanger", "secret");
|
||||
|
||||
String exchangedTokenString = response.getAccessToken();
|
||||
TokenVerifier<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
|
||||
AccessToken exchangedToken = verifier.parse().getToken();
|
||||
Assert.assertEquals(exchangedToken.getPreferredUsername(), "user");
|
||||
Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
|
||||
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue