keycloak-aplcache
Changes
adapters/oidc/cli-sso/login.sh 10(+10 -0)
adapters/oidc/cli-sso/logout.sh 9(+9 -0)
adapters/oidc/cli-sso/pom.xml 84(+84 -0)
adapters/oidc/cli-sso/README.md 9(+9 -0)
adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java 266(+266 -0)
adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java 152(+139 -13)
adapters/oidc/pom.xml 1(+1 -0)
services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java 3(+3 -0)
services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java 1(+1 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java 45(+45 -0)
Details
adapters/oidc/cli-sso/login.sh 10(+10 -0)
diff --git a/adapters/oidc/cli-sso/login.sh b/adapters/oidc/cli-sso/login.sh
new file mode 100755
index 0000000..ff33a01
--- /dev/null
+++ b/adapters/oidc/cli-sso/login.sh
@@ -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`
+
+
+
+
adapters/oidc/cli-sso/logout.sh 9(+9 -0)
diff --git a/adapters/oidc/cli-sso/logout.sh b/adapters/oidc/cli-sso/logout.sh
new file mode 100644
index 0000000..ca99f88
--- /dev/null
+++ b/adapters/oidc/cli-sso/logout.sh
@@ -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
+
+
+
+
adapters/oidc/cli-sso/pom.xml 84(+84 -0)
diff --git a/adapters/oidc/cli-sso/pom.xml b/adapters/oidc/cli-sso/pom.xml
new file mode 100755
index 0000000..216c3b7
--- /dev/null
+++ b/adapters/oidc/cli-sso/pom.xml
@@ -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>
adapters/oidc/cli-sso/README.md 9(+9 -0)
diff --git a/adapters/oidc/cli-sso/README.md b/adapters/oidc/cli-sso/README.md
new file mode 100755
index 0000000..fb0fdbe
--- /dev/null
+++ b/adapters/oidc/cli-sso/README.md
@@ -0,0 +1,9 @@
+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.
+
diff --git a/adapters/oidc/cli-sso/src/main/java/org/keycloak/adapters/KeycloakCliSsoMain.java b/adapters/oidc/cli-sso/src/main/java/org/keycloak/adapters/KeycloakCliSsoMain.java
new file mode 100644
index 0000000..3aaeb9b
--- /dev/null
+++ b/adapters/oidc/cli-sso/src/main/java/org/keycloak/adapters/KeycloakCliSsoMain.java
@@ -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);
+ }
+}
diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java
new file mode 100644
index 0000000..3c1d365
--- /dev/null
+++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java
@@ -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);
+ }
+ }
+}
diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
index 9834fe2..61ca06e 100644
--- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
+++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
@@ -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) {
}
}
+
}
+
}
adapters/oidc/pom.xml 1(+1 -0)
diff --git a/adapters/oidc/pom.xml b/adapters/oidc/pom.xml
index f9e9b8a..9207401 100755
--- a/adapters/oidc/pom.xml
+++ b/adapters/oidc/pom.xml
@@ -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>
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 234b632..6de35b8 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -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_EXCHANGER ="token-exchanger";
+
+
}
diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventType.java b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
index 920646f..b48e243 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/EventType.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
@@ -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;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 533b862..5b70d9b 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -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;
@@ -81,7 +83,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
@@ -135,6 +137,8 @@ public class TokenEndpoint {
return buildResourceOwnerPasswordCredentialsGrant();
case CLIENT_CREDENTIALS:
return buildClientCredentialsGrant();
+ case TOKEN_EXCHANGE:
+ return buildTokenExchange();
}
throw new RuntimeException("Unknown action " + action);
@@ -198,6 +202,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);
}
@@ -552,6 +560,115 @@ 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);
+ }
+
+ 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) {
+ if (authResult.getToken().getAudience() == null) {
+ logger.debug("Client doesn't have service account");
+ }
+ boolean tokenAllowed = false;
+ for (String aud : authResult.getToken().getAudience()) {
+ ClientModel audClient = realm.getClientByClientId(aud);
+ if (audClient == null) continue;
+ if (audClient.equals(client)) {
+ tokenAllowed = true;
+ break;
+ }
+ RoleModel audExchanger = audClient.getRole(OAuth2Constants.TOKEN_EXCHANGER);
+ if (audExchanger != null && serviceAccount.hasRole(audExchanger)) {
+ tokenAllowed = true;
+ break;
+ }
+ }
+ if (!tokenAllowed) {
+ logger.debug("Client does not have exchange rights for audience of token");
+ } else {
+ RoleModel targetExchangable = targetClient.getRole(OAuth2Constants.TOKEN_EXCHANGER);
+ RoleModel realmExchangeable = AdminPermissions.management(session, realm).getRealmManagementClient().getRole(OAuth2Constants.TOKEN_EXCHANGER);
+ allowed = (targetExchangable != null && serviceAccount.hasRole(targetExchangable)) || (realmExchangeable != null && serviceAccount.hasRole(realmExchangeable));
+ if (!allowed) {
+ logger.debug("Client does not have exchange rights for target audience");
+ }
+ }
+
+ } else {
+ logger.debug("Client doesn't have service account");
+ }
+
+ 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) {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java
index 2a94132..7df5b5e 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java
@@ -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();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
index 400cee1..449530c 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
@@ -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())) {
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index a3983a4..7961163 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -221,18 +221,6 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
- // only allow origins from client. Not sure we need this as I don't believe cookies can be
- // sent if CORS preflight requests can't execute.
- String origin = headers.getRequestHeaders().getFirst("Origin");
- if (origin != null) {
- String redirectOrigin = UriUtils.getOrigin(redirectUri);
- if (!redirectOrigin.equals(origin)) {
- event.error(Errors.ILLEGAL_ORIGIN);
- throw new ErrorPageException(session, Messages.INVALID_REQUEST);
-
- }
- }
-
AuthenticationManager.AuthResult cookieResult = AuthenticationManager.authenticateIdentityCookie(session, realmModel, true);
String errorParam = "link_error";
if (cookieResult == null) {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index 8118c10..d42158c 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -402,6 +402,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 {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java
index 02121bb..5e9a473 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java
@@ -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,91 @@ 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();
+
+ try {
+ 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());
+ } finally {
+ adminClient.realm("anotherRealm").remove();
+
+ }
+
+
+ }
+
+ // 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);
+
+ try {
+ 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());
+ } finally {
+ adminClient.realm("anotherRealm").remove();
+
+ }
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java
new file mode 100755
index 0000000..ff82166
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.TokenVerifier;
+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.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.idm.RealmRepresentation;
+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.runonserver.RunOnServerDeployment;
+import org.keycloak.testsuite.util.OAuthClient;
+
+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_EXCHANGER);
+
+ 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_EXCHANGER);
+
+ 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"));
+
+
+ }
+}