keycloak-aplcache

Merge pull request #4362 from patriot1burke/master token

8/1/2017 2:08:50 PM

Details

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`
+
+
+
+
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
+
+
+
+
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>
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) {
             }
         }
+
     }
 
+
 }
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"));
+
+
+    }
+}