keycloak-aplcache

Changes

.travis.yml 2(+1 -1)

integration/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java 72(+0 -72)

integration/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCommand.java 64(+0 -64)

integration/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ExitCommand.java 29(+0 -29)

integration/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/SetupCommand.java 44(+0 -44)

integration/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/Context.java 37(+0 -37)

pom.xml 11(+11 -0)

Details

.travis.yml 2(+1 -1)

diff --git a/.travis.yml b/.travis.yml
index 53a9440..35c4061 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,7 +19,7 @@ before_script:
   - export MAVEN_SKIP_RC=true
 
 install: 
-  - travis_wait 30 mvn install -Pdistribution -DskipTests=true -B -V -q
+  - travis_wait 60 mvn install -Pdistribution -DskipTests=true -B -V -q
 
 script:
   - mvn test -B
diff --git a/adapters/oidc/js/src/main/resources/keycloak.js b/adapters/oidc/js/src/main/resources/keycloak.js
index 2d8f421..01f0523 100755
--- a/adapters/oidc/js/src/main/resources/keycloak.js
+++ b/adapters/oidc/js/src/main/resources/keycloak.js
@@ -29,7 +29,7 @@
 
         var loginIframe = {
             enable: true,
-            callbackMap: [],
+            callbackList: [],
             interval: 5
         };
 
@@ -824,7 +824,7 @@
                 setTimeout(check, loginIframe.interval * 1000);
             }
 
-            var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html?client_id=' + encodeURIComponent(kc.clientId) + '&origin=' + getOrigin();
+            var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html';
             iframe.setAttribute('src', src );
             iframe.style.display = 'none';
             document.body.appendChild(iframe);
@@ -834,30 +834,21 @@
                     return;
                 }
 
-                try {
-                    var data = JSON.parse(event.data);
-                } catch (err) {
-                    return;
-                }
-
-                if (!data.callbackId) {
-                    return;
-                }
-
-                var promise = loginIframe.callbackMap[data.callbackId];
-                if (!promise) {
-                    return;
+                if (event.data != "unchanged") {
+                    kc.clearToken();
                 }
 
-                delete loginIframe.callbackMap[data.callbackId];
-
-                if ((!kc.sessionId || kc.sessionId == data.session) && data.loggedIn) {
-                    promise.setSuccess();
-                } else {
-                    kc.clearToken();
-                    promise.setError();
+                for (var i = loginIframe.callbackList.length - 1; i >= 0; --i) {
+                    var promise = loginIframe.callbackList[i];
+                    if (event.data == "unchanged") {
+                        promise.setSuccess();
+                    } else {
+                        promise.setError();
+                    }
+                    loginIframe.callbackList.splice(i, 1);
                 }
             };
+
             window.addEventListener('message', messageCallback, false);
 
             var check = function() {
@@ -873,12 +864,13 @@
         function checkLoginIframe() {
             var promise = createPromise();
 
-            if (loginIframe.iframe && loginIframe.iframeOrigin) {
-                var msg = {};
-                msg.callbackId = createCallbackId();
-                loginIframe.callbackMap[msg.callbackId] = promise;
+            if (loginIframe.iframe && loginIframe.iframeOrigin ) {
+                var msg = kc.clientId + ' ' + kc.sessionId;
+                loginIframe.callbackList.push(promise);
                 var origin = loginIframe.iframeOrigin;
-                loginIframe.iframe.contentWindow.postMessage(JSON.stringify(msg), origin);
+                if (loginIframe.callbackList.length == 1) {
+                    loginIframe.iframe.contentWindow.postMessage(msg, origin);
+                }
             } else {
                 promise.setSuccess();
             }
diff --git a/adapters/oidc/js/src/main/resources/login-status-iframe.html b/adapters/oidc/js/src/main/resources/login-status-iframe.html
index fe7eda1..8794d54 100755
--- a/adapters/oidc/js/src/main/resources/login-status-iframe.html
+++ b/adapters/oidc/js/src/main/resources/login-status-iframe.html
@@ -15,10 +15,56 @@
   ~ limitations under the License.
   -->
 
+<html>
+<body>
 <script>
-    function getCookie(cname)
+    var init;
+
+    function checkState(clientId, origin, sessionState, callback) {
+        if (!init) {
+            var req = new XMLHttpRequest();
+
+            var url = "http://localhost:8080/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init";
+            url += "?client_id=" + encodeURIComponent(clientId);
+            url += "&origin=" + encodeURIComponent(origin);
+            url += "&session_state=" + encodeURIComponent(sessionState);
+
+            req.open('GET', url, true);
+
+            req.onreadystatechange = function () {
+                if (req.readyState == 4) {
+                    if (req.status == 204) {
+                        init = {
+                            clientId: clientId,
+                            origin: origin
+                        }
+                        callback('unchanged');
+                    } else if (req.status = 404) {
+                        callback('changed');
+                    } else {
+                        callback('error');
+                    }
+                }
+            };
+
+            req.send();
+        } else {
+            if (clientId == init.clientId && origin == init.origin) {
+                var cookie = getCookie();
+                if (sessionState == cookie) {
+                    callback('unchanged');
+                } else {
+                    callback('changed');
+                }
+            } else {
+                callback('error');
+            }
+        }
+    }
+
+    function getCookie()
     {
-        var name = cname + "=";
+        var name = 'KEYCLOAK_SESSION=';
         var ca = document.cookie.split(';');
         for(var i=0; i<ca.length; i++)
         {
@@ -27,23 +73,24 @@
         }
         return null;
     }
+
     function receiveMessage(event)
     {
-        if (event.origin !== "ORIGIN") {
-            console.log(event.origin + " does not match built origin");
-            return;
-
-        }
-        var data = JSON.parse(event.data);
-        data.loggedIn = false;
-        var cookie = getCookie('KEYCLOAK_SESSION');
-        if (cookie) {
-            data.loggedIn = true;
-            data.session = cookie;
+        var origin = event.origin;
+        var data = event.data.split(' ');
+        if (data.length != 2) {
+            event.source.postMessage('error', origin);
         }
 
-        event.source.postMessage(JSON.stringify(data),
-                event.origin);
+        var clientId = data[0];
+        var sessionState = data[1];
+
+        checkState(clientId, event.origin, sessionState, function(result) {
+            event.source.postMessage(result, origin);
+        });
     }
+
     window.addEventListener("message", receiveMessage, false);
-</script>
\ No newline at end of file
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/core/src/main/java/org/keycloak/json/StringListMapDeserializer.java b/core/src/main/java/org/keycloak/json/StringListMapDeserializer.java
new file mode 100644
index 0000000..cd8ab4c
--- /dev/null
+++ b/core/src/main/java/org/keycloak/json/StringListMapDeserializer.java
@@ -0,0 +1,57 @@
+/*
+ * 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.json;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public class StringListMapDeserializer extends JsonDeserializer<Object> {
+
+    @Override
+    public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
+        JsonNode jsonNode = jsonParser.readValueAsTree();
+        Iterator<Map.Entry<String, JsonNode>> itr = jsonNode.fields();
+        Map<String, List<String>> map = new HashMap<>();
+        while (itr.hasNext()) {
+            Map.Entry<String, JsonNode> e = itr.next();
+            List<String> values = new LinkedList<>();
+            if (!e.getValue().isArray()) {
+                values.add(e.getValue().asText());
+            } else {
+                ArrayNode a = (ArrayNode) e.getValue();
+                Iterator<JsonNode> vitr = a.elements();
+                while (vitr.hasNext()) {
+                    values.add(vitr.next().asText());
+                }
+            }
+            map.put(e.getKey(), values);
+        }
+        return map;
+    }
+
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
index b715bf5..26bcdf6 100755
--- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
@@ -17,7 +17,8 @@
 
 package org.keycloak.representations.idm;
 
-import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.keycloak.json.StringListMapDeserializer;
 
 import java.util.Arrays;
 import java.util.HashMap;
@@ -43,8 +44,8 @@ public class UserRepresentation {
     protected String federationLink;
     protected String serviceAccountClientId; // For rep, it points to clientId (not DB ID)
 
-    // Currently there is Map<String, List<String>> but for backwards compatibility, we also need to support Map<String, String>
-    protected Map<String, Object> attributes;
+    @JsonDeserialize(using = StringListMapDeserializer.class)
+    protected Map<String, List<String>> attributes;
     protected List<CredentialRepresentation> credentials;
     protected List<String> requiredActions;
     protected List<FederatedIdentityRepresentation> federatedIdentities;
@@ -141,17 +142,11 @@ public class UserRepresentation {
         this.emailVerified = emailVerified;
     }
 
-    public Map<String, Object> getAttributes() {
+    public Map<String, List<String>> getAttributes() {
         return attributes;
     }
 
-    // This method can be removed once we can remove backwards compatibility with Keycloak 1.3 (then getAttributes() can be changed to return Map<String, List<String>> )
-    @JsonIgnore
-    public Map<String, List<String>> getAttributesAsListValues() {
-        return (Map) attributes;
-    }
-
-    public void setAttributes(Map<String, Object> attributes) {
+    public void setAttributes(Map<String, List<String>> attributes) {
         this.attributes = attributes;
     }
 
diff --git a/distribution/demo-dist/src/main/xslt/standalone.xsl b/distribution/demo-dist/src/main/xslt/standalone.xsl
index 6099aa2..b75b775 100755
--- a/distribution/demo-dist/src/main/xslt/standalone.xsl
+++ b/distribution/demo-dist/src/main/xslt/standalone.xsl
@@ -42,14 +42,14 @@
     <xsl:template match="//ds:datasources">
         <xsl:copy>
             <xsl:apply-templates select="node()[name(.)='datasource']"/>
-            <xa-datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" use-java-context="true">
-                <xa-datasource-property name="URL">jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</xa-datasource-property>
+            <datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" use-java-context="true">
+                <connection-url>jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</connection-url>
                 <driver>h2</driver>
                 <security>
                     <user-name>sa</user-name>
                     <password>sa</password>
                 </security>
-            </xa-datasource>
+            </datasource>
             <xsl:apply-templates select="node()[name(.)='drivers']"/>
         </xsl:copy>
     </xsl:template>
diff --git a/distribution/server-dist/assembly.xml b/distribution/server-dist/assembly.xml
index 0e58c83..b788158 100755
--- a/distribution/server-dist/assembly.xml
+++ b/distribution/server-dist/assembly.xml
@@ -79,5 +79,13 @@
                 <include>layers.conf</include>
             </includes>
         </fileSet>
+        <fileSet>
+            <directory>target/unpacked/keycloak-client-tools</directory>
+            <outputDirectory/>
+            <filtered>false</filtered>
+            <includes>
+                <include>**/*</include>
+            </includes>
+        </fileSet>
     </fileSets>
 </assembly>
diff --git a/distribution/server-dist/pom.xml b/distribution/server-dist/pom.xml
index 1bee96b..1b13474 100755
--- a/distribution/server-dist/pom.xml
+++ b/distribution/server-dist/pom.xml
@@ -81,6 +81,29 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>unpack-client-cli-dist</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>unpack</goal>
+                        </goals>
+                        <configuration>
+                            <artifactItems>
+                                <artifactItem>
+                                    <groupId>org.keycloak</groupId>
+                                    <artifactId>keycloak-client-cli-dist</artifactId>
+                                    <type>zip</type>
+                                    <outputDirectory>${project.build.directory}/unpacked</outputDirectory>
+                                </artifactItem>
+                            </artifactItems>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
         </plugins>
     </build>
 
diff --git a/examples/cors/angular-product-app/src/main/webapp/index.html b/examples/cors/angular-product-app/src/main/webapp/index.html
index 4627452..3ba1344 100755
--- a/examples/cors/angular-product-app/src/main/webapp/index.html
+++ b/examples/cors/angular-product-app/src/main/webapp/index.html
@@ -92,9 +92,20 @@
         <h2><span>Realm info</span></h2>
         <button type="submit" data-ng-click="loadPublicRealmInfo()">Load public realm info</button>
 
-        <div data-ng-show="realm">
-            Realm name: {{realm.realm}} <br/>
-            Public key: {{realm.public_key}} <br/>
+        <div data-ng-show="publicKeys">
+            <b>Realm issuer</b>: {{realmOIDCInfo.issuer}} <br/>
+            <table class="table" data-ng-show="publicKeys.keys.length > 0">
+                <thead>
+                <tr>
+                    <th>Public Key KIDs</th>
+                </tr>
+                </thead>
+                <tbody>
+                <tr data-ng-repeat="pk in publicKeys.keys">
+                    <td>{{pk.kid}}</td>
+                </tr>
+                </tbody>
+            </table>
         </div>
     </div>
     <hr />
diff --git a/examples/cors/angular-product-app/src/main/webapp/js/app.js b/examples/cors/angular-product-app/src/main/webapp/js/app.js
index 70db67e..5ddf077 100755
--- a/examples/cors/angular-product-app/src/main/webapp/js/app.js
+++ b/examples/cors/angular-product-app/src/main/webapp/js/app.js
@@ -87,8 +87,13 @@ module.controller('GlobalCtrl', function($scope, $http) {
     };
 
     $scope.loadPublicRealmInfo = function() {
-        $http.get("http://localhost-auth:8080/auth/realms/cors").success(function(data) {
-            $scope.realm = angular.fromJson(data);
+        $http.get("http://localhost-auth:8080/auth/realms/cors/.well-known/openid-configuration").success(function(data) {
+            $scope.realmOIDCInfo = angular.fromJson(data);
+
+            var jwksUri = $scope.realmOIDCInfo.jwks_uri;
+            $http.get(jwksUri).success(function(data) {
+                $scope.publicKeys = angular.fromJson(data);
+            });
         });
     };
 
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
index f86170e..d267d17 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
@@ -26,7 +26,9 @@ import org.keycloak.admin.client.resource.RealmsResource;
 import org.keycloak.admin.client.resource.ServerInfoResource;
 import org.keycloak.admin.client.token.TokenManager;
 
+import javax.net.ssl.SSLContext;
 import java.net.URI;
+import java.security.KeyStore;
 
 import static org.keycloak.OAuth2Constants.PASSWORD;
 
@@ -55,6 +57,15 @@ public class Keycloak {
         target.register(new BearerAuthFilter(tokenManager));
     }
 
+    public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext) {
+        ResteasyClient client = new ResteasyClientBuilder()
+                .sslContext(sslContext)
+                .hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.WILDCARD)
+                .connectionPoolSize(10).build();
+
+        return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, client);
+    }
+
     public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret) {
         return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, null);
     }
diff --git a/integration/client-cli/client-cli-dist/assembly.xml b/integration/client-cli/client-cli-dist/assembly.xml
new file mode 100755
index 0000000..ee27cb2
--- /dev/null
+++ b/integration/client-cli/client-cli-dist/assembly.xml
@@ -0,0 +1,49 @@
+<!--
+  ~ 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.
+  -->
+
+<assembly>
+    <id>keycloak-client-cli-dist</id>
+
+    <formats>
+        <format>zip</format>
+    </formats>
+
+    <includeBaseDirectory>false</includeBaseDirectory>
+
+    <files>
+        <file>
+            <source>../client-registration-cli/src/main/bin/kcreg.sh</source>
+            <outputDirectory>keycloak-client-tools/bin</outputDirectory>
+            <fileMode>0755</fileMode>
+            <filtered>true</filtered>
+        </file>
+        <file>
+            <source>../client-registration-cli/src/main/bin/kcreg.bat</source>
+            <outputDirectory>keycloak-client-tools/bin</outputDirectory>
+            <filtered>true</filtered>
+        </file>
+    </files>
+    <dependencySets>
+        <dependencySet>
+            <includes>
+                <include>org.keycloak:keycloak-client-registration-cli</include>
+            </includes>
+            <outputDirectory>keycloak-client-tools/bin/client</outputDirectory>
+        </dependencySet>
+    </dependencySets>
+
+</assembly>
diff --git a/integration/client-cli/client-cli-dist/pom.xml b/integration/client-cli/client-cli-dist/pom.xml
new file mode 100755
index 0000000..046a790
--- /dev/null
+++ b/integration/client-cli/client-cli-dist/pom.xml
@@ -0,0 +1,69 @@
+<!--
+  ~ 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">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>keycloak-client-cli-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>2.3.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>keycloak-client-cli-dist</artifactId>
+    <packaging>pom</packaging>
+    <name>Keycloak Client CLI Distribution</name>
+    <description/>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-client-registration-cli</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>keycloak-client-cli-${project.version}</finalName>
+        <plugins>
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>assemble</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <configuration>
+                            <descriptors>
+                                <descriptor>assembly.xml</descriptor>
+                            </descriptors>
+                            <outputDirectory>
+                                target
+                            </outputDirectory>
+                            <workDirectory>
+                                target/assembly/work
+                            </workDirectory>
+                            <appendAssemblyId>false</appendAssemblyId>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/integration/client-cli/client-registration-cli/pom.xml b/integration/client-cli/client-registration-cli/pom.xml
new file mode 100755
index 0000000..a140690
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/pom.xml
@@ -0,0 +1,143 @@
+<?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-client-cli-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>2.3.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>keycloak-client-registration-cli</artifactId>
+    <name>Keycloak Client Registration CLI</name>
+    <description/>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.jboss.aesh</groupId>
+            <artifactId>aesh</artifactId>
+            <version>0.66.10</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <!--version>2.4.3</version-->
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <filters>
+                                <filter>
+                                    <artifact>org.keycloak:keycloak-core</artifact>
+                                    <includes>
+                                        <include>org/keycloak/util/**</include>
+                                        <include>org/keycloak/json/**</include>
+                                        <include>org/keycloak/jose/jws/**</include>
+                                        <include>org/keycloak/jose/jwk/**</include>
+                                        <include>org/keycloak/representations/adapters/config/**</include>
+                                        <include>org/keycloak/representations/AccessTokenResponse.class</include>
+                                        <include>org/keycloak/representations/idm/ClientRepresentation.class</include>
+                                        <include>org/keycloak/representations/idm/ProtocolMapperRepresentation.class</include>
+                                        <include>org/keycloak/representations/oidc/OIDCClientRepresentation.class</include>
+                                        <include>org/keycloak/representations/idm/authorization/**</include>
+                                        <include>org/keycloak/representations/JsonWebToken.class</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>org.keycloak:keycloak-common</artifact>
+                                    <includes>
+                                        <include>org/keycloak/common/util/**</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>org.bouncycastle:bcprov-jdk15on</artifact>
+                                    <includes>
+                                        <include>**/**</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>org.bouncycastle:bcpkix-jdk15on</artifact>
+                                    <excludes>
+                                        <exclude>**/**</exclude>
+                                    </excludes>
+                                </filter>
+                                <filter>
+                                    <artifact>com.fasterxml.jackson.core:jackson-core</artifact>
+                                    <includes>
+                                        <include>**/**</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>com.fasterxml.jackson.core:jackson-databind</artifact>
+                                    <includes>
+                                        <include>**/**</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>com.fasterxml.jackson.core:jackson-annotations</artifact>
+                                    <includes>
+                                        <include>com/fasterxml/jackson/annotation/**</include>
+                                    </includes>
+                                </filter>
+
+                                <filter>
+                                    <artifact>*:*</artifact>
+                                    <excludes>
+                                        <exclude>META-INF/*.SF</exclude>
+                                        <exclude>META-INF/*.DSA</exclude>
+                                        <exclude>META-INF/*.RSA</exclude>
+                                    </excludes>
+                                </filter>
+                            </filters>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/integration/client-cli/client-registration-cli/src/main/bin/kcreg.bat b/integration/client-cli/client-registration-cli/src/main/bin/kcreg.bat
new file mode 100644
index 0000000..0436414
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/bin/kcreg.bat
@@ -0,0 +1,8 @@
+@echo off
+
+if "%OS%" == "Windows_NT" (
+  set "DIRNAME=%~dp0%"
+) else (
+  set DIRNAME=.\
+)
+java %KC_OPTS% -cp %DIRNAME%\client\keycloak-client-registration-cli-${project.version}.jar org.keycloak.client.registration.cli.KcRegMain %*
diff --git a/integration/client-cli/client-registration-cli/src/main/bin/kcreg.sh b/integration/client-cli/client-registration-cli/src/main/bin/kcreg.sh
new file mode 100755
index 0000000..2684c77
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/bin/kcreg.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+DIRNAME=`dirname "$0"`
+java $KC_OPTS -cp $DIRNAME/client/keycloak-client-registration-cli-${project.version}.jar org.keycloak.client.registration.cli.KcRegMain "$@"
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/AeshConsoleCallbackImpl.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/AeshConsoleCallbackImpl.java
new file mode 100644
index 0000000..8235f69
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/AeshConsoleCallbackImpl.java
@@ -0,0 +1,97 @@
+package org.keycloak.client.registration.cli.aesh;
+
+import org.jboss.aesh.cl.parser.OptionParserException;
+import org.jboss.aesh.cl.result.ResultHandler;
+import org.jboss.aesh.console.AeshConsoleCallback;
+import org.jboss.aesh.console.AeshConsoleImpl;
+import org.jboss.aesh.console.ConsoleOperation;
+import org.jboss.aesh.console.command.CommandNotFoundException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.container.CommandContainer;
+import org.jboss.aesh.console.command.container.CommandContainerResult;
+import org.jboss.aesh.console.command.invocation.AeshCommandInvocation;
+import org.jboss.aesh.console.command.invocation.AeshCommandInvocationProvider;
+import org.jboss.aesh.parser.AeshLine;
+import org.jboss.aesh.parser.ParserStatus;
+
+import java.lang.reflect.Method;
+
+class AeshConsoleCallbackImpl extends AeshConsoleCallback {
+
+    private final AeshConsoleImpl console;
+    private CommandResult result;
+
+    AeshConsoleCallbackImpl(AeshConsoleImpl aeshConsole) {
+        this.console = aeshConsole;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public int execute(ConsoleOperation output) throws InterruptedException {
+        if (output != null && output.getBuffer().trim().length() > 0) {
+            ResultHandler resultHandler = null;
+            //AeshLine aeshLine = Parser.findAllWords(output.getBuffer());
+            AeshLine aeshLine = new AeshLine(output.getBuffer(), Globals.args, ParserStatus.OK, "");
+            try (CommandContainer commandContainer = getCommand(output, aeshLine)) {
+                resultHandler = commandContainer.getParser().getProcessedCommand().getResultHandler();
+                CommandContainerResult ccResult =
+                        commandContainer.executeCommand(aeshLine, console.getInvocationProviders(), console.getAeshContext(),
+                                new AeshCommandInvocationProvider().enhanceCommandInvocation(
+                                new AeshCommandInvocation(console,
+                                    output.getControlOperator(), output.getPid(), this)));
+
+                result = ccResult.getCommandResult();
+
+                if(result == CommandResult.SUCCESS && resultHandler != null)
+                    resultHandler.onSuccess();
+                else if(resultHandler != null)
+                    resultHandler.onFailure(result);
+
+            } catch (Exception e) {
+                console.stop();
+
+                if (e instanceof OptionParserException) {
+                    System.err.println("Unknown command: " + aeshLine.getWords().get(0));
+                } else {
+                    System.err.println(e.getMessage());
+                }
+                if (Globals.dumpTrace) {
+                    e.printStackTrace();
+                }
+
+                System.exit(1);
+            }
+        }
+        // empty line
+        else if (output != null) {
+            result = CommandResult.FAILURE;
+        }
+        else {
+            //stop();
+            result = CommandResult.FAILURE;
+        }
+
+        if (result == CommandResult.SUCCESS) {
+            return 0;
+        } else {
+            return 1;
+        }
+    }
+
+    private CommandContainer getCommand(ConsoleOperation output, AeshLine aeshLine) throws CommandNotFoundException {
+        Method m;
+        try {
+            m = console.getClass().getDeclaredMethod("getCommand", AeshLine.class, String.class);
+        } catch (NoSuchMethodException e) {
+            throw new RuntimeException("Unexpected error: ", e);
+        }
+
+        m.setAccessible(true);
+
+        try {
+            return (CommandContainer) m.invoke(console, aeshLine, output.getBuffer());
+        } catch (Exception e) {
+            throw new RuntimeException("Unexpected error: ", e);
+        }
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/AeshEnhancer.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/AeshEnhancer.java
new file mode 100644
index 0000000..d68e392
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/AeshEnhancer.java
@@ -0,0 +1,25 @@
+package org.keycloak.client.registration.cli.aesh;
+
+import org.jboss.aesh.console.AeshConsoleImpl;
+import org.jboss.aesh.console.Console;
+
+import java.lang.reflect.Field;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AeshEnhancer {
+
+    public static void enhance(AeshConsoleImpl console) {
+        try {
+            Globals.stdin.setConsole(console);
+
+            Field field = AeshConsoleImpl.class.getDeclaredField("console");
+            field.setAccessible(true);
+            Console internalConsole = (Console) field.get(console);
+            internalConsole.setConsoleCallback(new AeshConsoleCallbackImpl(console));
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to install Aesh enhancement", e);
+        }
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/EndpointTypeConverter.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/EndpointTypeConverter.java
new file mode 100644
index 0000000..eb1c16a
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/EndpointTypeConverter.java
@@ -0,0 +1,17 @@
+package org.keycloak.client.registration.cli.aesh;
+
+import org.jboss.aesh.cl.converter.Converter;
+import org.jboss.aesh.cl.validator.OptionValidatorException;
+import org.jboss.aesh.console.command.converter.ConverterInvocation;
+import org.keycloak.client.registration.cli.common.EndpointType;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class EndpointTypeConverter implements Converter<EndpointType, ConverterInvocation> {
+
+    @Override
+    public EndpointType convert(ConverterInvocation converterInvocation) throws OptionValidatorException {
+        return EndpointType.of(converterInvocation.getInput());
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/Globals.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/Globals.java
new file mode 100644
index 0000000..bdeab20
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/Globals.java
@@ -0,0 +1,15 @@
+package org.keycloak.client.registration.cli.aesh;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class Globals {
+
+    public static boolean dumpTrace = false;
+
+    public static ValveInputStream stdin;
+
+    public static List<String> args;
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/ValveInputStream.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/ValveInputStream.java
new file mode 100644
index 0000000..76691dc
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/ValveInputStream.java
@@ -0,0 +1,71 @@
+package org.keycloak.client.registration.cli.aesh;
+
+import org.jboss.aesh.console.AeshConsoleImpl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * This stream blocks and waits, until there is some stream in the queue.
+ * It reads all streams from the queue, and then blocks until it receives more.
+ */
+public class ValveInputStream extends InputStream {
+
+    private BlockingQueue<InputStream> queue = new LinkedBlockingQueue<>(10);
+
+    private InputStream current;
+
+    private AeshConsoleImpl console;
+
+    @Override
+    public int read() throws IOException {
+        if (current == null) {
+            try {
+                current = queue.take();
+            } catch (InterruptedException e) {
+                throw new InterruptedIOException("Signalled to exit");
+            }
+        }
+        int c = current.read();
+        if (c == -1) {
+            //current = null;
+            if (console != null) {
+                console.stop();
+            }
+        }
+
+        return c;
+    }
+
+    /**
+     * For some reason AeshInputStream wants to do blocking read of whole buffers, which for stdin
+     * results in blocked input.
+     */
+    @Override
+    public int read(byte b[], int off, int len) throws IOException {
+        int c = read();
+        if (c == -1) {
+            return c;
+        }
+        b[off] = (byte) c;
+        return 1;
+    }
+
+    public void setInputStream(InputStream is) {
+        if (queue.contains(is)) {
+            return;
+        }
+        queue.add(is);
+    }
+
+    public void setConsole(AeshConsoleImpl console) {
+        this.console = console;
+    }
+
+    public boolean isStdinAvailable() {
+        return console.isRunning();
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AbstractAuthOptionsCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AbstractAuthOptionsCmd.java
new file mode 100644
index 0000000..8ad0430
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AbstractAuthOptionsCmd.java
@@ -0,0 +1,235 @@
+package org.keycloak.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.config.ConfigData;
+import org.keycloak.client.registration.cli.config.ConfigHandler;
+import org.keycloak.client.registration.cli.config.FileConfigHandler;
+import org.keycloak.client.registration.cli.config.InMemoryConfigHandler;
+import org.keycloak.client.registration.cli.config.RealmConfigData;
+import org.keycloak.client.registration.cli.util.ConfigUtil;
+import org.keycloak.client.registration.cli.util.HttpUtil;
+import org.keycloak.client.registration.cli.util.IoUtil;
+
+import java.io.File;
+
+import static org.keycloak.client.registration.cli.config.FileConfigHandler.setConfigFile;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.checkAuthInfo;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.checkServerInfo;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
+
+    static final String DEFAULT_CLIENT = "admin-cli";
+
+
+    @Option(name = "config", description = "Path to the config file (~/.keycloak/kcreg.config by default)", hasValue = true)
+    protected String config;
+
+    @Option(name = "no-config", description = "No configuration file should be used, no authentication info should be saved", hasValue = false)
+    protected boolean noconfig;
+
+    @Option(name = "server", description = "Server endpoint url (e.g. 'http://localhost:8080/auth')", hasValue = true)
+    protected String server;
+
+    @Option(name = "realm", description = "Realm name to authenticate against", hasValue = true)
+    protected String realm;
+
+    @Option(name = "client", description = "Realm name to authenticate against", hasValue = true)
+    protected String clientId;
+
+    @Option(name = "user", description = "Username to login with", hasValue = true)
+    protected String user;
+
+    @Option(name = "password", description = "Password to login with (prompted for if not specified and --user is used)", hasValue = true)
+    protected String password;
+
+    @Option(name = "secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)", hasValue = true)
+    protected String secret;
+
+    @Option(name = "keystore", description = "Path to a keystore containing private key", hasValue = true)
+    protected String keystore;
+
+    @Option(name = "storepass", description = "Keystore password (prompted for if not specified and --keystore is used)", hasValue = true)
+    protected String storePass;
+
+    @Option(name = "keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n                             otherwise defaults to keystore password)", hasValue = true)
+    protected String keyPass;
+
+    @Option(name = "alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)", hasValue = true)
+    protected String alias;
+
+    @Option(name = "truststore", description = "Path to a truststore", hasValue = true)
+    protected String trustStore;
+
+    @Option(name = "trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)", hasValue = true)
+    protected String trustPass;
+
+    @Option(shortName = 't', name = "token", description = "Initial / Registration access token to use)", hasValue = true)
+    protected String token;
+
+    protected void init(AbstractAuthOptionsCmd parent) {
+
+        super.init(parent);
+
+        noconfig = parent.noconfig;
+        config = parent.config;
+        server = parent.server;
+        realm = parent.realm;
+        clientId = parent.clientId;
+        user = parent.user;
+        password = parent.password;
+        secret = parent.secret;
+        keystore = parent.keystore;
+        storePass = parent.storePass;
+        keyPass = parent.keyPass;
+        alias = parent.alias;
+        trustStore = parent.trustStore;
+        trustPass = parent.trustPass;
+        token = parent.token;
+    }
+
+    protected void applyDefaultOptionValues() {
+        if (clientId == null) {
+            clientId = DEFAULT_CLIENT;
+        }
+    }
+
+    protected void processGlobalOptions() {
+
+        super.processGlobalOptions();
+
+        if (config != null && noconfig) {
+            throw new RuntimeException("Options --config and --no-config are mutually exclusive");
+        }
+
+        if (!noconfig) {
+            setConfigFile(config != null ? config : ConfigUtil.DEFAULT_CONFIG_FILE_PATH);
+            ConfigUtil.setHandler(new FileConfigHandler());
+        } else {
+            InMemoryConfigHandler handler = new InMemoryConfigHandler();
+            ConfigData data = new ConfigData();
+            initConfigData(data);
+            handler.setConfigData(data);
+            ConfigUtil.setHandler(handler);
+        }
+    }
+
+    protected void setupTruststore(ConfigData configData, CommandInvocation invocation ) {
+
+        if (!configData.getServerUrl().startsWith("https:")) {
+            return;
+        }
+
+        String truststore = trustStore;
+        if (truststore == null) {
+            truststore = configData.getTruststore();
+        }
+
+        if (truststore != null) {
+            String pass = trustPass;
+            if (pass == null) {
+                pass = configData.getTrustpass();
+            }
+            if (pass == null) {
+                pass = IoUtil.readSecret("Enter truststore password: ", invocation);
+            }
+
+            try {
+                HttpUtil.setTruststore(new File(truststore), pass);
+            } catch (Exception e) {
+                throw new RuntimeException("Failed to load truststore: " + truststore, e);
+            }
+        }
+    }
+
+    protected ConfigData ensureAuthInfo(ConfigData config, CommandInvocation commandInvocation) {
+
+        if (requiresLogin()) {
+            // make sure current handler is in-memory handler
+            // restore it at the end
+            ConfigHandler old = ConfigUtil.getHandler();
+            try {
+                // make sure all defaults are initialized after this point
+                applyDefaultOptionValues();
+
+                initConfigData(config);
+                ConfigUtil.setupInMemoryHandler(config);
+
+                ConfigCredentialsCmd login = new ConfigCredentialsCmd(this);
+                login.init(config);
+                login.process(commandInvocation);
+
+                // this must be executed before finally block which restores config handler
+                return loadConfig();
+
+            } catch (RuntimeException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            } finally {
+                ConfigUtil.setHandler(old);
+            }
+
+        } else {
+            checkAuthInfo(config);
+
+            // make sure all defaults are initialized after this point
+            applyDefaultOptionValues();
+            return loadConfig();
+        }
+    }
+
+    protected boolean requiresLogin() {
+        return user != null || password != null || secret != null || keystore != null
+                || keyPass != null || storePass != null || alias != null;
+    }
+
+    protected ConfigData copyWithServerInfo(ConfigData config) {
+
+        ConfigData result = config.deepcopy();
+
+        if (server != null) {
+            result.setServerUrl(server);
+        }
+        if (realm != null) {
+            result.setRealm(realm);
+        }
+
+        checkServerInfo(result);
+        return result;
+    }
+
+    private void initConfigData(ConfigData data) {
+        if (server != null)
+            data.setServerUrl(server);
+        if (realm != null)
+            data.setRealm(realm);
+        if (trustStore != null)
+            data.setTruststore(trustStore);
+
+        RealmConfigData rdata = data.sessionRealmConfigData();
+        if (clientId != null)
+            rdata.setClientId(clientId);
+        if (secret != null)
+            rdata.setSecret(secret);
+    }
+
+    protected void checkUnsupportedOptions(String ... options) {
+        if (options.length % 2 != 0) {
+            throw new IllegalArgumentException("Even number of argument required");
+        }
+
+        for (int i = 0; i < options.length; i++) {
+            String name = options[i];
+            String value = options[++i];
+
+            if (value != null) {
+                throw new RuntimeException("Unsupported option: " + name);
+            }
+        }
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AbstractGlobalOptionsCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AbstractGlobalOptionsCmd.java
new file mode 100644
index 0000000..a3dc6e1
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AbstractGlobalOptionsCmd.java
@@ -0,0 +1,22 @@
+package org.keycloak.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.Command;
+import org.keycloak.client.registration.cli.aesh.Globals;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractGlobalOptionsCmd implements Command {
+
+    @Option(shortName = 'x', description = "Print full stack trace when exiting with error", hasValue = false)
+    protected boolean dumpTrace;
+
+    protected void init(AbstractGlobalOptionsCmd parent) {
+        dumpTrace = parent.dumpTrace;
+    }
+
+    protected void processGlobalOptions() {
+        Globals.dumpTrace = dumpTrace;
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AttrsCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AttrsCmd.java
new file mode 100644
index 0000000..fb52357
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AttrsCmd.java
@@ -0,0 +1,152 @@
+package org.keycloak.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.Arguments;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.common.AttributeKey;
+import org.keycloak.client.registration.cli.common.EndpointType;
+import org.keycloak.client.registration.cli.util.ReflectionUtil;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
+import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
+import static org.keycloak.client.registration.cli.util.ReflectionUtil.getAttributeListWithJSonTypes;
+import static org.keycloak.client.registration.cli.util.ReflectionUtil.isBasicType;
+import static org.keycloak.client.registration.cli.util.ReflectionUtil.isListType;
+import static org.keycloak.client.registration.cli.util.ReflectionUtil.isMapType;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "attrs", description = "[ATTRIBUTE] [--endpoint TYPE]")
+public class AttrsCmd extends AbstractGlobalOptionsCmd {
+
+    @Option(shortName = 'e', name = "endpoint", description = "Endpoint type to use", hasValue = true)
+    protected String endpoint;
+
+    @Arguments
+    protected List<String> args;
+
+    protected String attr;
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            processGlobalOptions();
+
+
+            EndpointType regType = EndpointType.DEFAULT;
+            PrintStream out = commandInvocation.getShell().out();
+
+            if (endpoint != null) {
+                regType = EndpointType.of(endpoint);
+            }
+
+            if (args != null) {
+                if (args.size() > 1) {
+                    throw new RuntimeException("Invalid option: " + args.get(1));
+                }
+                attr = args.get(0);
+            }
+
+            Class type = regType == EndpointType.DEFAULT ? ClientRepresentation.class : (regType == EndpointType.OIDC ? OIDCClientRepresentation.class : null);
+            if (type == null) {
+                throw new RuntimeException("Endpoint not supported: " + regType);
+            }
+            AttributeKey key = attr == null ? new AttributeKey() : new AttributeKey(attr);
+
+            Field f = ReflectionUtil.resolveField(type, key);
+            String ts = f != null ? ReflectionUtil.getTypeString(null, f) : null;
+
+            if (f == null) {
+                out.printf("Attributes for %s format:\n", regType.getEndpoint());
+
+                LinkedHashMap<String, String> items = getAttributeListWithJSonTypes(type, key);
+                for (Map.Entry<String, String> item : items.entrySet()) {
+                    out.printf("  %-40s %s\n", item.getKey(), item.getValue());
+                }
+
+            } else {
+                out.printf("%-40s %s", attr, ts);
+                boolean eol = false;
+
+                Type t = f.getGenericType();
+                if (isListType(f.getType()) && t instanceof ParameterizedType) {
+                    t = ((ParameterizedType) t).getActualTypeArguments()[0];
+                    if (!isBasicType(t) && t instanceof Class) {
+                        eol = true;
+                        System.out.printf(", where value is:\n", ts);
+                        LinkedHashMap<String, String> items = ReflectionUtil.getAttributeListWithJSonTypes((Class) t, null);
+                        for (Map.Entry<String, String> item : items.entrySet()) {
+                            out.printf("    %-36s %s\n", item.getKey(), item.getValue());
+                        }
+                    }
+                } else if (isMapType(f.getType()) && t instanceof ParameterizedType) {
+                    t = ((ParameterizedType) t).getActualTypeArguments()[1];
+                    if (!isBasicType(t) && t instanceof Class) {
+                        eol = true;
+                        out.printf(", where value is:\n", ts);
+                        LinkedHashMap<String, String> items = ReflectionUtil.getAttributeListWithJSonTypes((Class) t, null);
+                        for (Map.Entry<String, String> item : items.entrySet()) {
+                            out.printf("    %-36s %s\n", item.getKey(), item.getValue());
+                        }
+                    }
+                }
+
+                if (!eol) {
+                    // add end of line
+                    out.println();
+                }
+            }
+
+            return CommandResult.SUCCESS;
+
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " attrs [ATTRIBUTE] [ARGUMENTS]");
+        out.println();
+        out.println("List available configuration attributes.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                   Print full stack trace when exiting with error");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    ATTRIBUTE            Attribute key (if omitted all attributes for the endpoint type are listed)");
+        out.println("                         Dot notation can be used to target sub-attributes.");
+        out.println("    -e, --endpoint TYPE  Endpoint type to use - one of: 'default', 'oidc' (if omitted 'default' is used)");
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("List all attributes for default endpoint:");
+        out.println("  " + PROMPT + " " + CMD + " attrs");
+        out.println();
+        out.println("List (sub)attributes of 'protocolMappers' attribute for default endpoint:");
+        out.println("  " + PROMPT + " " + CMD + " attrs protocolMappers");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigCmd.java
new file mode 100644
index 0000000..be9358d
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigCmd.java
@@ -0,0 +1,84 @@
+/*
+ * 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.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.Arguments;
+import org.jboss.aesh.cl.GroupCommandDefinition;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.util.OsUtil;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+
+@GroupCommandDefinition(name = "config", description = "COMMAND [ARGUMENTS]", groupCommands = {ConfigCredentialsCmd.class} )
+public class ConfigCmd extends AbstractAuthOptionsCmd implements Command {
+
+    @Arguments
+    protected List<String> args;
+
+
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            if (args.size() == 0) {
+                throw new RuntimeException("Sub-command required by '" + OsUtil.CMD + " config' - one of: 'credentials', 'truststore', 'initial-token', 'registration-token'");
+            }
+
+            String cmd = args.get(0);
+            switch (cmd) {
+                case "credentials": {
+                    return new ConfigCredentialsCmd(this).execute(commandInvocation);
+                }
+                case "truststore": {
+                    return new ConfigTruststoreCmd(this).execute(commandInvocation);
+                }
+                case "initial-token": {
+                    return new ConfigInitialTokenCmd(this).execute(commandInvocation);
+                }
+                case "registration-token": {
+                    return new ConfigRegistrationTokenCmd(this).execute(commandInvocation);
+                }
+                default:
+                    throw new RuntimeException("Unknown sub-command: " + cmd);
+            }
+
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + OsUtil.CMD + " config SUB_COMMAND [ARGUMENTS]");
+        out.println();
+        out.println("Where SUB_COMMAND is one of: 'credentials', 'truststore', 'initial-token', 'registration-token'");
+        out.println();
+        out.println();
+        out.println("Use '" + OsUtil.CMD + " help config SUB_COMMAND' for more info.");
+        out.println("Use '" + OsUtil.CMD + " help' for general information and a list of commands.");
+        return sb.toString();
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigCredentialsCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigCredentialsCmd.java
new file mode 100644
index 0000000..6d60530
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigCredentialsCmd.java
@@ -0,0 +1,246 @@
+package org.keycloak.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.config.ConfigData;
+import org.keycloak.client.registration.cli.config.RealmConfigData;
+import org.keycloak.client.registration.cli.util.AuthUtil;
+import org.keycloak.representations.AccessTokenResponse;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.URL;
+
+import static org.keycloak.client.registration.cli.util.AuthUtil.getAuthTokens;
+import static org.keycloak.client.registration.cli.util.AuthUtil.getAuthTokensByJWT;
+import static org.keycloak.client.registration.cli.util.AuthUtil.getAuthTokensBySecret;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.getHandler;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.saveTokens;
+import static org.keycloak.client.registration.cli.util.IoUtil.printErr;
+import static org.keycloak.client.registration.cli.util.IoUtil.readSecret;
+import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
+import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
+import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]")
+public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Command {
+
+    private int sigLifetime = 600;
+
+    public ConfigCredentialsCmd() {}
+
+    public ConfigCredentialsCmd(AbstractAuthOptionsCmd parent) {
+        init(parent);
+    }
+
+    public void init(ConfigData configData) {
+        if (server == null) {
+            server = configData.getServerUrl();
+        }
+        if (realm == null) {
+            realm = configData.getRealm();
+        }
+        if (trustStore == null) {
+            trustStore = configData.getTruststore();
+        }
+
+        RealmConfigData rdata = configData.getRealmConfigData(server, realm);
+        if (rdata == null) {
+            return;
+        }
+
+        if (clientId == null) {
+            clientId = rdata.getClientId();
+        }
+    }
+
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            processGlobalOptions();
+
+            return process(commandInvocation);
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        // check server
+        if (server == null) {
+            throw new RuntimeException("Required option not specified: --server");
+        }
+
+        try {
+            new URL(server);
+        } catch (Exception e) {
+            throw new RuntimeException("Invalid server endpoint url: " + server, e);
+        }
+
+        if (realm == null)
+            throw new RuntimeException("Required option not specified: --realm");
+
+        String signedRequestToken = null;
+        boolean clientSet = clientId != null;
+
+        applyDefaultOptionValues();
+
+        if (user != null) {
+            printErr("Logging into " + server + " as user " + user + " of realm " + realm);
+
+            // if user was set there needs to be a password so we can authenticate
+            if (password == null) {
+                password = readSecret("Enter password: ", commandInvocation);
+            }
+            // if secret was set to be read from stdin, then ask for it
+            if ("-".equals(secret) && keystore == null) {
+                secret = readSecret("Enter client secret: ", commandInvocation);
+            }
+        } else if (keystore != null || secret != null || clientSet) {
+            printErr("Logging into " + server + " as " + "service-account-" + clientId + " of realm " + realm);
+            if (keystore == null) {
+                if (secret == null) {
+                    secret = readSecret("Enter client secret: ", commandInvocation);
+                }
+            }
+        }
+
+        if (keystore != null) {
+            if (secret != null) {
+                throw new RuntimeException("Can't use both --keystore and --secret");
+            }
+
+            if (!new File(keystore).isFile()) {
+                throw new RuntimeException("No such keystore file: " + keystore);
+            }
+
+            if (storePass == null) {
+                storePass = readSecret("Enter keystore password: ", commandInvocation);
+                keyPass = readSecret("Enter key password: ", commandInvocation);
+            }
+
+            if (keyPass == null) {
+                keyPass = storePass;
+            }
+
+            if (alias == null) {
+                alias = clientId;
+            }
+
+            String realmInfoUrl = server + "/realms/" + realm;
+
+            signedRequestToken = AuthUtil.getSignedRequestToken(keystore, storePass, keyPass,
+                    alias, sigLifetime, clientId, realmInfoUrl);
+        }
+
+        // if only server and realm are set, just save config and be done
+        if (user == null && secret == null && keystore == null) {
+            getHandler().saveMergeConfig(config -> {
+                config.setServerUrl(server);
+                config.setRealm(realm);
+            });
+            return CommandResult.SUCCESS;
+        }
+
+        setupTruststore(copyWithServerInfo(loadConfig()), commandInvocation);
+
+        // now use the token endpoint to retrieve access token, and refresh token
+        AccessTokenResponse tokens = signedRequestToken != null ?
+                getAuthTokensByJWT(server, realm, user, password, clientId, signedRequestToken) :
+                secret != null ?
+                        getAuthTokensBySecret(server, realm, user, password, clientId, secret) :
+                        getAuthTokens(server, realm, user, password, clientId);
+
+        Long sigExpiresAt = signedRequestToken == null ? null : System.currentTimeMillis() + sigLifetime * 1000;
+
+        // save tokens to config file
+        saveTokens(tokens, server, realm, clientId, signedRequestToken, sigExpiresAt, secret);
+
+        return CommandResult.SUCCESS;
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " config credentials --server SERVER_URL --realm REALM [ARGUMENTS]");
+        out.println("       " + CMD + " config credentials --server SERVER_URL --realm REALM --user USER [--password PASSWORD] [ARGUMENTS]");
+        out.println("       " + CMD + " config credentials --server SERVER_URL --realm REALM --client CLIENT_ID [--secret SECRET] [ARGUMENTS]");
+        out.println("       " + CMD + " config credentials --server SERVER_URL --realm REALM --client CLIENT_ID [--keystore KEYSTORE] [ARGUMENTS]");
+        out.println();
+        out.println("Command to establish an authenticated client session with the server. There are many authentication");
+        out.println("options available, and it depends on server side client authentication configuration how client can or should authenticate.");
+        out.println("The information always required includes --server, and --realm. That is enough to establish unauthenticated session.");
+        out.println("If --client is not provided it defaults to 'admin-cli'. The authantication options / requirements depend on how this client is configured.");
+        out.println();
+        out.println("If you have an account configured with the rights to manage clients then you can use username, and password to authenticate.");
+        out.println("If confidential client authentication is also configured, you may have to specify a client id, and client credentials in addition to");
+        out.println("user credentials. Client credentials are either a client secret, or a keystore information to use Signed JWT mechanism.");
+        out.println("If only client credentials are provided, and no user credentials, then the service account is used for login.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                      Print full stack trace when exiting with error");
+        out.println("    --config                Path to a config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH       Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD    Truststore password (prompted for if not specified and --truststore is used)");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    --server SERVER_URL     Server endpoint url (e.g. 'http://localhost:8080/auth')");
+        out.println("    --realm REALM           Realm name to use");
+        out.println("    --user USER             Username to login with");
+        out.println("    --password PASSWORD     Password to login with (prompted for if not specified and --user is used)");
+        out.println("    --client CLIENT_ID      ClientId used by this client tool ('admin-cli' by default)");
+        out.println("    --secret SECRET         Secret to authenticate the client (prompted for if --client is specified, and no --keystore is specified)");
+        out.println("    --keystore PATH         Path to a keystore containing private key");
+        out.println("    --storepass PASSWORD    Keystore password (prompted for if not specified and --keystore is used)");
+        out.println("    --keypass PASSWORD      Key password (prompted for if not specified and --keystore is used without --storepass,");
+        out.println("                            otherwise defaults to keystore password)");
+        out.println("    --alias ALIAS           Alias of the key inside a keystore (defaults to the value of ClientId)");
+        out.println();
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Login as 'admin' user of 'master' realm to a local Keycloak server running on default port.");
+        out.println("You will be prompted for a password:");
+        out.println("  " + PROMPT + " " + CMD + " config credentials --server http://localhost:8080/auth --realm master --user admin");
+        out.println();
+        out.println("Login to Keycloak server at non-default endpoint passing the password via standard input:");
+        if (OS_ARCH.isWindows()) {
+            out.println("  " + PROMPT + " echo mypassword | " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin");
+        } else {
+            out.println("  " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin << EOF");
+            out.println("  mypassword");
+            out.println("  EOF");
+        }
+        out.println();
+        out.println("Login specifying a password through command line:");
+        out.println("  " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin --password " + OS_ARCH.envVar("PASSWORD"));
+        out.println();
+        out.println("Login using a client service account of a custom client. You will be prompted for a client secret:");
+        out.println("  " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --client reg-cli");
+        out.println();
+        out.println("Login using a client service account of a custom client, authenticating with signed JWT.");
+        out.println("You will be prompted for a keystore password, and a key password:");
+        out.println("  " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --client reg-cli --keystore " + OS_ARCH.path("~/.keycloak/keystore.jks"));
+        out.println();
+        out.println("Login as 'user' while also authenticating a custom client with signed JWT.");
+        out.println("You will be prompted for a user password, a keystore password, and a key password:");
+        out.println("  " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user user --client reg-cli --keystore " + OS_ARCH.path("~/.keycloak/keystore.jks"));
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigInitialTokenCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigInitialTokenCmd.java
new file mode 100644
index 0000000..5770e33
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigInitialTokenCmd.java
@@ -0,0 +1,169 @@
+package org.keycloak.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.config.RealmConfigData;
+import org.keycloak.client.registration.cli.util.IoUtil;
+import org.keycloak.client.registration.cli.util.ParseUtil;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
+import static org.keycloak.client.registration.cli.util.IoUtil.warnfOut;
+import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
+import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
+import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "initial-token", description = "[--server SERVER] --realm REALM [--delete | TOKEN] [ARGUMENTS]")
+public class ConfigInitialTokenCmd extends AbstractAuthOptionsCmd implements Command {
+
+    private ConfigCmd parent;
+
+    private boolean delete;
+    private boolean keepDomain;
+
+    public ConfigInitialTokenCmd() {}
+
+    public ConfigInitialTokenCmd(ConfigCmd parent) {
+        this.parent = parent;
+        init(parent);
+    }
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            return process(commandInvocation);
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        List<String> args = new ArrayList<>();
+
+        Iterator<String> it = parent.args.iterator();
+        // skip the first argument 'initial-token'
+        it.next();
+
+        while (it.hasNext()) {
+            String arg = it.next();
+            switch (arg) {
+                case "-d":
+                case "--delete": {
+                    delete = true;
+                    break;
+                }
+                case "-k":
+                case "--keep-domain": {
+                    keepDomain = true;
+                    break;
+                }
+                default: {
+                    args.add(arg);
+                }
+            }
+        }
+
+        if (args.size() > 1) {
+            throw new RuntimeException("Invalid option: " + args.get(1));
+        }
+
+        String token = args.size() == 1 ? args.get(0) : null;
+
+        if (realm == null) {
+            throw new RuntimeException("Realm not specified");
+        }
+
+        if (token != null && token.startsWith("-")) {
+            warnfOut(ParseUtil.TOKEN_OPTION_WARN, token);
+        }
+
+        checkUnsupportedOptions(
+                "--client", clientId,
+                "--user", user,
+                "--password", password,
+                "--secret", secret,
+                "--keystore", keystore,
+                "--storepass", storePass,
+                "--keypass", keyPass,
+                "--alias", alias,
+                "--truststore", trustStore,
+                "--trustpass", keyPass);
+
+
+        if (!delete && token == null) {
+            token = IoUtil.readSecret("Enter Initial Access Token: ", commandInvocation);
+        }
+
+        // now update the config
+        processGlobalOptions();
+
+        String initialToken = token;
+        saveMergeConfig(config -> {
+            if (!keepDomain && !delete) {
+                config.setServerUrl(server);
+                config.setRealm(realm);
+            }
+            if (delete) {
+                RealmConfigData rdata = config.getRealmConfigData(server, realm);
+                if (rdata != null) {
+                    rdata.setInitialToken(null);
+                }
+            } else {
+                RealmConfigData rdata = config.ensureRealmConfigData(server, realm);
+                rdata.setInitialToken(initialToken);
+            }
+        });
+
+        return CommandResult.SUCCESS;
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " config initial-token --server SERVER --realm REALM [--delete | TOKEN] [ARGUMENTS]");
+        out.println();
+        out.println("Command to configure an initial access token to be used with '" + CMD + " create' command. Even if an ");
+        out.println("authenticated session exists as a result of '" + CMD + " config credentials' its access token will not");
+        out.println("be used - initial access token will be used instead. By default, current server, and realm will");
+        out.println("be set to the new values thus subsequent commands will use these values as default.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                      Print full stack trace when exiting with error");
+        out.println("    --config                Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    --server SERVER         Server endpoint url (e.g. 'http://localhost:8080/auth')");
+        out.println("    --realm REALM           Realm name to use");
+        out.println("    -k, --keep-domain       Don't overwrite default server and realm");
+        out.println("    -d, --delete            Indicates that initial access token should be removed");
+        out.println("    TOKEN                   Initial access token (prompted for if not specified, unless -d is used)");
+        out.println();
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Specify initial access token for server, and realm. Token is passed via env variable:");
+        out.println("  " + PROMPT + " " + CMD + " config initial-token --server http://localhost:9080/auth --realm master " + OS_ARCH.envVar("TOKEN"));
+        out.println();
+        out.println("Remove initial access token:");
+        out.println("  " + PROMPT + " " + CMD + " config initial-token --server http://localhost:9080/auth --realm master --delete");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigRegistrationTokenCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigRegistrationTokenCmd.java
new file mode 100644
index 0000000..facea57
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigRegistrationTokenCmd.java
@@ -0,0 +1,158 @@
+package org.keycloak.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.config.RealmConfigData;
+import org.keycloak.client.registration.cli.util.IoUtil;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
+import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
+import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
+import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "registration-token", description = "[--server SERVER] --realm REALM --client CLIENT [--delete | TOKEN] [ARGUMENTS]")
+public class ConfigRegistrationTokenCmd extends AbstractAuthOptionsCmd implements Command {
+
+    private ConfigCmd parent;
+
+    private boolean delete;
+
+    public ConfigRegistrationTokenCmd() {}
+
+    public ConfigRegistrationTokenCmd(ConfigCmd parent) {
+        this.parent = parent;
+        init(parent);
+    }
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            return process(commandInvocation);
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        List<String> args = new ArrayList<>();
+
+        Iterator<String> it = parent.args.iterator();
+        // skip the first argument 'initial-token'
+        it.next();
+
+        while (it.hasNext()) {
+            String arg = it.next();
+            switch (arg) {
+                case "-d":
+                case "--delete": {
+                    delete = true;
+                    break;
+                }
+                default: {
+                    args.add(arg);
+                }
+            }
+        }
+
+        if (args.size() > 1) {
+            throw new RuntimeException("Invalid option: " + args.get(1));
+        }
+
+        String token = args.size() == 1 ? args.get(0) : null;
+
+        if (server == null) {
+            throw new RuntimeException("Required option not specified: --server");
+        }
+
+        if (realm == null) {
+            throw new RuntimeException("Required option not specified: --realm");
+        }
+
+        if (clientId == null) {
+            throw new RuntimeException("Required option not specified: --client");
+        }
+
+        checkUnsupportedOptions(
+                "--user", user,
+                "--password", password,
+                "--secret", secret,
+                "--keystore", keystore,
+                "--storepass", storePass,
+                "--keypass", keyPass,
+                "--alias", alias,
+                "--truststore", trustStore,
+                "--trustpass", keyPass);
+
+
+        if (!delete && token == null) {
+            token = IoUtil.readSecret("Enter Registration Access Token: ", commandInvocation);
+        }
+
+        // now update the config
+        processGlobalOptions();
+
+        String registrationToken = token;
+        saveMergeConfig(config -> {
+            RealmConfigData rdata = config.getRealmConfigData(server, realm);
+            if (delete) {
+                if (rdata != null) {
+                    rdata.getClients().remove(clientId);
+                }
+            } else {
+                config.ensureRealmConfigData(server, realm).getClients().put(clientId, registrationToken);
+            }
+        });
+
+        return CommandResult.SUCCESS;
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " config registration-token --server SERVER --realm REALM --client CLIENT [--delete | TOKEN] [ARGUMENTS]");
+        out.println();
+        out.println("Command to configure a registration access token to be used with 'kcreg get / update / delete' commands. Even if an ");
+        out.println("authenticated session exists as a result of '" + CMD + " config credentials' its access token will not be used - registration");
+        out.println("access token will be used instead.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                      Print full stack trace when exiting with error");
+        out.println("    --config                Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    --server SERVER         Server endpoint url (e.g. 'http://localhost:8080/auth')");
+        out.println("    --realm REALM           Realm name to use");
+        out.println("    --client CLIENT         ClientId of client whose token to set");
+        out.println("    -d, --delete            Indicates that registration access token should be removed");
+        out.println("    TOKEN                   Registration access token (prompted for if not specified, unless -d is used)");
+        out.println();
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Specify registration access token for server, and realm. Token is passed via env variable:");
+        out.println("  " + PROMPT + " " + CMD + " config registration-token --server http://localhost:9080/auth --realm master --client my_client " + OS_ARCH.envVar("TOKEN"));
+        out.println();
+        out.println("Remove registration access token:");
+        out.println("  " + PROMPT + " " + CMD + " config registration-token --server http://localhost:9080/auth --realm master --client my_client --delete");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java
new file mode 100644
index 0000000..99917f3
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java
@@ -0,0 +1,168 @@
+package org.keycloak.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
+import static org.keycloak.client.registration.cli.util.IoUtil.readSecret;
+import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
+import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
+import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "truststore", description = "PATH [ARGUMENTS]")
+public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Command {
+
+    private ConfigCmd parent;
+
+    private boolean delete;
+
+    public ConfigTruststoreCmd() {}
+
+    public ConfigTruststoreCmd(ConfigCmd parent) {
+        this.parent = parent;
+        init(parent);
+    }
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            return process(commandInvocation);
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        List<String> args = new ArrayList<>();
+
+        Iterator<String> it = parent.args.iterator();
+        // skip the first argument 'truststore'
+        it.next();
+
+        while (it.hasNext()) {
+            String arg = it.next();
+            switch (arg) {
+                case "-d":
+                case "--delete": {
+                    delete = true;
+                    break;
+                }
+                default: {
+                    args.add(arg);
+                }
+            }
+        }
+
+        if (args.size() > 1) {
+            throw new RuntimeException("Invalid option: " + args.get(1));
+        }
+
+        String truststore = null;
+        if (args.size() > 0) {
+            truststore = args.get(0);
+        }
+
+        checkUnsupportedOptions("--server", server,
+                "--realm", realm,
+                "--client", clientId,
+                "--user", user,
+                "--password", password,
+                "--secret", secret,
+                "--truststore", trustStore,
+                "--keystore", keystore,
+                "--keypass", keyPass,
+                "--alias", alias);
+
+        // now update the config
+        processGlobalOptions();
+
+        String store;
+        String pass;
+
+        if (!delete) {
+
+            if (truststore == null) {
+                throw new RuntimeException("No truststore specified");
+            }
+
+            if (!new File(truststore).isFile()) {
+                throw new RuntimeException("Truststore file not found: " + truststore);
+            }
+
+            if ("-".equals(trustPass)) {
+                trustPass = readSecret("Enter truststore password: ", commandInvocation);
+            }
+
+            store = truststore;
+            pass = trustPass;
+
+        } else {
+            if (truststore != null) {
+                throw new RuntimeException("Option --delete is mutually exclusive with specifying a TRUSTSTORE");
+            }
+            if (trustPass != null) {
+                throw new RuntimeException("Options --trustpass and --delete are mutually exclusive");
+            }
+            store = null;
+            pass = null;
+        }
+
+        saveMergeConfig(config -> {
+            config.setTruststore(store);
+            config.setTrustpass(pass);
+        });
+
+        return CommandResult.SUCCESS;
+    }
+
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWOD] [ARGUMENTS]");
+        out.println();
+        out.println("Command to configure a global truststore to use when using https to connect to Keycloak server.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                      Print full stack trace when exiting with error");
+        out.println("    --config                Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    TRUSTSTORE              Path to truststore file");
+        out.println("    --trustpass PASSWORD    Truststore password to unlock truststore (prompted for if set to '-')");
+        out.println("    -d, --delete            Remove truststore configuration");
+        out.println();
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Specify a truststore - you will be prompted for truststore password every time it is used:");
+        out.println("  " + PROMPT + " " + CMD + " config truststore " + OS_ARCH.path("~/.keycloak/truststore.jks"));
+        out.println();
+        out.println("Specify a truststore, and password - truststore will automatically without prompting for password:");
+        out.println("  " + PROMPT + " " + CMD + " config truststore --storepass " + OS_ARCH.envVar("PASSWORD") + " " + OS_ARCH.path("~/.keycloak/truststore.jks"));
+        out.println();
+        out.println("Remove truststore configuration:");
+        out.println("  " + PROMPT + " " + CMD + " config truststore --delete");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCmd.java
new file mode 100644
index 0000000..35f18a1
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCmd.java
@@ -0,0 +1,295 @@
+/*
+ * 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.client.registration.cli.commands;
+
+import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
+import org.jboss.aesh.cl.Arguments;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.aesh.EndpointTypeConverter;
+import org.keycloak.client.registration.cli.common.AttributeOperation;
+import org.keycloak.client.registration.cli.config.ConfigData;
+import org.keycloak.client.registration.cli.common.CmdStdinContext;
+import org.keycloak.client.registration.cli.common.EndpointType;
+import org.keycloak.client.registration.cli.util.HttpUtil;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.keycloak.client.registration.cli.common.AttributeOperation.Type.SET;
+import static org.keycloak.client.registration.cli.common.EndpointType.DEFAULT;
+import static org.keycloak.client.registration.cli.common.EndpointType.OIDC;
+import static org.keycloak.client.registration.cli.common.EndpointType.SAML2;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.credentialsAvailable;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
+import static org.keycloak.client.registration.cli.util.HttpUtil.getExpectedContentType;
+import static org.keycloak.client.registration.cli.util.IoUtil.printErr;
+import static org.keycloak.client.registration.cli.util.IoUtil.readFully;
+import static org.keycloak.client.registration.cli.util.IoUtil.readSecret;
+import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
+import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
+import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
+import static org.keycloak.client.registration.cli.util.ParseUtil.mergeAttributes;
+import static org.keycloak.client.registration.cli.util.ParseUtil.parseFileOrStdin;
+import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken;
+import static org.keycloak.client.registration.cli.util.HttpUtil.doPost;
+import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
+import static org.keycloak.client.registration.cli.util.ParseUtil.parseKeyVal;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "create", description = "[ARGUMENTS]")
+public class CreateCmd extends AbstractAuthOptionsCmd implements Command {
+
+    @Option(shortName = 'i', name = "clientId", description = "After creation only print clientId to standard output", hasValue = false)
+    protected boolean returnClientId = false;
+
+    @Option(shortName = 'e', name = "endpoint", description = "Endpoint type / document format to use - one of: 'default', 'oidc', 'saml2'",
+            hasValue = true, converter = EndpointTypeConverter.class)
+    protected EndpointType regType;
+
+    @Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'", hasValue = true)
+    protected String file;
+
+    @Option(shortName = 'o', name = "output", description = "After creation output the new client configuration to standard output", hasValue = false)
+    protected boolean outputClient = false;
+
+    @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
+    protected boolean compressed = false;
+
+    //@OptionGroup(shortName = 's', name = "set", description = "Set attribute to the specified value")
+    //Map<String, String> attributes = new LinkedHashMap<>();
+
+    @Arguments
+    protected List<String> args;
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        List<AttributeOperation> attrs = new LinkedList<>();
+
+        try {
+            processGlobalOptions();
+
+            if (args != null) {
+                Iterator<String> it = args.iterator();
+                while (it.hasNext()) {
+                    String option = it.next();
+                    switch (option) {
+                        case "-s":
+                        case "--set": {
+                            if (!it.hasNext()) {
+                                throw new RuntimeException("Option " + option + " requires a value");
+                            }
+                            String[] keyVal = parseKeyVal(it.next());
+                            attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
+                            break;
+                        }
+                        default: {
+                            throw new RuntimeException("Unsupported option: " + option);
+                        }
+                    }
+                }
+            }
+
+            if (file == null && attrs.size() == 0) {
+                throw new RuntimeException("No file nor attribute values specified");
+            }
+
+            if (outputClient && returnClientId) {
+                throw new RuntimeException("Options -o and -i can't be used together");
+            }
+
+            // if --token is specified read it
+            if ("-".equals(token)) {
+                token = readSecret("Enter Initial Access Token: ", commandInvocation);
+            }
+
+            CmdStdinContext ctx = new CmdStdinContext();
+            if (file != null) {
+                ctx = parseFileOrStdin(file, regType);
+            }
+
+            if (ctx.getEndpointType() == null) {
+                regType = regType != null ? regType : DEFAULT;
+                ctx.setEndpointType(regType);
+            } else if (regType != null && ctx.getEndpointType() != regType) {
+                throw new RuntimeException("Requested endpoint type not compatible with detected configuration format: " + ctx.getEndpointType());
+            }
+
+            if (attrs.size() > 0) {
+                ctx = mergeAttributes(ctx, attrs);
+            }
+
+            String contentType = getExpectedContentType(ctx.getEndpointType());
+
+            ConfigData config = loadConfig();
+            config = copyWithServerInfo(config);
+
+            if (token == null) {
+                // if initial token is not set, try use the one from configuration
+                token = config.sessionRealmConfigData().getInitialToken();
+            }
+
+            setupTruststore(config, commandInvocation);
+
+            String auth = token;
+            if (auth == null) {
+                config = ensureAuthInfo(config, commandInvocation);
+                config = copyWithServerInfo(config);
+                if (credentialsAvailable(config)) {
+                    auth = ensureToken(config);
+                }
+            }
+
+            auth = auth != null ? "Bearer " + auth : null;
+
+            final String server = config.getServerUrl();
+            final String realm = config.getRealm();
+
+            InputStream response = doPost(server + "/realms/" + realm + "/clients-registrations/" + ctx.getEndpointType().getEndpoint(),
+                    contentType, HttpUtil.APPLICATION_JSON, ctx.getContent(), auth);
+
+            try {
+                if (ctx.getEndpointType() == DEFAULT || ctx.getEndpointType() == SAML2) {
+                    ClientRepresentation client = JsonSerialization.readValue(response, ClientRepresentation.class);
+                    outputResult(client.getClientId(), client);
+
+                    saveMergeConfig(cfg -> {
+                        setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken());
+                    });
+                } else if (ctx.getEndpointType() == OIDC) {
+                    OIDCClientRepresentation client = JsonSerialization.readValue(response, OIDCClientRepresentation.class);
+                    outputResult(client.getClientId(), client);
+
+                    saveMergeConfig(cfg -> {
+                        setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken());
+                    });
+                } else {
+                    printOut("Response from server: " + readFully(response));
+                }
+            } catch (UnrecognizedPropertyException e) {
+                throw new RuntimeException("Failed to process HTTP reponse - " + e.getMessage(), e);
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to process HTTP response", e);
+            }
+
+            return CommandResult.SUCCESS;
+
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    private void outputResult(String clientId, Object result) throws IOException {
+        if (returnClientId) {
+            printOut(clientId);
+        } else if (outputClient) {
+            if (compressed) {
+                printOut(JsonSerialization.writeValueAsString(result));
+            } else {
+                printOut(JsonSerialization.writeValueAsPrettyString(result));
+            }
+        } else {
+            printErr("Registered new client with client_id '" + clientId + "'");
+        }
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " create [ARGUMENTS]");
+        out.println();
+        out.println("Command to create new client configurations on the server. If Initial Access Token is specified (-t TOKEN)");
+        out.println("or has previously been set for the server, and realm in the configuration ('" + CMD + " config initial-token'),");
+        out.println("then that will be used, otherwise session access / refresh tokens will be used.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                    Print full stack trace when exiting with error");
+        out.println("    --config              Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH     Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD  Truststore password (prompted for if not specified and --truststore is used)");
+        out.println("    CREDENTIALS OPTIONS   Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+        out.println("                          an authenticated sessions. This allows on-the-fly transient authentication that does");
+        out.println("                          not touch a config file.");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    -t, --token TOKEN     Use the specified Initial Access Token for authorization or read it from standard input ");
+        out.println("                          if '-' is specified. This overrides any token set by '" + CMD + " config initial-token'.");
+        out.println("                          If not specified, session credentials are used - see: CREDENTIALS OPTIONS.");
+        out.println("    -e, --endpoint TYPE   Endpoint type / document format to use - one of: 'default', 'oidc', 'saml2'.");
+        out.println("                          If not specified, the format is deduced from input file or falls back to 'default'.");
+        out.println("    -s, --set NAME=VALUE  Set a specific attribute NAME to a specified value VALUE");
+        out.println("    -f, --file FILENAME   Read object from file or standard input if FILENAME is set to '-'");
+        out.println("    -o, --output          After creation output the new client configuration to standard output");
+        out.println("    -c, --compressed      Don't pretty print the output");
+        out.println("    -i, --clientId        After creation only print clientId to standard output");
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Create a new client using configuration read from standard input:");
+        if (OS_ARCH.isWindows()) {
+            out.println("  " + PROMPT + " echo { \"clientId\": \"my_client\" } | " + CMD + " create -f -");
+        } else {
+            out.println("  " + PROMPT + " " + CMD + " create -f - << EOF");
+            out.println("  {");
+            out.println("    \"clientId\": \"my_client\"");
+            out.println("  }");
+            out.println("  EOF");
+        }
+        out.println();
+        out.println("Since we didn't specify an endpoint type it will be deduced from configuration format.");
+        out.println("Supported formats include Keycloak default format, OIDC format, and SAML SP Metadata.");
+        out.println();
+        out.println("Creating a client using file as a template, and overriding some attributes:");
+        out.println("  " + PROMPT + " " + CMD + " create -f my_client.json -s clientId=my_client2 -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]'");
+        out.println();
+        out.println("Creating a client using an Initial Access Token - you'll be prompted for a token:");
+        out.println("  " + PROMPT + " " + CMD + " create -s clientId=my_client2 -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]' -t -");
+        out.println();
+        out.println("Creating a client using 'oidc' endpoint. Without setting endpoint type here it would be 'default':");
+        out.println("  " + PROMPT + " " + CMD + " create -e oidc -s 'redirect_uris=[\"http://localhost:8980/myapp/*\"]'");
+        out.println();
+        out.println("Creating a client using 'saml2' endpoint. In this case setting endpoint type is redundant since it is deduced ");
+        out.println("from file content:");
+        out.println("  " + PROMPT + " " + CMD + " create -e saml2 -f saml-sp-metadata.xml");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/DeleteCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/DeleteCmd.java
new file mode 100644
index 0000000..1d6e817
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/DeleteCmd.java
@@ -0,0 +1,145 @@
+/*
+ * 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.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.Arguments;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.config.ConfigData;
+import org.keycloak.client.registration.cli.util.ParseUtil;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.credentialsAvailable;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.getRegistrationToken;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
+import static org.keycloak.client.registration.cli.util.HttpUtil.doDelete;
+import static org.keycloak.client.registration.cli.util.HttpUtil.urlencode;
+import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr;
+import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
+import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "delete", description = "CLIENT_ID [GLOBAL_OPTIONS]")
+public class DeleteCmd extends AbstractAuthOptionsCmd {
+
+    @Arguments
+    private List<String> args = new ArrayList<>();
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            processGlobalOptions();
+
+            if (args.isEmpty()) {
+                throw new RuntimeException("CLIENT_ID not specified");
+            }
+
+            if (args.size() > 1) {
+                throw new RuntimeException("Invalid option: " + args.get(1));
+            }
+
+            String clientId = args.get(0);
+
+            if (clientId.startsWith("-")) {
+                warnfErr(ParseUtil.CLIENTID_OPTION_WARN, clientId);
+            }
+
+            String regType = "default";
+
+            ConfigData config = loadConfig();
+            config = copyWithServerInfo(config);
+
+            if (token == null) {
+                // if registration access token is not set via -t, try use the one from configuration
+                token = getRegistrationToken(config.sessionRealmConfigData(), clientId);
+            }
+
+            setupTruststore(config, commandInvocation);
+
+            String auth = token;
+            if (auth == null) {
+                config = ensureAuthInfo(config, commandInvocation);
+                config = copyWithServerInfo(config);
+                if (credentialsAvailable(config)) {
+                    auth = ensureToken(config);
+                }
+            }
+
+            auth = auth != null ? "Bearer " + auth : null;
+
+
+            final String server = config.getServerUrl();
+            final String realm = config.getRealm();
+
+            doDelete(server + "/realms/" + realm + "/clients-registrations/" + regType + "/" + urlencode(clientId), auth);
+
+            saveMergeConfig(cfg -> {
+                cfg.ensureRealmConfigData(server, realm).getClients().remove(clientId);
+            });
+            return CommandResult.SUCCESS;
+
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " delete CLIENT [ARGUMENTS]");
+        out.println();
+        out.println("Command to delete a specific client configuration. If registration access token is specified or is available in ");
+        out.println("configuration file, then it is used. Otherwise, current active session is used.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                    Print full stack trace when exiting with error");
+        out.println("    --config              Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH     Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD  Truststore password (prompted for if not specified and --truststore is used)");
+        out.println("    --token TOKEN         Registration access token to use");
+        out.println("    CREDENTIALS OPTIONS   Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+        out.println("                          an authenticated sessions. This allows on-the-fly transient authentication that does");
+        out.println("                          not touch a config file.");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    CLIENT                ClientId of the client to delete");
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Delete a client:");
+        out.println("  " + PROMPT + " " + CMD + " delete my_client");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/GetCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/GetCmd.java
new file mode 100644
index 0000000..1b6b7cc
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/GetCmd.java
@@ -0,0 +1,215 @@
+/*
+ * 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.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.Arguments;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.config.ConfigData;
+import org.keycloak.client.registration.cli.common.EndpointType;
+import org.keycloak.client.registration.cli.util.ParseUtil;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.credentialsAvailable;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.getRegistrationToken;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken;
+import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_JSON;
+import static org.keycloak.client.registration.cli.util.HttpUtil.doGet;
+import static org.keycloak.client.registration.cli.util.HttpUtil.urlencode;
+import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr;
+import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
+import static org.keycloak.client.registration.cli.util.IoUtil.readFully;
+import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
+import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "get", description = "[ARGUMENTS]")
+public class GetCmd extends AbstractAuthOptionsCmd {
+
+    @Option(shortName = 'c', name = "compressed", description = "Print full stack trace when exiting with error", hasValue = false)
+    private boolean compressed = false;
+
+    @Option(shortName = 'e', name = "endpoint", description = "Endpoint type to use", hasValue = true)
+    private String endpoint;
+
+    @Arguments
+    private List<String> args = new ArrayList<>();
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        try {
+            processGlobalOptions();
+
+            if (args == null || args.isEmpty()) {
+                throw new RuntimeException("CLIENT not specified");
+            }
+
+            if (args.size() > 1) {
+                throw new RuntimeException("Invalid option: " + args.get(1));
+            }
+
+            String clientId = args.get(0);
+            EndpointType regType = endpoint != null ? EndpointType.of(endpoint) : EndpointType.DEFAULT;
+
+
+            if (clientId.startsWith("-")) {
+                warnfErr(ParseUtil.CLIENTID_OPTION_WARN, clientId);
+            }
+
+            ConfigData config = loadConfig();
+            config = copyWithServerInfo(config);
+
+            if (token == null) {
+                // if registration access token is not set via -t, try use the one from configuration
+                token = getRegistrationToken(config.sessionRealmConfigData(), clientId);
+            }
+
+            setupTruststore(config, commandInvocation);
+
+            String auth = token;
+            if (auth == null) {
+                config = ensureAuthInfo(config, commandInvocation);
+                config = copyWithServerInfo(config);
+                if (credentialsAvailable(config)) {
+                    auth = ensureToken(config);
+                }
+            }
+
+            auth = auth != null ? "Bearer " + auth : null;
+
+
+            final String server = config.getServerUrl();
+            final String realm = config.getRealm();
+
+            InputStream response = doGet(server + "/realms/" + realm + "/clients-registrations/" + regType.getEndpoint() + "/" + urlencode(clientId),
+                    APPLICATION_JSON, auth);
+
+            try {
+                String json = readFully(response);
+                Object result = null;
+
+                switch (regType) {
+                    case DEFAULT: {
+                        ClientRepresentation client = JsonSerialization.readValue(json, ClientRepresentation.class);
+                        result = client;
+
+                        saveMergeConfig(cfg -> {
+                            setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken());
+                        });
+                        break;
+                    }
+                    case OIDC: {
+                        OIDCClientRepresentation client = JsonSerialization.readValue(json, OIDCClientRepresentation.class);
+                        result = client;
+
+                        saveMergeConfig(cfg -> {
+                            setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken());
+                        });
+                        break;
+                    }
+                    case INSTALL: {
+                        result = JsonSerialization.readValue(json, AdapterConfig.class);
+                        break;
+                    }
+                    case SAML2: {
+                        break;
+                    }
+                    default: {
+                        throw new RuntimeException("Unexpected type: " + regType);
+                    }
+                }
+
+                if (!compressed && result != null) {
+                    json = JsonSerialization.writeValueAsPrettyString(result);
+                }
+
+                printOut(json);
+
+            //} catch (UnrecognizedPropertyException e) {
+            //    throw new RuntimeException("Failed to parse returned JSON - " + e.getMessage(), e);
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to process HTTP response", e);
+            }
+            return CommandResult.SUCCESS;
+
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " get CLIENT [ARGUMENTS]");
+        out.println();
+        out.println("Command to retrieve a client configuration description for a specified client. If registration access token");
+        out.println("is specified or is available in configuration file, then it is used. Otherwise, current active session is used.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                    Print full stack trace when exiting with error");
+        out.println("    --config              Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH     Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD  Truststore password (prompted for if not specified and --truststore is used)");
+        out.println("    -t, --token TOKEN     Registration access token to use");
+        out.println("    CREDENTIALS OPTIONS   Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+        out.println("                          an authenticated sessions. This allows on-the-fly transient authentication that does");
+        out.println("                          not touch a config file.");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    CLIENT                ClientId of the client to display");
+        out.println("    -c, --compressed      Don't pretty print the output");
+        out.println("    -e, --endpoint TYPE   Endpoint type to use - one of: 'default', 'oidc', 'install'");
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Get configuration in default format:");
+        out.println("  " + PROMPT + " " + CMD + " get my_client");
+        out.println();
+        out.println("Get configuration in OIDC format:");
+        out.println("  " + PROMPT + " " + CMD + " get my_client -e oidc");
+        out.println();
+        out.println("Get adapter configuration for the client:");
+        out.println("  " + PROMPT + " " + CMD + " get my_client -e install");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/HelpCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/HelpCmd.java
new file mode 100644
index 0000000..820f84a
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/HelpCmd.java
@@ -0,0 +1,90 @@
+package org.keycloak.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.Arguments;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+
+import java.util.List;
+
+import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "help", description = "This help")
+public class HelpCmd implements Command {
+
+    @Arguments
+    private List<String> args;
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            if (args == null || args.size() == 0) {
+                printOut(KcRegCmd.usage());
+            } else {
+                outer:
+                switch (args.get(0)) {
+                    case "config": {
+                        if (args.size() > 1) {
+                            switch (args.get(1)) {
+                                case "credentials": {
+                                    printOut(ConfigCredentialsCmd.usage());
+                                    break outer;
+                                }
+                                case "initial-token": {
+                                    printOut(ConfigInitialTokenCmd.usage());
+                                    break outer;
+                                }
+                                case "registration-token": {
+                                    printOut(ConfigRegistrationTokenCmd.usage());
+                                    break outer;
+                                }
+                                case "truststore": {
+                                    printOut(ConfigTruststoreCmd.usage());
+                                    break outer;
+                                }
+                            }
+                        }
+                        printOut(ConfigCmd.usage());
+                        break;
+                    }
+                    case "create": {
+                        printOut(CreateCmd.usage());
+                        break;
+                    }
+                    case "get": {
+                        printOut(GetCmd.usage());
+                        break;
+                    }
+                    case "update": {
+                        printOut(UpdateCmd.usage());
+                        break;
+                    }
+                    case "delete": {
+                        printOut(DeleteCmd.usage());
+                        break;
+                    }
+                    case "attrs": {
+                        printOut(AttrsCmd.usage());
+                        break;
+                    }
+                    case "update-token": {
+                        printOut(UpdateTokenCmd.usage());
+                        break;
+                    }
+                    default: {
+                        throw new RuntimeException("Unknown command: " + args.get(0));
+                    }
+                }
+            }
+
+            return CommandResult.SUCCESS;
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/KcRegCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/KcRegCmd.java
new file mode 100644
index 0000000..d2b8a85
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/KcRegCmd.java
@@ -0,0 +1,105 @@
+/*
+ * 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.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.GroupCommandDefinition;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.util.IoUtil;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
+import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+
+@GroupCommandDefinition(name = "kcreg", description = "COMMAND [ARGUMENTS]", groupCommands = {
+    HelpCmd.class, ConfigCmd.class, CreateCmd.class, UpdateCmd.class, GetCmd.class, DeleteCmd.class, AttrsCmd.class, UpdateTokenCmd.class} )
+public class KcRegCmd extends AbstractGlobalOptionsCmd {
+
+    //@Arguments
+    //private List<String> args;
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            IoUtil.printOut(usage());
+
+            return CommandResult.SUCCESS;
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Keycloak Client Registration CLI");
+        out.println();
+        out.println("Use '" + CMD + " config credentials' command with username and password to start a session against a specific");
+        out.println("server and realm.");
+        out.println();
+        out.println("For example:");
+        out.println();
+        out.println("  " + PROMPT + " " + CMD + " config credentials --server http://localhost:8080/auth --realm master --user admin");
+        out.println("  Enter password: ");
+        out.println("  Logging into http://localhost:8080/auth as user admin of realm master");
+        out.println();
+        out.println("Any configured username can be used for login, but to perform client registration operations the user");
+        out.println("needs proper roles, otherwise attempts to create, update, read, or delete clients will fail.");
+        out.println("Alternatively, the user without the necessary roles can use an Initial Access Token provided by realm");
+        out.println("administrator when creating a new client with 'create' command. For example:");
+        out.println();
+        out.println("  " + PROMPT + " " + CMD + " create -f my_client.json -t -");
+        out.println("  Enter Initial Access Token: ");
+        out.println("  Registered new client with client_id 'my_client'");
+        out.println();
+        out.println("When Initial Access Token is used the server issues a Registration Access Token which is automatically");
+        out.println("handled by " + CMD + ", saved into a local config file, and automatically used for any follow-up operations");
+        out.println("on the same client. For example:");
+        out.println();
+        out.println("  " + PROMPT + " " + CMD + " get my_client");
+        out.println("  " + PROMPT + " " + CMD + " update my_client -s enabled=false");
+        out.println("  " + PROMPT + " " + CMD + " delete my_client");
+        out.println();
+        out.println();
+        out.println("Usage: " + CMD + " COMMAND [ARGUMENTS]");
+        out.println();
+        out.println("Global options:");
+        out.println("  -x            Print full stack trace when exiting with error");
+        out.println("  -c, --config  Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println();
+        out.println("Commands: ");
+        out.println("  config        Set up credentials, and other configuration settings using the config file");
+        out.println("  create        Register a new client");
+        out.println("  get           Get configuration of existing client in Keycloak or OIDC format, or adapter install configuration");
+        out.println("  update        Update a client configuration");
+        out.println("  delete        Delete a client");
+        out.println("  attrs         List available attributes");
+        out.println("  update-token  Update Registration Access Token for a client");
+        out.println("  help          This help");
+        out.println();
+        out.println("Use '" + CMD + " help <command>' for more information about a given command.");
+        return sb.toString();
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/UpdateCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/UpdateCmd.java
new file mode 100644
index 0000000..87ab781
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/UpdateCmd.java
@@ -0,0 +1,410 @@
+/*
+ * 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.client.registration.cli.commands;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import org.jboss.aesh.cl.Arguments;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.aesh.EndpointTypeConverter;
+import org.keycloak.client.registration.cli.common.AttributeOperation;
+import org.keycloak.client.registration.cli.config.ConfigData;
+import org.keycloak.client.registration.cli.common.CmdStdinContext;
+import org.keycloak.client.registration.cli.common.EndpointType;
+import org.keycloak.client.registration.cli.util.ParseUtil;
+import org.keycloak.client.registration.cli.util.ReflectionUtil;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.keycloak.client.registration.cli.common.AttributeOperation.Type.DELETE;
+import static org.keycloak.client.registration.cli.common.AttributeOperation.Type.SET;
+import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.credentialsAvailable;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.getRegistrationToken;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken;
+import static org.keycloak.client.registration.cli.common.EndpointType.DEFAULT;
+import static org.keycloak.client.registration.cli.common.EndpointType.OIDC;
+import static org.keycloak.client.registration.cli.util.HttpUtil.doGet;
+import static org.keycloak.client.registration.cli.util.HttpUtil.doPut;
+import static org.keycloak.client.registration.cli.util.HttpUtil.urlencode;
+import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
+import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr;
+import static org.keycloak.client.registration.cli.util.IoUtil.readFully;
+import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_JSON;
+import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
+import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
+import static org.keycloak.client.registration.cli.util.ParseUtil.mergeAttributes;
+import static org.keycloak.client.registration.cli.util.ParseUtil.parseFileOrStdin;
+import static org.keycloak.client.registration.cli.util.ParseUtil.parseKeyVal;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "update", description = "CLIENT_ID [ARGUMENTS]")
+public class UpdateCmd extends AbstractAuthOptionsCmd {
+
+    @Option(shortName = 'e', name = "endpoint", description = "Endpoint type to use - one of: 'default', 'oidc'", hasValue = true, converter = EndpointTypeConverter.class)
+    private EndpointType regType = null;
+
+    //@GroupOption(shortName = 's', name = "set", description = "Set specific attribute to a specified value", hasValue = true)
+    //private List<String> attributes = new ArrayList<>();
+
+    @Option(shortName = 'f', name = "file", description = "Use the file or standard input if '-' is specified", hasValue = true)
+    private String file = null;
+
+    @Option(shortName = 'm', name = "merge", description = "Merge new values with existing configuration on the server", hasValue = false)
+    private boolean mergeMode = true;
+
+    @Option(shortName = 'u', name = "unsafe", description = "Allow updating without registration access token - no optimistic locking", hasValue = false)
+    private boolean allowUnsafe = true;
+
+    @Option(shortName = 'o', name = "output", description = "After update output the new client configuration", hasValue = false)
+    private boolean outputClient = false;
+
+    @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
+    private boolean compressed = false;
+
+    @Arguments
+    private List<String> args;
+
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        List<AttributeOperation> attrs = new LinkedList<>();
+
+        try {
+            processGlobalOptions();
+
+            String clientId = null;
+
+            if (args != null) {
+                Iterator<String> it = args.iterator();
+                if (!it.hasNext()) {
+                    throw new RuntimeException("CLIENT_ID not specified");
+                }
+
+                clientId = it.next();
+
+                if (clientId.startsWith("-")) {
+                    warnfErr(ParseUtil.CLIENTID_OPTION_WARN, clientId);
+                }
+
+                while (it.hasNext()) {
+                    String option = it.next();
+                    switch (option) {
+                        case "-s":
+                        case "--set": {
+                            if (!it.hasNext()) {
+                                throw new RuntimeException("Option " + option + " requires a value");
+                            }
+                            String[] keyVal = parseKeyVal(it.next());
+                            attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
+                            break;
+                        }
+                        case "-d":
+                        case "--delete": {
+                            attrs.add(new AttributeOperation(DELETE, it.next()));
+                            break;
+                        }
+                        default: {
+                            throw new RuntimeException("Unsupported option: " + option);
+                        }
+                    }
+                }
+            }
+
+            if (file == null && attrs.size() == 0) {
+                throw new RuntimeException("No file nor attribute values specified");
+            }
+
+            // We have several options for update:
+            //
+            // A) if a file is specified, then we can overwrite server state with that file
+            //   (that's the normal flow - get and save locally, edit, post to server)
+            //
+            //   update my_client -f new_client.json
+            //
+            // B) if a file is specified, and overrides are specified, then we override the file values with those from command line
+            //   (that allows us to have a local file as a template, it's also batch job friendly)
+            //
+            //   update my_client -s public=true -s enableDirectGrants=false -f new_client.json
+            //
+            // C) if no file is specified, then we can fetch the client definition from server, apply changes to it, and post it back
+            //   (again a batch job friendly mode)
+            //
+            //   update my_client -s public=true -s enableDirectGrants=false
+            //
+            //   This is merge mode by default - if --merge is additionally specified, it is ignored
+            //
+            // D) if a file is specified, then we can merge the file with current state on the server
+            //   (that is similar to previous mode except that the overrides are also taken from a file)
+            //
+            //   update my_client --merge -f new_client.json
+            //   update my_client --merge -s public=true -s enableDirectGrants=false -f new_client.json
+            //
+            // We could also support environment variables in input file, and apply them before parsing it.
+            //
+            // One problem - what if it is SAML XML? No problem as we don't support update for SAML - only create.
+            //
+            if (file == null && attrs.size() > 0) {
+                mergeMode = true;
+            }
+
+            CmdStdinContext ctx = new CmdStdinContext();
+            if (file != null) {
+                ctx = parseFileOrStdin(file, regType);
+                regType = ctx.getEndpointType();
+            }
+
+            if (regType == null) {
+                regType = DEFAULT;
+                ctx.setEndpointType(regType);
+            } else if (regType != DEFAULT && regType != OIDC) {
+                throw new RuntimeException("Update not supported for endpoint type: " + regType.getEndpoint());
+            }
+
+            // initialize config only after reading from stdin,
+            // to allow proper operation when piping 'get' - which consumes the old
+            // registration access token, and saves the new one to the config
+            ConfigData config = loadConfig();
+            config = copyWithServerInfo(config);
+
+            final String server = config.getServerUrl();
+            final String realm = config.getRealm();
+
+            if (token == null) {
+                // if registration access token is not set via --token, see if it's in the body of any input file
+                // but first see if it's overridden by --set, or maybe deliberately muted via -d registrationAccessToken
+                boolean processed = false;
+                for (AttributeOperation op: attrs) {
+                    if ("registrationAccessToken".equals(op.getKey().toString())) {
+                        processed = true;
+                        if (op.getType() == AttributeOperation.Type.SET) {
+                            token = op.getValue();
+                        }
+                        // otherwise it's delete - meaning it should stay null
+                        break;
+                    }
+                }
+                if (!processed) {
+                    token = ctx.getRegistrationAccessToken();
+                }
+            }
+
+            if (token == null) {
+                // if registration access token is not set, try use the one from configuration
+                token = getRegistrationToken(config.sessionRealmConfigData(), clientId);
+            }
+
+            setupTruststore(config, commandInvocation);
+
+            String auth = token;
+            if (auth == null) {
+                config = ensureAuthInfo(config, commandInvocation);
+                config = copyWithServerInfo(config);
+                if (credentialsAvailable(config)) {
+                    auth = ensureToken(config);
+                }
+            }
+
+            auth = auth != null ? "Bearer " + auth : null;
+
+
+            if (mergeMode) {
+                InputStream response = doGet(server + "/realms/" + realm + "/clients-registrations/" + regType.getEndpoint() + "/" + urlencode(clientId),
+                        APPLICATION_JSON, auth);
+
+                String json = readFully(response);
+
+                CmdStdinContext ctxremote = new CmdStdinContext();
+                ctxremote.setContent(json);
+                ctxremote.setEndpointType(regType);
+                try {
+
+                    if (regType == DEFAULT) {
+                        ctxremote.setClient(JsonSerialization.readValue(json, ClientRepresentation.class));
+                        token = ctxremote.getClient().getRegistrationAccessToken();
+                    } else if (regType == OIDC) {
+                        ctxremote.setOidcClient(JsonSerialization.readValue(json, OIDCClientRepresentation.class));
+                        token = ctxremote.getOidcClient().getRegistrationAccessToken();
+                    }
+                } catch (JsonParseException e) {
+                    throw new RuntimeException("Not a valid JSON document. " + e.getMessage(), e);
+                } catch (IOException e) {
+                    throw new RuntimeException("Not a valid JSON document", e);
+                }
+
+                // we have to use registration access token retrieved from previous operation
+                // that ensures optimistic locking semantics
+                if (token != null) {
+                    // we use auth with doPost later
+                    auth = "Bearer " + token;
+
+                    String newToken = token;
+                    String clientToUpdate = clientId;
+                    saveMergeConfig(cfg -> {
+                        setRegistrationToken(cfg.ensureRealmConfigData(server, realm), clientToUpdate, newToken);
+                    });
+                } else if (!allowUnsafe) {
+                    throw new RuntimeException("No Registration Access Token found for client: " + clientId + ". Provide one or use --unsafe.");
+                }
+
+                // merge local representation over remote one
+                if (ctx.getClient() != null) {
+                    ReflectionUtil.merge(ctx.getClient(), ctxremote.getClient());
+                } else if (ctx.getOidcClient() != null) {
+                    ReflectionUtil.merge(ctx.getOidcClient(), ctxremote.getOidcClient());
+                }
+                ctx = ctxremote;
+            }
+
+            if (attrs.size() > 0) {
+                ctx = mergeAttributes(ctx, attrs);
+            }
+
+            // now update
+            InputStream response = doPut(server + "/realms/" + realm + "/clients-registrations/" + regType.getEndpoint() + "/" + urlencode(clientId),
+                    APPLICATION_JSON, APPLICATION_JSON, ctx.getContent(), auth);
+            try {
+                if (regType == DEFAULT) {
+                    ClientRepresentation clirep = JsonSerialization.readValue(response, ClientRepresentation.class);
+                    outputResult(clirep);
+                    token = clirep.getRegistrationAccessToken();
+                } else if (regType == OIDC) {
+                    OIDCClientRepresentation clirep = JsonSerialization.readValue(response, OIDCClientRepresentation.class);
+                    outputResult(clirep);
+                    token = clirep.getRegistrationAccessToken();
+                }
+
+                String newToken = token;
+                String clientToUpdate = clientId;
+                saveMergeConfig(cfg -> {
+                    setRegistrationToken(cfg.ensureRealmConfigData(server, realm), clientToUpdate, newToken);
+                });
+
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to process HTTP response", e);
+            }
+
+            return CommandResult.SUCCESS;
+
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    private void outputResult(Object result) throws IOException {
+        if (outputClient) {
+            if (compressed) {
+                printOut(JsonSerialization.writeValueAsString(result));
+            } else {
+                printOut(JsonSerialization.writeValueAsPrettyString(result));
+            }
+        }
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " update CLIENT [ARGUMENTS]");
+        out.println();
+        out.println("Command to update an existing client configuration. If registration access token is specified it is used.");
+        out.println("Otherwise, if 'registrationAccessToken' attribute is set, that is used. Otherwise, if registration access");
+        out.println("token is available in configuration file, we use that. Finally, if it's not available anywhere, the current ");
+        out.println("active session is used.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                    Print full stack trace when exiting with error");
+        out.println("    -c, --config          Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH     Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD  Truststore password (prompted for if not specified and --truststore is used)");
+        out.println("    --token TOKEN         Registration access token to use");
+        out.println("    CREDENTIALS OPTIONS   Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+        out.println("                          an authenticated sessions. This allows on-the-fly transient authentication that does");
+        out.println("                          not touch a config file.");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    CLIENT                ClientId of the client to update");
+        out.println("    -s, --set KEY=VALUE   Set specific attribute to a specified value");
+        out.println("              KEY+=VALUE  Add item to an array");
+        out.println("    -d, --delete NAME     Delete the specific attribute, or array item");
+        out.println("    -e, --endpoint TYPE   Endpoint type to use - one of: 'default', 'oidc'");
+        out.println("    -f, --file FILENAME   Use the file or standard input if '-' is specified");
+        out.println("    -m, --merge           Merge new values with existing configuration on the server");
+        out.println("                          Merge is automatically enabled unless --file is specified");
+        out.println("    -u, --unsafe          Allow updating without registration access token - no optimistic locking");
+        out.println("    -o, --output          After update output the new client configuration");
+        out.println("    -c, --compressed      Don't pretty print the output");
+        out.println();
+        out.println();
+        out.println("Nested attributes are supported by using '.' to separate components of a KEY. Optionaly, the KEY components ");
+        out.println("can be quoted with double quotes - e.g. my_client.attributes.\"external.user.id\". If VALUE starts with [ and ");
+        out.println("ends with ] the attribute will be set as a JSON array. If VALUE starts with { and ends with } the attribute ");
+        out.println("will be set as a JSON object. If KEY ends with an array index - e.g. clients[3]=VALUE - then the specified item");
+        out.println("of the array is updated. If KEY+=VALUE syntax is used, then KEY is assumed to be an array, and another item is");
+        out.println("added to it.");
+        out.println();
+        out.println("Attributes can also be deleted. If KEY ends with an array index, then the targeted item of an array is removed");
+        out.println("and the following items are shifted.");
+        out.println();
+        out.println("Merged mode fetches current configuration from the server, applies attribute changes to it, and sends it");
+        out.println("back to the server, overwriting existing configuration there. To ensure there are no unexpected changes");
+        out.println("Registration Access Token is used for authorization when doing changes. Alternatively, one can specify to use");
+        out.println("unsafe mode in which case login session's authorization is used - user requires manage-clients permission.");
+        out.println();
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Update a client by fetching current configuration from server, and applying specified changes.");
+        out.println("  " + PROMPT + " " + CMD + " update my_client -s enabled=true -s 'redirectUris=[\"http://localhost:8080/myapp/*\"]'");
+        out.println();
+        out.println("Update a client by overwriting existing configuration on the server with a new one:");
+        out.println("  " + PROMPT + " " + CMD + " update my_client -f new_my_client.json");
+        out.println();
+        out.println("Update a client by overwriting existing configuration using local file as a template:");
+        out.println("  " + PROMPT + " " + CMD + " update my_client -f new_my_client.json -s enabled=true");
+        out.println();
+        out.println("Update client by fetching current configuration from server and merging with specified changes:");
+        out.println("  " + PROMPT + " " + CMD + " update my_client -f new_my_client.json -s enabled=true --merge");
+        out.println();
+        out.println("Update a client using 'oidc' endpoint:");
+        out.println("  " + PROMPT + " " + CMD + " update my_client -e oidc -s 'redirect_uris=[\"http://localhost:8080/myapp/*\"]'");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/UpdateTokenCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/UpdateTokenCmd.java
new file mode 100644
index 0000000..e52f5a2
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/UpdateTokenCmd.java
@@ -0,0 +1,164 @@
+/*
+ * 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.client.registration.cli.commands;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.jboss.aesh.cl.Arguments;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.config.ConfigData;
+import org.keycloak.client.registration.cli.util.ParseUtil;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken;
+import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_JSON;
+import static org.keycloak.client.registration.cli.util.HttpUtil.doGet;
+import static org.keycloak.client.registration.cli.util.HttpUtil.doPost;
+import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
+import static org.keycloak.client.registration.cli.util.IoUtil.warnfOut;
+import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
+import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "update-token", description = "CLIENT [ARGUMENTS]")
+public class UpdateTokenCmd extends AbstractAuthOptionsCmd {
+
+    @Arguments
+    private List<String> args = new ArrayList<>();
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        try {
+            processGlobalOptions();
+
+            if (args.isEmpty()) {
+                throw new RuntimeException("CLIENT not specified");
+            }
+
+            String clientId = args.get(0);
+
+            if (clientId.startsWith("-")) {
+                warnfOut(ParseUtil.CLIENTID_OPTION_WARN, clientId);
+            }
+
+            ConfigData config = loadConfig();
+            config = copyWithServerInfo(config);
+            setupTruststore(config, commandInvocation);
+
+            config = ensureAuthInfo(config, commandInvocation);
+            String auth = ensureToken(config);
+
+            String cid = null;
+
+            final String server = config.getServerUrl();
+            final String realm = config.getRealm();
+
+            // first we need to get id of the client with client_id == clientId
+            InputStream response = doGet(server + "/admin/realms/" + realm + "/clients", APPLICATION_JSON, "Bearer " + auth);
+            try {
+                List<ClientRepresentation> clients = JsonSerialization.readValue(response, new TypeReference<List<ClientRepresentation>>() {});
+                for (ClientRepresentation client: clients) {
+                    if (clientId.equals(client.getClientId())) {
+                        cid = client.getId();
+                        break;
+                    }
+                }
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to process response from server", e);
+            }
+
+            if (cid == null) {
+                throw new RuntimeException("No client found for: " + clientId);
+            }
+
+            response = doPost(server + "/admin/realms/" + realm + "/clients/" + cid + "/registration-access-token",
+                    APPLICATION_JSON, APPLICATION_JSON, null, "Bearer " + auth);
+
+            try {
+                ClientRepresentation client = JsonSerialization.readValue(response, ClientRepresentation.class);
+
+                if (noconfig) {
+                    // output to stdout
+                    printOut(client.getRegistrationAccessToken());
+                } else {
+                    saveMergeConfig(cfg -> {
+                        setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken());
+                    });
+                }
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to process response from server", e);
+            }
+
+            //System.out.println("Token updated for client " + clientId);
+            return CommandResult.SUCCESS;
+
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " update-token CLIENT [ARGUMENTS]");
+        out.println();
+        out.println("Command to reissue, and set a new registration access token if an old one is lost or becomes invalid.");
+        out.println("It requires an authenticated session using an account with administrator priviliges.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                    Print full stack trace when exiting with error");
+        out.println("    -c, --config          Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH     Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD  Truststore password (prompted for if not specified and --truststore is used)");
+        out.println("    CREDENTIALS OPTIONS   Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+        out.println("                          an authenticated sessions. This allows on-the-fly transient authentication that leaves");
+        out.println("                          no tokens in config file.");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    CLIENT                ClientId of the client to reissue a new Registration Access Token for");
+        out.println("                          The new token is saved to a config file or printed to stdout if on-the-fly\n");
+        out.println("                          authentication is used");
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Request a new Registration Access Token from the server using current authenticated session:");
+        out.println("  " + PROMPT + " " + CMD + " update-token my_client");
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/AttributeKey.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/AttributeKey.java
new file mode 100644
index 0000000..49e3ab4
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/AttributeKey.java
@@ -0,0 +1,154 @@
+package org.keycloak.client.registration.cli.common;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AttributeKey {
+
+    private static final int START = 0;
+    private static final int QUOTED = 1;
+    private static final int UNQUOTED = 2;
+    private static final int END = 3;
+
+    private List<Component> components;
+    private boolean append;
+
+    public AttributeKey() {
+        components = Collections.emptyList();
+    }
+
+    public AttributeKey(String key) {
+        if (key.endsWith("+")) {
+            append = true;
+            key = key.substring(0, key.length() - 1);
+        }
+        components = parse(key);
+    }
+
+    static List<Component> parse(String key) {
+
+        if (key == null || "".equals(key)) {
+            return Collections.emptyList();
+        }
+
+        List<Component> cs = new LinkedList<>();
+        StringBuilder sb = new StringBuilder();
+        int state = START;
+
+        char[] buf = key.toCharArray();
+
+        for (int pos = 0; pos < buf.length; pos++) {
+            char c = buf[pos];
+
+            if (state == START) {
+                if ('\"' == c) {
+                    state = QUOTED;
+                } else if ('.' == c) {
+                    throw new RuntimeException("Invalid attribute key: " + key + " (at position " + (pos + 1) + ")");
+                } else {
+                    state = UNQUOTED;
+                    sb.append(c);
+                }
+            } else if (state == QUOTED) {
+                if ('\"' == c) {
+                    state = END;
+                } else {
+                    sb.append(c);
+                }
+            } else if (state == UNQUOTED || state == END) {
+                if ('.' == c) {
+                    state = START;
+                    cs.add(new Component(sb.toString()));
+                    sb.setLength(0);
+                } else if (state == END || '\"' == c) {
+                    throw new RuntimeException("Invalid attribute key: " + key + " (at position " + (pos + 1) + ")");
+                } else {
+                    sb.append(c);
+                }
+            }
+        }
+
+        boolean ok = false;
+        if (sb.length() > 0) {
+            if (state == UNQUOTED || state == END) {
+                cs.add(new Component(sb.toString()));
+                ok = true;
+            }
+        } else if (state == END) {
+            ok = true;
+        }
+
+        if (!ok) {
+            throw new RuntimeException("Invalid attribute key: " + key + " (at position " + (buf.length) + ")");
+        }
+
+        return Collections.unmodifiableList(cs);
+    }
+
+    public List<Component> getComponents() {
+        return components;
+    }
+
+    public boolean isAppend() {
+        return append;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        for (Component c: components) {
+            if (sb.length() > 0) {
+                sb.append(".");
+            }
+            sb.append(c.toString());
+        }
+        return sb.toString();
+    }
+
+
+
+    public static class Component {
+
+        private int index = -1;
+        private String name;
+
+        Component(String name) {
+            if (name.endsWith("]")) {
+                int pos = name.lastIndexOf("[", name.length() - 1);
+                if (pos == -1) {
+                    throw new RuntimeException("Invalid attribute key: " + name + " (']' not allowed here)");
+                }
+                String idx = name.substring(pos + 1, name.length() - 1);
+                try {
+                    index = Integer.parseInt(idx);
+                } catch (Exception e) {
+                    throw new RuntimeException("Invalid attribute key: " + name + " (Invalid array index: '[" + idx + "]')");
+                }
+                this.name = name.substring(0, pos);
+            } else {
+                this.name = name;
+            }
+        }
+
+        public boolean isArray() {
+            return index >= 0;
+        }
+
+        public int getIndex() {
+            return index;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String toString() {
+            return name + (index != -1 ? "[" + index + "]" : "");
+        }
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/AttributeOperation.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/AttributeOperation.java
new file mode 100644
index 0000000..cfc3a87
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/AttributeOperation.java
@@ -0,0 +1,42 @@
+package org.keycloak.client.registration.cli.common;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AttributeOperation {
+
+    private Type type;
+    private AttributeKey key;
+    private String value;
+
+    public AttributeOperation(Type type, String key) {
+        this(type, key, null);
+    }
+
+    public AttributeOperation(Type type, String key, String value) {
+        if (type == Type.DELETE && value != null) {
+            throw new IllegalArgumentException("When type is DELETE, value has to be null");
+        }
+        this.type = type;
+        this.key = new AttributeKey(key);
+        this.value = value;
+    }
+
+    public Type getType() {
+        return type;
+    }
+
+    public AttributeKey getKey() {
+        return key;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+
+    public enum Type {
+        SET,
+        DELETE
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/CmdStdinContext.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/CmdStdinContext.java
new file mode 100644
index 0000000..b3b31a9
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/CmdStdinContext.java
@@ -0,0 +1,75 @@
+/*
+ * 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.client.registration.cli.common;
+
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class CmdStdinContext {
+
+    private EndpointType regType;
+    private ClientRepresentation client;
+    private OIDCClientRepresentation oidcClient;
+    private String content;
+
+    public CmdStdinContext() {}
+
+    public EndpointType getEndpointType() {
+        return regType;
+    }
+
+    public void setEndpointType(EndpointType regType) {
+        this.regType = regType;
+    }
+
+    public ClientRepresentation getClient() {
+        return client;
+    }
+
+    public void setClient(ClientRepresentation client) {
+        this.client = client;
+    }
+
+    public OIDCClientRepresentation getOidcClient() {
+        return oidcClient;
+    }
+
+    public void setOidcClient(OIDCClientRepresentation oidcClient) {
+        this.oidcClient = oidcClient;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public String getRegistrationAccessToken() {
+        if (client != null) {
+            return client.getRegistrationAccessToken();
+        } else if (oidcClient != null) {
+            return oidcClient.getRegistrationAccessToken();
+        }
+        return null;
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/EndpointType.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/EndpointType.java
new file mode 100644
index 0000000..5114ada
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/EndpointType.java
@@ -0,0 +1,63 @@
+/*
+ * 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.client.registration.cli.common;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public enum EndpointType {
+    DEFAULT("default", "default"),
+    OIDC("openid-connect", "oidc", "oidc"),
+    INSTALL("install", "install", "adapter"),
+    SAML2("saml2-entity-descriptor", "saml2", "saml2");
+
+    private String endpoint;
+    private String preferredName;
+    private Set<String> alternativeNames;
+
+    private EndpointType(String endpoint, String preferredName, String ... alternativeNames) {
+        this.endpoint = endpoint;
+        this.preferredName = preferredName;
+        this.alternativeNames = new HashSet(Arrays.asList(alternativeNames));
+    }
+
+    public static EndpointType of(String name) {
+        if (DEFAULT.endpoint.equals(name) || DEFAULT.alternativeNames.contains(name)) {
+            return DEFAULT;
+        } else if (OIDC.endpoint.equals(name) || OIDC.alternativeNames.contains(name)) {
+            return OIDC;
+        } else if (INSTALL.endpoint.equals(name) || INSTALL.alternativeNames.contains(name)) {
+            return INSTALL;
+        } else if (SAML2.endpoint.equals(name) || SAML2.alternativeNames.contains(name)) {
+            return SAML2;
+        }
+        throw new IllegalArgumentException("Endpoint not supported: " + name);
+    }
+
+    public String getEndpoint() {
+        return endpoint;
+    }
+
+    public String getName() {
+        return preferredName;
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/ParsingContext.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/ParsingContext.java
new file mode 100644
index 0000000..f5fbb78
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/common/ParsingContext.java
@@ -0,0 +1,113 @@
+package org.keycloak.client.registration.cli.common;
+
+
+/**
+ * An iterator wrapping command line
+ *
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ParsingContext {
+
+    private int offset;
+    private int pos = -1;
+    private String [] args;
+
+    public ParsingContext(String [] args) {
+        this(args, 0, -1);
+    }
+
+    public ParsingContext(String [] args, int offset) {
+        this(args, offset, -1);
+    }
+
+    public ParsingContext(String [] args, int offset, int pos) {
+        this.args = args.clone();
+        this.offset = offset;
+        this.pos = pos;
+    }
+
+    public boolean hasNext() {
+        return pos < args.length-1;
+    }
+
+
+    public boolean hasNext(int count) {
+        return pos < args.length - count;
+    }
+
+    public boolean hasPrevious() {
+        return pos > 0;
+    }
+
+    /**
+     * Get next argument
+     *
+     * @return Next argument or null if beyond the end of arguments
+     */
+    public String next() {
+        if (hasNext()) {
+            return args[++pos];
+        } else {
+            pos = args.length;
+            return null;
+        }
+    }
+
+    /**
+     * Check that a next argument is available
+     *
+     * @return Next argument or RuntimeException if next argument is not available
+     */
+    public String nextRequired() {
+        if (!hasNext()) {
+            throw new RuntimeException("Option " + current() + " requires a value");
+        }
+        return next();
+    }
+
+    /**
+     * Get next n-th argument
+     *
+     * @return Next n-th argument or null if beyond the end of arguments
+     */
+    public String next(int n) {
+        if (hasNext(n)) {
+            pos += n;
+            return args[pos];
+        } else {
+            pos = args.length;
+            return null;
+        }
+    }
+
+    /**
+     * Get previous argument
+     *
+     * @return Previous argument or null if previous call was at the beginning of the arguments (pos == 0)
+     */
+    public String previous() {
+        if (hasPrevious()) {
+            return args[--pos];
+        } else {
+            pos = -1;
+            return null;
+        }
+    }
+
+    /**
+     * Get current argument
+     *
+     * @return Current argument or null if current parsing position is beyond end, or before start
+     */
+    public String current() {
+        if (pos < 0 || pos >= args.length) {
+            return null;
+        } else {
+            return args[pos];
+        }
+    }
+
+    public String [] getArgs() {
+        return args;
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/ConfigData.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/ConfigData.java
new file mode 100644
index 0000000..0ae56e0
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/ConfigData.java
@@ -0,0 +1,177 @@
+/*
+ * 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.client.registration.cli.config;
+
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ConfigData {
+
+    private String serverUrl;
+
+    private String realm;
+
+    private String truststore;
+
+    private String trustpass;
+
+    private Map<String, Map<String, RealmConfigData>> endpoints = new HashMap<>();
+
+
+    public String getServerUrl() {
+        return serverUrl;
+    }
+
+    public void setServerUrl(String serverUrl) {
+        this.serverUrl = serverUrl;
+    }
+
+    public String getRealm() {
+        return realm;
+    }
+
+    public void setRealm(String realm) {
+        this.realm = realm;
+    }
+
+    public String getTruststore() {
+        return truststore;
+    }
+
+    public void setTruststore(String truststore) {
+        this.truststore = truststore;
+    }
+
+    public String getTrustpass() {
+        return trustpass;
+    }
+
+    public void setTrustpass(String trustpass) {
+        this.trustpass = trustpass;
+    }
+
+    public Map<String, Map<String, RealmConfigData>> getEndpoints() {
+        return endpoints;
+    }
+
+    public void setEndpoints(Map<String, Map<String, RealmConfigData>> endpoints) {
+        for (Map.Entry<String, Map<String, RealmConfigData>> entry: endpoints.entrySet()) {
+            String endpoint = entry.getKey();
+            for (Map.Entry<String, RealmConfigData> sub: entry.getValue().entrySet()) {
+                RealmConfigData rdata = sub.getValue();
+                rdata.serverUrl(endpoint);
+                rdata.realm(sub.getKey());
+            }
+        }
+        this.endpoints = endpoints;
+    }
+
+    public RealmConfigData sessionRealmConfigData() {
+        if (serverUrl == null)
+            throw new RuntimeException("Illegal state - no current endpoint in config data");
+        if (realm == null)
+            throw new RuntimeException("Illegal state - no current realm in config data");
+        return ensureRealmConfigData(serverUrl, realm);
+    }
+
+    public RealmConfigData getRealmConfigData(String endpoint, String realm) {
+        Map<String, RealmConfigData> realmData = endpoints.get(endpoint);
+        if (realmData == null) {
+            return null;
+        }
+        return realmData.get(realm);
+    }
+
+    public RealmConfigData ensureRealmConfigData(String endpoint, String realm) {
+        RealmConfigData result = getRealmConfigData(endpoint, realm);
+        if (result == null) {
+            result = new RealmConfigData();
+            result.serverUrl(endpoint);
+            result.realm(realm);
+            setRealmConfigData(result);
+        }
+        return result;
+    }
+
+
+    public void setRealmConfigData(RealmConfigData data) {
+        Map<String, RealmConfigData> realm = endpoints.get(data.serverUrl());
+        if (realm == null) {
+            realm = new HashMap<>();
+            endpoints.put(data.serverUrl(), realm);
+        }
+        realm.put(data.realm(), data);
+    }
+
+    public void merge(ConfigData source) {
+        serverUrl = source.serverUrl;
+        realm = source.realm;
+        truststore = source.truststore;
+        trustpass = source.trustpass;
+
+        RealmConfigData current = getRealmConfigData(serverUrl, realm);
+        RealmConfigData sourceRealm = source.getRealmConfigData(serverUrl, realm);
+
+        if (current == null) {
+            setRealmConfigData(sourceRealm);
+        } else {
+            current.merge(sourceRealm);
+        }
+    }
+
+    public ConfigData deepcopy() {
+        ConfigData data = new ConfigData();
+        data.serverUrl = serverUrl;
+        data.realm = realm;
+        data.truststore = truststore;
+        data.trustpass = trustpass;
+        data.endpoints = new HashMap<>();
+
+        for (Map.Entry<String, Map<String, RealmConfigData>> item: endpoints.entrySet()) {
+
+            Map<String, RealmConfigData> nuitems = new HashMap<>();
+            Map<String, RealmConfigData> curitems = item.getValue();
+
+            if (curitems != null) {
+                for (Map.Entry<String, RealmConfigData> ditem : curitems.entrySet()) {
+                    RealmConfigData nudata = ditem.getValue();
+                    if (nudata != null) {
+                        nuitems.put(ditem.getKey(), nudata.deepcopy());
+                    }
+                }
+                data.endpoints.put(item.getKey(), nuitems);
+            }
+        }
+        return data;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return JsonSerialization.writeValueAsPrettyString(this);
+        } catch (IOException e) {
+            return super.toString() + " - Error: " + e.toString();
+        }
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/ConfigHandler.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/ConfigHandler.java
new file mode 100644
index 0000000..2b33c04
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/ConfigHandler.java
@@ -0,0 +1,12 @@
+package org.keycloak.client.registration.cli.config;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public interface ConfigHandler {
+
+    void saveMergeConfig(ConfigUpdateOperation op);
+
+    ConfigData loadConfig();
+
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/ConfigUpdateOperation.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/ConfigUpdateOperation.java
new file mode 100644
index 0000000..98b0c85
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/ConfigUpdateOperation.java
@@ -0,0 +1,10 @@
+package org.keycloak.client.registration.cli.config;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public interface  ConfigUpdateOperation {
+
+    void update(ConfigData data);
+
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/FileConfigHandler.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/FileConfigHandler.java
new file mode 100644
index 0000000..46e7a00
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/FileConfigHandler.java
@@ -0,0 +1,119 @@
+package org.keycloak.client.registration.cli.config;
+
+import org.keycloak.client.registration.cli.util.IoUtil;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.keycloak.client.registration.cli.util.IoUtil.printErr;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class FileConfigHandler implements ConfigHandler {
+
+    private static final long MAX_SIZE = 10 * 1024 * 1024;
+    private static String configFile;
+
+    public static void setConfigFile(String filename) {
+        configFile = filename;
+    }
+
+    public static String getConfigFile() {
+        return configFile;
+    }
+
+    public ConfigData loadConfig() {
+        // for now just dumb impl ignoring file locks for read
+        File file = new File(configFile);
+        if (!file.isFile() || file.length() == 0) {
+            return new ConfigData();
+        }
+
+        try {
+            try (FileInputStream is = new FileInputStream(configFile)) {
+                return JsonSerialization.readValue(is, ConfigData.class);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to load " + configFile, e);
+        }
+    }
+
+    public static void ensureFile() {
+        Path path = null;
+        try {
+            path = Paths.get(new File(configFile).getAbsolutePath());
+            IoUtil.ensureFile(path);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create config file: " + path, e);
+        }
+    }
+
+    public void saveMergeConfig(ConfigUpdateOperation op) {
+        try {
+            ensureFile();
+
+            try (RandomAccessFile file = new RandomAccessFile(new File(configFile), "rw")) {
+                FileChannel fileChannel = file.getChannel();
+
+                FileLock fileLock = null;
+
+                // lock file for write
+                int tryCount = 0;
+                do try {
+                    fileLock = fileChannel.tryLock();
+                    break;
+                } catch (OverlappingFileLockException e) {
+                    // sleep a little, and try again
+                    try {
+                        Thread.sleep(100);
+                        continue;
+                    } catch (InterruptedException e1) {
+                        throw new RuntimeException("Interrupted");
+                    }
+                } while (tryCount++ < 10);
+
+                if (fileLock != null) {
+                    try {
+                        // load config from file
+                        ConfigData config = new ConfigData();
+                        long size = file.length();
+                        if (size > MAX_SIZE) {
+                            printErr("Config file " + configFile + " is too big. It will be overwritten.");
+                            file.setLength(0);
+                        } else if (size > 0){
+                            byte[] buf = new byte[(int) size];
+                            file.readFully(buf);
+                            config = JsonSerialization.readValue(new ByteArrayInputStream(buf), ConfigData.class);
+                        }
+
+                        // update loaded config
+                        op.update(config);
+
+                        // save config to file
+                        byte [] content = JsonSerialization.writeValueAsPrettyString(config).getBytes("utf-8");
+                        file.seek(0);
+                        file.write(content);
+                        file.setLength(content.length);
+
+                    } finally {
+                        fileLock.release();
+                    }
+                } else {
+                    throw new RuntimeException("Failed to get lock on " + configFile);
+                }
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to save " + configFile, e);
+        }
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/InMemoryConfigHandler.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/InMemoryConfigHandler.java
new file mode 100644
index 0000000..1d572b2
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/InMemoryConfigHandler.java
@@ -0,0 +1,24 @@
+package org.keycloak.client.registration.cli.config;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class InMemoryConfigHandler implements ConfigHandler {
+
+    private ConfigData cached;
+
+    @Override
+    public void saveMergeConfig(ConfigUpdateOperation config) {
+        config.update(cached);
+    }
+
+    @Override
+    public ConfigData loadConfig() {
+        return cached;
+    }
+
+    public void setConfigData(ConfigData data) {
+        this.cached = data;
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/RealmConfigData.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/RealmConfigData.java
new file mode 100644
index 0000000..58b34fa
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/config/RealmConfigData.java
@@ -0,0 +1,220 @@
+/*
+ * 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.client.registration.cli.config;
+
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class RealmConfigData {
+
+    private String serverUrl;
+
+    private String realm;
+
+    private String clientId;
+
+    private String token;
+
+    private String refreshToken;
+
+    private String signingToken;
+
+    private String secret;
+
+    private Long expiresAt;
+
+    private Long refreshExpiresAt;
+
+    private Long sigExpiresAt;
+
+    private String initialToken;
+
+    private Map<String, String> clients = new LinkedHashMap<String, String>();
+
+
+    public String serverUrl() {
+        return serverUrl;
+    }
+
+    public void serverUrl(String serverUrl) {
+        this.serverUrl = serverUrl;
+    }
+
+    public String realm() {
+        return realm;
+    }
+
+    public void realm(String realm) {
+        this.realm = realm;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(String clientId) {
+        this.clientId = clientId;
+    }
+
+    public String getToken() {
+        return token;
+    }
+
+    public void setToken(String token) {
+        this.token = token;
+    }
+
+    public String getRefreshToken() {
+        return refreshToken;
+    }
+
+    public void setRefreshToken(String refreshToken) {
+        this.refreshToken = refreshToken;
+    }
+
+    public String getSigningToken() {
+        return signingToken;
+    }
+
+    public void setSigningToken(String signingToken) {
+        this.signingToken = signingToken;
+    }
+
+    public String getSecret() {
+        return secret;
+    }
+
+    public void setSecret(String secret) {
+        this.secret = secret;
+    }
+
+    public Long getExpiresAt() {
+        return expiresAt;
+    }
+
+    public void setExpiresAt(Long expiresAt) {
+        this.expiresAt = expiresAt;
+    }
+
+    public Long getRefreshExpiresAt() {
+        return refreshExpiresAt;
+    }
+
+    public void setRefreshExpiresAt(Long refreshExpiresAt) {
+        this.refreshExpiresAt = refreshExpiresAt;
+    }
+
+    public Long getSigExpiresAt() {
+        return sigExpiresAt;
+    }
+
+    public void setSigExpiresAt(Long sigExpiresAt) {
+        this.sigExpiresAt = sigExpiresAt;
+    }
+
+    public String getInitialToken() {
+        return initialToken;
+    }
+
+    public void setInitialToken(String initialToken) {
+        this.initialToken = initialToken;
+    }
+
+    public Map<String, String> getClients() {
+        return clients;
+    }
+
+    public void merge(RealmConfigData source) {
+        serverUrl = source.serverUrl;
+        realm = source.realm;
+        clientId = source.clientId;
+        token = source.token;
+        refreshToken = source.refreshToken;
+        signingToken = source.signingToken;
+        secret = source.secret;
+        expiresAt = source.expiresAt;
+        refreshExpiresAt = source.refreshExpiresAt;
+        sigExpiresAt = source.sigExpiresAt;
+        initialToken = source.initialToken;
+
+        mergeClients(source);
+    }
+
+    private void mergeClients(RealmConfigData source) {
+        if (source.clients != null) {
+            if (clients == null) {
+                clients = source.clients;
+            } else {
+                for (String key: source.clients.keySet()) {
+                    String val = source.clients.get(key);
+                    if (!"".equals(val)) {
+                        clients.put(key, val);
+                    } else {
+                        clients.remove(key);
+                    }
+                }
+            }
+        }
+    }
+
+    public void mergeRefreshTokens(RealmConfigData source) {
+        token = source.token;
+        refreshToken = source.refreshToken;
+        expiresAt = source.expiresAt;
+        refreshExpiresAt = source.refreshExpiresAt;
+
+        mergeClients(source);
+    }
+
+    public void mergeRegistrationTokens(RealmConfigData source) {
+        initialToken = source.initialToken;
+        mergeClients(source);
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return JsonSerialization.writeValueAsPrettyString(this);
+        } catch (IOException e) {
+            return super.toString() + " - Error: " + e.toString();
+        }
+    }
+
+    public RealmConfigData deepcopy() {
+        RealmConfigData data = new RealmConfigData();
+        data.serverUrl = serverUrl;
+        data.realm = realm;
+        data.clientId = clientId;
+        data.token = token;
+        data.refreshToken = refreshToken;
+        data.signingToken = signingToken;
+        data.secret = secret;
+        data.expiresAt = expiresAt;
+        data.refreshExpiresAt = refreshExpiresAt;
+        data.sigExpiresAt = sigExpiresAt;
+        data.initialToken = initialToken;
+        data.clients = new LinkedHashMap<>(clients);
+        return data;
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/KcRegMain.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/KcRegMain.java
new file mode 100644
index 0000000..57fe0f6
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/KcRegMain.java
@@ -0,0 +1,75 @@
+package org.keycloak.client.registration.cli;
+
+import org.jboss.aesh.console.AeshConsoleBuilder;
+import org.jboss.aesh.console.AeshConsoleImpl;
+import org.jboss.aesh.console.Prompt;
+import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder;
+import org.jboss.aesh.console.command.registry.CommandRegistry;
+import org.jboss.aesh.console.settings.Settings;
+import org.jboss.aesh.console.settings.SettingsBuilder;
+import org.keycloak.client.registration.cli.aesh.AeshEnhancer;
+import org.keycloak.client.registration.cli.aesh.ValveInputStream;
+import org.keycloak.client.registration.cli.aesh.Globals;
+import org.keycloak.client.registration.cli.commands.KcRegCmd;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcRegMain {
+
+    public static void main(String [] args) {
+
+        Globals.stdin = new ValveInputStream();
+
+        Settings settings = new SettingsBuilder()
+                .logging(false)
+                .readInputrc(false)
+                .disableCompletion(true)
+                .disableHistory(true)
+                .enableAlias(false)
+                .enableExport(false)
+                .inputStream(Globals.stdin)
+                .create();
+
+        CommandRegistry registry = new AeshCommandRegistryBuilder()
+                .command(KcRegCmd.class)
+                .create();
+
+        AeshConsoleImpl console = (AeshConsoleImpl) new AeshConsoleBuilder()
+                .settings(settings)
+                .commandRegistry(registry)
+                .prompt(new Prompt(""))
+                .create();
+
+        AeshEnhancer.enhance(console);
+
+        // work around parser issues with quotes and brackets
+        ArrayList<String> arguments = new ArrayList<>();
+        arguments.add("kcreg");
+        arguments.addAll(Arrays.asList(args));
+        Globals.args = arguments;
+
+        StringBuilder b = new StringBuilder();
+        for (String s : args) {
+            // quote if necessary
+            boolean needQuote = false;
+            needQuote = s.indexOf(' ') != -1 || s.indexOf('\"') != -1 || s.indexOf('\'') != -1;
+            b.append(' ');
+            if (needQuote) {
+                b.append('\'');
+            }
+            b.append(s);
+            if (needQuote) {
+                b.append('\'');
+            }
+        }
+        console.setEcho(false);
+
+        console.execute("kcreg" + b.toString());
+
+        console.start();
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/AttributeException.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/AttributeException.java
new file mode 100644
index 0000000..30f3caa
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/AttributeException.java
@@ -0,0 +1,23 @@
+package org.keycloak.client.registration.cli.util;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AttributeException extends RuntimeException {
+
+    private final String attrName;
+
+    public AttributeException(String attrName, String message) {
+        super(message);
+        this.attrName = attrName;
+    }
+
+    public AttributeException(String attrName, String message, Throwable th) {
+        super(message, th);
+        this.attrName = attrName;
+    }
+
+    public String getAttributeName() {
+        return attrName;
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/AuthUtil.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/AuthUtil.java
new file mode 100644
index 0000000..8752e60
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/AuthUtil.java
@@ -0,0 +1,206 @@
+/*
+ * 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.client.registration.cli.util;
+
+import org.keycloak.client.registration.cli.config.ConfigData;
+import org.keycloak.client.registration.cli.config.RealmConfigData;
+import org.keycloak.common.util.KeystoreUtil;
+import org.keycloak.common.util.Time;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.util.BasicAuthHelper;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.util.UUID;
+
+import static java.lang.System.currentTimeMillis;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.checkAuthInfo;
+import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
+import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_FORM_URL_ENCODED;
+import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_JSON;
+import static org.keycloak.client.registration.cli.util.HttpUtil.doPost;
+import static org.keycloak.client.registration.cli.util.HttpUtil.urlencode;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AuthUtil {
+
+    public static String ensureToken(ConfigData config) {
+
+        checkAuthInfo(config);
+
+        RealmConfigData realmConfig = config.sessionRealmConfigData();
+
+        long now = currentTimeMillis();
+
+        // check expires of access_token against time
+        // if it's less than 5s to expiry, renew it
+        if (realmConfig.getExpiresAt() - now < 5000) {
+
+            // check refresh_token against expiry time
+            // if it's less than 5s to expiry, fail with credentials expired
+            if (realmConfig.getRefreshExpiresAt() - now < 5000) {
+                throw new RuntimeException("Session has expired. Login again with '" + OsUtil.CMD + " config credentials'");
+            }
+
+            if (realmConfig.getSigExpiresAt() != null && realmConfig.getSigExpiresAt() - now < 5000) {
+                throw new RuntimeException("Session has expired. Login again with '" + OsUtil.CMD + " config credentials'");
+            }
+
+            try {
+                String authorization = null;
+
+                StringBuilder body = new StringBuilder("grant_type=refresh_token")
+                        .append("&refresh_token=").append(realmConfig.getRefreshToken())
+                        .append("&client_id=").append(urlencode(realmConfig.getClientId()));
+
+                if (realmConfig.getSigningToken() != null) {
+                    body.append("&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
+                            .append("&client_assertion=").append(realmConfig.getSigningToken());
+                } else if (realmConfig.getSecret() != null) {
+                    authorization = BasicAuthHelper.createHeader(realmConfig.getClientId(), realmConfig.getSecret());
+                }
+
+                InputStream result = doPost(realmConfig.serverUrl() + "/realms/" + realmConfig.realm() + "/protocol/openid-connect/token",
+                        APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), authorization);
+
+                AccessTokenResponse token = JsonSerialization.readValue(result, AccessTokenResponse.class);
+
+                saveMergeConfig(cfg -> {
+                    RealmConfigData realmData = cfg.sessionRealmConfigData();
+                    realmData.setToken(token.getToken());
+                    realmData.setRefreshToken(token.getRefreshToken());
+                    realmData.setExpiresAt(currentTimeMillis() + token.getExpiresIn() * 1000);
+                    realmData.setRefreshExpiresAt(currentTimeMillis() + token.getRefreshExpiresIn() * 1000);
+                });
+                return token.getToken();
+
+            } catch (UnsupportedEncodingException e) {
+                throw new RuntimeException("Unexpected error", e);
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to read Refresh Token response", e);
+            }
+        }
+
+        return realmConfig.getToken();
+    }
+
+    public static AccessTokenResponse getAuthTokens(String server, String realm, String user, String password, String clientId) {
+        StringBuilder body = new StringBuilder();
+        try {
+            body.append("grant_type=password")
+                    .append("&username=").append(urlencode(user))
+                    .append("&password=").append(urlencode(password))
+                    .append("&client_id=").append(urlencode(clientId));
+
+            InputStream result = doPost(server + "/realms/" + realm + "/protocol/openid-connect/token",
+                    APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), null);
+            return JsonSerialization.readValue(result, AccessTokenResponse.class);
+
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Unexpected error: ", e);
+        } catch (IOException e) {
+            throw new RuntimeException("Error receiving response: ", e);
+        }
+    }
+
+    public static AccessTokenResponse getAuthTokensByJWT(String server, String realm, String user, String password, String clientId, String signedRequestToken) {
+        StringBuilder body = new StringBuilder();
+        try {
+            body.append("client_id=").append(urlencode(clientId))
+                    .append("&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
+                    .append("&client_assertion=").append(signedRequestToken);
+
+            if (user != null) {
+                if (password == null) {
+                    throw new RuntimeException("No password specified");
+                }
+                body.append("&grant_type=password")
+                        .append("&username=").append(urlencode(user))
+                        .append("&password=").append(urlencode(password));
+            } else {
+                body.append("&grant_type=client_credentials");
+            }
+
+            InputStream result = doPost(server + "/realms/" + realm + "/protocol/openid-connect/token",
+                    APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), null);
+            return JsonSerialization.readValue(result, AccessTokenResponse.class);
+
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Unexpected error: ", e);
+        } catch (IOException e) {
+            throw new RuntimeException("Error receiving response: ", e);
+        }
+    }
+
+    public static AccessTokenResponse getAuthTokensBySecret(String server, String realm, String user, String password, String clientId, String secret) {
+
+        StringBuilder body = new StringBuilder();
+        try {
+            if (user != null) {
+                if (password == null) {
+                    throw new RuntimeException("No password specified");
+                }
+
+                body.append("client_id=").append(urlencode(clientId))
+                        .append("&grant_type=password")
+                        .append("&username=").append(urlencode(user))
+                        .append("&password=").append(urlencode(password));
+            } else {
+                body.append("grant_type=client_credentials");
+            }
+
+            InputStream result = doPost(server + "/realms/" + realm + "/protocol/openid-connect/token",
+                    APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), BasicAuthHelper.createHeader(clientId, secret));
+            return JsonSerialization.readValue(result, AccessTokenResponse.class);
+
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Unexpected error: ", e);
+        } catch (IOException e) {
+            throw new RuntimeException("Error receiving response: ", e);
+        }
+    }
+
+    public static String getSignedRequestToken(String keystore, String storePass, String keyPass, String alias, int sigLifetime, String clientId, String realmInfoUrl) {
+
+        KeyPair keypair = KeystoreUtil.loadKeyPairFromKeystore(keystore, storePass, keyPass, alias, KeystoreUtil.KeystoreFormat.JKS);
+
+        JsonWebToken reqToken = new JsonWebToken();
+        reqToken.id(UUID.randomUUID().toString());
+        reqToken.issuer(clientId);
+        reqToken.subject(clientId);
+        reqToken.audience(realmInfoUrl);
+
+        int now = Time.currentTime();
+        reqToken.issuedAt(now);
+        reqToken.expiration(now + sigLifetime);
+        reqToken.notBefore(now);
+
+        String signedRequestToken = new JWSBuilder()
+                .jsonContent(reqToken)
+                .rsa256(keypair.getPrivate());
+        return signedRequestToken;
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/ConfigUtil.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/ConfigUtil.java
new file mode 100644
index 0000000..96996a2
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/ConfigUtil.java
@@ -0,0 +1,114 @@
+/*
+ * 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.client.registration.cli.util;
+
+import org.keycloak.client.registration.cli.config.ConfigData;
+import org.keycloak.client.registration.cli.config.ConfigHandler;
+import org.keycloak.client.registration.cli.config.ConfigUpdateOperation;
+import org.keycloak.client.registration.cli.config.InMemoryConfigHandler;
+import org.keycloak.client.registration.cli.config.RealmConfigData;
+import org.keycloak.representations.AccessTokenResponse;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ConfigUtil {
+
+    public static final String DEFAULT_CONFIG_FILE_STRING = OsUtil.OS_ARCH.isWindows() ? "%HOMEDRIVE%%HOMEPATH%\\.keycloak\\kcreg.config" : "~/.keycloak/kcreg.config";
+
+    public static final String DEFAULT_CONFIG_FILE_PATH = System.getProperty("user.home") + "/.keycloak/kcreg.config";
+
+    private static ConfigHandler handler;
+
+    public static ConfigHandler getHandler() {
+        return handler;
+    }
+
+    public static void setHandler(ConfigHandler handler) {
+        ConfigUtil.handler = handler;
+    }
+
+    public static String getRegistrationToken(RealmConfigData data, String clientId) {
+        String token = data.getClients().get(clientId);
+        return token == null || token.length() == 0 ? null : token;
+    }
+
+    public static void setRegistrationToken(RealmConfigData data, String clientId, String token) {
+        data.getClients().put(clientId, token == null ? "" : token);
+    }
+
+    public static void saveTokens(AccessTokenResponse tokens, String endpoint, String realm, String clientId, String signKey, Long sigExpiresAt, String secret) {
+        handler.saveMergeConfig(config -> {
+            config.setServerUrl(endpoint);
+            config.setRealm(realm);
+
+            RealmConfigData realmConfig = config.ensureRealmConfigData(endpoint, realm);
+            realmConfig.setToken(tokens.getToken());
+            realmConfig.setRefreshToken(tokens.getRefreshToken());
+            realmConfig.setSigningToken(signKey);
+            realmConfig.setSecret(secret);
+            realmConfig.setExpiresAt(System.currentTimeMillis() + tokens.getExpiresIn() * 1000);
+            realmConfig.setRefreshExpiresAt(tokens.getRefreshExpiresIn() == 0 ?
+                    Long.MAX_VALUE : System.currentTimeMillis() + tokens.getRefreshExpiresIn() * 1000);
+            realmConfig.setSigExpiresAt(sigExpiresAt);
+            realmConfig.setClientId(clientId);
+        });
+    }
+
+    public static void checkServerInfo(ConfigData config) {
+        if (config.getServerUrl() == null || config.getRealm() == null) {
+            throw new RuntimeException("No server or realm specified. Use --server, --realm, or '" + OsUtil.CMD + " config credentials'.");
+        }
+    }
+
+    public static void checkAuthInfo(ConfigData config) {
+        checkServerInfo(config);
+    }
+
+    public static boolean credentialsAvailable(ConfigData config) {
+        return config.getServerUrl() != null && config.getRealm() != null
+                && config.sessionRealmConfigData() != null && config.sessionRealmConfigData().getRefreshToken() != null;
+    }
+
+    public static ConfigData loadConfig() {
+        if (handler == null) {
+            throw new RuntimeException("No ConfigHandler set");
+        }
+
+        return handler.loadConfig();
+    }
+
+    public static void saveMergeConfig(ConfigUpdateOperation op) {
+        if (handler == null) {
+            throw new RuntimeException("No ConfigHandler set");
+        }
+
+        handler.saveMergeConfig(op);
+    }
+
+    public static void setupInMemoryHandler(ConfigData config) {
+        InMemoryConfigHandler memhandler = null;
+        if (handler instanceof InMemoryConfigHandler) {
+            memhandler = (InMemoryConfigHandler) handler;
+        } else {
+            memhandler = new InMemoryConfigHandler();
+            handler = memhandler;
+        }
+        memhandler.setConfigData(config);
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/DebugBufferedInputStream.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/DebugBufferedInputStream.java
new file mode 100644
index 0000000..fa200d6
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/DebugBufferedInputStream.java
@@ -0,0 +1,78 @@
+package org.keycloak.client.registration.cli.util;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class DebugBufferedInputStream extends BufferedInputStream {
+
+    public DebugBufferedInputStream(InputStream in) {
+        super(in);
+    }
+
+    @Override
+    public synchronized int read() throws IOException {
+        log("read() >>>");
+        int b = super.read();
+        log("read() <<< " + (char) b + " (" + b + ")");
+        return b;
+    }
+
+    @Override
+    public synchronized int read(byte[] b, int off, int len) throws IOException {
+        log("read(buf, off, len) >>>");
+        int c = super.read(b, off, len);
+        log("read(buf, off, len) <<< " + (c != -1 ? "[" + new String(b, off, c) + "]" : "-1"));
+        return c;
+    }
+
+    @Override
+    public synchronized long skip(long n) throws IOException {
+        log("skip()");
+        return super.skip(n);
+    }
+
+    @Override
+    public synchronized int available() throws IOException {
+        log("available() >>>");
+        int c = super.available();
+        log("available() >>> " + c);
+        return c;
+    }
+
+    @Override
+    public synchronized void mark(int readlimit) {
+        log("mark()");
+        super.mark(readlimit);
+    }
+
+    @Override
+    public synchronized void reset() throws IOException {
+        log("reset()");
+        super.reset();
+    }
+
+    @Override
+    public boolean markSupported() {
+        log("markSupported()");
+        return super.markSupported();
+    }
+
+    @Override
+    public void close() throws IOException {
+        log("close()");
+        super.close();
+    }
+
+    @Override
+    public int read(byte[] b) throws IOException {
+        return read(b, 0, b.length);
+    }
+
+    private void log(String msg) {
+        System.err.println(msg);
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/HttpUtil.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/HttpUtil.java
new file mode 100644
index 0000000..ec6e9bd
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/HttpUtil.java
@@ -0,0 +1,188 @@
+/*
+ * 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.client.registration.cli.util;
+
+import org.apache.http.Header;
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.ssl.SSLContexts;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.keycloak.client.registration.cli.common.EndpointType;
+import org.keycloak.util.JsonSerialization;
+
+import javax.net.ssl.SSLContext;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.security.KeyManagementException;
+
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class HttpUtil {
+
+    public static final String APPLICATION_XML = "application/xml";
+    public static final String APPLICATION_JSON = "application/json";
+    public static final String APPLICATION_FORM_URL_ENCODED = "application/x-www-form-urlencoded";
+    public static final String UTF_8 = "utf-8";
+
+    private static HttpClient httpClient;
+    private static SSLConnectionSocketFactory sslsf;
+
+    public static InputStream doGet(String url, String acceptType, String authorization) {
+        try {
+            HttpGet request = new HttpGet(url);
+            request.setHeader(HttpHeaders.ACCEPT, acceptType);
+            return doRequest(authorization, request);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
+        }
+    }
+
+    public static InputStream doPost(String url, String contentType, String acceptType, String content, String authorization) {
+        try {
+            return doPostOrPut(contentType, acceptType, content, authorization, new HttpPost(url));
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
+        }
+    }
+
+    public static InputStream doPut(String url, String contentType, String acceptType, String content, String authorization) {
+        try {
+            return doPostOrPut(contentType, acceptType, content, authorization, new HttpPut(url));
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
+        }
+    }
+
+    public static void doDelete(String url, String authorization) {
+        try {
+            HttpDelete request = new HttpDelete(url);
+            doRequest(authorization, request);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
+        }
+    }
+
+    private static InputStream doPostOrPut(String contentType, String acceptType, String content, String authorization, HttpEntityEnclosingRequestBase request) throws IOException {
+        request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
+        request.setHeader(HttpHeaders.ACCEPT, acceptType);
+        if (content != null) {
+            request.setEntity(new StringEntity(content));
+        }
+
+        return doRequest(authorization, request);
+    }
+
+    private static InputStream doRequest(String authorization, HttpRequestBase request) throws IOException {
+        addAuth(request, authorization);
+
+        HttpResponse response = getHttpClient().execute(request);
+        InputStream responseStream = null;
+        if (response.getEntity() != null) {
+            responseStream = response.getEntity().getContent();
+        }
+
+        int code = response.getStatusLine().getStatusCode();
+        if (code >= 200 && code < 300) {
+            return responseStream;
+        } else {
+            Map<String, String> error = null;
+            try {
+                Header header = response.getEntity().getContentType();
+                if (header != null && APPLICATION_JSON.equals(header.getValue())) {
+                    error = JsonSerialization.readValue(responseStream, Map.class);
+                }
+            } catch (Exception e) {
+                throw new RuntimeException("Failed to read error response - " + e.getMessage(), e);
+            } finally {
+                responseStream.close();
+            }
+
+            String message = null;
+            if (error != null) {
+                message = error.get("error_description") + " [" + error.get("error") + "]";
+            }
+            throw new RuntimeException(message != null ? message : response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase());
+        }
+    }
+
+    private static void addAuth(HttpRequestBase request, String authorization) {
+        if (authorization != null) {
+            request.setHeader(HttpHeaders.AUTHORIZATION, authorization);
+        }
+    }
+
+    public static HttpClient getHttpClient() {
+        if (httpClient == null) {
+            if (sslsf != null) {
+                httpClient = HttpClientBuilder.create().useSystemProperties().setSSLSocketFactory(sslsf).build();
+            } else {
+                httpClient = HttpClientBuilder.create().useSystemProperties().build();
+            }
+        }
+        return httpClient;
+    }
+
+    public static String getExpectedContentType(EndpointType type) {
+        switch (type) {
+            case DEFAULT:
+            case OIDC:
+                return APPLICATION_JSON;
+            case SAML2:
+                return APPLICATION_XML;
+            default:
+                throw new RuntimeException("Unsupported endpoint type: " + type);
+        }
+    }
+
+    public static String urlencode(String value) {
+        try {
+            return URLEncoder.encode(value, UTF_8);
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Failed to urlencode", e);
+        }
+    }
+
+    public static void setTruststore(File file, String password) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException {
+        if (!file.isFile()) {
+            throw new RuntimeException("Truststore file not found: " + file.getAbsolutePath());
+        }
+        SSLContext theContext = SSLContexts.custom()
+                .useProtocol("TLS")
+                .loadTrustMaterial(file, password == null ? null : password.toCharArray())
+                .build();
+        sslsf = new SSLConnectionSocketFactory(theContext);
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/IoUtil.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/IoUtil.java
new file mode 100644
index 0000000..7b38505
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/IoUtil.java
@@ -0,0 +1,235 @@
+/*
+ * 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.client.registration.cli.util;
+
+import org.jboss.aesh.console.AeshConsoleBufferBuilder;
+import org.jboss.aesh.console.AeshInputProcessorBuilder;
+import org.jboss.aesh.console.ConsoleBuffer;
+import org.jboss.aesh.console.InputProcessor;
+import org.jboss.aesh.console.Prompt;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.aesh.Globals;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.AclEntry;
+import java.nio.file.attribute.AclEntryPermission;
+import java.nio.file.attribute.AclEntryType;
+import java.nio.file.attribute.AclFileAttributeView;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.UserPrincipal;
+import java.util.Formatter;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+import static java.nio.file.Files.createDirectories;
+import static java.nio.file.Files.createFile;
+import static java.nio.file.Files.isDirectory;
+import static java.nio.file.Files.isRegularFile;
+import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class IoUtil {
+
+    public static String readFileOrStdin(String file) {
+        String content;
+        if ("-".equals(file)) {
+            content = readFully(System.in);
+        } else {
+            try (InputStream is = new FileInputStream(file)) {
+                content = readFully(is);
+            } catch (FileNotFoundException e) {
+                throw new RuntimeException("File not found: " + file);
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to read file: " + file, e);
+            }
+        }
+        return content;
+    }
+
+    public static void waitFor(long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (InterruptedException e) {
+            throw new RuntimeException("Interrupted");
+        }
+    }
+
+    public static String readSecret(String prompt, CommandInvocation invocation) {
+
+        // TODO Windows hack - masking not working on Windows
+        char maskChar = OS_ARCH.isWindows() ? 0 : '*';
+        ConsoleBuffer consoleBuffer = new AeshConsoleBufferBuilder()
+                .shell(invocation.getShell())
+                .prompt(new Prompt(prompt, maskChar))
+                .create();
+        InputProcessor inputProcessor = new AeshInputProcessorBuilder()
+                .consoleBuffer(consoleBuffer)
+                .create();
+
+        consoleBuffer.displayPrompt();
+
+        // activate stdin
+        Globals.stdin.setInputStream(System.in);
+
+        String result;
+        try {
+            do {
+                result = inputProcessor.parseOperation(invocation.getInput());
+            } while (result == null);
+        } catch (Exception e) {
+            throw new RuntimeException("^C", e);
+        }
+        /*
+        if (!Globals.stdin.isStdinAvailable()) {
+            try {
+                return readLine(new InputStreamReader(System.in));
+            } catch (IOException e) {
+                throw new RuntimeException("Standard input not available");
+            }
+        }
+         */
+        // Windows hack - get rid of any \n
+        result = result.replaceAll("\\n", "");
+        return result;
+    }
+
+    public static String readFully(InputStream is) {
+        Charset charset = Charset.forName("utf-8");
+        StringBuilder out = new StringBuilder();
+        byte [] buf = new byte[8192];
+
+        int rc;
+        try {
+            while ((rc = is.read(buf)) != -1) {
+                out.append(new String(buf, 0, rc, charset));
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to read stream", e);
+        }
+        return out.toString();
+    }
+
+    public static void ensureFile(Path path) throws IOException {
+
+        FileSystem fs = FileSystems.getDefault();
+        Set<String> supportedViews = fs.supportedFileAttributeViews();
+        Path parent = path.getParent();
+
+        if (!isDirectory(parent)) {
+            createDirectories(parent);
+            // make sure only owner can read/write it
+            if (supportedViews.contains("posix")) {
+                setUnixPermissions(parent);
+            } else if (supportedViews.contains("acl")) {
+                setWindowsPermissions(parent);
+            } else {
+                warnErr("Failed to restrict access permissions on .keycloak directory: " + parent);
+            }
+        }
+        if (!isRegularFile(path)) {
+            createFile(path);
+            // make sure only owner can read/write it
+            if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
+                setUnixPermissions(path);
+            } else if (supportedViews.contains("acl")) {
+                setWindowsPermissions(path);
+            } else {
+                warnErr("Failed to restrict access permissions on config file: " + path);
+            }
+        }
+    }
+
+    private static void setUnixPermissions(Path path) throws IOException {
+        Set<PosixFilePermission> perms = new HashSet<>();
+        perms.add(PosixFilePermission.OWNER_READ);
+        perms.add(PosixFilePermission.OWNER_WRITE);
+        if (isDirectory(path)) {
+            perms.add(PosixFilePermission.OWNER_EXECUTE);
+        }
+        Files.setPosixFilePermissions(path, perms);
+    }
+
+    private static void setWindowsPermissions(Path path) throws IOException {
+        AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class);
+        UserPrincipal owner = view.getOwner();
+        List<AclEntry> acl = view.getAcl();
+        ListIterator<AclEntry> it = acl.listIterator();
+        while (it.hasNext()) {
+            AclEntry entry = it.next();
+            if ("BUILTIN\\Administrators".equals(entry.principal().getName()) || "NT AUTHORITY\\SYSTEM".equals(entry.principal().getName())) {
+                continue;
+            }
+            it.remove();
+        }
+        AclEntry entry = AclEntry.newBuilder()
+                .setType(AclEntryType.ALLOW)
+                .setPrincipal(owner)
+                .setPermissions(AclEntryPermission.READ_DATA, AclEntryPermission.WRITE_DATA,
+                        AclEntryPermission.APPEND_DATA, AclEntryPermission.READ_NAMED_ATTRS,
+                        AclEntryPermission.WRITE_NAMED_ATTRS, AclEntryPermission.EXECUTE,
+                        AclEntryPermission.READ_ATTRIBUTES, AclEntryPermission.WRITE_ATTRIBUTES,
+                        AclEntryPermission.DELETE, AclEntryPermission.READ_ACL, AclEntryPermission.SYNCHRONIZE)
+                .build();
+        acl.add(entry);
+        view.setAcl(acl);
+    }
+
+    public static void printOut(String msg) {
+        System.out.println(msg);
+    }
+
+    public static void printErr(String msg) {
+        System.err.println(msg);
+    }
+
+    public static void printfOut(String format, String ... params) {
+        System.out.println(new Formatter().format("WARN: " + format, params));
+    }
+
+    public static void warnOut(String msg) {
+        System.out.println("WARN: " + msg);
+    }
+
+    public static void warnErr(String msg) {
+        System.err.println("WARN: " + msg);
+    }
+
+    public static void warnfOut(String format, String ... params) {
+        System.out.println(new Formatter().format("WARN: " + format, params));
+    }
+
+    public static void warnfErr(String format, String ... params) {
+        System.err.println(new Formatter().format("WARN: " + format, params));
+    }
+
+    public static void logOut(String msg) {
+        System.out.println("LOG: " + msg);
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/OsArch.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/OsArch.java
new file mode 100644
index 0000000..823d1fb
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/OsArch.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2014 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Eclipse Public License version 1.0, available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.keycloak.client.registration.cli.util;
+
+/**
+ * @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
+ */
+public class OsArch {
+
+    private String os;
+    private String arch;
+    private boolean legacy;
+
+    public OsArch(String os, String arch) {
+        this(os, arch, false);
+    }
+
+    public OsArch(String os, String arch, boolean legacy) {
+        this.os = os;
+        this.arch = arch;
+        this.legacy = legacy;
+    }
+
+    public String os() {
+        return os;
+    }
+
+    public String arch() {
+        return arch;
+    }
+
+    public boolean isLegacy() {
+        return legacy;
+    }
+
+    public boolean isWindows() {
+        return "win32".equals(os);
+    }
+
+    public String envVar(String var) {
+        if (isWindows()) {
+            return "%" + var + "%";
+        } else {
+            return "$" + var;
+        }
+    }
+
+    public String path(String path) {
+        if (isWindows()) {
+            path = path.replaceAll("/", "\\\\");
+            if (path.startsWith("~")) {
+                path =  "%HOMEPATH%" + path.substring(1);
+            }
+        }
+        return path;
+    }
+}
\ No newline at end of file
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/OsUtil.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/OsUtil.java
new file mode 100644
index 0000000..c0b9974
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/OsUtil.java
@@ -0,0 +1,48 @@
+package org.keycloak.client.registration.cli.util;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class OsUtil {
+
+    public static final OsArch OS_ARCH = determineOSAndArch();
+
+    public static final String CMD = OS_ARCH.isWindows() ? "kcreg.bat" : "kcreg.sh";
+
+    public static final String PROMPT = OS_ARCH.isWindows() ? "c:\\>" : "$";
+
+    public static final String EOL = OS_ARCH.isWindows() ? "\r\n" : "\n";
+
+
+    public static OsArch determineOSAndArch() {
+        String os = System.getProperty("os.name").toLowerCase();
+        String arch = System.getProperty("os.arch");
+
+        if (arch.equals("amd64")) {
+            arch = "x86_64";
+        }
+
+        if (os.startsWith("linux")) {
+            if (arch.equals("x86") || arch.equals("i386") || arch.equals("i586")) {
+                arch = "i686";
+            }
+            return new OsArch("linux", arch);
+        } else if (os.startsWith("windows")) {
+            if (arch.equals("x86")) {
+                arch = "i386";
+            }
+            if (os.indexOf("2008") != -1 || os.indexOf("2003") != -1 || os.indexOf("vista") != -1) {
+                return new OsArch("win32", arch, true);
+            } else {
+                return new OsArch("win32", arch);
+            }
+        } else if (os.startsWith("sunos")) {
+            return new OsArch("sunos5", "x86_64");
+        } else if (os.startsWith("mac os x")) {
+            return new OsArch("osx", "x86_64");
+        }
+
+        // unsupported platform
+        throw new RuntimeException("Could not determine OS and architecture for this operating system: " + os);
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/ParseUtil.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/ParseUtil.java
new file mode 100644
index 0000000..02ed27a
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/ParseUtil.java
@@ -0,0 +1,167 @@
+/*
+ * 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.client.registration.cli.util;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
+import org.keycloak.client.registration.cli.common.AttributeOperation;
+import org.keycloak.client.registration.cli.common.CmdStdinContext;
+import org.keycloak.client.registration.cli.common.EndpointType;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.util.List;
+
+import static java.lang.System.arraycopy;
+import static org.keycloak.client.registration.cli.util.IoUtil.readFileOrStdin;
+import static org.keycloak.client.registration.cli.util.ReflectionUtil.setAttributes;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ParseUtil {
+
+    public static final String CLIENTID_OPTION_WARN = "You're using what looks like an OPTION as CLIENT_ID: %s";
+    public static final String TOKEN_OPTION_WARN = "You're using what looks like an OPTION as TOKEN: %s";
+
+    public static String[] shift(String[] args) {
+        if (args.length == 1)
+            return new String[0];
+        String [] nu = new String [args.length-1];
+        arraycopy(args, 1, nu, 0, args.length-1);
+        return nu;
+    }
+
+    public static String[] parseKeyVal(String keyval) {
+        // we expect = as a separator
+        int pos = keyval.indexOf("=");
+        if (pos <= 0) {
+            throw new RuntimeException("Invalid key=value parameter: [" + keyval + "]");
+        }
+
+        String [] parsed = new String[2];
+        parsed[0] = keyval.substring(0, pos);
+        parsed[1] = keyval.substring(pos+1);
+
+        return parsed;
+    }
+
+    public static CmdStdinContext parseFileOrStdin(String file, EndpointType type) {
+
+        String content = readFileOrStdin(file).trim();
+        ClientRepresentation client = null;
+        OIDCClientRepresentation oidcClient = null;
+
+        if (type == null) {
+            // guess the correct endpoint from content of the file
+            if (content.startsWith("<")) {
+                // looks like XML
+                type = EndpointType.SAML2;
+            } else if (content.startsWith("{")) {
+                // looks like JSON?
+                // try parse as ClientRepresentation
+                try {
+                    client = JsonSerialization.readValue(content, ClientRepresentation.class);
+                    type = EndpointType.DEFAULT;
+
+                } catch (JsonParseException e) {
+                    throw new RuntimeException("Failed to read the input document as JSON: " + e.getMessage(), e);
+                } catch (Exception ignored) {
+                    // deliberately not logged
+                }
+
+                if (client == null) {
+                    // try parse as OIDCClientRepresentation
+                    try {
+                        oidcClient = JsonSerialization.readValue(content, OIDCClientRepresentation.class);
+                        type = EndpointType.OIDC;
+                    } catch (IOException ne) {
+                        throw new RuntimeException("Unable to determine input document type. Use -e TYPE to specify the registration endpoint to use");
+                    } catch (Exception e) {
+                        throw new RuntimeException("Failed to read the input document as JSON", e);
+                    }
+                }
+
+            } else if (content.length() == 0) {
+                throw new RuntimeException("Document provided by --file option is empty");
+            } else {
+                throw new RuntimeException("Unable to determine input document type. Use -e TYPE to specify the registration endpoint to use");
+            }
+        }
+
+        // check content type, making sure it can be parsed into .json if it's not saml xml
+        if (content != null) {
+            try {
+                if (type == EndpointType.DEFAULT && client == null) {
+                    client = JsonSerialization.readValue(content, ClientRepresentation.class);
+                } else if (type == EndpointType.OIDC && oidcClient == null) {
+                    oidcClient = JsonSerialization.readValue(content, OIDCClientRepresentation.class);
+                }
+            } catch (JsonParseException e) {
+                throw new RuntimeException("Not a valid JSON document - " + e.getMessage(), e);
+            } catch (UnrecognizedPropertyException e) {
+                throw new RuntimeException("Attribute '" + e.getPropertyName() + "' not supported on document type '" + type.getName() + "'", e);
+            } catch (IOException e) {
+                throw new RuntimeException("Not a valid JSON document", e);
+            }
+        }
+
+        CmdStdinContext ctx = new CmdStdinContext();
+        ctx.setEndpointType(type);
+        ctx.setContent(content);
+        ctx.setClient(client);
+        ctx.setOidcClient(oidcClient);
+        return ctx;
+    }
+
+    public static CmdStdinContext mergeAttributes(CmdStdinContext ctx, List<AttributeOperation> attrs) {
+        String content = ctx.getContent();
+        ClientRepresentation client = ctx.getClient();
+        OIDCClientRepresentation oidcClient = ctx.getOidcClient();
+        EndpointType type = ctx.getEndpointType();
+        try {
+            if (content == null) {
+                if (type == EndpointType.DEFAULT) {
+                    client = new ClientRepresentation();
+                } else if (type == EndpointType.OIDC) {
+                    oidcClient = new OIDCClientRepresentation();
+                }
+            }
+            Object rep = client != null ? client : oidcClient;
+            if (rep != null) {
+                try {
+                    setAttributes(rep, attrs);
+                } catch (AttributeException e) {
+                    throw new RuntimeException("Failed to set attribute '" + e.getAttributeName() + "' on document type '" + type.getName() + "'", e);
+                }
+                content = JsonSerialization.writeValueAsString(rep);
+            } else {
+                throw new RuntimeException("Setting attributes is not supported for type: " + type.getName());
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to merge set attributes with configuration from file", e);
+        }
+
+        ctx.setContent(content);
+        ctx.setClient(client);
+        ctx.setOidcClient(oidcClient);
+        return ctx;
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/ReflectionUtil.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/ReflectionUtil.java
new file mode 100644
index 0000000..1fe500b
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/ReflectionUtil.java
@@ -0,0 +1,498 @@
+/*
+ * 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.client.registration.cli.util;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import org.keycloak.client.registration.cli.common.AttributeKey;
+import org.keycloak.client.registration.cli.common.AttributeOperation;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ReflectionUtil {
+
+    static Map<Class, Map<String, Field>> index = new HashMap<>();
+
+    static void populateAttributesIndex(Class type) {
+        // We are using fields rather than getters / setters
+        // because it seems like JSON mapping sometimes also uses fields as well
+        // This may have to be changed some day due to reliance on Field.setAccessible()
+        Map<String, Field> map = new HashMap<>();
+        Field [] fields  = type.getDeclaredFields();
+        for (Field f: fields) {
+            // make sure to also have access to non-public fields
+            f.setAccessible(true);
+            map.put(f.getName(), f);
+        }
+        index.put(type, map);
+    }
+
+    public static Map<String, Field> getAttrFieldsForType(Type gtype) {
+        Class type;
+        if (gtype instanceof Class) {
+            type = (Class) gtype;
+        } else if (gtype instanceof ParameterizedType) {
+            type = (Class) ((ParameterizedType) gtype).getRawType();
+        } else {
+            throw new RuntimeException("Unexpected type: " + gtype);
+        }
+
+        if (isListType(type) || isMapType(type)) {
+            return Collections.emptyMap();
+        }
+        Map<String, Field> map = index.get(type);
+        if (map == null) {
+            populateAttributesIndex(type);
+            map = index.get(type);
+        }
+        return map;
+    }
+
+    public static boolean isListType(Class type) {
+        return List.class.isAssignableFrom(type) || type.isArray();
+    }
+
+    public static boolean isBasicType(Type type) {
+        return type == String.class || type == Boolean.class || type == boolean.class
+                || type == Integer.class || type == int.class || type == Long.class || type == long.class
+                || type == Float.class || type == float.class || type == Double.class || type == double.class;
+    }
+
+    public static boolean isMapType(Class type) {
+        return Map.class.isAssignableFrom(type);
+    }
+
+    public static Object convertValueToType(Object value, Class<?> type) throws IOException {
+
+        if (value == null) {
+            return null;
+
+        } else if (value instanceof String) {
+            if (type == String.class) {
+                return value;
+            } else if (type == Boolean.class) {
+                return Boolean.valueOf((String) value);
+            } else if (type == Integer.class) {
+                return Integer.valueOf((String) value);
+            } else if (type == Long.class) {
+                return Long.valueOf((String) value);
+            } else {
+                return JsonSerialization.readValue((String) value, type);
+            }
+        } else if (value instanceof Number) {
+            if (type == Integer.class) {
+                return ((Number) value).intValue();
+            } else if (type == Long.class) {
+                return ((Long) value).longValue();
+            } else if (type == String.class) {
+                return String.valueOf(value);
+            }
+        } else if (value instanceof Boolean) {
+            if (type == Boolean.class) {
+                return value;
+            } else if (type == String.class) {
+                return String.valueOf(value);
+            }
+        }
+
+        throw new RuntimeException("Unable to handle type [" + type + "]");
+    }
+
+    public static void setAttributes(Object client, List<AttributeOperation> attrs) {
+
+        for (AttributeOperation item: attrs) {
+
+            AttributeKey attr = item.getKey();
+            Object nested = client;
+
+            List<AttributeKey.Component> cs = attr.getComponents();
+            for (int i = 0; i < cs.size(); i++) {
+                AttributeKey.Component c = cs.get(i);
+
+                Class type = nested.getClass();
+                Field field = null;
+
+                if (!isMapType(type)) {
+                    Map<String, Field> fields = getAttrFieldsForType(type);
+                    if (fields == null) {
+                        throw new AttributeException(attr.toString(), "Unexpected condition - unknown type: " + type);
+                    }
+
+                    field = fields.get(c.getName());
+                    Class parent = type;
+                    while (field == null) {
+                        parent = parent.getSuperclass();
+                        if (parent == Object.class) {
+                            throw new AttributeException(attr.toString(), "Unknown attribute '" + c.getName() + "' on " + client.getClass());
+                        }
+
+                        fields = getAttrFieldsForType(parent);
+                        field = fields.get(c.getName());
+                    }
+                }
+                // if it's a 'basic' type we directly use setter
+                type = field == null ? type : field.getType();
+                if (isBasicType(type)) {
+                    if (i < cs.size() - 1) {
+                        throw new AttributeException(attr.toString(), "Attribute is of primitive type, and can't be nested further: " + c);
+                    }
+
+                    try {
+                        Object val = convertValueToType(item.getValue(), type);
+                        field.set(nested, val);
+                    } catch (Exception e) {
+                        throw new AttributeException(attr.toString(), "Failed to set attribute " + attr, e);
+                    }
+                } else if (isListType(type)) {
+                    if (i < cs.size() -1) {
+                        // not the target component
+                        try {
+                            nested = field.get(nested);
+                        } catch (Exception e) {
+                            throw new AttributeException(attr.toString(), "Failed to get attribute \"" + c + "\" in " + attr, e);
+                        }
+                        if (c.getIndex() >= 0) {
+                            // list item
+                            // get idx-th item
+                            List l = (List) nested;
+                            if (c.getIndex() >= l.size()) {
+                                throw new AttributeException(attr.toString(), "Array index out of bounds for \"" + c + "\" in " + attr);
+                            }
+                            nested = l.get(c.getIndex());
+                        }
+                    } else {
+                        // target component
+                        Class itype = type;
+                        Type gtype = field.getGenericType();
+                        if (gtype instanceof ParameterizedType) {
+                            Type[] typeArgs = ((ParameterizedType) gtype).getActualTypeArguments();
+                            if (typeArgs.length >= 1 && typeArgs[0] instanceof Class) {
+                                itype = (Class) typeArgs[0];
+                            } else {
+                                itype = String.class;
+                            }
+                        }
+                        if (c.getIndex() >= 0 || attr.isAppend()) {
+                            // some list item
+                            // get the list first
+                            List target;
+                            try {
+                                target = (List) field.get(nested);
+                            } catch (Exception e) {
+                                throw new AttributeException(attr.toString(), "Failed to get list attribute: " + attr, e);
+                            }
+
+                            // now replace or add idx-th item
+                            if (target == null) {
+                                target = createNewList(type);
+                                try {
+                                    field.set(nested, target);
+                                } catch (Exception e) {
+                                    throw new AttributeException(attr.toString(), "Failed to set list attribute " + attr, e);
+                                }
+                            }
+                            if (c.getIndex() >= target.size()) {
+                                throw new AttributeException(attr.toString(), "Array index out of bounds for \"" + c + "\" in " + attr);
+                            }
+
+                            if (attr.isAppend()) {
+                                try {
+                                    Object value = convertValueToType(item.getValue(), itype);
+                                    if (c.getIndex() >= 0) {
+                                        target.add(c.getIndex(), value);
+                                    } else {
+                                        target.add(value);
+                                    }
+                                } catch (Exception e) {
+                                    throw new AttributeException(attr.toString(), "Failed to set list attribute " + attr, e);
+                                }
+
+                            } else {
+                                if (item.getType() == AttributeOperation.Type.SET) {
+                                    try {
+                                        Object value = convertValueToType(item.getValue(), itype);
+                                        target.set(c.getIndex(), value);
+                                    } catch (Exception e) {
+                                        throw new AttributeException(attr.toString(), "Failed to set list attribute " + attr, e);
+                                    }
+                                } else {
+                                    try {
+                                        target.remove(c.getIndex());
+                                    } catch (Exception e) {
+                                        throw new AttributeException(attr.toString(), "Failed to remove list attribute " + attr, e);
+                                    }
+                                }
+                            }
+
+                        } else {
+                            // set the whole list field itself
+                            List value = createNewList(type);;
+                            if (item.getType() == AttributeOperation.Type.SET) {
+                                List converted = convertValueToList(item.getValue(), itype);
+                                value.addAll(converted);
+                            }
+                            try {
+                                field.set(nested, value);
+                            } catch (Exception e) {
+                                throw new AttributeException(attr.toString(), "Failed to set list attribute " + attr, e);
+                            }
+                        }
+                    }
+                } else {
+                    // object type
+                    if (i < cs.size() -1) {
+                        // not the target component
+                        Object value;
+                        if (field == null) {
+                            if (isMapType(nested.getClass())) {
+                                value = ((Map) nested).get(c.getName());
+                            } else {
+                                throw new RuntimeException("Unexpected condition while processing: " + attr);
+                            }
+                        } else {
+                            try {
+                                value = field.get(nested);
+                            } catch (Exception e) {
+                                throw new AttributeException(attr.toString(), "Failed to get attribute \"" + c + "\" in " + attr, e);
+                            }
+                        }
+                        if (value == null) {
+                            // create the target attribute
+                            if (isMapType(nested.getClass())) {
+                                throw new RuntimeException("Creating nested object trees not supported");
+                            } else {
+                                try {
+                                    value = createNewObject(type);
+                                    field.set(nested, value);
+                                } catch (Exception e) {
+                                    throw new AttributeException(attr.toString(), "Failed to set attribute " + attr, e);
+                                }
+                            }
+                        }
+                        nested = value;
+                    } else {
+                        // target component
+                        // todo implement map put
+                        if (isMapType(nested.getClass())) {
+                            try {
+                                ((Map) nested).put(c.getName(), item.getValue());
+                            } catch (Exception e) {
+                                throw new AttributeException(attr.toString(), "Failed to set map key " + attr, e);
+                            }
+                        } else {
+                            try {
+                                Object value = convertValueToType(item.getValue(), type);
+                                field.set(nested, value);
+                            } catch (Exception e) {
+                                throw new AttributeException(attr.toString(), "Failed to set attribute " + attr, e);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private static Object createNewObject(Class type) throws Exception {
+        return type.newInstance();
+    }
+
+    public static List createNewList(Class type) {
+
+        if (type == List.class) {
+            return new ArrayList();
+        } else if (type.isInterface()) {
+            throw new RuntimeException("Can't instantiate a list type: " + type);
+        }
+
+        try {
+            return (List) type.newInstance();
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to instantiate a list type: " + type, e);
+        }
+    }
+
+    public static List convertValueToList(String value, Class itemType) {
+        try {
+            List result = new LinkedList();
+            if (!value.startsWith("[")) {
+                throw new RuntimeException("List attribute value has to start with '[' - '" + value + "'");
+            }
+            List parsed = JsonSerialization.readValue(value, List.class);
+            for (Object item: parsed) {
+                if (itemType.isAssignableFrom(item.getClass())) {
+                    result.add(item);
+                } else {
+                    result.add(convertValueToType(item, itemType));
+                }
+            }
+            return result;
+
+        } catch (JsonParseException e) {
+            throw new RuntimeException("Failed to parse list value: " + e.getMessage(), e);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to parse list value: " + value, e);
+        }
+    }
+
+    public static <T> void merge(T source, T dest) {
+        // Use existing index for type, then iterate over all attributes and
+        // use setter on dest, and getter on source to copy value over
+        Map<String, Field> fieldMap = getAttrFieldsForType(source.getClass());
+        try {
+            for (String attrName : fieldMap.keySet()) {
+                Field field = fieldMap.get(attrName);
+                Object localValue = field.get(source);
+                if (localValue != null) {
+                    field.set(dest, localValue);
+                }
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to merge changes", e);
+        }
+    }
+
+
+    public static LinkedHashMap<String, String> getAttributeListWithJSonTypes(Class type, AttributeKey attr) {
+
+        LinkedHashMap<String, String> result = new LinkedHashMap<>();
+        attr = attr != null ? attr : new AttributeKey();
+
+        Map<String, Field> fields = getAttrFieldsForType(type);
+        for (AttributeKey.Component c: attr.getComponents()) {
+            Field f = fields.get(c.getName());
+            if (f == null) {
+                throw new AttributeException(attr.toString(), "No such attribute: " + attr);
+            }
+
+            type = f.getType();
+            if (isBasicType(type) || isListType(type) || isMapType(type)) {
+                return result;
+            } else {
+                fields = getAttrFieldsForType(type);
+            }
+        }
+
+        for (Map.Entry<String, Field> item : fields.entrySet()) {
+            String key = item.getKey();
+            Class clazz = item.getValue().getType();
+            String t = getTypeString(clazz, item.getValue());
+
+            result.put(key, t);
+        }
+        return result;
+    }
+
+    public static Field resolveField(Class type, AttributeKey attr) {
+        Field f = null;
+        Type gtype = type;
+
+        for (AttributeKey.Component c: attr.getComponents()) {
+            if (f != null) {
+                gtype = f.getGenericType();
+                if (gtype instanceof ParameterizedType) {
+                    Type[] typeargs = ((ParameterizedType) gtype).getActualTypeArguments();
+                    if (typeargs.length > 0) {
+                        gtype = typeargs[typeargs.length-1];
+                    }
+                }
+            }
+            Map<String, Field> fields = getAttrFieldsForType(gtype);
+            f = fields.get(c.getName());
+            if (f == null) {
+                throw new AttributeException(attr.toString(), "No such attribute: " + attr);
+            }
+        }
+        return f;
+    }
+
+    public static String getTypeString(Type type, Field field) {
+        Class clazz = null;
+        if (type == null) {
+            if (field == null) {
+                throw new IllegalArgumentException("type == null and field == null");
+            }
+            type = field.getGenericType();
+        }
+        if (type instanceof Class) {
+            clazz = (Class) type;
+        } else if (type instanceof ParameterizedType) {
+            StringBuilder sb = new StringBuilder();
+            String rtype = getTypeString(((ParameterizedType) type).getRawType(), null);
+
+            sb.append(rtype);
+            sb.append(" ").append("(");
+            Type[] typeArgs = ((ParameterizedType) type).getActualTypeArguments();
+
+            for (int i = 0; i < typeArgs.length; i++) {
+                if (i > 0) {
+                    sb.append(", ");
+                }
+                sb.append(getTypeString(typeArgs[i], null));
+            }
+            sb.append(")");
+            return sb.toString();
+        }
+
+        if (CharSequence.class.isAssignableFrom(clazz)) {
+            return "string";
+        } else if (Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz)) {
+            return "int";
+        } else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz)) {
+            return "long";
+        } else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) {
+            return "float";
+        } else if (Double.class.isAssignableFrom(clazz) || double.class.isAssignableFrom(clazz)) {
+            return "double";
+        } else if (Number.class.isAssignableFrom(clazz)) {
+            return "number";
+        } else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz)) {
+            return "boolean";
+        } else if (isListType(clazz)) {
+            if (field != null) {
+                Type gtype = field.getGenericType();
+                if (gtype == clazz && clazz.isArray()) {
+                    return "array (" + getTypeString(clazz.getComponentType(), null) + ")";
+                }
+                return getTypeString(gtype, null);
+            }
+            return "array";
+        } else if (isMapType(clazz)) {
+            if (field != null) {
+                Type gtype = field.getGenericType();
+                return getTypeString(gtype, null);
+            }
+            return "object";
+        } else {
+            return "object";
+        }
+    }
+}
diff --git a/integration/client-cli/client-registration-cli/src/test/java/org/keycloak/client/registration/cli/util/ReflectionUtilTest.java b/integration/client-cli/client-registration-cli/src/test/java/org/keycloak/client/registration/cli/util/ReflectionUtilTest.java
new file mode 100644
index 0000000..2a20e54
--- /dev/null
+++ b/integration/client-cli/client-registration-cli/src/test/java/org/keycloak/client/registration/cli/util/ReflectionUtilTest.java
@@ -0,0 +1,355 @@
+package org.keycloak.client.registration.cli.util;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.keycloak.client.registration.cli.common.AttributeKey;
+import org.keycloak.client.registration.cli.common.AttributeKey.Component;
+import org.keycloak.client.registration.cli.common.AttributeOperation;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.client.registration.cli.common.AttributeOperation.Type.DELETE;
+import static org.keycloak.client.registration.cli.common.AttributeOperation.Type.SET;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ReflectionUtilTest {
+
+    @Ignore
+    @Test
+    public void testListAttributes() {
+        LinkedHashMap<String, String> items = null;
+/*
+        items = getAttributeListWithJSonTypes(Data.class, new AttributeKey(""));
+
+        for (Map.Entry<String, String> item: items.entrySet()) {
+            System.out.printf("%-40s %s\n", item.getKey(), item.getValue());
+        }
+*/
+/*
+        System.out.println("\n-- nested ------------------------\n");
+
+        items = getAttributeListWithJSonTypes(Data.class, new AttributeKey("nested"));
+        for (Map.Entry<String, String> item: items.entrySet()) {
+            System.out.printf("%-40s %s\n", item.getKey(), item.getValue());
+        }
+*/
+
+        System.out.println("\n-- dataList ----------------------\n");
+
+        items = ReflectionUtil.getAttributeListWithJSonTypes(Data.class, new AttributeKey("dataList"));
+        for (Map.Entry<String, String> item: items.entrySet()) {
+            System.out.printf("%-40s %s\n", item.getKey(), item.getValue());
+        }
+
+        if (items.size() == 0) {
+            Field f = ReflectionUtil.resolveField(Data.class, new AttributeKey("dataList"));
+            String ts = ReflectionUtil.getTypeString(null, f);
+            Type t = f.getGenericType();
+            if ((List.class.isAssignableFrom(f.getType()) || f.getType().isArray()) && t instanceof ParameterizedType) {
+                System.out.printf("%s, where object is:\n", ts);
+            }
+            t = ((ParameterizedType) t).getActualTypeArguments()[0];
+            if (t instanceof Class) {
+                items = ReflectionUtil.getAttributeListWithJSonTypes((Class) t, null);
+                for (Map.Entry<String, String> item: items.entrySet()) {
+                    System.out.printf("   %-37s %s\n", item.getKey(), item.getValue());
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testSettingAttibutes() {
+        Data data = new Data();
+
+        LinkedList<AttributeOperation> attrs = new LinkedList<>();
+
+        attrs.add(new AttributeOperation(SET, "longAttr", "42"));
+        attrs.add(new AttributeOperation(SET, "strAttr", "not null"));
+        attrs.add(new AttributeOperation(SET, "strList+", "two"));
+        attrs.add(new AttributeOperation(SET, "strList+", "three"));
+        attrs.add(new AttributeOperation(SET, "strList[0]+", "one"));
+        attrs.add(new AttributeOperation(SET, "config", "{\"key1\": \"value1\"}"));
+        attrs.add(new AttributeOperation(SET, "config.key2", "value2"));
+        attrs.add(new AttributeOperation(SET, "nestedConfig", "{\"key1\": {\"sub key1\": \"sub value1\"}}"));
+        attrs.add(new AttributeOperation(SET, "nestedConfig.key1.\"sub key2\"", "sub value2"));
+        attrs.add(new AttributeOperation(SET, "nested.strList", "[1,2,3,4]"));
+        attrs.add(new AttributeOperation(SET, "nested.dataList+", "{\"baseAttr\": \"item1\", \"strList\": [\"confidential\", \"public\"]}"));
+        attrs.add(new AttributeOperation(SET, "nested.dataList+", "{\"baseAttr\": \"item2\", \"strList\": [\"external\"]}"));
+        attrs.add(new AttributeOperation(SET, "nested.dataList[1].baseAttr", "changed item2"));
+        attrs.add(new AttributeOperation(SET, "nested.nested.strList", "[\"first\",\"second\"]"));
+        attrs.add(new AttributeOperation(DELETE, "nested.strList[1]"));
+        attrs.add(new AttributeOperation(SET, "nested.nested.nested", "{\"baseAttr\": \"NEW VALUE\", \"strList\": [true, false]}"));
+        attrs.add(new AttributeOperation(SET, "nested.strAttr", "NOT NULL"));
+        attrs.add(new AttributeOperation(DELETE, "nested.strAttr"));
+
+        ReflectionUtil.setAttributes(data, attrs);
+
+        Assert.assertEquals("longAttr", Long.valueOf(42), data.getLongAttr());
+        Assert.assertEquals("strAttr", "not null", data.getStrAttr());
+        Assert.assertEquals("strList", Arrays.asList("one", "two", "three"), data.getStrList());
+
+        Map<String, String> expectedMap = new HashMap<>();
+        expectedMap.put("key1", "value1");
+        expectedMap.put("key2", "value2");
+        Assert.assertEquals("config", expectedMap, data.getConfig());
+
+
+        expectedMap = new HashMap<>();
+        expectedMap.put("sub key1", "sub value1");
+        expectedMap.put("sub key2", "sub value2");
+
+        Assert.assertNotNull("nestedConfig", data.getNestedConfig());
+        Assert.assertEquals("nestedConfig has one element", 1, data.getNestedConfig().size());
+        Assert.assertEquals("nestedConfig.key1", expectedMap, data.getNestedConfig().get("key1"));
+
+
+        Data nested = data.getNested();
+        Assert.assertEquals("nested.strAttr", null, nested.getStrAttr());
+        Assert.assertEquals("nested.strList", Arrays.asList("1", "3", "4"), nested.getStrList());
+        Assert.assertEquals("nested.dataList[0].baseAttr", "item1", nested.getDataList().get(0).getBaseAttr());
+        Assert.assertEquals("nested.dataList[0].strList", Arrays.asList("confidential", "public"), nested.getDataList().get(0).getStrList());
+        Assert.assertEquals("nested.dataList[1].baseAttr", "changed item2", nested.getDataList().get(1).getBaseAttr());
+        Assert.assertEquals("nested.dataList[1].strList", Arrays.asList("external"), nested.getDataList().get(1).getStrList());
+
+        nested = nested.getNested();
+        Assert.assertEquals("nested.nested.strList", Arrays.asList("first", "second"), nested.getStrList());
+
+        nested = nested.getNested();
+        Assert.assertEquals("nested.nested.nested.baseAttr", "NEW VALUE", nested.getBaseAttr());
+        Assert.assertEquals("nested.nested.nested.strList", Arrays.asList("true", "false"), nested.getStrList());
+    }
+
+    @Test
+    public void testKeyParsing() {
+
+        assertAttributeKey(new AttributeKey("am.bam.pet"), "am", -1, "bam", -1, "pet", -1);
+
+        assertAttributeKey(new AttributeKey("a"), "a", -1);
+
+        assertAttributeKey(new AttributeKey("a.b"), "a", -1, "b", -1);
+
+        assertAttributeKey(new AttributeKey("a.b[1]"), "a", -1, "b", 1);
+
+        assertAttributeKey(new AttributeKey("a[12].b"), "a", 12, "b", -1);
+
+        assertAttributeKey(new AttributeKey("a[10].b[20]"), "a", 10, "b", 20);
+
+        assertAttributeKey(new AttributeKey("\"am\".\"bam\".\"pet\""), "am", -1, "bam", -1, "pet", -1);
+
+        assertAttributeKey(new AttributeKey("\"am\".bam.\"pet\""), "am", -1, "bam", -1, "pet", -1);
+
+        assertAttributeKey(new AttributeKey("\"am.bam\".\"pet\""), "am.bam", -1, "pet", -1);
+
+        assertAttributeKey(new AttributeKey("\"am.bam[2]\".\"pet[6]\""), "am.bam", 2, "pet", 6);
+
+        try {
+            new AttributeKey("a.");
+
+            Assert.fail("Should have failed");
+        } catch (RuntimeException expected) {
+        }
+
+        try {
+            new AttributeKey("a[]");
+
+            Assert.fail("Should have failed");
+        } catch (RuntimeException expected) {
+        }
+
+        try {
+            new AttributeKey("a[lala]");
+
+            Assert.fail("Should have failed");
+        } catch (RuntimeException expected) {
+        }
+
+        try {
+            new AttributeKey("a[\"lala\"]");
+
+            Assert.fail("Should have failed");
+        } catch (RuntimeException expected) {
+        }
+
+        try {
+            new AttributeKey(".a");
+
+            Assert.fail("Should have failed");
+        } catch (RuntimeException expected) {
+        }
+
+        try {
+            new AttributeKey("\"am\"..\"bam\".\"pet\"");
+
+            Assert.fail("Should have failed");
+        } catch (RuntimeException expected) {
+        }
+
+        try {
+            new AttributeKey("\"am\"ups.\"bam\".\"pet\"");
+
+            Assert.fail("Should have failed");
+        } catch (RuntimeException expected) {
+        }
+
+        try {
+            new AttributeKey("ups\"am\"ups.\"bam\".\"pet\"");
+
+            Assert.fail("Should have failed");
+        } catch (RuntimeException expected) {
+        }
+    }
+
+    private void assertAttributeKey(AttributeKey key, Object ... args) {
+        Iterator<Component> it = key.getComponents().iterator();
+
+        for (int i = 0; i < args.length; i++) {
+            String name = String.valueOf(args[i++]);
+            int idx = Integer.valueOf(String.valueOf(args[i]));
+
+            Component component = it.next();
+            Assert.assertEquals(name, component.getName());
+            Assert.assertEquals(idx, component.getIndex());
+        }
+    }
+
+
+    public static class BaseData {
+
+        String baseAttr;
+
+        public String getBaseAttr() {
+            return baseAttr;
+        }
+
+        public void setBaseAttr(String baseAttr) {
+            this.baseAttr = baseAttr;
+        }
+    }
+
+    public static class Data extends BaseData {
+
+        String strAttr;
+
+        Integer intAttr;
+
+        Long longAttr;
+
+        Boolean boolAttr;
+
+        List<String> strList;
+
+        List<Integer> intList;
+
+        List<Data> dataList;
+
+        List<List<String>> deepList;
+
+        Data nested;
+
+        Map<String, String> config;
+
+        Map<String, Map<String, Data>> nestedConfig;
+
+
+        public String getStrAttr() {
+            return strAttr;
+        }
+
+        public void setStrAttr(String strAttr) {
+            this.strAttr = strAttr;
+        }
+
+        public Integer getIntAttr() {
+            return intAttr;
+        }
+
+        public void setIntAttr(Integer intAttr) {
+            this.intAttr = intAttr;
+        }
+
+        public Long getLongAttr() {
+            return longAttr;
+        }
+
+        public void setLongAttr(Long longAttr) {
+            this.longAttr = longAttr;
+        }
+
+        public Boolean getBoolAttr() {
+            return boolAttr;
+        }
+
+        public void setBoolAttr(Boolean boolAttr) {
+            this.boolAttr = boolAttr;
+        }
+
+        public List<String> getStrList() {
+            return strList;
+        }
+
+        public void setStrList(List<String> strList) {
+            this.strList = strList;
+        }
+
+        public List<Integer> getIntList() {
+            return intList;
+        }
+
+        public void setIntList(List<Integer> intList) {
+            this.intList = intList;
+        }
+
+        public List<Data> getDataList() {
+            return dataList;
+        }
+
+        public void setDataList(List<Data> dataList) {
+            this.dataList = dataList;
+        }
+
+        public Data getNested() {
+            return nested;
+        }
+
+        public void setNested(Data nested) {
+            this.nested = nested;
+        }
+
+        public List<List<String>> getDeepList() {
+            return deepList;
+        }
+
+        public void setDeepList(List<List<String>> deepList) {
+            this.deepList = deepList;
+        }
+
+        public void setConfig(Map<String, String> config) {
+            this.config = config;
+        }
+
+        public Map<String, String> getConfig() {
+            return config;
+        }
+
+        public void setNestedConfig(Map<String, Map<String, Data>> nestedConfig) {
+            this.nestedConfig = nestedConfig;
+        }
+
+        public Map<String, Map<String, Data>> getNestedConfig() {
+            return nestedConfig;
+        }
+    }
+}
\ No newline at end of file
diff --git a/integration/pom.xml b/integration/pom.xml
index a635afb..bcbe812 100755
--- a/integration/pom.xml
+++ b/integration/pom.xml
@@ -33,5 +33,6 @@
     <modules>
         <module>admin-client</module>
         <module>client-registration</module>
+        <module>client-cli</module>
     </modules>
 </project>
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
index f3b304d..207cef8 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
@@ -79,10 +79,14 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
         // Need ThreadLocal as liquibase doesn't seem to have API to inject custom objects into tasks
         ThreadLocalSessionContext.setCurrentSession(session);
 
+        Writer exportWriter = null;
         try {
             // Run update with keycloak master changelog first
             Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema);
-            updateChangeSet(liquibase, liquibase.getChangeLogFile(), file);
+            if (file != null) {
+                exportWriter = new FileWriter(file);
+            }
+            updateChangeSet(liquibase, liquibase.getChangeLogFile(), exportWriter);
 
             // Run update for each custom JpaEntityProvider
             Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
@@ -92,18 +96,24 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
                     String factoryId = jpaProvider.getFactoryId();
                     String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
                     liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
-                    updateChangeSet(liquibase, liquibase.getChangeLogFile(), file);
+                    updateChangeSet(liquibase, liquibase.getChangeLogFile(), exportWriter);
                 }
             }
         } catch (Exception e) {
             throw new RuntimeException("Failed to update database", e);
         } finally {
             ThreadLocalSessionContext.removeCurrentSession();
+            if (exportWriter != null) {
+                try {
+                    exportWriter.close();
+                } catch (IOException ioe) {
+                    // ignore
+                }
+            }
         }
     }
 
-
-    protected void updateChangeSet(Liquibase liquibase, String changelog, File exportFile) throws LiquibaseException, IOException {
+    protected void updateChangeSet(Liquibase liquibase, String changelog, Writer exportWriter) throws LiquibaseException, IOException {
         List<ChangeSet> changeSets = getChangeSets(liquibase);
         if (!changeSets.isEmpty()) {
             List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
@@ -117,13 +127,11 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
                 }
             }
 
-            if (exportFile != null) {
-                try (Writer exportWriter = new FileWriter(exportFile)) {
-                    if (ranChangeSets.isEmpty()) {
-                        outputChangeLogTableCreationScript(liquibase, exportWriter);
-                    }
-                    liquibase.update((Contexts) null, new LabelExpression(), exportWriter, false);
+            if (exportWriter != null) {
+                if (ranChangeSets.isEmpty()) {
+                    outputChangeLogTableCreationScript(liquibase, exportWriter);
                 }
+                liquibase.update((Contexts) null, new LabelExpression(), exportWriter, false);
             } else {
                 liquibase.update((Contexts) null);
             }

pom.xml 11(+11 -0)

diff --git a/pom.xml b/pom.xml
index d928b9c..d6e110f 100755
--- a/pom.xml
+++ b/pom.xml
@@ -1305,6 +1305,17 @@
                 <version>${project.version}</version>
                 <type>war</type>
             </dependency>
+            <dependency>
+                <groupId>org.keycloak</groupId>
+                <artifactId>keycloak-client-registration-cli</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.keycloak</groupId>
+                <artifactId>keycloak-client-cli-dist</artifactId>
+                <version>${project.version}</version>
+                <type>zip</type>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 
diff --git a/server-spi/src/main/java/org/keycloak/migration/migrators/MigrationUtils.java b/server-spi/src/main/java/org/keycloak/migration/migrators/MigrationUtils.java
index e9d0cb9..ae3a99f 100644
--- a/server-spi/src/main/java/org/keycloak/migration/migrators/MigrationUtils.java
+++ b/server-spi/src/main/java/org/keycloak/migration/migrators/MigrationUtils.java
@@ -33,7 +33,7 @@ public class MigrationUtils {
 
     public static void addAdminRole(RealmModel realm, String roleName) {
         ClientModel client = realm.getMasterAdminClient();
-        if (client.getRole(roleName) == null) {
+        if (client != null && client.getRole(roleName) == null) {
             RoleModel role = client.addRole(roleName);
             role.setDescription("${role_" + roleName + "}");
             role.setScopeParamRequired(false);
@@ -43,7 +43,7 @@ public class MigrationUtils {
 
         if (!realm.getName().equals(Config.getAdminRealm())) {
             client = realm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID);
-            if (client.getRole(roleName) == null) {
+            if (client != null && client.getRole(roleName) == null) {
                 RoleModel role = client.addRole(roleName);
                 role.setDescription("${role_" + roleName + "}");
                 role.setScopeParamRequired(false);
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index e647aec..2317352 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -197,7 +197,7 @@ public class ModelToRepresentation {
         rep.setRequiredActions(reqActions);
 
         if (user.getAttributes() != null && !user.getAttributes().isEmpty()) {
-            Map<String, Object> attrs = new HashMap<>();
+            Map<String, List<String>> attrs = new HashMap<>();
             attrs.putAll(user.getAttributes());
             rep.setAttributes(attrs);
         }
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index f1e4ea7..8ae1caa 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -1342,16 +1342,10 @@ public class RepresentationToModel {
         user.setLastName(userRep.getLastName());
         user.setFederationLink(userRep.getFederationLink());
         if (userRep.getAttributes() != null) {
-            for (Map.Entry<String, Object> entry : userRep.getAttributes().entrySet()) {
-                Object value = entry.getValue();
-
-                if (value instanceof Collection) {
-                    Collection<String> colVal = (Collection<String>) value;
-                    user.setAttribute(entry.getKey(), new ArrayList<>(colVal));
-                } else if (value instanceof String) {
-                    // TODO: This is here just for backwards compatibility with KC 1.3 and earlier
-                    String stringVal = (String) value;
-                    user.setSingleAttribute(entry.getKey(), stringVal);
+            for (Map.Entry<String, List<String>> entry : userRep.getAttributes().entrySet()) {
+                List<String> value = entry.getValue();
+                if (value != null) {
+                    user.setAttribute(entry.getKey(), new ArrayList<>(value));
                 }
             }
         }
@@ -2226,20 +2220,11 @@ public class RepresentationToModel {
     public static void importFederatedUser(KeycloakSession session, RealmModel newRealm, UserRepresentation userRep) {
         UserFederatedStorageProvider federatedStorage = session.userFederatedStorage();
         if (userRep.getAttributes() != null) {
-            for (Map.Entry<String, Object> entry : userRep.getAttributes().entrySet()) {
+            for (Map.Entry<String, List<String>> entry : userRep.getAttributes().entrySet()) {
                 String key = entry.getKey();
-                Object value = entry.getValue();
-                if (value == null) continue;
-
-                if (value instanceof Collection) {
-                    Collection<String> colVal = (Collection<String>) value;
-                    List<String> list = new LinkedList<>();
-                    list.addAll(colVal);
-                    federatedStorage.setAttribute(newRealm, userRep.getId(), key, list);
-                } else if (value instanceof String) {
-                    // TODO: This is here just for backwards compatibility with KC 1.3 and earlier
-                    String stringVal = (String) value;
-                    federatedStorage.setSingleAttribute(newRealm, userRep.getId(), key, stringVal);
+                List<String> value = entry.getValue();
+                if (value != null) {
+                    federatedStorage.setAttribute(newRealm, userRep.getId(), key, new LinkedList<>(value));
                 }
             }
         }
diff --git a/services/src/main/java/org/keycloak/exportimport/dir/DirImportProvider.java b/services/src/main/java/org/keycloak/exportimport/dir/DirImportProvider.java
index 0555218..e31cb8e 100755
--- a/services/src/main/java/org/keycloak/exportimport/dir/DirImportProvider.java
+++ b/services/src/main/java/org/keycloak/exportimport/dir/DirImportProvider.java
@@ -62,6 +62,10 @@ public class DirImportProvider implements ImportProvider {
     public DirImportProvider(File rootDirectory) {
         this.rootDirectory = rootDirectory;
 
+        if (!this.rootDirectory.exists()) {
+            throw new IllegalStateException("Directory " + this.rootDirectory + " doesn't exists");
+        }
+
         logger.infof("Importing from directory %s", this.rootDirectory.getAbsolutePath());
     }
 
diff --git a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
index 96d514c..62c3157 100755
--- a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
+++ b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
@@ -635,7 +635,7 @@ public class ExportUtils {
         userRep.setId(id);
         MultivaluedHashMap<String, String> attributes = session.userFederatedStorage().getAttributes(realm, id);
         if (attributes.size() > 0) {
-            Map<String, Object> attrs = new HashMap<>();
+            Map<String, List<String>> attrs = new HashMap<>();
             attrs.putAll(attributes);
             userRep.setAttributes(attrs);
         }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
index f1132af..8dbb01b 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
@@ -50,27 +50,28 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
         try {
             boolean valid = true;
 
-            RSATokenVerifier verifier = RSATokenVerifier.create(token)
-                    .realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
+            AccessToken toIntrospect = null;
 
-            PublicKey publicKey = session.keys().getPublicKey(realm, verifier.getHeader().getKeyId());
-            if (publicKey == null) {
-                valid = false;
-            } else {
-                try {
+            try {
+                RSATokenVerifier verifier = RSATokenVerifier.create(token)
+                        .realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
+
+                PublicKey publicKey = session.keys().getPublicKey(realm, verifier.getHeader().getKeyId());
+                if (publicKey == null) {
+                    valid = false;
+                } else {
                     verifier.publicKey(publicKey);
                     verifier.verify();
-                } catch (VerificationException e) {
-                    valid = false;
+                    toIntrospect = verifier.getToken();
                 }
+            } catch (VerificationException e) {
+                valid = false;
             }
 
             RealmModel realm = this.session.getContext().getRealm();
             ObjectNode tokenMetadata;
 
-            AccessToken toIntrospect = verifier.getToken();
-
-            if (valid) {
+            if (valid && toIntrospect != null) {
                 valid = tokenManager.isTokenValid(session, realm, toIntrospect);
             }
 
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java
index b41f52d..c46ba10 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java
@@ -17,27 +17,25 @@
 
 package org.keycloak.protocol.oidc.endpoints;
 
-import org.jboss.resteasy.spi.NotFoundException;
-import org.keycloak.Config;
-import org.keycloak.common.util.StreamUtil;
 import org.keycloak.common.util.UriUtils;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
-import org.keycloak.protocol.oidc.utils.RedirectUtils;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.oidc.utils.WebOriginsUtils;
+import org.keycloak.services.util.CacheControlUtil;
 import org.keycloak.services.util.P3PHelper;
 
 import javax.ws.rs.GET;
+import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.CacheControl;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
-import java.io.IOException;
 import java.io.InputStream;
+import java.util.Set;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -58,62 +56,40 @@ public class LoginStatusIframeEndpoint {
 
     @GET
     @Produces(MediaType.TEXT_HTML)
-    public Response getLoginStatusIframe(@QueryParam("client_id") String client_id,
-                                         @QueryParam("origin") String origin) {
-        if (client_id == null || origin == null) {
-            throw new WebApplicationException(Response.Status.BAD_REQUEST);
-        }
-
-        if (!UriUtils.isOrigin(origin)) {
-            throw new WebApplicationException(Response.Status.BAD_REQUEST);
-        }
-
-        ClientModel client = realm.getClientByClientId(client_id);
-        if (client == null) {
-            throw new WebApplicationException(Response.Status.BAD_REQUEST);
+    public Response getLoginStatusIframe() {
+        InputStream resource = getClass().getClassLoader().getResourceAsStream("login-status-iframe.html");
+        if (resource != null) {
+            P3PHelper.addP3PHeader(session);
+            return Response.ok(resource).type(MediaType.TEXT_HTML_TYPE).cacheControl(CacheControlUtil.getDefaultCacheControl()).build();
+        } else {
+            return Response.status(Response.Status.NOT_FOUND).build();
         }
+    }
 
-        InputStream is = getClass().getClassLoader().getResourceAsStream("login-status-iframe.html");
-        if (is == null) throw new NotFoundException("Could not find login-status-iframe.html ");
-
-        boolean valid = false;
-        for (String o : client.getWebOrigins()) {
-            if (o.equals("*") || o.equals(origin)) {
-                valid = true;
-                break;
+    @GET
+    @Path("init")
+    public Response preCheck(@QueryParam("client_id") String clientId, @QueryParam("origin") String origin, @QueryParam("session_state") String sessionState) {
+        try {
+            RealmModel realm = session.getContext().getRealm();
+            String sessionId = sessionState.split("/")[2];
+            UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
+            if (userSession == null) {
+                return Response.status(Response.Status.NOT_FOUND).build();
             }
-        }
 
-        for (String r : RedirectUtils.resolveValidRedirects(uriInfo, client.getRootUrl(), client.getRedirectUris())) {
-            int i = r.indexOf('/', 8);
-            if (i != -1) {
-                r = r.substring(0, i);
-            }
+            ClientModel client = session.realms().getClientByClientId(clientId, realm);
+            if (client != null) {
+                Set<String> validWebOrigins = WebOriginsUtils.resolveValidWebOrigins(uriInfo, client);
+                validWebOrigins.add(UriUtils.getOrigin(uriInfo.getRequestUri()));
 
-            if (r.equals(origin)) {
-                valid = true;
-                break;
+                if (validWebOrigins.contains(origin)) {
+                    return Response.noContent().build();
+                }
             }
+        } catch (Throwable t) {
         }
 
-        if (!valid) {
-            throw new WebApplicationException(Response.Status.BAD_REQUEST);
-        }
-
-        try {
-            String file = StreamUtil.readString(is);
-            file = file.replace("ORIGIN", origin);
-
-            P3PHelper.addP3PHeader(session);
-
-            CacheControl cacheControl = new CacheControl();
-            cacheControl.setNoTransform(false);
-            cacheControl.setMaxAge(Config.scope("theme").getInt("staticMaxAge", -1));
-
-            return Response.ok(file).cacheControl(cacheControl).build();
-        } catch (IOException e) {
-            throw new WebApplicationException(e, Response.Status.BAD_REQUEST);
-        }
+        return Response.status(Response.Status.FORBIDDEN).build();
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
index 2c983ed..b07f06a 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
@@ -18,6 +18,7 @@
 package org.keycloak.protocol.oidc;
 
 import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.HttpRequest;
 import org.jboss.resteasy.spi.ResteasyProviderFactory;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.forms.login.LoginFormsProvider;
@@ -32,9 +33,12 @@ import org.keycloak.protocol.oidc.endpoints.LoginStatusIframeEndpoint;
 import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint;
 import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
 import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint;
+import org.keycloak.services.resources.Cors;
 import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.services.util.CacheControlUtil;
 
 import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
@@ -67,6 +71,9 @@ public class OIDCLoginProtocolService {
     @Context
     private HttpHeaders headers;
 
+    @Context
+    private HttpRequest request;
+
     public OIDCLoginProtocolService(RealmModel realm, EventBuilder event) {
         this.realm = realm;
         this.tokenManager = new TokenManager();
@@ -168,11 +175,18 @@ public class OIDCLoginProtocolService {
         return endpoint;
     }
 
+    @OPTIONS
+    @Path("certs")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getVersionPreflight() {
+        return Cors.add(request, Response.ok()).allowedMethods("GET").preflight().auth().build();
+    }
+
     @GET
     @Path("certs")
     @Produces(MediaType.APPLICATION_JSON)
     @NoCache
-    public JSONWebKeySet certs() {
+    public Response certs() {
         List<KeyMetadata> publicKeys = session.keys().getKeys(realm, false);
         JWK[] keys = new JWK[publicKeys.size()];
 
@@ -183,7 +197,9 @@ public class OIDCLoginProtocolService {
 
         JSONWebKeySet keySet = new JSONWebKeySet();
         keySet.setKeys(keys);
-        return keySet;
+
+        Response.ResponseBuilder responseBuilder = Response.ok(keySet).cacheControl(CacheControlUtil.getDefaultCacheControl());
+        return Cors.add(request, responseBuilder).allowedOrigins("*").auth().build();
     }
 
     @Path("userinfo")
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
index c2f4296..0485e3f 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
@@ -90,6 +90,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
         config.setUserinfoEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "issueUserInfo").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
         config.setLogoutEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "logout").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
         config.setJwksUri(uriBuilder.clone().path(OIDCLoginProtocolService.class, "certs").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
+        config.setCheckSessionIframe(uriBuilder.clone().path(OIDCLoginProtocolService.class, "getLoginStatusIframe").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
         config.setRegistrationEndpoint(RealmsResource.clientRegistrationUrl(uriInfo).path(ClientRegistrationService.class, "provider").build(realm.getName(), OIDCClientRegistrationProviderFactory.ID).toString());
 
         config.setIdTokenSigningAlgValuesSupported(DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
index 181e0d2..cb94c1c 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
@@ -52,6 +52,9 @@ public class OIDCConfigurationRepresentation {
     @JsonProperty("jwks_uri")
     private String jwksUri;
 
+    @JsonProperty("check_session_iframe")
+    private String checkSessionIframe;
+
     @JsonProperty("grant_types_supported")
     private List<String> grantTypesSupported;
 
@@ -150,6 +153,14 @@ public class OIDCConfigurationRepresentation {
         this.jwksUri = jwksUri;
     }
 
+    public String getCheckSessionIframe() {
+        return checkSessionIframe;
+    }
+
+    public void setCheckSessionIframe(String checkSessionIframe) {
+        this.checkSessionIframe = checkSessionIframe;
+    }
+
     public String getLogoutEndpoint() {
         return logoutEndpoint;
     }
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 021011a..d0eb66b 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -121,7 +121,6 @@ public class AuthenticationManager {
             UserSessionModel cookieSession = session.sessions().getUserSession(realm, token.getSessionState());
             if (cookieSession == null || !cookieSession.getId().equals(userSession.getId())) return;
             expireIdentityCookie(realm, uriInfo, connection);
-            expireRememberMeCookie(realm, uriInfo, connection);
         } catch (Exception e) {
         }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java
index 5a6f4f1..b7dcddf 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java
@@ -211,7 +211,7 @@ public class AdminRoot {
             logger.debug("authenticated admin access for: " + auth.getUser().getUsername());
         }
 
-        Cors.add(request).allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(response);
+        Cors.add(request).allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").exposedHeaders("Location").auth().build(response);
 
         RealmsAdminResource adminResource = new RealmsAdminResource(auth, tokenManager);
         ResteasyProviderFactory.getInstance().injectProperties(adminResource);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index f76761f..7728ecf 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -262,8 +262,8 @@ public class UsersResource {
             }
         }
 
-        if (rep.getAttributesAsListValues() != null) {
-            for (Map.Entry<String, List<String>> attr : rep.getAttributesAsListValues().entrySet()) {
+        if (rep.getAttributes() != null) {
+            for (Map.Entry<String, List<String>> attr : rep.getAttributes().entrySet()) {
                 user.setAttribute(attr.getKey(), attr.getValue());
             }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index f30665b..3ee7938 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -40,6 +40,7 @@ import org.keycloak.wellknown.WellKnownProvider;
 
 import javax.ws.rs.GET;
 import javax.ws.rs.NotFoundException;
+import javax.ws.rs.OPTIONS;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
@@ -240,6 +241,14 @@ public class RealmsResource {
         return brokerService;
     }
 
+    @OPTIONS
+    @Path("{realm}/.well-known/{provider}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getVersionPreflight(final @PathParam("realm") String name,
+                                        final @PathParam("provider") String providerName) {
+        return Cors.add(request, Response.ok()).allowedMethods("GET").preflight().auth().build();
+    }
+
     @GET
     @Path("{realm}/.well-known/{provider}")
     @Produces(MediaType.APPLICATION_JSON)
@@ -250,7 +259,7 @@ public class RealmsResource {
         WellKnownProvider wellKnown = session.getProvider(WellKnownProvider.class, providerName);
 
         ResponseBuilder responseBuilder = Response.ok(wellKnown.getConfig()).cacheControl(CacheControlUtil.getDefaultCacheControl());
-        return Cors.add(request, responseBuilder).allowedOrigins("*").build();
+        return Cors.add(request, responseBuilder).allowedOrigins("*").auth().build();
     }
 
     @Path("{realm}/authz")
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java
index 4695e55..b2e1a42 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java
@@ -18,9 +18,12 @@
 
 package org.keycloak.testsuite.authorization;
 
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import org.junit.Test;
 import org.keycloak.authorization.model.Scope;
+import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+import org.keycloak.util.JsonSerialization;
 
 import javax.ws.rs.client.Entity;
 import javax.ws.rs.client.Invocation.Builder;
@@ -28,6 +31,9 @@ import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 
+import java.io.IOException;
+import java.util.LinkedList;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/datasource.xsl b/testsuite/integration-arquillian/servers/auth-server/jboss/common/datasource.xsl
index 72b0403..f5ac4cd 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/common/datasource.xsl
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/datasource.xsl
@@ -29,42 +29,24 @@
     <!-- Remove keycloak datasource definition. -->
     <xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $nsDS)]
 		         /*[local-name()='datasources' and starts-with(namespace-uri(), $nsDS)]
-                         /*[local-name()='xa-datasource' and starts-with(namespace-uri(), $nsDS) and @pool-name='KeycloakDS']">
+                         /*[local-name()='datasource' and starts-with(namespace-uri(), $nsDS) and @pool-name='KeycloakDS']">
     </xsl:template>
     
     <xsl:param name="db.jdbc_url"/>
-    <xsl:param name="db.hostname"/>
-    <xsl:param name="db.name"/>
-    <xsl:param name="db.port"/>
     <xsl:param name="driver"/>
-    <xsl:param name="datasource.class.xa"/>
+
+    <xsl:param name="min.poolsize" select="'10'"/>
+    <xsl:param name="max.poolsize" select="'50'"/>
+    <xsl:param name="pool.prefill" select="'true'"/>
     
     <xsl:param name="username"/>
     <xsl:param name="password"/>
     
     <xsl:variable name="newDatasourceDefinition">
-        <xa-datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" use-java-context="true">
-            <xsl:choose>
-                <xsl:when test="contains($driver, 'oracle')">
-                    <xa-datasource-property name="URL">
-                        <xsl:value-of select="$db.jdbc_url"/>
-                    </xa-datasource-property>
-                </xsl:when>
-                <xsl:otherwise>
-                    <xa-datasource-property name="ServerName">
-                        <xsl:value-of select="$db.hostname"/>
-                    </xa-datasource-property>
-                    <xa-datasource-property name="PortNumber">
-                        <xsl:value-of select="$db.port"/>
-                    </xa-datasource-property>
-                    <xa-datasource-property name="DatabaseName">
-                        <xsl:value-of select="$db.name"/>
-                    </xa-datasource-property>
-                    <xsl:if test="contains($driver, 'db2')">
-                        <xa-datasource-property name="DriverType">4</xa-datasource-property>
-                    </xsl:if>
-                </xsl:otherwise>
-            </xsl:choose>
+        <datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" use-java-context="true">
+            <connection-url>
+                <xsl:value-of select="$db.jdbc_url"/>
+            </connection-url>
             <driver>
                 <xsl:value-of select="$driver"/>
             </driver>
@@ -76,16 +58,23 @@
                     <xsl:value-of select="$password"/>
                 </password>
             </security>
-        </xa-datasource>
+            <pool>
+                <min-pool-size>
+                    <xsl:value-of select="$min.poolsize"/>
+                </min-pool-size>
+                <max-pool-size>
+                    <xsl:value-of select="$max.poolsize"/>
+                </max-pool-size>
+                <prefill>
+                    <xsl:value-of select="$pool.prefill"/>
+                </prefill>
+            </pool>
+        </datasource>
     </xsl:variable>
     
     <xsl:variable name="newDriverDefinition">
         <xsl:if test="$driver != 'h2'">
-            <driver name="{$driver}" module="com.{$driver}">
-                <xa-datasource-class>
-                    <xsl:value-of select="$datasource.class.xa"/>
-                </xa-datasource-class>
-            </driver>
+            <driver name="{$driver}" module="com.{$driver}" />
         </xsl:if>
     </xsl:variable>
     
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/datasource-jdbc-url.xsl b/testsuite/integration-arquillian/servers/auth-server/jboss/common/datasource-jdbc-url.xsl
index f7d35d3..445b973 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/common/datasource-jdbc-url.xsl
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/datasource-jdbc-url.xsl
@@ -15,11 +15,11 @@
     <!-- replace JDBC URL -->
     <xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $nsDS)]
 		         /*[local-name()='datasources' and starts-with(namespace-uri(), $nsDS)]
-                         /*[local-name()='xa-datasource' and starts-with(namespace-uri(), $nsDS) and @pool-name=$pool.name]
-                         /*[local-name()='xa-datasource-property' and starts-with(namespace-uri(), $nsDS) and @name='URL']">
-        <xa-datasource-property name="URL">
+                         /*[local-name()='datasource' and starts-with(namespace-uri(), $nsDS) and @pool-name=$pool.name]
+                         /*[local-name()='connection-url' and starts-with(namespace-uri(), $nsDS)]">
+        <connection-url>
             <xsl:value-of select="$jdbc.url"/>
-        </xa-datasource-property>
+        </connection-url>
     </xsl:template>
 
     <!-- Copy everything else. -->
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
index dc65870..303da20 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
@@ -429,9 +429,6 @@
                                                 <property>jdbc.mvn.version</property>
                                             </requireProperty>
                                             <requireProperty>
-                                                <property>datasource.class.xa</property>
-                                            </requireProperty>
-                                            <requireProperty>
                                                 <property>keycloak.connectionsJpa.user</property>
                                             </requireProperty>
                                             <requireProperty>
@@ -440,15 +437,6 @@
                                             <requireProperty>
                                                 <property>keycloak.connectionsJpa.url</property>
                                             </requireProperty>
-                                            <requireProperty>
-                                                <property>db.hostname</property>
-                                            </requireProperty>
-                                            <requireProperty>
-                                                <property>db.name</property>
-                                            </requireProperty>
-                                            <requireProperty>
-                                                <property>db.port</property>
-                                            </requireProperty>
                                         </rules>
                                     </configuration>
                                 </execution>
@@ -524,22 +512,6 @@
                                                         <value>${keycloak.connectionsJpa.url}</value>
                                                     </parameter>
                                                     <parameter>
-                                                        <name>db.hostname</name>
-                                                        <value>${db.hostname}</value>
-                                                    </parameter>
-                                                    <parameter>
-                                                        <name>db.port</name>
-                                                        <value>${db.port}</value>
-                                                    </parameter>
-                                                    <parameter>
-                                                        <name>db.name</name>
-                                                        <value>${db.name}</value>
-                                                    </parameter>
-                                                    <parameter>
-                                                        <name>datasource.class.xa</name>
-                                                        <value>${datasource.class.xa}</value>
-                                                    </parameter>
-                                                    <parameter>
                                                         <name>driver</name>
                                                         <value>${jdbc.mvn.artifactId}</value>
                                                     </parameter>
diff --git a/testsuite/integration-arquillian/servers/migration/pom.xml b/testsuite/integration-arquillian/servers/migration/pom.xml
index d6091dd..b08f84a 100644
--- a/testsuite/integration-arquillian/servers/migration/pom.xml
+++ b/testsuite/integration-arquillian/servers/migration/pom.xml
@@ -67,9 +67,6 @@
                                     <property>jdbc.mvn.version</property>
                                 </requireProperty>
                                 <requireProperty>
-                                    <property>datasource.class.xa</property>
-                                </requireProperty>
-                                <requireProperty>
                                     <property>keycloak.connectionsJpa.user</property>
                                 </requireProperty>
                                 <requireProperty>
@@ -78,15 +75,6 @@
                                 <requireProperty>
                                     <property>keycloak.connectionsJpa.url</property>
                                 </requireProperty>
-                                <requireProperty>
-                                    <property>db.hostname</property>
-                                </requireProperty>
-                                <requireProperty>
-                                    <property>db.name</property>
-                                </requireProperty>
-                                <requireProperty>
-                                    <property>db.port</property>
-                                </requireProperty>
                             </rules>
                         </configuration>
                     </execution>
@@ -180,22 +168,6 @@
                                             <value>${keycloak.connectionsJpa.url}</value>
                                         </parameter>
                                         <parameter>
-                                            <name>db.hostname</name>
-                                            <value>${db.hostname}</value>
-                                        </parameter>
-                                        <parameter>
-                                            <name>db.port</name>
-                                            <value>${db.port}</value>
-                                        </parameter>
-                                        <parameter>
-                                            <name>db.name</name>
-                                            <value>${db.name}</value>
-                                        </parameter>
-                                        <parameter>
-                                            <name>datasource.class.xa</name>
-                                            <value>${datasource.class.xa}</value>
-                                        </parameter>
-                                        <parameter>
                                             <name>driver</name>
                                             <value>${jdbc.mvn.artifactId}</value>
                                         </parameter>
diff --git a/testsuite/integration-arquillian/servers/migration/src/main/xslt/datasource.xsl b/testsuite/integration-arquillian/servers/migration/src/main/xslt/datasource.xsl
index 909c797..3ca8aa4 100644
--- a/testsuite/integration-arquillian/servers/migration/src/main/xslt/datasource.xsl
+++ b/testsuite/integration-arquillian/servers/migration/src/main/xslt/datasource.xsl
@@ -25,51 +25,27 @@
 
     <xsl:variable name="nsDS" select="'urn:jboss:domain:datasources:'"/>
     
-    <!-- Remove keycloak datasource definition. For versions from 2.3.0-->
-    <xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $nsDS)]
-		         /*[local-name()='datasources' and starts-with(namespace-uri(), $nsDS)]
-                         /*[local-name()='xa-datasource' and starts-with(namespace-uri(), $nsDS) and @pool-name='KeycloakDS']">
-    </xsl:template>
-
-    <!-- Remove keycloak xa-datasource definition. For versions below 2.3.0-->
+    <!-- Remove keycloak datasource definition -->
     <xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $nsDS)]
 		         /*[local-name()='datasources' and starts-with(namespace-uri(), $nsDS)]
                          /*[local-name()='datasource' and starts-with(namespace-uri(), $nsDS) and @pool-name='KeycloakDS']">
     </xsl:template>
     
     <xsl:param name="db.jdbc_url"/>
-    <xsl:param name="db.hostname"/>
-    <xsl:param name="db.name"/>
-    <xsl:param name="db.port"/>
     <xsl:param name="driver"/>
-    <xsl:param name="datasource.class.xa"/>
+
+    <xsl:param name="min.poolsize" select="'10'"/>
+    <xsl:param name="max.poolsize" select="'50'"/>
+    <xsl:param name="pool.prefill" select="'true'"/>
     
     <xsl:param name="username"/>
     <xsl:param name="password"/>
     
     <xsl:variable name="newDatasourceDefinition">
-        <xa-datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true">
-            <xsl:choose>
-                <xsl:when test="contains($driver, 'oracle')">
-                    <xa-datasource-property name="URL">
-                        <xsl:value-of select="$db.jdbc_url"/>
-                    </xa-datasource-property>
-                </xsl:when>
-                <xsl:otherwise>
-                    <xa-datasource-property name="ServerName">
-                        <xsl:value-of select="$db.hostname"/>
-                    </xa-datasource-property>
-                    <xa-datasource-property name="PortNumber">
-                        <xsl:value-of select="$db.port"/>
-                    </xa-datasource-property>
-                    <xa-datasource-property name="DatabaseName">
-                        <xsl:value-of select="$db.name"/>
-                    </xa-datasource-property>
-                    <xsl:if test="contains($driver, 'db2')">
-                        <xa-datasource-property name="DriverType">4</xa-datasource-property>
-                    </xsl:if>
-                </xsl:otherwise>
-            </xsl:choose>
+        <datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true">
+            <connection-url>
+                <xsl:value-of select="$db.jdbc_url"/>
+            </connection-url>
             <driver>
                 <xsl:value-of select="$driver"/>
             </driver>
@@ -81,16 +57,23 @@
                     <xsl:value-of select="$password"/>
                 </password>
             </security>
-        </xa-datasource>
+            <pool>
+                <min-pool-size>
+                    <xsl:value-of select="$min.poolsize"/>
+                </min-pool-size>
+                <max-pool-size>
+                    <xsl:value-of select="$max.poolsize"/>
+                </max-pool-size>
+                <prefill>
+                    <xsl:value-of select="$pool.prefill"/>
+                </prefill>
+            </pool>
+        </datasource>
     </xsl:variable>
     
     <xsl:variable name="newDriverDefinition">
         <xsl:if test="$driver != 'h2'">
-            <driver name="{$driver}" module="com.{$driver}">
-                <xa-datasource-class>
-                    <xsl:value-of select="$datasource.class.xa"/>
-                </xa-datasource-class>
-            </driver>
+            <driver name="{$driver}" module="com.{$driver}"/>
         </xsl:if>
     </xsl:variable>
     
diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index 687710e..10a0f74 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -32,6 +32,8 @@
     <description></description>
 
     <properties>
+        <cli.log.output>false</cli.log.output>
+        <test.intermittent>false</test.intermittent>
         <exclude.test>-</exclude.test>
         <exclude.console>-</exclude.console>
         <exclude.account>-</exclude.account>
@@ -149,7 +151,30 @@
                     </execution>
                 </executions>
             </plugin>
-            
+
+            <plugin>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>unpack-client-cli-dist</id>
+                        <phase>generate-test-resources</phase>
+                        <goals>
+                            <goal>unpack</goal>
+                        </goals>
+                        <configuration>
+                            <artifactItems>
+                                <artifactItem>
+                                    <groupId>org.keycloak</groupId>
+                                    <artifactId>keycloak-client-cli-dist</artifactId>
+                                    <version>${project.version}</version>
+                                    <type>zip</type>
+                                    <outputDirectory>${containers.home}</outputDirectory>
+                                </artifactItem>
+                            </artifactItems>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
         </plugins>
 
     </build>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/ExecutionException.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/ExecutionException.java
new file mode 100644
index 0000000..986f486
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/ExecutionException.java
@@ -0,0 +1,27 @@
+package org.keycloak.testsuite.cli;
+
+/**
+ * @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
+ */
+public class ExecutionException extends RuntimeException {
+
+    private int exitCode = -1;
+
+    public ExecutionException(int exitCode) {
+        this.exitCode = exitCode;
+    }
+
+    public ExecutionException(String message, int exitCode) {
+        super(message);
+        this.exitCode = exitCode;
+    }
+
+    public int exitCode() {
+        return exitCode;
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + ", exitCode: " + exitCode;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcRegExec.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcRegExec.java
new file mode 100644
index 0000000..5f20c2a
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcRegExec.java
@@ -0,0 +1,487 @@
+package org.keycloak.testsuite.cli;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcRegExec {
+
+    public static final String WORK_DIR = System.getProperty("user.dir") + "/target/containers/keycloak-client-tools";
+
+    public static final OsArch OS_ARCH = OsUtils.determineOSAndArch();
+
+    public static final String CMD = OS_ARCH.isWindows() ? "kcreg.bat" : "kcreg.sh";
+
+    private long waitTimeout = 30000;
+
+    private Process process;
+
+    private int exitCode = -1;
+
+    private boolean logStreams = Boolean.valueOf(System.getProperty("cli.log.output", "true"));
+
+    private boolean dumpStreams;
+
+    private String workDir = WORK_DIR;
+
+    private String env;
+
+    private String argsLine;
+
+    private ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+
+    private ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+
+    private InputStream stdin = new InteractiveInputStream();
+
+    private Throwable err;
+
+    private KcRegExec(String workDir, String argsLine, InputStream stdin) {
+        this(workDir, argsLine, null, stdin);
+    }
+
+    private KcRegExec(String workDir, String argsLine, String env, InputStream stdin) {
+        if (workDir != null) {
+            this.workDir = workDir;
+        }
+
+        this.argsLine = argsLine;
+        this.env = env;
+
+        if (stdin != null) {
+            this.stdin = stdin;
+        }
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static KcRegExec execute(String args) {
+        return newBuilder()
+                .argsLine(args)
+                .execute();
+    }
+
+    public void execute() {
+        executeAsync();
+        if (err == null) {
+            waitCompletion();
+        }
+    }
+
+
+    public void executeAsync() {
+
+        try {
+            if (OS_ARCH.isWindows()) {
+                String cmd = (env != null ? "set " + env + " & " : "") + "bin\\" + CMD + " " + fixQuotes(argsLine);
+                System.out.println("Executing: cmd.exe /c " + cmd);
+                process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd}, null, new File(workDir));
+            } else {
+                String cmd = (env != null ? env + " " : "") + "bin/" + CMD + " " + argsLine;
+                System.out.println("Executing: sh -c " + cmd);
+                process = Runtime.getRuntime().exec(new String[]{"sh", "-c", cmd}, null, new File(workDir));
+            }
+
+            new StreamReaderThread(process.getInputStream(), logStreams ? new LoggingOutputStream("STDOUT", stdout) : stdout)
+                    .start();
+
+            new StreamReaderThread(process.getErrorStream(), logStreams ? new LoggingOutputStream("STDERR", stderr) : stderr)
+                    .start();
+
+            new StreamReaderThread(stdin, process.getOutputStream())
+                    .start();
+
+        } catch (Throwable t) {
+            err = t;
+        }
+    }
+
+    private String fixQuotes(String argsLine) {
+        argsLine = argsLine + " ";
+        argsLine = argsLine.replaceAll("\"", "\\\\\"");
+        argsLine = argsLine.replaceAll(" '", " \"");
+        argsLine = argsLine.replaceAll("' ", "\" ");
+        return argsLine;
+    }
+
+    public void waitCompletion() {
+
+        //if (stdin instanceof InteractiveInputStream) {
+        //    ((InteractiveInputStream) stdin).close();
+        //}
+        try {
+            if (process.waitFor(waitTimeout, TimeUnit.MILLISECONDS)) {
+                exitCode = process.exitValue();
+                if (exitCode != 0) {
+                    dumpStreams = true;
+                }
+            } else {
+                if (process.isAlive()) {
+                    process.destroyForcibly();
+                }
+                throw new RuntimeException("Timeout after " + (waitTimeout / 1000) + " seconds.");
+            }
+        } catch (InterruptedException e) {
+            dumpStreams = true;
+            throw new RuntimeException("Interrupted ...", e);
+        } catch (Throwable t) {
+            dumpStreams = true;
+            err = t;
+        } finally {
+            if (!logStreams && dumpStreams) try {
+                System.out.println("STDOUT: ");
+                copyStream(new ByteArrayInputStream(stdout.toByteArray()), System.out);
+                System.out.println("STDERR: ");
+                copyStream(new ByteArrayInputStream(stderr.toByteArray()), System.out);
+            } catch (Exception ignored) {
+            }
+        }
+    }
+
+    public int exitCode() {
+        return exitCode;
+    }
+
+    public Throwable error() {
+        return err;
+    }
+
+    public InputStream stdout() {
+        return new ByteArrayInputStream(stdout.toByteArray());
+    }
+
+    public List<String> stdoutLines() {
+        return parseStreamAsLines(new ByteArrayInputStream(stdout.toByteArray()));
+    }
+
+    public String stdoutString() {
+        return new String(stdout.toByteArray());
+    }
+
+    public InputStream stderr() {
+        return new ByteArrayInputStream(stderr.toByteArray());
+    }
+
+    public List<String> stderrLines() {
+        return parseStreamAsLines(new ByteArrayInputStream(stderr.toByteArray()));
+    }
+
+    public String stderrString() {
+        return new String(stderr.toByteArray());
+    }
+
+    static List<String> parseStreamAsLines(InputStream stream) {
+        List<String> lines = new ArrayList<>();
+        try {
+            BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+
+            String line;
+            while ((line = reader.readLine()) != null) {
+                lines.add(line);
+            }
+            return lines;
+        } catch (IOException e) {
+            throw new RuntimeException("Unexpected I/O error", e);
+        }
+    }
+
+    public void waitForStdout(String content) {
+        long start = System.currentTimeMillis();
+        while (System.currentTimeMillis() - start < waitTimeout) {
+            if (stdoutString().indexOf(content) != -1) {
+                return;
+            }
+            try {
+                Thread.sleep(10);
+            } catch (InterruptedException e) {
+                throw new RuntimeException("Interrupted ...", e);
+            }
+        }
+
+        throw new RuntimeException("Timed while waiting for content to appear in stdout");
+    }
+
+    public void sendToStdin(String s) {
+        if (stdin instanceof InteractiveInputStream) {
+            ((InteractiveInputStream) stdin).pushBytes(s.getBytes());
+        } else {
+            throw new RuntimeException("Can't push to stdin - not interactive");
+        }
+    }
+
+    static class StreamReaderThread extends Thread {
+
+        private InputStream is;
+        private OutputStream os;
+
+        StreamReaderThread(InputStream is, OutputStream os) {
+            this.is = is;
+            this.os = os;
+        }
+
+        public void run() {
+            try {
+                copyStream(is, os);
+            } catch (IOException e) {
+                throw new RuntimeException("Unexpected I/O error", e);
+            } finally {
+                try {
+                    os.close();
+                } catch (IOException ignored) {
+                    System.err.print("IGNORED: error while closing output stream: ");
+                    ignored.printStackTrace();
+                }
+            }
+        }
+    }
+
+    static void copyStream(InputStream is, OutputStream os) throws IOException {
+        byte [] buf = new byte[8192];
+
+        try (InputStream iss = is) {
+            int c;
+            while ((c = iss.read(buf)) != -1) {
+                os.write(buf, 0, c);
+                os.flush();
+            }
+        }
+    }
+
+    public static class Builder {
+
+        private String workDir;
+        private String argsLine;
+        private InputStream stdin;
+        private String env;
+        private boolean dumpStreams;
+
+        public Builder workDir(String path) {
+            this.workDir = path;
+            return this;
+        }
+
+        public Builder argsLine(String cmd) {
+            this.argsLine = cmd;
+            return this;
+        }
+
+        public Builder stdin(InputStream is) {
+            this.stdin = is;
+            return this;
+        }
+
+        public Builder env(String env) {
+            this.env = env;
+            return this;
+        }
+
+        public Builder fullStreamDump() {
+            this.dumpStreams = true;
+            return this;
+        }
+
+        public KcRegExec execute() {
+            KcRegExec exe = new KcRegExec(workDir, argsLine, env, stdin);
+            exe.dumpStreams = dumpStreams;
+            exe.execute();
+            return exe;
+        }
+
+        public KcRegExec executeAsync() {
+            KcRegExec exe = new KcRegExec(workDir, argsLine, env, stdin);
+            exe.dumpStreams = dumpStreams;
+            exe.executeAsync();
+            return exe;
+        }
+    }
+
+    static class NullInputStream extends InputStream {
+
+        @Override
+        public int read() throws IOException {
+            return -1;
+        }
+    }
+
+    static class InteractiveInputStream extends InputStream {
+
+        private LinkedList<Byte> queue = new LinkedList<>();
+
+        private Thread consumer;
+
+        private boolean closed;
+
+        @Override
+        public int read(byte b[]) throws IOException {
+            return read(b, 0, b.length);
+        }
+
+        @Override
+        public synchronized int read(byte[] b, int off, int len) throws IOException {
+
+            Byte current = null;
+            int rc = 0;
+            try {
+                consumer = Thread.currentThread();
+
+                do {
+                    current = queue.poll();
+                    if (current == null) {
+                        if (rc > 0) {
+                            return rc;
+                        } else {
+                            do {
+                                if (closed) {
+                                    return -1;
+                                }
+                                wait();
+                            }
+                            while ((current = queue.poll()) == null);
+                        }
+                    }
+
+                    b[off + rc] = current;
+                    rc++;
+                } while (rc < len);
+
+            } catch (InterruptedException e) {
+                throw new InterruptedIOException("Signalled to exit");
+            } finally {
+                consumer = null;
+            }
+            return rc;
+        }
+
+        @Override
+        public long skip(long n) throws IOException {
+            return super.skip(n);
+        }
+
+        @Override
+        public int available() throws IOException {
+            return super.available();
+        }
+
+        @Override
+        public synchronized void mark(int readlimit) {
+            super.mark(readlimit);
+        }
+
+        @Override
+        public synchronized void reset() throws IOException {
+            super.reset();
+        }
+
+        @Override
+        public boolean markSupported() {
+            return super.markSupported();
+        }
+
+        @Override
+        public synchronized int read() throws IOException {
+            if (closed) {
+                return -1;
+            }
+
+            // when input is available pass it on
+            Byte current;
+            try {
+                consumer = Thread.currentThread();
+
+                while ((current = queue.poll()) == null) {
+                    wait();
+                    if (closed) {
+                        return -1;
+                    }
+                }
+
+            } catch (InterruptedException e) {
+                throw new InterruptedIOException("Signalled to exit");
+            } finally {
+                consumer = null;
+            }
+            return current;
+        }
+
+        @Override
+        public synchronized void close() {
+            closed = true;
+            new RuntimeException("IIS || close").printStackTrace();
+            if (consumer != null) {
+                consumer.interrupt();
+            }
+        }
+
+        public synchronized void pushBytes(byte [] buff) {
+            for (byte b : buff) {
+                queue.add(b);
+            }
+            notify();
+        }
+    }
+
+
+    static class LoggingOutputStream extends FilterOutputStream {
+
+        private ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        private String name;
+
+        public LoggingOutputStream(String name, OutputStream os) {
+            super(os);
+            this.name = name;
+        }
+
+        @Override
+        public void write(int b) throws IOException {
+            super.write(b);
+            if (b == 10) {
+                log();
+            } else {
+                buffer.write(b);
+            }
+        }
+
+        @Override
+        public void write(byte[] buf) throws IOException {
+            write(buf, 0, buf.length);
+        }
+
+        @Override
+        public void write(byte[] buf, int offs, int len) throws IOException {
+            for (int i = 0; i < len; i++) {
+                write(buf[offs+i]);
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            super.close();
+            if (buffer.size() > 0) {
+                log();
+            }
+        }
+
+        private void log() {
+            String log = new String(buffer.toByteArray());
+            buffer.reset();
+            System.out.println("[" + name + "] " + log);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/OsArch.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/OsArch.java
new file mode 100644
index 0000000..cdd6b86
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/OsArch.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2014 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Eclipse Public License version 1.0, available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.keycloak.testsuite.cli;
+
+/**
+ * @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
+ */
+public class OsArch {
+
+    private String os;
+    private String arch;
+    private boolean legacy;
+
+    public OsArch(String os, String arch) {
+        this(os, arch, false);
+    }
+
+    public OsArch(String os, String arch, boolean legacy) {
+        this.os = os;
+        this.arch = arch;
+        this.legacy = legacy;
+    }
+
+    public String os() {
+        return os;
+    }
+
+    public String arch() {
+        return arch;
+    }
+
+    public boolean isLegacy() {
+        return legacy;
+    }
+
+    public boolean isWindows() {
+        return "win32".equals(os);
+    }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/OsUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/OsUtils.java
new file mode 100644
index 0000000..4944603
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/OsUtils.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2014 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Eclipse Public License version 1.0, available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.keycloak.testsuite.cli;
+
+/**
+ * @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
+ */
+public class OsUtils {
+
+    public static OsArch determineOSAndArch() {
+        String os = System.getProperty("os.name").toLowerCase();
+        String arch = System.getProperty("os.arch");
+
+        //System.out.println("OS: " + os + ", architecture: " + arch);
+        if (arch.equals("amd64")) {
+            arch = "x86_64";
+        }
+
+        if (os.startsWith("linux")) {
+            if (arch.equals("x86") || arch.equals("i386") || arch.equals("i586")) {
+                arch = "i686";
+            }
+            return new OsArch("linux", arch);
+        } else if (os.startsWith("windows")) {
+            if (arch.equals("x86")) {
+                arch = "i386";
+            }
+            if (os.indexOf("2008") != -1 || os.indexOf("2003") != -1 || os.indexOf("vista") != -1) {
+                return new OsArch("win32", arch, true);
+            } else {
+                return new OsArch("win32", arch);
+            }
+        } else if (os.startsWith("sunos")) {
+            return new OsArch("sunos5", "x86_64");
+        } else if (os.startsWith("mac os x")) {
+            return new OsArch("osx", "x86_64");
+        }
+
+        // unsupported platform
+        throw new RuntimeException("Could not determine OS and architecture for this operating system: " + os);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
index 676a40f..89d5c5b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
@@ -18,6 +18,27 @@ package org.keycloak.testsuite;
 
 import org.apache.commons.configuration.ConfigurationException;
 import org.apache.commons.configuration.PropertiesConfiguration;
+import org.apache.http.ssl.SSLContexts;
+import org.keycloak.common.util.KeycloakUriBuilder;
+import org.keycloak.common.util.Time;
+import org.keycloak.testsuite.arquillian.TestContext;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLContext;
+import javax.ws.rs.NotFoundException;
 import org.jboss.arquillian.container.test.api.RunAsClient;
 import org.jboss.arquillian.drone.api.annotation.Drone;
 import org.jboss.arquillian.graphene.page.Page;
@@ -33,8 +54,6 @@ import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.admin.client.resource.RealmsResource;
 import org.keycloak.admin.client.resource.UserResource;
 import org.keycloak.admin.client.resource.UsersResource;
-import org.keycloak.common.util.KeycloakUriBuilder;
-import org.keycloak.common.util.Time;
 import org.keycloak.models.Constants;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
@@ -42,7 +61,6 @@ import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.testsuite.admin.ApiUtil;
 import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
 import org.keycloak.testsuite.arquillian.SuiteContext;
-import org.keycloak.testsuite.arquillian.TestContext;
 import org.keycloak.testsuite.auth.page.AuthRealm;
 import org.keycloak.testsuite.auth.page.AuthServer;
 import org.keycloak.testsuite.auth.page.AuthServerContextRoot;
@@ -56,16 +74,6 @@ import org.keycloak.testsuite.util.TestEventsLogger;
 import org.keycloak.testsuite.util.WaitUtils;
 import org.openqa.selenium.WebDriver;
 
-import javax.ws.rs.NotFoundException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-
 import static org.keycloak.testsuite.admin.Users.setPasswordFor;
 import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN;
 import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
@@ -124,8 +132,12 @@ public abstract class AbstractKeycloakTest {
 
     @Before
     public void beforeAbstractKeycloakTest() throws Exception {
+        SSLContext ssl = null;
+        if ("true".equals(System.getProperty("auth.server.ssl.required"))) {
+            ssl = getSSLContextWithTrustore(new File("src/test/resources/keystore/keycloak.truststore"), "secret");
+        }
         adminClient = Keycloak.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth",
-                MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID);
+                MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID, null, ssl);
 
         getTestingClient();
 
@@ -366,4 +378,14 @@ public abstract class AbstractKeycloakTest {
         }
     }
 
+    public static SSLContext getSSLContextWithTrustore(File file, String password) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException {
+        if (!file.isFile()) {
+            throw new RuntimeException("Truststore file not found: " + file.getAbsolutePath());
+        }
+        SSLContext theContext = SSLContexts.custom()
+                .useProtocol("TLS")
+                .loadTrustMaterial(file, password == null ? null : password.toCharArray())
+                .build();
+        return theContext;
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java
index 8d34b02..0230842 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java
@@ -159,11 +159,7 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
         setConditionalOTPForm(config);
 
         //add skip user attribute to user
-        Map<String, Object> userAttributes = new HashMap<>();
-        List<String> attributeValues = new ArrayList<>();
-        attributeValues.add("skip");
-        userAttributes.put("userSkipAttribute", attributeValues);
-        testUser.setAttributes(userAttributes);
+        testUser.singleAttribute("userSkipAttribute", "skip");
         testRealmResource().users().get(testUser.getId()).update(testUser);
         
         //test OTP is skipped
@@ -182,11 +178,7 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
         setConditionalOTPForm(config);
 
         //add force user attribute to user
-        Map<String, Object> userAttributes = new HashMap<>();
-        List<String> attributeValues = new ArrayList<>();
-        attributeValues.add("force");
-        userAttributes.put("userSkipAttribute", attributeValues);
-        testUser.setAttributes(userAttributes);
+        testUser.singleAttribute("userSkipAttribute", "force");
         testRealmResource().users().get(testUser.getId()).update(testUser);
         
         //test OTP is required
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
index 4d83bd9..7e2614b 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
@@ -72,13 +72,8 @@ public class ProfileTest extends TestRealmKeycloakTest {
         UserRepresentation user = RealmRepUtil.findUser(testRealm, "test-user@localhost");
         user.setFirstName("First");
         user.setLastName("Last");
-        Map<String, Object> attributes = user.getAttributes();
-        if (attributes == null) {
-            attributes = new HashMap<>();
-            user.setAttributes(attributes);
-        }
-        attributes.put("key1", "value1");
-        attributes.put("key2", "value2");
+        user.singleAttribute("key1", "value1");
+        user.singleAttribute("key2", "value2");
 
         UserRepresentation user2 = UserBuilder.create()
                                               .enabled(true)
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java
index 6fbd614..c7da3a9 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java
@@ -94,7 +94,7 @@ public class TermsAndConditionsTest extends TestRealmKeycloakTest {
 
         // assert user attribute is properly set
         UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
-        Map<String,List<String>> attributes = user.getAttributesAsListValues();
+        Map<String,List<String>> attributes = user.getAttributes();
         assertNotNull("timestamp for terms acceptance was not stored in user attributes", attributes);
         List<String> termsAndConditions = attributes.get(TermsAndConditions.USER_ATTRIBUTE);
         assertTrue("timestamp for terms acceptance was not stored in user attributes as "
@@ -128,7 +128,7 @@ public class TermsAndConditionsTest extends TestRealmKeycloakTest {
 
         // assert user attribute is properly removed
         UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
-        Map<String,List<String>> attributes = user.getAttributesAsListValues();
+        Map<String,List<String>> attributes = user.getAttributes();
         if (attributes != null) {
             assertNull("expected null for terms acceptance user attribute " + TermsAndConditions.USER_ATTRIBUTE,
                     attributes.get(TermsAndConditions.USER_ATTRIBUTE));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
index 9db0e51..2906778 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
@@ -61,13 +61,25 @@ import org.keycloak.testsuite.page.AbstractPage;
 import org.keycloak.testsuite.util.IOUtil;
 import org.openqa.selenium.By;
 import org.w3c.dom.Document;
+import org.xml.sax.SAXException;
 
 import javax.ws.rs.client.Client;
 import javax.ws.rs.client.ClientBuilder;
 import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
 import javax.ws.rs.core.Form;
+import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Response;
+import javax.xml.XMLConstants;
+import javax.xml.transform.Source;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.validation.Validator;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
 import java.net.URI;
+import java.net.URL;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -724,6 +736,42 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
         checkLoggedOut(employee2ServletPage, testRealmSAMLPostLoginPage);
     }
 
+    @Test
+    public void idpMetadataValidation() throws Exception {
+        driver.navigate().to(authServerPage.toString() + "/realms/" + SAMLSERVLETDEMO + "/protocol/saml/descriptor");
+        validateXMLWithSchema(driver.getPageSource(), "/adapter-test/keycloak-saml/metadata-schema/saml-schema-metadata-2.0.xsd");
+    }
+
+
+    @Test
+    public void spMetadataValidation() throws Exception {
+        ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), "http://localhost:8081/sales-post-sig/");
+        ClientRepresentation representation = clientResource.toRepresentation();
+        Client client = ClientBuilder.newClient();
+        WebTarget target = client.target(authServerPage.toString() + "/admin/realms/" + SAMLSERVLETDEMO + "/clients/" + representation.getId() + "/installation/providers/saml-sp-descriptor");
+        Response response = target.request().header(HttpHeaders.AUTHORIZATION, "Bearer " + adminClient.tokenManager().getAccessToken().getToken()).get();
+        validateXMLWithSchema(response.readEntity(String.class), "/adapter-test/keycloak-saml/metadata-schema/saml-schema-metadata-2.0.xsd");
+        response.close();
+    }
+
+    private void validateXMLWithSchema(String xml, String schemaFileName) throws SAXException, IOException {
+        URL schemaFile = getClass().getResource(schemaFileName);
+
+        Source xmlFile = new StreamSource(new ByteArrayInputStream(xml.getBytes()), xml);
+        SchemaFactory schemaFactory = SchemaFactory
+                .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+        Schema schema = schemaFactory.newSchema(schemaFile);
+        Validator validator = schema.newValidator();
+        try {
+            validator.validate(xmlFile);
+            System.out.println(xmlFile.getSystemId() + " is valid");
+        } catch (SAXException e) {
+            System.out.println(xmlFile.getSystemId() + " is NOT valid");
+            System.out.println("Reason: " + e.getLocalizedMessage());
+            Assert.fail();
+        }
+    }
+
     private void createProtocolMapper(ProtocolMappersResource resource, String name, String protocol, String protocolMapper, Map<String, String> config) {
         ProtocolMapperRepresentation representation = new ProtocolMapperRepresentation();
         representation.setName(name);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index c3f6c0d..81af8c6 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -417,19 +417,19 @@ public class UserTest extends AbstractAdminTest {
         List<String> vals = new ArrayList<>();
         vals.add("value2user2");
         vals.add("value2user2_2");
-        user2.getAttributesAsListValues().put("attr2", vals);
+        user2.getAttributes().put("attr2", vals);
 
         String user2Id = createUser(user2);
 
         user1 = realm.users().get(user1Id).toRepresentation();
-        assertEquals(2, user1.getAttributesAsListValues().size());
-        assertAttributeValue("value1user1", user1.getAttributesAsListValues().get("attr1"));
-        assertAttributeValue("value2user1", user1.getAttributesAsListValues().get("attr2"));
+        assertEquals(2, user1.getAttributes().size());
+        assertAttributeValue("value1user1", user1.getAttributes().get("attr1"));
+        assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
 
         user2 = realm.users().get(user2Id).toRepresentation();
-        assertEquals(2, user2.getAttributesAsListValues().size());
-        assertAttributeValue("value1user2", user2.getAttributesAsListValues().get("attr1"));
-        vals = user2.getAttributesAsListValues().get("attr2");
+        assertEquals(2, user2.getAttributes().size());
+        assertAttributeValue("value1user2", user2.getAttributes().get("attr1"));
+        vals = user2.getAttributes().get("attr2");
         assertEquals(2, vals.size());
         assertTrue(vals.contains("value2user2") && vals.contains("value2user2_2"));
 
@@ -439,18 +439,18 @@ public class UserTest extends AbstractAdminTest {
         updateUser(realm.users().get(user1Id), user1);
 
         user1 = realm.users().get(user1Id).toRepresentation();
-        assertEquals(3, user1.getAttributesAsListValues().size());
-        assertAttributeValue("value3user1", user1.getAttributesAsListValues().get("attr1"));
-        assertAttributeValue("value2user1", user1.getAttributesAsListValues().get("attr2"));
-        assertAttributeValue("value4user1", user1.getAttributesAsListValues().get("attr3"));
+        assertEquals(3, user1.getAttributes().size());
+        assertAttributeValue("value3user1", user1.getAttributes().get("attr1"));
+        assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
+        assertAttributeValue("value4user1", user1.getAttributes().get("attr3"));
 
         user1.getAttributes().remove("attr1");
         updateUser(realm.users().get(user1Id), user1);
 
         user1 = realm.users().get(user1Id).toRepresentation();
-        assertEquals(2, user1.getAttributesAsListValues().size());
-        assertAttributeValue("value2user1", user1.getAttributesAsListValues().get("attr2"));
-        assertAttributeValue("value4user1", user1.getAttributesAsListValues().get("attr3"));
+        assertEquals(2, user1.getAttributes().size());
+        assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
+        assertAttributeValue("value4user1", user1.getAttributes().get("attr3"));
 
         user1.getAttributes().clear();
         updateUser(realm.users().get(user1Id), user1);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractCliTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractCliTest.java
new file mode 100644
index 0000000..01ef404
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractCliTest.java
@@ -0,0 +1,539 @@
+package org.keycloak.testsuite.cli.registration;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.keycloak.admin.client.resource.ClientInitialAccessResource;
+import org.keycloak.admin.client.resource.ClientRegistrationTrustedHostResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
+import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
+import org.keycloak.client.registration.cli.config.ConfigData;
+import org.keycloak.client.registration.cli.config.FileConfigHandler;
+import org.keycloak.client.registration.cli.config.RealmConfigData;
+import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
+import org.keycloak.representations.idm.ClientInitialAccessPresentation;
+import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ComponentRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
+import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager;
+import org.keycloak.services.clientregistration.policy.RegistrationAuth;
+import org.keycloak.services.clientregistration.policy.impl.TrustedHostClientRegistrationPolicyFactory;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.cli.KcRegExec;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.JsonSerialization;
+
+import javax.ws.rs.core.Response;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+import static org.keycloak.testsuite.cli.KcRegExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractCliTest extends AbstractKeycloakTest {
+
+    protected String serverUrl = isAuthServerSSL() ?
+            "https://localhost:" + getAuthServerHttpsPort() + "/auth" :
+            "http://localhost:" + getAuthServerHttpPort() + "/auth";
+
+
+    @Before
+    public void deleteDefaultConfig() {
+        getDefaultConfigFilePath().delete();
+    }
+
+    static boolean runIntermittentlyFailingTests() {
+        return "true".equals(System.getProperty("test.intermittent"));
+    }
+
+    static boolean isAuthServerSSL() {
+        return "true".equals(System.getProperty("auth.server.ssl.required"));
+    }
+
+    static int getAuthServerHttpsPort() {
+        try {
+            return Integer.valueOf(System.getProperty("auth.server.https.port"));
+        } catch (Exception e) {
+            throw new RuntimeException("System property 'auth.server.https.port' not set or invalid: '"
+                    + System.getProperty("auth.server.https.port") + "'");
+        }
+    }
+
+    static int getAuthServerHttpPort() {
+        try {
+            return Integer.valueOf(System.getProperty("auth.server.http.port"));
+        } catch (Exception e) {
+            throw new RuntimeException("System property 'auth.server.http.port' not set or invalid: '"
+                    + System.getProperty("auth.server.http.port") + "'");
+        }
+    }
+
+    static File getDefaultConfigFilePath() {
+        return new File(System.getProperty("user.home") + "/.keycloak/kcreg.config");
+    }
+
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
+        testRealms.add(realmRepresentation);
+
+        // create admin user account with permissions to manage clients
+        UserRepresentation admin = UserBuilder.create()
+                .username("user1")
+                .password("userpass")
+                .enabled(true)
+                .build();
+        HashMap<String, List<String>> clientRoles = new HashMap<>();
+        clientRoles.put("realm-management", Arrays.asList("manage-clients"));
+        admin.setClientRoles(clientRoles);
+        realmRepresentation.getUsers().add(admin);
+
+
+
+        // create client with service account to use Signed JWT credentials with
+        ClientRepresentation regClient = ClientBuilder.create()
+                .clientId("reg-cli-jwt")
+                .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFXUhpRTTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdyZWctY2xpMB4XDTE2MDkyMjEzMzIxOFoXDTI2MDkyMjEzMzM1OFowEjEQMA4GA1UEAwwHcmVnLWNsaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMHZn/0Bk1M9oKcTHxzn2cGvBWwO1m6OVLQ8LSVwNIf4ixfGkVIkhI5iEGYND+uD8ame54ZPClTVxMra3JldClLIG+L+ymnbT2vKIhEsVvCROs9PnYxbFALt1dXneLIio2uzF+d7/zQWlmeaWfNunSJT1aHNJDkGgDeUuQa25b0IMqsFjsN8Dg4ATkA97r3wKn4Tp3SE7sTM/B2pmra4atNxGeShVrgihqUiQ/PwDiDGwry64AsexkZnQsCR3bJWBAVUiHef3JWzTfWWN5bfCBG6Mnq1xw7YN+YpV1nR3CGmcKJuLe6aTe7Ps8hYejYiQA7Mp7ZQsoImsVFV5HDOlb0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZl8XvLfKXTPYvq/QyHOg7EDlAdlV3HkmHP9SBAV4BccmHmorMkm5I6I21UA5mfju+0nhbEd0bm0kvJFxIfNU6lJyyVvQx3Gns37KYUOzIV/ocWZuOTBLp5tfIBYbBwfE/s1J4PhpA/3WhBY9JKiLvdJfxECGIgaLs2M0UsylW/7o04+18Od8j/m7crQc7fpe5gJB5m/+hxUDowIjG5CumffX9OHYGDvHBpaUl7QNSGgjP8Bn9ogmIMUBJ7XSYUcohKuk2Cnj6p+GlLuqHbOISUXLVjf0DxhCu6diVxvacKbgAZmyCIO1tGL/UVRxg9GOYdCiC9vHfPuZ8US+ZB0P9g==")
+                .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
+                .serviceAccount()
+                .build();
+
+        realmRepresentation.getClients().add(regClient);
+
+        // create service account for client reg-cli with permissions to manage clients
+        addServiceAccount(realmRepresentation, "reg-cli-jwt");
+
+
+
+        // create client to use with user account - enable direct grants
+        regClient = ClientBuilder.create()
+                .clientId("reg-cli-jwt-direct")
+                .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFXUhpRTTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdyZWctY2xpMB4XDTE2MDkyMjEzMzIxOFoXDTI2MDkyMjEzMzM1OFowEjEQMA4GA1UEAwwHcmVnLWNsaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMHZn/0Bk1M9oKcTHxzn2cGvBWwO1m6OVLQ8LSVwNIf4ixfGkVIkhI5iEGYND+uD8ame54ZPClTVxMra3JldClLIG+L+ymnbT2vKIhEsVvCROs9PnYxbFALt1dXneLIio2uzF+d7/zQWlmeaWfNunSJT1aHNJDkGgDeUuQa25b0IMqsFjsN8Dg4ATkA97r3wKn4Tp3SE7sTM/B2pmra4atNxGeShVrgihqUiQ/PwDiDGwry64AsexkZnQsCR3bJWBAVUiHef3JWzTfWWN5bfCBG6Mnq1xw7YN+YpV1nR3CGmcKJuLe6aTe7Ps8hYejYiQA7Mp7ZQsoImsVFV5HDOlb0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZl8XvLfKXTPYvq/QyHOg7EDlAdlV3HkmHP9SBAV4BccmHmorMkm5I6I21UA5mfju+0nhbEd0bm0kvJFxIfNU6lJyyVvQx3Gns37KYUOzIV/ocWZuOTBLp5tfIBYbBwfE/s1J4PhpA/3WhBY9JKiLvdJfxECGIgaLs2M0UsylW/7o04+18Od8j/m7crQc7fpe5gJB5m/+hxUDowIjG5CumffX9OHYGDvHBpaUl7QNSGgjP8Bn9ogmIMUBJ7XSYUcohKuk2Cnj6p+GlLuqHbOISUXLVjf0DxhCu6diVxvacKbgAZmyCIO1tGL/UVRxg9GOYdCiC9vHfPuZ8US+ZB0P9g==")
+                .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
+                .directAccessGrants()
+                .build();
+
+        realmRepresentation.getClients().add(regClient);
+
+
+
+
+        // create client with service account to use client secret with
+        regClient = ClientBuilder.create()
+                .clientId("reg-cli-secret")
+                .secret("password")
+                .authenticatorType(ClientIdAndSecretAuthenticator.PROVIDER_ID)
+                .serviceAccount()
+                .build();
+
+        realmRepresentation.getClients().add(regClient);
+
+        // create service account for client reg-cli with permissions to manage clients
+        addServiceAccount(realmRepresentation, "reg-cli-secret");
+
+
+
+
+        // create client to use with user account - enable direct grants
+        regClient = ClientBuilder.create()
+                .clientId("reg-cli-secret-direct")
+                .secret("password")
+                .authenticatorType(ClientIdAndSecretAuthenticator.PROVIDER_ID)
+                .directAccessGrants()
+                .build();
+
+        realmRepresentation.getClients().add(regClient);
+
+    }
+
+
+    void loginAsUser(File configFile, String server, String realm, String user, String password) {
+
+        KcRegExec exe = execute("config credentials --server " + server + " --realm " + realm +
+                " --user " + user + " --password " + password + " --config " + configFile.getAbsolutePath());
+
+        Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+
+        List<String> lines = exe.stdoutLines();
+        Assert.assertTrue("stdout output empty", lines.size() == 0);
+
+        lines = exe.stderrLines();
+        Assert.assertTrue("stderr output one line", lines.size() == 1);
+        Assert.assertEquals("stderr first line", "Logging into " + server + " as user " + user + " of realm " + realm, lines.get(0));
+    }
+
+    void assertFieldsEqualWithExclusions(ConfigData config1, ConfigData config2, String ... excluded) {
+
+        HashSet<String> exclusions = new HashSet<>(Arrays.asList(excluded));
+
+        if (!exclusions.contains("serverUrl")) {
+            Assert.assertEquals("serverUrl", config1.getServerUrl(), config2.getServerUrl());
+        }
+        if (!exclusions.contains("realm")) {
+            Assert.assertEquals("realm", config1.getRealm(), config2.getRealm());
+        }
+        if (!exclusions.contains("truststore")) {
+            Assert.assertEquals("truststore", config1.getTruststore(), config2.getTruststore());
+        }
+        if (!exclusions.contains("endpoints")) {
+            Map<String, Map<String, RealmConfigData>> endp1 = config1.getEndpoints();
+            Map<String, Map<String, RealmConfigData>> endp2 = config2.getEndpoints();
+
+            Iterator<Map.Entry<String, Map<String, RealmConfigData>>> it1 = endp1.entrySet().iterator();
+            Iterator<Map.Entry<String, Map<String, RealmConfigData>>> it2 = endp2.entrySet().iterator();
+
+            while (it1.hasNext()) {
+                Map.Entry<String, Map<String, RealmConfigData>> ent1 = it1.next();
+                Map.Entry<String, Map<String, RealmConfigData>> ent2 = it2.next();
+
+                String serverUrl = ent1.getKey();
+                String endpskey = "endpoints." + serverUrl;
+                if (!exclusions.contains(endpskey)) {
+                    Assert.assertEquals(endpskey, ent1.getKey(), ent2.getKey());
+
+                    Map<String, RealmConfigData> realms1 = ent1.getValue();
+                    Map<String, RealmConfigData> realms2 = ent2.getValue();
+
+                    Iterator<Map.Entry<String, RealmConfigData>> rit1 = realms1.entrySet().iterator();
+                    Iterator<Map.Entry<String, RealmConfigData>> rit2 = realms2.entrySet().iterator();
+
+                    while (rit1.hasNext()) {
+                        Map.Entry<String, RealmConfigData> rent1 = rit1.next();
+                        Map.Entry<String, RealmConfigData> rent2 = rit2.next();
+
+                        String realm = rent1.getKey();
+                        String rkey = endpskey + "." + realm;
+                        if (!exclusions.contains(endpskey)) {
+                            Assert.assertEquals(rkey, rent1.getKey(), rent2.getKey());
+
+                            RealmConfigData rdata1 = rent1.getValue();
+                            RealmConfigData rdata2 = rent2.getValue();
+
+                            assertFieldsEqualWithExclusions(serverUrl, realm, rdata1, rdata2, excluded);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    void assertFieldsEqualWithExclusions(RealmConfigData data1, RealmConfigData data2, String ... excluded) {
+        assertFieldsEqualWithExclusions(null, null, data1, data2, excluded);
+    }
+
+    void assertFieldsEqualWithExclusions(String server, String realm, RealmConfigData data1, RealmConfigData data2, String ... excluded) {
+
+        HashSet<String> exclusions = new HashSet<>(Arrays.asList(excluded));
+
+        String pfix = "";
+        if (server != null || realm != null) {
+            pfix = "endpoints." + server + "." + realm + ".";
+        }
+
+        String ekey = pfix + "serverUrl";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.serverUrl(), data2.serverUrl());
+        }
+
+        ekey = pfix + "realm";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.realm(), data2.realm());
+        }
+
+        ekey = pfix + "clientId";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getClientId(), data2.getClientId());
+        }
+
+        ekey = pfix + "initialToken";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getInitialToken(), data2.getInitialToken());
+        }
+
+        ekey = pfix + "token";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getToken(), data2.getToken());
+        }
+
+        ekey = pfix + "refreshToken";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getRefreshToken(), data2.getRefreshToken());
+        }
+
+        ekey = pfix + "expiresAt";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getExpiresAt(), data2.getExpiresAt());
+        }
+
+        ekey = pfix + "refreshExpiresAt";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getRefreshExpiresAt(), data2.getRefreshExpiresAt());
+        }
+
+        ekey = pfix + "secret";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getSecret(), data2.getSecret());
+        }
+
+        ekey = pfix + "signingToken";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getSigningToken(), data2.getSigningToken());
+        }
+
+        ekey = pfix + "sigExpiresAt";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getSigExpiresAt(), data2.getSigExpiresAt());
+        }
+
+        ekey = pfix + "clients";
+        if (!exclusions.contains(ekey)) {
+            Map<String, String> clients1 = data1.getClients();
+            Map<String, String> clients2 = data2.getClients();
+
+            Iterator<Map.Entry<String, String>> cit1 = clients1.entrySet().iterator();
+            Iterator<Map.Entry<String, String>> cit2 = clients2.entrySet().iterator();
+
+            while (cit1.hasNext() || cit2.hasNext()) {
+                Map.Entry<String, String> ckey1 = cit1.hasNext() ? cit1.next() : null;
+                Map.Entry<String, String> ckey2 = cit2.hasNext() ? cit2.next() : null;
+
+                String ckey = ekey + "." + (ckey1 != null ? ckey1.getKey() : ckey2.getKey());
+                if (!exclusions.contains(ckey)) {
+                    Assert.assertNotNull(ckey + " left not null", ckey1);
+                    Assert.assertNotNull(ckey + " right not null", ckey2);
+                    Assert.assertEquals(ckey, ckey1.getKey(), ckey2.getKey());
+                    Assert.assertEquals(ckey + " value", ckey1.getValue(), ckey2.getValue());
+                }
+            }
+        }
+    }
+
+    void addServiceAccount(RealmRepresentation realm, String clientId) {
+
+        UserRepresentation account = UserBuilder.create()
+                .username("service-account-" + clientId)
+                .enabled(true)
+                .serviceAccountId(clientId)
+                .build();
+
+        HashMap<String, List<String>> clientRoles = new HashMap<>();
+        clientRoles.put("realm-management", Arrays.asList("manage-clients"));
+
+        account.setClientRoles(clientRoles);
+
+        realm.getUsers().add(account);
+    }
+
+
+    void waitFor(long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (InterruptedException e) {
+            throw new RuntimeException("Interrupted");
+        }
+    }
+
+
+    FileConfigHandler initCustomConfigFile() {
+        String filename = UUID.randomUUID().toString() + ".config";
+        File cfgFile = new File(KcRegExec.WORK_DIR + "/" + filename);
+        FileConfigHandler handler = new FileConfigHandler();
+        handler.setConfigFile(cfgFile.getAbsolutePath());
+        return handler;
+    }
+
+    File initTempFile(String extension) throws IOException {
+        return initTempFile(extension, null);
+    }
+
+    File initTempFile(String extension, String content) throws IOException {
+        String filename = UUID.randomUUID().toString() + extension;
+        File file = new File(KcRegExec.WORK_DIR + "/" + filename);
+        if (content != null) {
+            OutputStream os = new FileOutputStream(file);
+            os.write(content.getBytes(Charset.forName("iso_8859_1")));
+            os.close();
+        }
+        return file;
+    }
+
+    String issueInitialAccessToken(String realm) {
+        ClientInitialAccessResource resource = adminClient.realm(realm).clientInitialAccess();
+
+        ClientInitialAccessCreatePresentation rep = new ClientInitialAccessCreatePresentation();
+        rep.setCount(10);
+        rep.setExpiration(100);
+
+        ClientInitialAccessPresentation response = resource.create(rep);
+
+        String token = response.getToken();
+        Assert.assertNotNull("Issued initial access token not null", token);
+        return token;
+    }
+
+    private ComponentRepresentation findPolicyByProviderAndAuth(String realm, String providerId, String authType) {
+        // Change the policy to avoid checking hosts
+        List<ComponentRepresentation> reps = adminClient.realm(realm).components().query(realm, ClientRegistrationPolicy.class.getName());
+        for (ComponentRepresentation rep : reps) {
+            if (rep.getSubType().equals(authType) && rep.getProviderId().equals(providerId)) {
+                return rep;
+            }
+        }
+        return null;
+    }
+
+    void addLocalhostToAllowedHosts(String realm) {
+        RealmResource realmResource = adminClient.realm(realm);
+        String anonPolicy = ClientRegistrationPolicyManager.getComponentTypeKey(RegistrationAuth.ANONYMOUS);
+
+        ComponentRepresentation trustedHostRep = findPolicyByProviderAndAuth(realm, TrustedHostClientRegistrationPolicyFactory.PROVIDER_ID, anonPolicy);
+        trustedHostRep.getConfig().putSingle(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS, "localhost");
+        realmResource.components().component(trustedHostRep.getId()).update(trustedHostRep);
+    }
+
+    void testCRUDWithOnTheFlyAuth(String serverUrl, String credentials, String extraOptions, String loginMessage) throws IOException {
+
+        File configFile = getDefaultConfigFilePath();
+        long lastModified = configFile.exists() ? configFile.lastModified() : 0;
+
+        // This test assumes it is the only user of any instance of on the system
+        KcRegExec exe = execute("create --no-config --server " + serverUrl +
+                " --realm test " + credentials + " " + extraOptions + " -s clientId=test-client -o");
+
+        Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+        Assert.assertEquals("login message", loginMessage, exe.stderrLines().get(0));
+
+        ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+        Assert.assertEquals("clientId", "test-client", client.getClientId());
+        Assert.assertNotNull("registrationAccessToken not null", client.getRegistrationAccessToken());
+
+        long lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+        Assert.assertEquals("config file not modified", lastModified, lastModified2);
+
+
+
+
+        exe = execute("get test-client --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
+
+        Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+
+        ClientRepresentation client2 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+        Assert.assertEquals("clientId", "test-client", client2.getClientId());
+
+        // we did not provide a token, thus no registrationAccessToken is present
+        Assert.assertNull("registrationAccessToken is null", client2.getRegistrationAccessToken());
+
+        lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+        Assert.assertEquals("config file not modified", lastModified, lastModified2);
+
+
+
+
+
+        // the token works even though an intermediary invocation was performed,
+        // because the previous invocation didn't use a registration access token
+        exe = execute("get test-client --no-config --server " + serverUrl + " --realm test " + extraOptions + " -t " + client.getRegistrationAccessToken());
+
+        Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+
+        ClientRepresentation client3 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+        Assert.assertEquals("clientId", "test-client", client3.getClientId());
+
+        Assert.assertNotEquals("registrationAccessToken in returned json is different than one returned by create",
+                client.getRegistrationAccessToken(), client3.getRegistrationAccessToken());
+
+        lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+        Assert.assertEquals("config file not modified", lastModified, lastModified2);
+
+
+
+
+
+
+        exe = execute("update test-client --no-config --server " + serverUrl + " --realm test " +
+                credentials + " " + extraOptions + " -s enabled=false -o --unsafe");
+
+        Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+
+        ClientRepresentation client4 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+        Assert.assertEquals("clientId", "test-client", client4.getClientId());
+        Assert.assertFalse("enabled", client4.isEnabled());
+
+        Assert.assertNull("registrationAccessToken in null", client4.getRegistrationAccessToken());
+
+        lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+        Assert.assertEquals("config file not modified", lastModified, lastModified2);
+
+
+
+
+
+
+
+        exe = execute("update test-client --no-config --server " + serverUrl + " --realm test " + extraOptions +
+                " -s enabled=true -o -t " + client3.getRegistrationAccessToken());
+
+        Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+
+        ClientRepresentation client5 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+        Assert.assertEquals("clientId", "test-client", client5.getClientId());
+        Assert.assertTrue("enabled", client5.isEnabled());
+
+        Assert.assertNotEquals("registrationAccessToken in returned json is different than one returned by get",
+                client3.getRegistrationAccessToken(), client5.getRegistrationAccessToken());
+
+        lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+        Assert.assertEquals("config file not modified", lastModified, lastModified2);
+
+
+
+
+
+
+
+        exe = execute("delete test-client --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
+
+        assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+
+        lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+        Assert.assertEquals("config file not modified", lastModified, lastModified2);
+
+
+
+
+
+        // subsequent delete should fail
+        exe = execute("delete test-client --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("error message", "Client not found [invalid_request]", exe.stderrLines().get(1));
+
+        lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+        Assert.assertEquals("config file not modified", lastModified, lastModified2);
+    }
+
+    void assertExitCodeAndStreamSizes(KcRegExec exe, int exitCode, int stdOutLineCount, int stdErrLineCount) {
+        Assert.assertEquals("exitCode == " + exitCode, exitCode, exe.exitCode());
+        Assert.assertTrue("stdout output has " + stdOutLineCount + " lines", exe.stdoutLines().size() == stdOutLineCount);
+        Assert.assertTrue("stderr output has " + stdErrLineCount + " lines", exe.stderrLines().size() == stdErrLineCount);
+
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java
new file mode 100644
index 0000000..bd8fc7c
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java
@@ -0,0 +1,69 @@
+package org.keycloak.testsuite.cli.registration;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.registration.cli.config.FileConfigHandler;
+import org.keycloak.testsuite.cli.KcRegExec;
+import org.keycloak.testsuite.util.TempFileResource;
+
+import java.io.IOException;
+
+import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
+import static org.keycloak.testsuite.cli.KcRegExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcRegConfigTest extends AbstractCliTest {
+
+    @Test
+    public void testRegistrationToken() throws IOException {
+
+        FileConfigHandler handler = initCustomConfigFile();
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+            // forget --server
+            KcRegExec exe = execute("config registration-token --config '" + configFile.getName() + "' ");
+            assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+            Assert.assertEquals("error message", "Required option not specified: --server", exe.stderrLines().get(0));
+
+            // forget --realm
+            exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth");
+            assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+            Assert.assertEquals("error message", "Required option not specified: --realm", exe.stderrLines().get(0));
+
+            // forget --client
+            exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth --realm test");
+            assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+            Assert.assertEquals("error message", "Required option not specified: --client", exe.stderrLines().get(0));
+
+            // specify token on cmdline
+            exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth --realm test --client my_client NEWTOKEN");
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+            if (runIntermittentlyFailingTests()) {
+                // don't specify token - must be prompted for it
+                exe = KcRegExec.newBuilder()
+                        .argsLine("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth --realm test --client my_client")
+                        .executeAsync();
+
+                exe.waitForStdout("Enter Registration Access Token:");
+                exe.sendToStdin("NEWTOKEN" + EOL);
+                exe.waitCompletion();
+                assertExitCodeAndStreamSizes(exe, 0, 1, 0);
+
+            } else {
+                System.out.println("TEST SKIPPED PARTIALLY - This test currently suffers from intermittent failures. Use -Dtest.intermittent=true to run it in full.");
+            }
+
+            // delete non-existent token
+            exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth --realm test --client nonexistent --delete");
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+            // delete token
+            exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth --realm test --client my_client --delete");
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+        }
+    }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegCreateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegCreateTest.java
new file mode 100644
index 0000000..6b6cb27
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegCreateTest.java
@@ -0,0 +1,231 @@
+package org.keycloak.testsuite.cli.registration;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.registration.cli.config.ConfigData;
+import org.keycloak.client.registration.cli.config.FileConfigHandler;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+import org.keycloak.testsuite.cli.KcRegExec;
+import org.keycloak.testsuite.util.TempFileResource;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+
+import static org.keycloak.testsuite.cli.KcRegExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcRegCreateTest extends AbstractCliTest {
+
+    @Test
+    public void testCreateWithRealmOverride() throws IOException {
+
+        FileConfigHandler handler = initCustomConfigFile();
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+            // authenticate as a regular user against one realm
+            KcRegExec exe = execute("config credentials -x --config '" + configFile.getName() +
+                    "' --server " + serverUrl + " --realm master --user admin --password admin");
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+
+            // use initial token of another realm with server, and realm override
+            String token = issueInitialAccessToken("test");
+            exe = execute("create --config '" + configFile.getName() + "' --server " + serverUrl + " --realm test -s clientId=my_first_client -t " + token);
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+        }
+    }
+
+
+    @Test
+    public void testCreateThoroughly() throws IOException {
+
+        FileConfigHandler handler = initCustomConfigFile();
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+            // set initial access token in config
+            String token = issueInitialAccessToken("test");
+
+            final String realm = "test";
+
+            KcRegExec exe = execute("config initial-token -x --config '" + configFile.getName() +
+                    "' --server " + serverUrl + " --realm " + realm + " " + token);
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+
+            // check that current server, realm, and initial token are saved in the file
+            ConfigData config = handler.loadConfig();
+            Assert.assertEquals("Config serverUrl", serverUrl, config.getServerUrl());
+            Assert.assertEquals("Config realm", realm, config.getRealm());
+            Assert.assertEquals("Config initial access token", token, config.ensureRealmConfigData(serverUrl, realm).getInitialToken());
+
+            // create configuration from file using stdin redirect ... output an object
+            String content = "{\n" +
+                    "        \"clientId\": \"my_client\",\n" +
+                    "        \"enabled\": true,\n" +
+                    "        \"redirectUris\": [\"http://localhost:8980/myapp/*\"],\n" +
+                    "        \"serviceAccountsEnabled\": true,\n" +
+                    "        \"name\": \"My Client App\",\n" +
+                    "        \"implicitFlowEnabled\": false,\n" +
+                    "        \"publicClient\": true,\n" +
+                    "        \"protocol\": \"leycloak-oidc\",\n" +
+                    "        \"webOrigins\": [\"http://localhost:8980/myapp\"],\n" +
+                    "        \"consentRequired\": false,\n" +
+                    "        \"baseUrl\": \"http://localhost:8980/myapp\",\n" +
+                    "        \"rootUrl\": \"http://localhost:8980/myapp\",\n" +
+                    "        \"bearerOnly\": true,\n" +
+                    "        \"standardFlowEnabled\": true\n" +
+                    "}";
+
+            try (TempFileResource tmpFile = new TempFileResource(initTempFile(".json", content))) {
+
+                exe = execute("create --config '" + configFile.getName() + "' -o -f - < '" + tmpFile.getName() + "'");
+
+                Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+                Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
+
+                ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+                Assert.assertNotNull("id", client.getId());
+                Assert.assertEquals("clientId", "my_client", client.getClientId());
+                Assert.assertEquals("enabled", true, client.isEnabled());
+                Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
+                Assert.assertEquals("serviceAccountsEnabled", true, client.isServiceAccountsEnabled());
+                Assert.assertEquals("name", "My Client App", client.getName());
+                Assert.assertEquals("implicitFlowEnabled", false, client.isImplicitFlowEnabled());
+                Assert.assertEquals("publicClient", true, client.isPublicClient());
+                // note there is no server-side check if protocol is supported
+                Assert.assertEquals("protocol", "leycloak-oidc", client.getProtocol());
+                Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8980/myapp"), client.getWebOrigins());
+                Assert.assertEquals("consentRequired", false, client.isConsentRequired());
+                Assert.assertEquals("baseUrl", "http://localhost:8980/myapp", client.getBaseUrl());
+                Assert.assertEquals("rootUrl", "http://localhost:8980/myapp", client.getRootUrl());
+                Assert.assertEquals("bearerOnly", true, client.isStandardFlowEnabled());
+                Assert.assertFalse("mappers not empty", client.getProtocolMappers().isEmpty());
+
+                // create configuration from file as a template and override clientId and other attributes ... output an object
+                exe = execute("create --config '" + configFile.getName() + "' -o -f '" + tmpFile.getName() +
+                        "' -s clientId=my_client2 -s enabled=false -s 'redirectUris=[\"http://localhost:8980/myapp2/*\"]'" +
+                        " -s 'name=My Client App II' -s protocol=keycloak-oidc -s 'webOrigins=[\"http://localhost:8980/myapp2\"]'" +
+                        " -s baseUrl=http://localhost:8980/myapp2 -s rootUrl=http://localhost:8980/myapp2");
+
+                Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+                Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
+
+                ClientRepresentation client2 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+                Assert.assertNotNull("id", client2.getId());
+                Assert.assertEquals("clientId", "my_client2", client2.getClientId());
+                Assert.assertEquals("enabled", false, client2.isEnabled());
+                Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp2/*"), client2.getRedirectUris());
+                Assert.assertEquals("serviceAccountsEnabled", true, client2.isServiceAccountsEnabled());
+                Assert.assertEquals("name", "My Client App II", client2.getName());
+                Assert.assertEquals("implicitFlowEnabled", false, client2.isImplicitFlowEnabled());
+                Assert.assertEquals("publicClient", true, client2.isPublicClient());
+                Assert.assertEquals("protocol", "keycloak-oidc", client2.getProtocol());
+                Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8980/myapp2"), client2.getWebOrigins());
+                Assert.assertEquals("consentRequired", false, client2.isConsentRequired());
+                Assert.assertEquals("baseUrl", "http://localhost:8980/myapp2", client2.getBaseUrl());
+                Assert.assertEquals("rootUrl", "http://localhost:8980/myapp2", client2.getRootUrl());
+                Assert.assertEquals("bearerOnly", true, client2.isStandardFlowEnabled());
+                Assert.assertFalse("mappers not empty", client2.getProtocolMappers().isEmpty());
+
+
+                // check that using an invalid attribute key is not ignored
+                exe = execute("create --config '" + configFile.getName() + "' -o -f '" + tmpFile.getName() + "' -s client_id=my_client3");
+
+                Assert.assertEquals("exitCode == 1", 1, exe.exitCode());
+                Assert.assertEquals("stderr has one line", 1, exe.stderrLines().size());
+                Assert.assertEquals("Failed to set attribute 'client_id' on document type 'default'", exe.stderrLines().get(0));
+            }
+
+            // simple create, output an id
+            exe = execute("create --config '" + configFile.getName() + "' -i -s clientId=my_client3");
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+            Assert.assertEquals("stderr is empty", 0, exe.stderrLines().size());
+
+            Assert.assertEquals("stdout has 1 line", 1, exe.stdoutLines().size());
+            Assert.assertEquals("only clientId returned", "my_client3", exe.stdoutLines().get(0));
+
+            // simple create, default output
+            exe = execute("create --config '" + configFile.getName() + "' -s clientId=my_client4");
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+            Assert.assertEquals("stderr has 1 line", 1, exe.stderrLines().size());
+            Assert.assertEquals("only clientId returned", "Registered new client with client_id 'my_client4'", exe.stderrLines().get(0));
+
+            Assert.assertEquals("stdout is empty", 0, exe.stdoutLines().size());
+
+
+
+            // create using oidc endpoint - autodetect format
+            content = "        {\n" +
+                    "            \"redirect_uris\" : [ \"http://localhost:8980/myapp/*\" ],\n" +
+                    "            \"grant_types\" : [ \"authorization_code\", \"client_credentials\", \"refresh_token\" ],\n" +
+                    "            \"response_types\" : [ \"code\", \"none\" ],\n" +
+                    "            \"client_name\" : \"My Client App\",\n" +
+                    "            \"client_uri\" : \"http://localhost:8980/myapp\"\n" +
+                    "        }";
+
+            try (TempFileResource tmpFile = new TempFileResource(initTempFile(".json", content))) {
+
+                exe = execute("create --config '" + configFile.getName() + "' -s 'client_name=My Client App V' " +
+                        " -s 'redirect_uris=[\"http://localhost:8980/myapp5/*\"]' -s client_uri=http://localhost:8980/myapp5" +
+                        " -o -f - < '" + tmpFile.getName() + "'");
+
+                Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+                Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
+
+                OIDCClientRepresentation client = JsonSerialization.readValue(exe.stdout(), OIDCClientRepresentation.class);
+
+                Assert.assertNotNull("clientId", client.getClientId());
+                Assert.assertEquals("redirect_uris", Arrays.asList("http://localhost:8980/myapp5/*"), client.getRedirectUris());
+                Assert.assertEquals("grant_types", Arrays.asList("authorization_code", "client_credentials", "refresh_token"), client.getGrantTypes());
+                Assert.assertEquals("response_types", Arrays.asList("code", "none"), client.getResponseTypes());
+                Assert.assertEquals("client_name", "My Client App V", client.getClientName());
+                Assert.assertEquals("client_uri", "http://localhost:8980/myapp5", client.getClientUri());
+
+
+
+                // try use incompatible endpoint override
+                exe = execute("create --config '" + configFile.getName() + "' -e default -f '" + tmpFile.getName() + "'");
+
+                Assert.assertEquals("exitCode == 1", 1, exe.exitCode());
+                Assert.assertFalse("stderr not empty", exe.stderrLines().isEmpty());
+                Assert.assertEquals("Error message", "Attribute 'redirect_uris' not supported on document type 'default'", exe.stderrLines().get(0));
+
+            }
+
+
+            // test create saml formated xml - format autodetection
+            File samlSpMetaFile = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcreg/saml-sp-metadata.xml");
+            Assert.assertTrue("saml-sp-metadata.xml exists", samlSpMetaFile.isFile());
+
+            exe = execute("create --config '" + configFile.getName() + "' -o -f - < '" + samlSpMetaFile.getAbsolutePath() + "'");
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+            Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
+
+            ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+            Assert.assertNotNull("id", client.getId());
+            Assert.assertEquals("clientId", "http://localhost:8080/sales-post-enc/", client.getClientId());
+            Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8081/sales-post-enc/saml"), client.getRedirectUris());
+            Assert.assertEquals("attributes.saml_name_id_format", "username", client.getAttributes().get("saml_name_id_format"));
+            Assert.assertEquals("attributes.saml_assertion_consumer_url_post", "http://localhost:8081/sales-post-enc/saml", client.getAttributes().get("saml_assertion_consumer_url_post"));
+            Assert.assertEquals("attributes.saml.signature.algorithm", "RSA_SHA256", client.getAttributes().get("saml.signature.algorithm"));
+
+
+            // delete initial token
+            exe = execute("config initial-token --config '" + configFile.getName() + "' --server " + serverUrl + " --realm " + realm + " --delete");
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+            config = handler.loadConfig();
+            Assert.assertNull("initial token == null", config.ensureRealmConfigData(serverUrl, realm).getInitialToken());
+        }
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java
new file mode 100644
index 0000000..b4ea473
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java
@@ -0,0 +1,512 @@
+package org.keycloak.testsuite.cli.registration;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.registration.cli.config.ConfigData;
+import org.keycloak.client.registration.cli.config.FileConfigHandler;
+import org.keycloak.client.registration.cli.config.RealmConfigData;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.testsuite.cli.KcRegExec;
+import org.keycloak.testsuite.util.TempFileResource;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
+import static org.keycloak.testsuite.cli.KcRegExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcRegTest extends AbstractCliTest {
+
+    @Test
+    public void testNoArgs() {
+        /*
+         *  Test most basic execution that returns the initial help
+         */
+        KcRegExec exe = execute("");
+
+        Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+
+        List<String> lines = exe.stdoutLines();
+        Assert.assertTrue("stdout output not empty", lines.size() > 0);
+        Assert.assertEquals("stdout first line", "Keycloak Client Registration CLI", lines.get(0));
+        Assert.assertEquals("stdout one but last line", "Use '" + KcRegExec.CMD + " help <command>' for more information about a given command.", lines.get(lines.size() - 2));
+        Assert.assertEquals("stdout last line", "", lines.get(lines.size() - 1));
+
+        lines = exe.stderrLines();
+        Assert.assertTrue("stderr output empty", lines.size() == 0);
+    }
+
+    @Test
+    public void testBadCommand() {
+        /*
+         *  Test most basic execution with non-existent command
+         */
+        KcRegExec exe = execute("nonexistent");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+        Assert.assertEquals("stderr first line", "Unknown command: nonexistent", exe.stderrLines().get(0));
+    }
+
+    @Test
+    public void testBadOptionInPlaceOfCommand() {
+        /*
+         *  Test most basic execution with non-existent option
+         */
+        KcRegExec exe = execute("--nonexistent");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+        Assert.assertEquals("stderr first line", "Unknown command: --nonexistent", exe.stderrLines().get(0));
+    }
+
+    @Test
+    public void testBadOption() {
+        /*
+         *  Test sub-command execution with non-existent option
+         */
+
+        KcRegExec exe = execute("get my_client --nonexistent");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+        Assert.assertEquals("stderr first line", "Invalid option: --nonexistent", exe.stderrLines().get(0));
+    }
+
+    @Test
+    public void testCredentialsServerAndRealmWithDefaultConfig() {
+        /*
+         *  Test without --server specified
+         */
+        KcRegExec exe = execute("config credentials --server " + serverUrl + " --realm master");
+
+        assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+    }
+
+    @Test
+    public void testCredentialsNoServerWithDefaultConfig() {
+        /*
+         *  Test without --server specified
+         */
+        KcRegExec exe = execute("config credentials --realm master --user admin --password admin");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+        Assert.assertEquals("stderr first line", "Required option not specified: --server", exe.stderrLines().get(0));
+    }
+
+    @Test
+    public void testCredentialsNoRealmWithDefaultConfig() {
+        /*
+         *  Test without --server specified
+         */
+        KcRegExec exe = execute("config credentials --server " + serverUrl + " --user admin --password admin");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+        Assert.assertEquals("stderr first line", "Required option not specified: --realm", exe.stderrLines().get(0));
+    }
+
+    @Test
+    public void testUserLoginWithDefaultConfig() {
+        /*
+         *  Test most basic user login, using the default admin-cli as a client
+         */
+        KcRegExec exe = execute("config credentials --server " + serverUrl + " --realm master --user admin --password admin");
+
+        assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+        Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
+    }
+
+    @Test
+    public void testUserLoginWithDefaultConfigInteractive() throws IOException {
+        /*
+         *  Test user login with interaction - provide user password after prompted for it
+         */
+
+        if (!runIntermittentlyFailingTests()) {
+            System.out.println("TEST SKIPPED - This test currently suffers from intermittent failures. Use -Dtest.intermittent=true to run it.");
+            return;
+        }
+
+        KcRegExec exe = KcRegExec.newBuilder()
+                .argsLine("config credentials --server " + serverUrl + " --realm master --user admin")
+                .executeAsync();
+
+        exe.waitForStdout("Enter password: ");
+        exe.sendToStdin("admin" + EOL);
+        exe.waitCompletion();
+
+        assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+        Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
+
+
+        /*
+         *  Run the test one more time with stdin redirect
+         */
+        File tmpFile = new File(KcRegExec.WORK_DIR + "/" + UUID.randomUUID().toString() + ".tmp");
+        try {
+            FileOutputStream tmpos = new FileOutputStream(tmpFile);
+            tmpos.write("admin".getBytes());
+            tmpos.write(EOL.getBytes());
+            tmpos.close();
+
+            exe = execute("config credentials --server " + serverUrl + " --realm master --user admin < '" + tmpFile.getName() + "'");
+
+            assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+            Assert.assertTrue("Enter password prompt", exe.stdoutLines().get(0).startsWith("Enter password: "));
+            Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
+
+        } finally {
+            tmpFile.delete();
+        }
+    }
+
+    @Test
+    public void testClientLoginWithDefaultConfigInteractive() throws IOException {
+        /*
+         *  Test client login with interaction - login using service account, and provide a client secret after prompted for it
+         */
+
+        if (!runIntermittentlyFailingTests()) {
+            System.out.println("TEST SKIPPED - This test currently suffers from intermittent failures. Use -Dtest.intermittent=true to run it.");
+            return;
+        }
+
+        // use -Dtest.intermittent=true to run this test
+        KcRegExec exe = KcRegExec.newBuilder()
+                .argsLine("config credentials --server " + serverUrl + " --realm test --client reg-cli-secret")
+                .executeAsync();
+
+        exe.waitForStdout("Enter client secret: ");
+        exe.sendToStdin("password" + EOL);
+        exe.waitCompletion();
+
+        assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+        Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as service-account-reg-cli-secret of realm test", exe.stderrLines().get(0));
+
+        /*
+         *  Run the test one more time with stdin redirect
+         */
+        File tmpFile = new File(KcRegExec.WORK_DIR + "/" + UUID.randomUUID().toString() + ".tmp");
+        try {
+            FileOutputStream tmpos = new FileOutputStream(tmpFile);
+            tmpos.write("password".getBytes());
+            tmpos.write(EOL.getBytes());
+            tmpos.close();
+
+            exe = KcRegExec.newBuilder()
+                    .argsLine("config credentials --server " + serverUrl + " --realm test --client reg-cli-secret < '" + tmpFile.getName() + "'")
+                    .execute();
+
+            assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+            Assert.assertTrue("Enter client secret prompt", exe.stdoutLines().get(0).startsWith("Enter client secret: "));
+            Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as service-account-reg-cli-secret of realm test", exe.stderrLines().get(0));
+        } finally {
+            tmpFile.delete();
+        }
+    }
+
+    @Test
+    public void testUserLoginWithCustomConfig() {
+        /*
+         *  Test user login using a custom config file
+         */
+        FileConfigHandler handler = initCustomConfigFile();
+
+        File configFile = new File(handler.getConfigFile());
+        try {
+            KcRegExec exe = execute("config credentials --server " + serverUrl + " --realm master" +
+                    " --user admin --password admin --config '" + configFile.getName() + "'");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+            Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
+
+            // make sure the config file exists, and has the right content
+            ConfigData config = handler.loadConfig();
+            Assert.assertEquals("serverUrl", serverUrl, config.getServerUrl());
+            Assert.assertEquals("realm", "master", config.getRealm());
+            RealmConfigData realmcfg = config.sessionRealmConfigData();
+            Assert.assertNotNull("realm config data no null", realmcfg);
+            Assert.assertEquals("realm cfg serverUrl", serverUrl, realmcfg.serverUrl());
+            Assert.assertEquals("realm cfg realm", "master", realmcfg.realm());
+            Assert.assertEquals("client id", "admin-cli", realmcfg.getClientId());
+            Assert.assertNotNull("token not null", realmcfg.getToken());
+            Assert.assertNotNull("refresh token not null", realmcfg.getRefreshToken());
+            Assert.assertNotNull("token expires not null", realmcfg.getExpiresAt());
+            Assert.assertNotNull("token expires in future", realmcfg.getExpiresAt() > System.currentTimeMillis());
+            Assert.assertNotNull("refresh token expires not null", realmcfg.getRefreshExpiresAt());
+            Assert.assertNotNull("refresh token expires in future", realmcfg.getRefreshExpiresAt() > System.currentTimeMillis());
+            Assert.assertTrue("clients is empty", realmcfg.getClients().isEmpty());
+
+        } finally {
+            configFile.delete();
+        }
+    }
+
+    @Test
+    public void testCustomConfigLoginCreateDelete() throws IOException {
+        /*
+         *  Test user login, create, delete session using a custom config file
+         */
+
+        // prepare for loading a config file
+        FileConfigHandler handler = initCustomConfigFile();
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+            KcRegExec exe = execute("config credentials --server " + serverUrl +
+                    " --realm master --user admin --password admin --config '" + configFile.getName() + "'");
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+
+            // remember the state of config file
+            ConfigData config1 = handler.loadConfig();
+
+
+
+
+            exe = execute("create --config '" + configFile.getName() + "' -s clientId=test-client -o");
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+
+            // check changes to config file
+            ConfigData config2 = handler.loadConfig();
+            assertFieldsEqualWithExclusions(config1, config2, "endpoints." + serverUrl + ".master.clients.test-client");
+
+            // check that registration access token is now set
+            Assert.assertNotNull(config2.sessionRealmConfigData().getClients().get("test-client"));
+
+
+            ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+            Assert.assertEquals("clientId", "test-client", client.getClientId());
+            Assert.assertNotNull("registrationAccessToken", client.getRegistrationAccessToken());
+            Assert.assertEquals("registrationAccessToken in returned json same as in config",
+                    config2.sessionRealmConfigData().getClients().get("test-client"), client.getRegistrationAccessToken());
+
+
+
+
+            exe = execute("delete test-client --config '" + configFile.getName() + "'");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+            // check changes to config file
+            ConfigData config3 = handler.loadConfig();
+            assertFieldsEqualWithExclusions(config2, config3, "endpoints." + serverUrl + ".master.clients.test-client");
+
+            // check that registration access token is no longer there
+            Assert.assertTrue("clients empty", config3.sessionRealmConfigData().getClients().isEmpty());
+        }
+    }
+
+    @Test
+    public void testCRUDWithOnTheFlyUserAuth() throws IOException {
+        /*
+         *  Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+         *  Login is performed by each operation again, and again using username, and password.
+         */
+        testCRUDWithOnTheFlyAuth(serverUrl, "--user user1 --password userpass", "",
+                "Logging into " + serverUrl + " as user user1 of realm test");
+    }
+
+    @Test
+    public void testCRUDWithOnTheFlyUserAuthWithClientSecret() throws IOException {
+        /*
+         *  Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+         *  Login is performed by each operation again, and again using username, password, and client secret.
+         */
+        // try client without direct grants enabled
+        KcRegExec exe = execute("get test-client --no-config --server " + serverUrl + " --realm test" +
+                " --user user1 --password userpass --client reg-cli-secret --secret password");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+        Assert.assertEquals("error message", "Client not allowed for direct access grants [invalid_grant]", exe.stderrLines().get(1));
+
+
+        // try wrong user password
+        exe = execute("get test-client --no-config --server " + serverUrl + " --realm test" +
+                " --user user1 --password wrong --client reg-cli-secret-direct --secret password");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+        Assert.assertEquals("error message", "Invalid user credentials [invalid_grant]", exe.stderrLines().get(1));
+
+
+        // try wrong client secret
+        exe = execute("get test-client --no-config --server " + serverUrl + " --realm test" +
+                " --user user1 --password userpass --client reg-cli-secret-direct --secret wrong");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+        Assert.assertEquals("error message", "Invalid client secret [unauthorized_client]", exe.stderrLines().get(1));
+
+
+        // try whole CRUD
+        testCRUDWithOnTheFlyAuth(serverUrl, "--user user1 --password userpass --client reg-cli-secret-direct --secret password", "",
+                "Logging into " + serverUrl + " as user user1 of realm test");
+    }
+
+    @Test
+    public void testCRUDWithOnTheFlyUserAuthWithSignedJwtClient() throws IOException {
+        /*
+         *  Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+         *  Login is performed by each operation again, and again using username, password, and client JWT signature.
+         */
+        File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcreg/reg-cli-keystore.jks");
+        Assert.assertTrue("reg-cli-keystore.jks exists", keystore.isFile());
+
+        // try client without direct grants enabled
+        KcRegExec exe = execute("get test-client --no-config --server " + serverUrl + " --realm test" +
+                " --user user1 --password userpass --client reg-cli-jwt --keystore '" + keystore.getAbsolutePath() + "'" +
+                " --storepass storepass --keypass keypass --alias reg-cli");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+        Assert.assertEquals("error message", "Client not allowed for direct access grants [invalid_grant]", exe.stderrLines().get(1));
+
+
+        // try wrong user password
+        exe = execute("get test-client --no-config --server " + serverUrl + " --realm test" +
+                " --user user1 --password wrong --client reg-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
+                " --storepass storepass --keypass keypass --alias reg-cli");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+        Assert.assertEquals("error message", "Invalid user credentials [invalid_grant]", exe.stderrLines().get(1));
+
+
+        // try wrong storepass
+        exe = execute("get test-client --no-config --server " + serverUrl + " --realm test" +
+                " --user user1 --password userpass --client reg-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
+                " --storepass wrong --keypass keypass --alias reg-cli");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+        Assert.assertEquals("error message", "Failed to load private key: Keystore was tampered with, or password was incorrect", exe.stderrLines().get(1));
+
+
+        // try whole CRUD
+        testCRUDWithOnTheFlyAuth(serverUrl,
+                "--user user1 --password userpass  --client reg-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
+                        " --storepass storepass --keypass keypass --alias reg-cli", "",
+                "Logging into " + serverUrl + " as user user1 of realm test");
+
+    }
+
+    @Test
+    public void testCRUDWithOnTheFlyServiceAccountWithClientSecret() throws IOException {
+        /*
+         *  Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+         *  Login is performed by each operation again, and again using only client secret - service account is used.
+         */
+        testCRUDWithOnTheFlyAuth(serverUrl, "--client reg-cli-secret --secret password", "",
+                "Logging into " + serverUrl + " as service-account-reg-cli-secret of realm test");
+    }
+
+    @Test
+    public void testCRUDWithOnTheFlyServiceAccountWithSignedJwtClient() throws IOException {
+        /*
+         *  Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+         *  Login is performed by each operation again, and again using only client JWT signature - service account is used.
+         */
+        File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcreg/reg-cli-keystore.jks");
+        Assert.assertTrue("reg-cli-keystore.jks exists", keystore.isFile());
+
+        testCRUDWithOnTheFlyAuth(serverUrl,
+                "--client reg-cli-jwt --keystore '" + keystore.getAbsolutePath() + "' --storepass storepass --keypass keypass --alias reg-cli", "",
+                "Logging into " + serverUrl + " as service-account-reg-cli-jwt of realm test");
+    }
+
+    @Test
+    public void testCreateDeleteWithInitialAndRegistrationTokens() throws IOException {
+        /*
+         *  Test create using initial client token, and subsequent delete using registration access token.
+         *  A config file is used to save registration access token for newly created client.
+         */
+        testCreateDeleteWithInitialAndRegistrationTokens(true);
+    }
+
+    @Test
+    public void testCreateDeleteWithInitialAndRegistrationTokensNoConfig() throws IOException {
+        /*
+         *  Test create using initial client token, and subsequent delete using registration access token.
+         *  No config file is used so registration access token for newly created client is not saved to config.
+         */
+        testCreateDeleteWithInitialAndRegistrationTokens(false);
+    }
+
+    private void testCreateDeleteWithInitialAndRegistrationTokens(boolean useConfig) throws IOException {
+
+        // prepare for loading a config file
+        // only used when useConfig is true
+        FileConfigHandler handler = initCustomConfigFile();
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+            String token = issueInitialAccessToken("master");
+
+            final String realm = "master";
+
+            KcRegExec exe = execute("create " + (useConfig ? ("--config '" + configFile.getAbsolutePath()) + "'" : "--no-config")
+                    + " --server " + serverUrl + " --realm " + realm + " -s clientId=test-client2 -o -t " + token);
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+
+
+            ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+
+            Assert.assertEquals("clientId", "test-client2", client.getClientId());
+            Assert.assertNotNull("registrationAccessToken", client.getRegistrationAccessToken());
+
+
+            if (useConfig) {
+                ConfigData config = handler.loadConfig();
+                Assert.assertEquals("Registration Access Token in config file", client.getRegistrationAccessToken(),
+                        config.ensureRealmConfigData(serverUrl, realm).getClients().get("test-client2"));
+            } else {
+                Assert.assertFalse("There should be no config file", configFile.isFile());
+            }
+
+            exe = execute("delete test-client2 " + (useConfig ? ("--config '" + configFile.getAbsolutePath()) + "'" : "--no-config")
+                    + " --server " + serverUrl + " --realm " + realm + " -t " + client.getRegistrationAccessToken());
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+        }
+    }
+
+    @Test
+    public void testCreateWithAllowedHostsWithoutAuthenticationNoConfig() throws IOException {
+
+        testCreateWithAllowedHostsWithoutAuthentication("test", false);
+    }
+
+    @Test
+    public void testCreateWithAllowedHostsWithoutAuthentication() throws IOException {
+
+        testCreateWithAllowedHostsWithoutAuthentication("test", true);
+    }
+
+    private void testCreateWithAllowedHostsWithoutAuthentication(String realm, boolean useConfig) throws IOException {
+
+        addLocalhostToAllowedHosts(realm);
+
+        FileConfigHandler handler = initCustomConfigFile();
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+            KcRegExec exe = execute("create " + (useConfig ? ("--config '" + configFile.getAbsolutePath()) + "'" : "--no-config")
+                    + " --server " + serverUrl + " --realm " + realm + " -s clientId=test-client -o");
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+
+            ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+
+            Assert.assertEquals("clientId", "test-client", client.getClientId());
+            Assert.assertNotNull("registrationAccessToken", client.getRegistrationAccessToken());
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java
new file mode 100644
index 0000000..5885459
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java
@@ -0,0 +1,123 @@
+package org.keycloak.testsuite.cli.registration;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.registration.cli.config.ConfigData;
+import org.keycloak.client.registration.cli.config.FileConfigHandler;
+import org.keycloak.client.registration.cli.util.ConfigUtil;
+import org.keycloak.testsuite.cli.KcRegExec;
+import org.keycloak.testsuite.util.TempFileResource;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
+import static org.keycloak.testsuite.cli.KcRegExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcRegTruststoreTest extends AbstractCliTest {
+
+    @Test
+    public void testTruststore() throws IOException {
+
+        // only run this test if ssl protected keycloak server is available
+        if (!isAuthServerSSL()) {
+            System.out.println("TEST SKIPPED - This test requires HTTPS. Run with '-Pauth-server-wildfly -Dauth.server.ssl.required=true'");
+            return;
+        }
+
+        File truststore = new File("src/test/resources/keystore/keycloak.truststore");
+
+        FileConfigHandler handler = initCustomConfigFile();
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+            if (runIntermittentlyFailingTests()) {
+                // configure truststore
+                KcRegExec exe = execute("config truststore --config '" + configFile.getName() + "' '" + truststore.getAbsolutePath() + "'");
+
+                assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+
+                // perform authentication against server - asks for password, then for truststore password
+                exe = KcRegExec.newBuilder()
+                        .argsLine("config credentials --server " + serverUrl + " --realm test --user user1" +
+                                " --config '" + configFile.getName() + "'")
+                        .executeAsync();
+
+                exe.waitForStdout("Enter password: ");
+                exe.sendToStdin("userpass" + EOL);
+                exe.waitForStdout("Enter truststore password: ");
+                exe.sendToStdin("secret" + EOL);
+                exe.waitCompletion();
+
+                assertExitCodeAndStreamSizes(exe, 0, 2, 1);
+
+
+                // configure truststore with password
+                exe = execute("config truststore --config '" + configFile.getName() + "' --trustpass secret '" + truststore.getAbsolutePath() + "'");
+
+                assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+                // perform authentication against server - asks for password, then for truststore password
+                exe = KcRegExec.newBuilder()
+                        .argsLine("config credentials --server " + serverUrl + " --realm test --user user1" +
+                                " --config '" + configFile.getName() + "'")
+                        .executeAsync();
+
+                exe.waitForStdout("Enter password: ");
+                exe.sendToStdin("userpass" + EOL);
+                exe.waitCompletion();
+
+                assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+
+            } else {
+                System.out.println("TEST SKIPPED PARTIALLY - This test currently suffers from intermittent failures. Use -Dtest.intermittent=true to run it in full.");
+            }
+        }
+
+        // Check missing argument error
+        KcRegExec exe = execute("config truststore");
+        assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+        Assert.assertEquals("no truststore error", "No truststore specified", exe.stderrLines().get(0));
+
+        // configure truststore with password
+        exe = execute("config truststore --trustpass secret '" + truststore.getAbsolutePath() + "'");
+        assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+        // perform authentication against server - asks for password, then for truststore password
+        exe = execute("config credentials --server " + serverUrl + " --realm test --user user1 --password userpass");
+        assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+
+        exe = execute("config truststore --delete");
+        assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+        exe = execute("config truststore --delete '" + truststore.getAbsolutePath() + "'");
+        assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+        Assert.assertEquals("incompatible", "Option --delete is mutually exclusive with specifying a TRUSTSTORE", exe.stderrLines().get(0));
+
+        exe = execute("config truststore --delete --trustpass secret");
+        assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+        Assert.assertEquals("no truststore error", "Options --trustpass and --delete are mutually exclusive", exe.stderrLines().get(0));
+
+        FileConfigHandler cfghandler = new FileConfigHandler();
+        cfghandler.setConfigFile(ConfigUtil.DEFAULT_CONFIG_FILE_PATH);
+        ConfigData config = cfghandler.loadConfig();
+        Assert.assertNull("truststore null", config.getTruststore());
+        Assert.assertNull("trustpass null", config.getTrustpass());
+
+
+        // perform no-config CRUD test against ssl protected endpoint
+        testCRUDWithOnTheFlyAuth(serverUrl,
+                "--user user1 --password userpass", " --truststore '" + truststore.getAbsolutePath() + "' --trustpass secret",
+                "Logging into " + serverUrl + " as user user1 of realm test");
+    }
+
+
+    @Test
+    public void testUpdateTokenTruststore() {
+        // TODO
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java
new file mode 100644
index 0000000..d21c1a3
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java
@@ -0,0 +1,170 @@
+package org.keycloak.testsuite.cli.registration;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.registration.cli.config.FileConfigHandler;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.testsuite.cli.KcRegExec;
+import org.keycloak.testsuite.util.TempFileResource;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import static org.keycloak.testsuite.cli.KcRegExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcRegUpdateTest extends AbstractCliTest {
+
+
+    @Test
+    public void testUpdateThoroughly() throws IOException {
+
+        FileConfigHandler handler = initCustomConfigFile();
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+            final String realm = "test";
+
+            loginAsUser(configFile.getFile(), serverUrl, realm, "user1", "userpass");
+
+
+            // create an object so we can update it
+            KcRegExec exe = execute("create --config '" + configFile.getName() + "' -o -s clientId=my_client");
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+            ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+
+            Assert.assertEquals("enabled", true, client.isEnabled());
+            Assert.assertEquals("publicClient", false, client.isPublicClient());
+            Assert.assertEquals("bearerOnly", false, client.isBearerOnly());
+            Assert.assertTrue("redirectUris is empty", client.getRedirectUris().isEmpty());
+
+
+            // Merge update
+            exe = execute("update my_client --config '" + configFile.getName() + "' -o " +
+                        " -s enabled=false -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]'");
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+            Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
+
+            client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+            Assert.assertEquals("enabled", false, client.isEnabled());
+            Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
+
+
+
+            // Another merge update - test deleting an attribute, deleting a list item and adding a list item
+            exe = execute("update my_client --config '" + configFile.getName() + "' -o -d redirectUris -s webOrigins+=http://localhost:8980/myapp -s webOrigins+=http://localhost:8981/myapp -d webOrigins[0]");
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+            Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
+
+            client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+
+            Assert.assertTrue("redirectUris is empty", client.getRedirectUris().isEmpty());
+            Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8981/myapp"), client.getWebOrigins());
+
+
+
+            // Another merge update - test nested attributes and setting an attribute using json format
+            // TODO KEYCLOAK-3705 Updating protocolMapper config via client registration endpoint has no effect
+            /*
+            exe = execute("update my_client --config '" + configFile.getName() + "' -o -s 'protocolMappers[0].config.\"id.token.claim\"=false' " +
+                    "-s 'protocolMappers[4].config={\"single\": \"true\", \"attribute.nameformat\": \"Basic\", \"attribute.name\": \"Role\"}'");
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+            Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
+
+            client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+            Assert.assertEquals("protocolMapper[0].config.\"id.token.claim\"", "false", client.getProtocolMappers().get(0).getConfig().get("id.token.claim"));
+            Assert.assertEquals("protocolMappers[4].config.single", "true", client.getProtocolMappers().get(4).getConfig().get("single"));
+            Assert.assertEquals("protocolMappers[4].config.\"attribute.nameformat\"", "Basic", client.getProtocolMappers().get(4).getConfig().get("attribute.nameformat"));
+            Assert.assertEquals("protocolMappers[4].config.\"attribute.name\"", "Role", client.getProtocolMappers().get(4).getConfig().get("attribute.name"));
+            */
+
+            // update using oidc format
+
+
+            // check that using an invalid attribute key is not ignored
+            exe = execute("update my_client --nonexisting --config '" + configFile.getName() + "'");
+
+            assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+            Assert.assertEquals("error message", "Unsupported option: --nonexisting", exe.stderrLines().get(0));
+
+
+
+            // try use incompatible endpoint
+            exe = execute("update my_client --config '" + configFile.getName() + "' -o -s enabled=true -e oidc");
+
+            assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+            Assert.assertEquals("error message", "Failed to set attribute 'enabled' on document type 'oidc'", exe.stderrLines().get(0));
+
+
+
+            // test overwrite from file
+            exe = KcRegExec.newBuilder()
+                    .argsLine("update my_client --config '" + configFile.getName() +
+                            "' -o  -s clientId=my_client -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]' -f -")
+                    .stdin(new ByteArrayInputStream("{ \"enabled\": false }".getBytes()))
+                    .execute();
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+            Assert.assertTrue("stderr has error", exe.stderrLines().isEmpty());
+
+            client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+            // web origin is not sent to the server, thus it retains the current value
+            Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8981/myapp"), client.getWebOrigins());
+            Assert.assertFalse("enabled is false", client.isEnabled());
+            Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
+
+
+
+            // test using merge with file
+            exe = KcRegExec.newBuilder()
+                    .argsLine("update my_client --config '" + configFile.getName() +
+                            "' -o -s enabled=true -m -f -")
+                    .stdin(new ByteArrayInputStream("{ \"webOrigins\": [\"http://localhost:8980/myapp\"] }".getBytes()))
+                    .execute();
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+            Assert.assertTrue("stderr has error", exe.stderrLines().isEmpty());
+
+            client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+            Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8980/myapp"), client.getWebOrigins());
+            Assert.assertTrue("enabled is true", client.isEnabled());
+            Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
+
+
+
+            // remove registration access token
+            exe = execute("config registration-token --config '" + configFile.getName() + "' --server " + serverUrl +
+                    " --realm " + realm + " --client my_client -d");
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+            Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
+
+            Assert.assertNull("my_client registration token", handler.loadConfig().ensureRealmConfigData(serverUrl, realm).getClients().get("my_client"));
+
+
+
+            // test update without registration access token to produce 'unsafe' error
+            exe = execute("update my_client --config '" + configFile.getName() + "' -o -s bearerOnly=true");
+
+            Assert.assertEquals("exitCode == 1", 1, exe.exitCode());
+            Assert.assertFalse("stderr is not empty", exe.stderrLines().isEmpty());
+            Assert.assertEquals("error message", "No Registration Access Token found for client: my_client. Provide one or use --unsafe.", exe.stderrLines().get(0));
+
+
+            // test using unsafe to perform update
+            exe = execute("update my_client --config '" + configFile.getName() + "' -o -s bearerOnly=true --unsafe");
+
+            Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+            Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
+
+        }
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTokenTest.java
new file mode 100644
index 0000000..e2b4193
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTokenTest.java
@@ -0,0 +1,68 @@
+package org.keycloak.testsuite.cli.registration;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.registration.cli.config.ConfigData;
+import org.keycloak.client.registration.cli.config.FileConfigHandler;
+import org.keycloak.client.registration.cli.config.RealmConfigData;
+import org.keycloak.client.registration.cli.util.ConfigUtil;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.testsuite.cli.KcRegExec;
+import org.keycloak.testsuite.util.TempFileResource;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+
+import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
+import static org.keycloak.testsuite.cli.KcRegExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcRegUpdateTokenTest extends AbstractCliTest {
+
+    @Test
+    public void testUpdateToken() throws IOException {
+
+        FileConfigHandler handler = initCustomConfigFile();
+        ConfigUtil.setHandler(handler);
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+            KcRegExec exe = execute("config credentials --config '" + configFile.getName() + "' --server " + serverUrl + " --realm master --user admin --password admin");
+            assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+
+            // read current registration access token
+            ConfigData data = ConfigUtil.loadConfig();
+            RealmConfigData rdata = data.getRealmConfigData(serverUrl, "test");
+            Assert.assertNull("realm info set", rdata);
+
+            // update registration access token
+            exe = execute("update-token --config '" + configFile.getName() + "' reg-cli-secret-direct  --server " + serverUrl + " --realm test --user user1 --password userpass");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+
+            // read current registration token
+            data = ConfigUtil.loadConfig();
+            rdata = data.getRealmConfigData(serverUrl, "test");
+            Assert.assertEquals("current session realm unchanged", "master", data.getRealm());
+            Assert.assertNotNull("realm info set", rdata);
+            Assert.assertNull("on the fly login was transient", rdata.getToken());
+            Assert.assertNotNull("client info has registration access token", rdata.getClients().get("reg-cli-secret-direct"));
+
+            // use --no-config and on-the-fly auth
+            exe = execute("update-token reg-cli-secret-direct --no-config --server " + serverUrl + " --realm test --user user1 --password userpass");
+            assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+
+            // save the token
+            String token = exe.stdoutLines().get(0);
+
+            // test that the token works
+            exe = execute("get reg-cli-secret-direct --no-config --server " + serverUrl + " --realm test -t " + token);
+            Assert.assertEquals("exit code", 0, exe.exitCode());
+
+            ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+            Assert.assertEquals("client representation returned", "reg-cli-secret-direct", client.getClientId());
+        }
+    }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java
index 23de485..491470e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java
@@ -49,6 +49,14 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest 
 
         client = createClient(c);
 
+        c = new ClientRepresentation();
+        c.setEnabled(true);
+        c.setClientId("SomeOtherClient");
+        c.setSecret("RegistrationAccessTokenTestClientSecret");
+        c.setRootUrl("http://root");
+
+        createClient(c);
+
         reg.auth(Auth.token(client.getRegistrationAccessToken()));
     }
 
@@ -82,6 +90,24 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest 
     }
 
     @Test
+    public void getClientWrongClient() throws ClientRegistrationException {
+        try {
+            reg.get("SomeOtherClient");
+        } catch (ClientRegistrationException e) {
+            assertEquals(401, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
+        }
+    }
+
+    @Test
+    public void getClientMissingClient() throws ClientRegistrationException {
+        try {
+            reg.get("nosuch");
+        } catch (ClientRegistrationException e) {
+            assertEquals(401, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
+        }
+    }
+
+    @Test
     public void getClientWithBadRegistrationToken() throws ClientRegistrationException {
         reg.auth(Auth.token("invalid"));
         try {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
index 030330c..386b291 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
@@ -164,13 +164,13 @@ public class ExportImportUtil {
         Assert.assertEquals("app-admin", appRoles.iterator().next().getName());
 
         // Test attributes
-        Map<String, List<String>> attrs = wburke.getAttributesAsListValues();
+        Map<String, List<String>> attrs = wburke.getAttributes();
         Assert.assertEquals(1, attrs.size());
         List<String> attrVals = attrs.get("email");
         Assert.assertEquals(1, attrVals.size());
         Assert.assertEquals("bburke@redhat.com", attrVals.get(0));
 
-        attrs = admin.getAttributesAsListValues();
+        attrs = admin.getAttributes();
         Assert.assertEquals(2, attrs.size());
         attrVals = attrs.get("key1");
         Assert.assertEquals(1, attrVals.size());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java
index d42a91b..42f85e6 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java
@@ -52,8 +52,7 @@ public class EmailTest extends AbstractI18NTest {
 
     private void changeUserLocale(String locale) {
         UserRepresentation user = findUser("login-test");
-        if (user.getAttributes() == null) user.setAttributes(new HashMap<String, Object>());
-        user.getAttributes().put(UserModel.LOCALE, Collections.singletonList(locale));
+        user.singleAttribute(UserModel.LOCALE, locale);
         ApiUtil.findUserByUsernameId(testRealm(), "login-test").update(user);
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java
index 437e7d8..2d0a269 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java
@@ -53,7 +53,7 @@ import static org.junit.Assert.assertTrue;
 public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest {
 
     @Test
-    public void checkIframeP3PHeader() throws IOException {
+    public void checkIframe() throws IOException {
         CookieStore cookieStore = new BasicCookieStore();
 
         CloseableHttpClient client = HttpClients.custom().setDefaultCookieStore(cookieStore).build();
@@ -115,17 +115,66 @@ public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest {
             }
             assertNotNull(sessionCookie);
 
-            get = new HttpGet(
-                    suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html?client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID + "&origin=" + suiteContext.getAuthServerInfo().getContextRoot());
+            get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html");
             response = client.execute(get);
 
             assertEquals(200, response.getStatusLine().getStatusCode());
             s = IOUtils.toString(response.getEntity().getContent());
-            assertTrue(s.contains("function getCookie(cname)"));
+            assertTrue(s.contains("function getCookie()"));
 
             assertEquals("CP=\"This is not a P3P policy!\"", response.getFirstHeader("P3P").getValue());
 
             response.close();
+
+            get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init");
+            response = client.execute(get);
+            assertEquals(403, response.getStatusLine().getStatusCode());
+            response.close();
+
+            get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init?"
+                + "client_id=invalid"
+                + "&session_state=" + sessionCookie.getValue()
+                + "&origin=" + suiteContext.getAuthServerInfo().getContextRoot()
+            );
+            response = client.execute(get);
+            assertEquals(403, response.getStatusLine().getStatusCode());
+            response.close();
+
+            get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init?"
+                + "client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID
+                + "&session_state=invalid"
+                + "&origin=" + suiteContext.getAuthServerInfo().getContextRoot()
+            );
+            response = client.execute(get);
+            assertEquals(403, response.getStatusLine().getStatusCode());
+            response.close();
+
+            get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init?"
+                + "client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID
+                + "&session_state=" + sessionCookie.getValue()
+                + "&origin=http://invalid"
+            );
+            response = client.execute(get);
+            assertEquals(403, response.getStatusLine().getStatusCode());
+            response.close();
+
+            get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init?"
+                + "client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID
+                + "&session_state=master/random/random"
+                + "&origin=" + suiteContext.getAuthServerInfo().getContextRoot()
+            );
+            response = client.execute(get);
+            assertEquals(404, response.getStatusLine().getStatusCode());
+            response.close();
+
+            get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init?"
+                + "client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID
+                + "&session_state=" + sessionCookie.getValue()
+                + "&origin=" + suiteContext.getAuthServerInfo().getContextRoot()
+            );
+            response = client.execute(get);
+            assertEquals(204, response.getStatusLine().getStatusCode());
+            response.close();
         } finally {
             client.close();
         }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
index 85bd77f..cae907e 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
@@ -189,6 +189,24 @@ public class TokenIntrospectionTest extends TestRealmKeycloakTest {
     }
 
     @Test
+    public void testUnsupportedToken() throws Exception {
+        oauth.doLogin("test-user@localhost", "password");
+        String inactiveAccessToken = "unsupported";
+        String tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "secret1", inactiveAccessToken);
+        ObjectMapper objectMapper = new ObjectMapper();
+        JsonNode jsonNode = objectMapper.readTree(tokenResponse);
+
+        assertFalse(jsonNode.get("active").asBoolean());
+
+        TokenMetadataRepresentation rep = objectMapper.readValue(tokenResponse, TokenMetadataRepresentation.class);
+
+        assertFalse(rep.isActive());
+        assertNull(rep.getUserName());
+        assertNull(rep.getClientId());
+        assertNull(rep.getSubject());
+    }
+
+    @Test
     public void testIntrospectAccessToken() throws Exception {
         oauth.doLogin("test-user@localhost", "password");
         String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/TempFileResource.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/TempFileResource.java
new file mode 100644
index 0000000..9050687
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/TempFileResource.java
@@ -0,0 +1,43 @@
+package org.keycloak.testsuite.util;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class TempFileResource implements Closeable {
+
+    private File file;
+
+    public TempFileResource(String filepath) {
+        file = new File(filepath);
+    }
+
+    public TempFileResource(File file) {
+        this.file = file;
+    }
+
+    public File getFile() {
+        return file;
+    }
+
+    public String getName() {
+        return file.getName();
+    }
+
+    public String getAbsolutePath() {
+        return file.getAbsolutePath();
+    }
+
+    public boolean isFile() {
+        return file.isFile();
+    }
+
+    @Override
+    public void close() throws IOException {
+        // delete file if it exists
+        file.delete();
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/metadata-schema/saml-schema-assertion-2.0.xsd b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/metadata-schema/saml-schema-assertion-2.0.xsd
new file mode 100644
index 0000000..cdd365d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/metadata-schema/saml-schema-assertion-2.0.xsd
@@ -0,0 +1,283 @@
+<?xml version="1.0" encoding="US-ASCII"?>
+<schema
+        targetNamespace="urn:oasis:names:tc:SAML:2.0:assertion"
+        xmlns="http://www.w3.org/2001/XMLSchema"
+        xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+        xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+        xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"
+        elementFormDefault="unqualified"
+        attributeFormDefault="unqualified"
+        blockDefault="substitution"
+        version="2.0">
+  <import namespace="http://www.w3.org/2000/09/xmldsig#"
+          schemaLocation="http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd"/>
+  <import namespace="http://www.w3.org/2001/04/xmlenc#"
+          schemaLocation="http://www.w3.org/TR/2002/REC-xmlenc-core-20021210/xenc-schema.xsd"/>
+  <annotation>
+    <documentation>
+      Document identifier: saml-schema-assertion-2.0
+      Location: http://docs.oasis-open.org/security/saml/v2.0/
+      Revision history:
+      V1.0 (November, 2002):
+      Initial Standard Schema.
+      V1.1 (September, 2003):
+      Updates within the same V1.0 namespace.
+      V2.0 (March, 2005):
+      New assertion schema for SAML V2.0 namespace.
+    </documentation>
+  </annotation>
+  <attributeGroup name="IDNameQualifiers">
+    <attribute name="NameQualifier" type="string" use="optional"/>
+    <attribute name="SPNameQualifier" type="string" use="optional"/>
+  </attributeGroup>
+  <element name="BaseID" type="saml:BaseIDAbstractType"/>
+  <complexType name="BaseIDAbstractType" abstract="true">
+    <attributeGroup ref="saml:IDNameQualifiers"/>
+  </complexType>
+  <element name="NameID" type="saml:NameIDType"/>
+  <complexType name="NameIDType">
+    <simpleContent>
+      <extension base="string">
+        <attributeGroup ref="saml:IDNameQualifiers"/>
+        <attribute name="Format" type="anyURI" use="optional"/>
+        <attribute name="SPProvidedID" type="string" use="optional"/>
+      </extension>
+    </simpleContent>
+  </complexType>
+  <complexType name="EncryptedElementType">
+    <sequence>
+      <element ref="xenc:EncryptedData"/>
+      <element ref="xenc:EncryptedKey" minOccurs="0" maxOccurs="unbounded"/>
+    </sequence>
+  </complexType>
+  <element name="EncryptedID" type="saml:EncryptedElementType"/>
+  <element name="Issuer" type="saml:NameIDType"/>
+  <element name="AssertionIDRef" type="NCName"/>
+  <element name="AssertionURIRef" type="anyURI"/>
+  <element name="Assertion" type="saml:AssertionType"/>
+  <complexType name="AssertionType">
+    <sequence>
+      <element ref="saml:Issuer"/>
+      <element ref="ds:Signature" minOccurs="0"/>
+      <element ref="saml:Subject" minOccurs="0"/>
+      <element ref="saml:Conditions" minOccurs="0"/>
+      <element ref="saml:Advice" minOccurs="0"/>
+      <choice minOccurs="0" maxOccurs="unbounded">
+        <element ref="saml:Statement"/>
+        <element ref="saml:AuthnStatement"/>
+        <element ref="saml:AuthzDecisionStatement"/>
+        <element ref="saml:AttributeStatement"/>
+      </choice>
+    </sequence>
+    <attribute name="Version" type="string" use="required"/>
+    <attribute name="ID" type="ID" use="required"/>
+    <attribute name="IssueInstant" type="dateTime" use="required"/>
+  </complexType>
+  <element name="Subject" type="saml:SubjectType"/>
+  <complexType name="SubjectType">
+    <choice>
+      <sequence>
+        <choice>
+          <element ref="saml:BaseID"/>
+          <element ref="saml:NameID"/>
+          <element ref="saml:EncryptedID"/>
+        </choice>
+        <element ref="saml:SubjectConfirmation" minOccurs="0" maxOccurs="unbounded"/>
+      </sequence>
+      <element ref="saml:SubjectConfirmation" maxOccurs="unbounded"/>
+    </choice>
+  </complexType>
+  <element name="SubjectConfirmation" type="saml:SubjectConfirmationType"/>
+  <complexType name="SubjectConfirmationType">
+    <sequence>
+      <choice minOccurs="0">
+        <element ref="saml:BaseID"/>
+        <element ref="saml:NameID"/>
+        <element ref="saml:EncryptedID"/>
+      </choice>
+      <element ref="saml:SubjectConfirmationData" minOccurs="0"/>
+    </sequence>
+    <attribute name="Method" type="anyURI" use="required"/>
+  </complexType>
+  <element name="SubjectConfirmationData" type="saml:SubjectConfirmationDataType"/>
+  <complexType name="SubjectConfirmationDataType" mixed="true">
+    <complexContent>
+      <restriction base="anyType">
+        <sequence>
+          <any namespace="##any" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
+        </sequence>
+        <attribute name="NotBefore" type="dateTime" use="optional"/>
+        <attribute name="NotOnOrAfter" type="dateTime" use="optional"/>
+        <attribute name="Recipient" type="anyURI" use="optional"/>
+        <attribute name="InResponseTo" type="NCName" use="optional"/>
+        <attribute name="Address" type="string" use="optional"/>
+        <anyAttribute namespace="##other" processContents="lax"/>
+      </restriction>
+    </complexContent>
+  </complexType>
+  <complexType name="KeyInfoConfirmationDataType" mixed="false">
+    <complexContent>
+      <restriction base="saml:SubjectConfirmationDataType">
+        <sequence>
+          <element ref="ds:KeyInfo" maxOccurs="unbounded"/>
+        </sequence>
+      </restriction>
+    </complexContent>
+  </complexType>
+  <element name="Conditions" type="saml:ConditionsType"/>
+  <complexType name="ConditionsType">
+    <choice minOccurs="0" maxOccurs="unbounded">
+      <element ref="saml:Condition"/>
+      <element ref="saml:AudienceRestriction"/>
+      <element ref="saml:OneTimeUse"/>
+      <element ref="saml:ProxyRestriction"/>
+    </choice>
+    <attribute name="NotBefore" type="dateTime" use="optional"/>
+    <attribute name="NotOnOrAfter" type="dateTime" use="optional"/>
+  </complexType>
+  <element name="Condition" type="saml:ConditionAbstractType"/>
+  <complexType name="ConditionAbstractType" abstract="true"/>
+  <element name="AudienceRestriction" type="saml:AudienceRestrictionType"/>
+  <complexType name="AudienceRestrictionType">
+    <complexContent>
+      <extension base="saml:ConditionAbstractType">
+        <sequence>
+          <element ref="saml:Audience" maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="Audience" type="anyURI"/>
+  <element name="OneTimeUse" type="saml:OneTimeUseType"/>
+  <complexType name="OneTimeUseType">
+    <complexContent>
+      <extension base="saml:ConditionAbstractType"/>
+    </complexContent>
+  </complexType>
+  <element name="ProxyRestriction" type="saml:ProxyRestrictionType"/>
+  <complexType name="ProxyRestrictionType">
+    <complexContent>
+      <extension base="saml:ConditionAbstractType">
+        <sequence>
+          <element ref="saml:Audience" minOccurs="0" maxOccurs="unbounded"/>
+        </sequence>
+        <attribute name="Count" type="nonNegativeInteger" use="optional"/>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="Advice" type="saml:AdviceType"/>
+  <complexType name="AdviceType">
+    <choice minOccurs="0" maxOccurs="unbounded">
+      <element ref="saml:AssertionIDRef"/>
+      <element ref="saml:AssertionURIRef"/>
+      <element ref="saml:Assertion"/>
+      <element ref="saml:EncryptedAssertion"/>
+      <any namespace="##other" processContents="lax"/>
+    </choice>
+  </complexType>
+  <element name="EncryptedAssertion" type="saml:EncryptedElementType"/>
+  <element name="Statement" type="saml:StatementAbstractType"/>
+  <complexType name="StatementAbstractType" abstract="true"/>
+  <element name="AuthnStatement" type="saml:AuthnStatementType"/>
+  <complexType name="AuthnStatementType">
+    <complexContent>
+      <extension base="saml:StatementAbstractType">
+        <sequence>
+          <element ref="saml:SubjectLocality" minOccurs="0"/>
+          <element ref="saml:AuthnContext"/>
+        </sequence>
+        <attribute name="AuthnInstant" type="dateTime" use="required"/>
+        <attribute name="SessionIndex" type="string" use="optional"/>
+        <attribute name="SessionNotOnOrAfter" type="dateTime" use="optional"/>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="SubjectLocality" type="saml:SubjectLocalityType"/>
+  <complexType name="SubjectLocalityType">
+    <attribute name="Address" type="string" use="optional"/>
+    <attribute name="DNSName" type="string" use="optional"/>
+  </complexType>
+  <element name="AuthnContext" type="saml:AuthnContextType"/>
+  <complexType name="AuthnContextType">
+    <sequence>
+      <choice>
+        <sequence>
+          <element ref="saml:AuthnContextClassRef"/>
+          <choice minOccurs="0">
+            <element ref="saml:AuthnContextDecl"/>
+            <element ref="saml:AuthnContextDeclRef"/>
+          </choice>
+        </sequence>
+        <choice>
+          <element ref="saml:AuthnContextDecl"/>
+          <element ref="saml:AuthnContextDeclRef"/>
+        </choice>
+      </choice>
+      <element ref="saml:AuthenticatingAuthority" minOccurs="0" maxOccurs="unbounded"/>
+    </sequence>
+  </complexType>
+  <element name="AuthnContextClassRef" type="anyURI"/>
+  <element name="AuthnContextDeclRef" type="anyURI"/>
+  <element name="AuthnContextDecl" type="anyType"/>
+  <element name="AuthenticatingAuthority" type="anyURI"/>
+  <element name="AuthzDecisionStatement" type="saml:AuthzDecisionStatementType"/>
+  <complexType name="AuthzDecisionStatementType">
+    <complexContent>
+      <extension base="saml:StatementAbstractType">
+        <sequence>
+          <element ref="saml:Action" maxOccurs="unbounded"/>
+          <element ref="saml:Evidence" minOccurs="0"/>
+        </sequence>
+        <attribute name="Resource" type="anyURI" use="required"/>
+        <attribute name="Decision" type="saml:DecisionType" use="required"/>
+      </extension>
+    </complexContent>
+  </complexType>
+  <simpleType name="DecisionType">
+    <restriction base="string">
+      <enumeration value="Permit"/>
+      <enumeration value="Deny"/>
+      <enumeration value="Indeterminate"/>
+    </restriction>
+  </simpleType>
+  <element name="Action" type="saml:ActionType"/>
+  <complexType name="ActionType">
+    <simpleContent>
+      <extension base="string">
+        <attribute name="Namespace" type="anyURI" use="required"/>
+      </extension>
+    </simpleContent>
+  </complexType>
+  <element name="Evidence" type="saml:EvidenceType"/>
+  <complexType name="EvidenceType">
+    <choice maxOccurs="unbounded">
+      <element ref="saml:AssertionIDRef"/>
+      <element ref="saml:AssertionURIRef"/>
+      <element ref="saml:Assertion"/>
+      <element ref="saml:EncryptedAssertion"/>
+    </choice>
+  </complexType>
+  <element name="AttributeStatement" type="saml:AttributeStatementType"/>
+  <complexType name="AttributeStatementType">
+    <complexContent>
+      <extension base="saml:StatementAbstractType">
+        <choice maxOccurs="unbounded">
+          <element ref="saml:Attribute"/>
+          <element ref="saml:EncryptedAttribute"/>
+        </choice>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="Attribute" type="saml:AttributeType"/>
+  <complexType name="AttributeType">
+    <sequence>
+      <element ref="saml:AttributeValue" minOccurs="0" maxOccurs="unbounded"/>
+    </sequence>
+    <attribute name="Name" type="string" use="required"/>
+    <attribute name="NameFormat" type="anyURI" use="optional"/>
+    <attribute name="FriendlyName" type="string" use="optional"/>
+    <anyAttribute namespace="##other" processContents="lax"/>
+  </complexType>
+  <element name="AttributeValue" type="anyType" nillable="true"/>
+  <element name="EncryptedAttribute" type="saml:EncryptedElementType"/>
+</schema>
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/metadata-schema/saml-schema-metadata-2.0.xsd b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/metadata-schema/saml-schema-metadata-2.0.xsd
new file mode 100644
index 0000000..5c8d217
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/metadata-schema/saml-schema-metadata-2.0.xsd
@@ -0,0 +1,337 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema
+        targetNamespace="urn:oasis:names:tc:SAML:2.0:metadata"
+        xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
+        xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+        xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"
+        xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+        xmlns="http://www.w3.org/2001/XMLSchema"
+        elementFormDefault="unqualified"
+        attributeFormDefault="unqualified"
+        blockDefault="substitution"
+        version="2.0">
+  <import namespace="http://www.w3.org/2000/09/xmldsig#"
+          schemaLocation="http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd"/>
+  <import namespace="http://www.w3.org/2001/04/xmlenc#"
+          schemaLocation="http://www.w3.org/TR/2002/REC-xmlenc-core-20021210/xenc-schema.xsd"/>
+  <import namespace="urn:oasis:names:tc:SAML:2.0:assertion"
+          schemaLocation="saml-schema-assertion-2.0.xsd"/>
+  <import namespace="http://www.w3.org/XML/1998/namespace"
+          schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+  <annotation>
+    <documentation>
+      Document identifier: saml-schema-metadata-2.0
+      Location: http://docs.oasis-open.org/security/saml/v2.0/
+      Revision history:
+      V2.0 (March, 2005):
+      Schema for SAML metadata, first published in SAML 2.0.
+    </documentation>
+  </annotation>
+
+  <simpleType name="entityIDType">
+    <restriction base="anyURI">
+      <maxLength value="1024"/>
+    </restriction>
+  </simpleType>
+  <complexType name="localizedNameType">
+    <simpleContent>
+      <extension base="string">
+        <attribute ref="xml:lang" use="required"/>
+      </extension>
+    </simpleContent>
+  </complexType>
+  <complexType name="localizedURIType">
+    <simpleContent>
+      <extension base="anyURI">
+        <attribute ref="xml:lang" use="required"/>
+      </extension>
+    </simpleContent>
+  </complexType>
+
+  <element name="Extensions" type="md:ExtensionsType"/>
+  <complexType final="#all" name="ExtensionsType">
+    <sequence>
+      <any namespace="##other" processContents="lax" maxOccurs="unbounded"/>
+    </sequence>
+  </complexType>
+
+  <complexType name="EndpointType">
+    <sequence>
+      <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
+    </sequence>
+    <attribute name="Binding" type="anyURI" use="required"/>
+    <attribute name="Location" type="anyURI" use="required"/>
+    <attribute name="ResponseLocation" type="anyURI" use="optional"/>
+    <anyAttribute namespace="##other" processContents="lax"/>
+  </complexType>
+
+  <complexType name="IndexedEndpointType">
+    <complexContent>
+      <extension base="md:EndpointType">
+        <attribute name="index" type="unsignedShort" use="required"/>
+        <attribute name="isDefault" type="boolean" use="optional"/>
+      </extension>
+    </complexContent>
+  </complexType>
+
+  <element name="EntitiesDescriptor" type="md:EntitiesDescriptorType"/>
+  <complexType name="EntitiesDescriptorType">
+    <sequence>
+      <element ref="ds:Signature" minOccurs="0"/>
+      <element ref="md:Extensions" minOccurs="0"/>
+      <choice minOccurs="1" maxOccurs="unbounded">
+        <element ref="md:EntityDescriptor"/>
+        <element ref="md:EntitiesDescriptor"/>
+      </choice>
+    </sequence>
+    <attribute name="validUntil" type="dateTime" use="optional"/>
+    <attribute name="cacheDuration" type="duration" use="optional"/>
+    <attribute name="ID" type="ID" use="optional"/>
+    <attribute name="Name" type="string" use="optional"/>
+  </complexType>
+
+  <element name="EntityDescriptor" type="md:EntityDescriptorType"/>
+  <complexType name="EntityDescriptorType">
+    <sequence>
+      <element ref="ds:Signature" minOccurs="0"/>
+      <element ref="md:Extensions" minOccurs="0"/>
+      <choice>
+        <choice maxOccurs="unbounded">
+          <element ref="md:RoleDescriptor"/>
+          <element ref="md:IDPSSODescriptor"/>
+          <element ref="md:SPSSODescriptor"/>
+          <element ref="md:AuthnAuthorityDescriptor"/>
+          <element ref="md:AttributeAuthorityDescriptor"/>
+          <element ref="md:PDPDescriptor"/>
+        </choice>
+        <element ref="md:AffiliationDescriptor"/>
+      </choice>
+      <element ref="md:Organization" minOccurs="0"/>
+      <element ref="md:ContactPerson" minOccurs="0" maxOccurs="unbounded"/>
+      <element ref="md:AdditionalMetadataLocation" minOccurs="0" maxOccurs="unbounded"/>
+    </sequence>
+    <attribute name="entityID" type="md:entityIDType" use="required"/>
+    <attribute name="validUntil" type="dateTime" use="optional"/>
+    <attribute name="cacheDuration" type="duration" use="optional"/>
+    <attribute name="ID" type="ID" use="optional"/>
+    <anyAttribute namespace="##other" processContents="lax"/>
+  </complexType>
+
+  <element name="Organization" type="md:OrganizationType"/>
+  <complexType name="OrganizationType">
+    <sequence>
+      <element ref="md:Extensions" minOccurs="0"/>
+      <element ref="md:OrganizationName" maxOccurs="unbounded"/>
+      <element ref="md:OrganizationDisplayName" maxOccurs="unbounded"/>
+      <element ref="md:OrganizationURL" maxOccurs="unbounded"/>
+    </sequence>
+    <anyAttribute namespace="##other" processContents="lax"/>
+  </complexType>
+  <element name="OrganizationName" type="md:localizedNameType"/>
+  <element name="OrganizationDisplayName" type="md:localizedNameType"/>
+  <element name="OrganizationURL" type="md:localizedURIType"/>
+  <element name="ContactPerson" type="md:ContactType"/>
+  <complexType name="ContactType">
+    <sequence>
+      <element ref="md:Extensions" minOccurs="0"/>
+      <element ref="md:Company" minOccurs="0"/>
+      <element ref="md:GivenName" minOccurs="0"/>
+      <element ref="md:SurName" minOccurs="0"/>
+      <element ref="md:EmailAddress" minOccurs="0" maxOccurs="unbounded"/>
+      <element ref="md:TelephoneNumber" minOccurs="0" maxOccurs="unbounded"/>
+    </sequence>
+    <attribute name="contactType" type="md:ContactTypeType" use="required"/>
+    <anyAttribute namespace="##other" processContents="lax"/>
+  </complexType>
+  <element name="Company" type="string"/>
+  <element name="GivenName" type="string"/>
+  <element name="SurName" type="string"/>
+  <element name="EmailAddress" type="anyURI"/>
+  <element name="TelephoneNumber" type="string"/>
+  <simpleType name="ContactTypeType">
+    <restriction base="string">
+      <enumeration value="technical"/>
+      <enumeration value="support"/>
+      <enumeration value="administrative"/>
+      <enumeration value="billing"/>
+      <enumeration value="other"/>
+    </restriction>
+  </simpleType>
+
+  <element name="AdditionalMetadataLocation" type="md:AdditionalMetadataLocationType"/>
+  <complexType name="AdditionalMetadataLocationType">
+    <simpleContent>
+      <extension base="anyURI">
+        <attribute name="namespace" type="anyURI" use="required"/>
+      </extension>
+    </simpleContent>
+  </complexType>
+
+  <element name="RoleDescriptor" type="md:RoleDescriptorType"/>
+  <complexType name="RoleDescriptorType" abstract="true">
+    <sequence>
+      <element ref="ds:Signature" minOccurs="0"/>
+      <element ref="md:Extensions" minOccurs="0"/>
+      <element ref="md:KeyDescriptor" minOccurs="0" maxOccurs="unbounded"/>
+      <element ref="md:Organization" minOccurs="0"/>
+      <element ref="md:ContactPerson" minOccurs="0" maxOccurs="unbounded"/>
+    </sequence>
+    <attribute name="ID" type="ID" use="optional"/>
+    <attribute name="validUntil" type="dateTime" use="optional"/>
+    <attribute name="cacheDuration" type="duration" use="optional"/>
+    <attribute name="protocolSupportEnumeration" type="md:anyURIListType" use="required"/>
+    <attribute name="errorURL" type="anyURI" use="optional"/>
+    <anyAttribute namespace="##other" processContents="lax"/>
+  </complexType>
+  <simpleType name="anyURIListType">
+    <list itemType="anyURI"/>
+  </simpleType>
+
+  <element name="KeyDescriptor" type="md:KeyDescriptorType"/>
+  <complexType name="KeyDescriptorType">
+    <sequence>
+      <element ref="ds:KeyInfo"/>
+      <element ref="md:EncryptionMethod" minOccurs="0" maxOccurs="unbounded"/>
+    </sequence>
+    <attribute name="use" type="md:KeyTypes" use="optional"/>
+  </complexType>
+  <simpleType name="KeyTypes">
+    <restriction base="string">
+      <enumeration value="encryption"/>
+      <enumeration value="signing"/>
+    </restriction>
+  </simpleType>
+  <element name="EncryptionMethod" type="xenc:EncryptionMethodType"/>
+
+  <complexType name="SSODescriptorType" abstract="true">
+    <complexContent>
+      <extension base="md:RoleDescriptorType">
+        <sequence>
+          <element ref="md:ArtifactResolutionService" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="md:SingleLogoutService" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="md:ManageNameIDService" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="md:NameIDFormat" minOccurs="0" maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="ArtifactResolutionService" type="md:IndexedEndpointType"/>
+  <element name="SingleLogoutService" type="md:EndpointType"/>
+  <element name="ManageNameIDService" type="md:EndpointType"/>
+  <element name="NameIDFormat" type="anyURI"/>
+
+  <element name="IDPSSODescriptor" type="md:IDPSSODescriptorType"/>
+  <complexType name="IDPSSODescriptorType">
+    <complexContent>
+      <extension base="md:SSODescriptorType">
+        <sequence>
+          <element ref="md:SingleSignOnService" maxOccurs="unbounded"/>
+          <element ref="md:NameIDMappingService" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="md:AssertionIDRequestService" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="md:AttributeProfile" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="saml:Attribute" minOccurs="0" maxOccurs="unbounded"/>
+        </sequence>
+        <attribute name="WantAuthnRequestsSigned" type="boolean" use="optional"/>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="SingleSignOnService" type="md:EndpointType"/>
+  <element name="NameIDMappingService" type="md:EndpointType"/>
+  <element name="AssertionIDRequestService" type="md:EndpointType"/>
+  <element name="AttributeProfile" type="anyURI"/>
+
+  <element name="SPSSODescriptor" type="md:SPSSODescriptorType"/>
+  <complexType name="SPSSODescriptorType">
+    <complexContent>
+      <extension base="md:SSODescriptorType">
+        <sequence>
+          <element ref="md:AssertionConsumerService" maxOccurs="unbounded"/>
+          <element ref="md:AttributeConsumingService" minOccurs="0" maxOccurs="unbounded"/>
+        </sequence>
+        <attribute name="AuthnRequestsSigned" type="boolean" use="optional"/>
+        <attribute name="WantAssertionsSigned" type="boolean" use="optional"/>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AssertionConsumerService" type="md:IndexedEndpointType"/>
+  <element name="AttributeConsumingService" type="md:AttributeConsumingServiceType"/>
+  <complexType name="AttributeConsumingServiceType">
+    <sequence>
+      <element ref="md:ServiceName" maxOccurs="unbounded"/>
+      <element ref="md:ServiceDescription" minOccurs="0" maxOccurs="unbounded"/>
+      <element ref="md:RequestedAttribute" maxOccurs="unbounded"/>
+    </sequence>
+    <attribute name="index" type="unsignedShort" use="required"/>
+    <attribute name="isDefault" type="boolean" use="optional"/>
+  </complexType>
+  <element name="ServiceName" type="md:localizedNameType"/>
+  <element name="ServiceDescription" type="md:localizedNameType"/>
+  <element name="RequestedAttribute" type="md:RequestedAttributeType"/>
+  <complexType name="RequestedAttributeType">
+    <complexContent>
+      <extension base="saml:AttributeType">
+        <attribute name="isRequired" type="boolean" use="optional"/>
+      </extension>
+    </complexContent>
+  </complexType>
+
+  <element name="AuthnAuthorityDescriptor" type="md:AuthnAuthorityDescriptorType"/>
+  <complexType name="AuthnAuthorityDescriptorType">
+    <complexContent>
+      <extension base="md:RoleDescriptorType">
+        <sequence>
+          <element ref="md:AuthnQueryService" maxOccurs="unbounded"/>
+          <element ref="md:AssertionIDRequestService" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="md:NameIDFormat" minOccurs="0" maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AuthnQueryService" type="md:EndpointType"/>
+
+  <element name="PDPDescriptor" type="md:PDPDescriptorType"/>
+  <complexType name="PDPDescriptorType">
+    <complexContent>
+      <extension base="md:RoleDescriptorType">
+        <sequence>
+          <element ref="md:AuthzService" maxOccurs="unbounded"/>
+          <element ref="md:AssertionIDRequestService" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="md:NameIDFormat" minOccurs="0" maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AuthzService" type="md:EndpointType"/>
+
+  <element name="AttributeAuthorityDescriptor" type="md:AttributeAuthorityDescriptorType"/>
+  <complexType name="AttributeAuthorityDescriptorType">
+    <complexContent>
+      <extension base="md:RoleDescriptorType">
+        <sequence>
+          <element ref="md:AttributeService" maxOccurs="unbounded"/>
+          <element ref="md:AssertionIDRequestService" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="md:NameIDFormat" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="md:AttributeProfile" minOccurs="0" maxOccurs="unbounded"/>
+          <element ref="saml:Attribute" minOccurs="0" maxOccurs="unbounded"/>
+        </sequence>
+      </extension>
+    </complexContent>
+  </complexType>
+  <element name="AttributeService" type="md:EndpointType"/>
+
+  <element name="AffiliationDescriptor" type="md:AffiliationDescriptorType"/>
+  <complexType name="AffiliationDescriptorType">
+    <sequence>
+      <element ref="ds:Signature" minOccurs="0"/>
+      <element ref="md:Extensions" minOccurs="0"/>
+      <element ref="md:AffiliateMember" maxOccurs="unbounded"/>
+      <element ref="md:KeyDescriptor" minOccurs="0" maxOccurs="unbounded"/>
+    </sequence>
+    <attribute name="affiliationOwnerID" type="md:entityIDType" use="required"/>
+    <attribute name="validUntil" type="dateTime" use="optional"/>
+    <attribute name="cacheDuration" type="duration" use="optional"/>
+    <attribute name="ID" type="ID" use="optional"/>
+    <anyAttribute namespace="##other" processContents="lax"/>
+  </complexType>
+  <element name="AffiliateMember" type="md:entityIDType"/>
+</schema>
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/cli/kcreg/reg-cli-keystore.jks b/testsuite/integration-arquillian/tests/base/src/test/resources/cli/kcreg/reg-cli-keystore.jks
new file mode 100644
index 0000000..feb5e82
Binary files /dev/null and b/testsuite/integration-arquillian/tests/base/src/test/resources/cli/kcreg/reg-cli-keystore.jks differ
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/cli/kcreg/saml-sp-metadata.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/cli/kcreg/saml-sp-metadata.xml
new file mode 100644
index 0000000..2af8e01
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/cli/kcreg/saml-sp-metadata.xml
@@ -0,0 +1,20 @@
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://localhost:8080/sales-post-enc/">
+    <SPSSODescriptor AuthnRequestsSigned="true"
+                     protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol http://schemas.xmlsoap.org/ws/2003/07/secext">
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/sales-post-enc/saml"/>
+        <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
+        </NameIDFormat>
+        <AssertionConsumerService
+                Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/sales-post-enc/saml"
+                index="1" isDefault="true" />
+        <KeyDescriptor use="signing">
+            <dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
+                <dsig:X509Data>
+                    <dsig:X509Certificate>
+                        MIIB1DCCAT0CBgFJGVacCDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1lbmMvMB4XDTE0MTAxNjE0MjA0NloXDTI0MTAxNjE0MjIyNlowMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3QtZW5jLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA2+5MCT5BnVN+IYnKZcH6ev1pjXGi4feE0nOycq/VJ3aeaZMi4G9AxOxCBPupErOC7Kgm/Bw5AdJyw+Q12wSRXfJ9FhqCrLXpb7YOhbVSTJ8De5O8mW35DxAlh/cxe9FXjqPb286wKTUZ3LfGYR+X235UQeCTAPS/Ufi21EXaEikCAwEAATANBgkqhkiG9w0BAQsFAAOBgQBMrfGD9QFfx5v7ld/OAto5rjkTe3R1Qei8XRXfcs83vLaqEzjEtTuLGrJEi55kXuJgBpVmQpnwCCkkjSy0JxbqLDdVi9arfWUxEGmOr01ZHycELhDNaQcFqVMPr5kRHIHgktT8hK2IgCvd3Fy9/JCgUgCPxKfhwecyEOKxUc857g==
+                    </dsig:X509Certificate>
+                </dsig:X509Data>
+            </dsig:KeyInfo>
+        </KeyDescriptor>
+    </SPSSODescriptor>
+</EntityDescriptor>
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index d30721c..8dcdc2a 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -172,6 +172,8 @@
                             <migration.import.properties>${migration.import.properties}</migration.import.properties>
 
                             <testsuite.constants>${testsuite.constants}</testsuite.constants>
+                            <cli.log.output>${cli.log.output}</cli.log.output>
+                            <test.intermittent>${test.intermittent}</test.intermittent>
 
                             <browser>${browser}</browser>
                             <firefox_binary>${firefox_binary}</firefox_binary>
@@ -781,6 +783,18 @@
                     <version>${mariadb.version}</version>
                 </dependency>
 
+                <!-- CLI -->
+                <!--
+                   - This dependency must come after org.bouncycastle dependencies since it contains BC classes,
+                   - and MAC signature check on classes would fail otherwise with:
+                   - 'java.lang.SecurityException: JCE cannot authenticate the provider BC'
+                 -->
+                <dependency>
+                    <groupId>org.keycloak</groupId>
+                    <artifactId>keycloak-client-cli-dist</artifactId>
+                    <type>zip</type>
+                </dependency>
+
             </dependencies>
             <build>
                 <plugins>
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_lt.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_lt.properties
index b65bb7c..c40c438 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_lt.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_lt.properties
@@ -164,6 +164,10 @@ usermodel.clientRoleMapping.rolePrefix.label=Kliento rol\u0117s prefiksas
 usermodel.clientRoleMapping.rolePrefix.tooltip=Prefiksas, pridedamas prie\u0161 kiekvien\u0105 kliento rol\u0119 (neprivalomas)
 usermodel.realmRoleMapping.rolePrefix.label=Srities rol\u0117s prefiksas
 usermodel.realmRoleMapping.rolePrefix.tooltip=Prefiksas, pridedamas prie\u0161 kiekvien\u0105 srities rol\u0119 (neprivalomas)
+sectorIdentifierUri.label=Sektoriaus identifikatoriaus URI
+sectorIdentifierUri.tooltip=Paslaug\u0173 teik\u0117jai, kurie naudoja porines subreik\u0161mes ir palaiko dinamin\u0119 klient\u0173 registracij\u0105 (Dynamic Client Registration) tur\u0117t\u0173 naudoti sector_identifier_uri parametr\u0105. Teikiamas funkcionalumas leid\u017Eia svetaini\u0173 grup\u0117ms, valdomoms centralizuotos administravimo panel\u0117s, tur\u0117ti pastovias porines subreik\u0161mes nepriklausomas nuo domeno vard\u0173. Tokiu b\u016Bdu klientai gali keisti domen\u0173 redirect_uri neperregistruojant vis\u0173 naudotoj\u0173.
+pairwiseSubAlgorithmSalt.label=Druska
+pairwiseSubAlgorithmSalt.tooltip=Druska naudojama porinio objekto identifikatoriaus skai\u010Diavimo metu. Jei paliekama tu\u0161\u010Dia reik\u0161m\u0117, tuomet druskos reik\u0161m\u0117 bus automatik\u0161ai sugeneruota.
 
 # client details
 clients.tooltip=Klientai - tai srities nar\u0161ykl\u0117s program\u0117l\u0117s arba tinklin\u0117s paslaugos, kuriomis pasitikima. Klientai gali jungtis prie sistemos. Klientams galima nurodyti specifines roles.
@@ -287,6 +291,12 @@ gen-new-keys-and-cert=Nauj\u0173 rakt\u0173 ir sertifikat\u0173 generavimas
 import-certificate=Importuoti sertifikat\u0105
 gen-client-private-key=Generuoti kliento privat\u0173 rakt\u0105
 generate-private-key=Generuoti privat\u0173 rakt\u0105
+kid=Kid
+kid.tooltip=Kliento vie\u0161ojo rakto identifikatorius (Key ID) importuotas i\u0161 JWKS.
+use-jwks-url=Naudoti JWKS URL
+use-jwks-url.tooltip=Jei \u012Fgalinta, tuomet kliento vie\u0161asis raktas atsiun\u010Diamas i\u0161 pateiktos JWKS URL. \u012Egalinimas suteikia lankstumo, nes klientui pergeneravus raktus jie automati\u0161kai atsiun\u010Diami. Jei \u0161i nuostata i\u0161jungta, tuomet naudojamas Keycloak DB saugomas vie\u0161asis raktas (arba sertifikatas) ir klientui sugeneravus naujus raktus juos rankiniu b\u016Bdu reik\u0117s importuoti \u012F Keycloak DB.
+jwks-url=JWKS URL
+jwks-url.tooltip=URL, kuriuo pasiekiami kliento JWK formatu saugomi raktai. \u017Di\u016Br\u0117kite JWK specifikacij\u0105 detalesnei informacijai. Jei naudojamas kliento adapteris su "jwt" kredencialais, tuomet galite naudoti j\u016Bs\u0173 programos URL su '/k_jwks' sufiksu. Pavyzd\u017Eiui 'http://www.myhost.com/myapp/k_jwks' .
 archive-format=Archyvo formatas
 archive-format.tooltip=Java rakt\u0173 saugykla (keystore) arba PKCS12 formato rinkmena.
 key-alias=Rakto pseudonimas
@@ -412,7 +422,9 @@ post-broker-login-flow=Sekan\u010Di\u0173 prisijungim\u0173 eiga
 redirect-uri=Nukreipimo URI
 redirect-uri.tooltip=Tapatyb\u0117s teik\u0117jo konfig\u016Bravimo nuoroda.
 alias=Pseudonimas
+display-name=Rodomas vardas
 identity-provider.alias.tooltip=Pseudonimas, kuris vienareik\u0161mi\u0161kai identifikuoja tapatyb\u0117s teik\u0117j\u0105 ir yra naudojamas konstruojant nukreipimo nuorod\u0105.
+identity-provider.display-name.tooltip=\u017Dmogui suprantamas, draugi\u0161kas tapatyb\u0117s teik\u0117jo pavadinimas.
 identity-provider.enabled.tooltip=\u012Egalinti \u0161\u012F tapatyb\u0117s teik\u0117j\u0105.
 authenticate-by-default=Autentifikuoti i\u0161 karto
 identity-provider.authenticate-by-default.tooltip=Jei \u012Fgalinta, tuomet bus bandoma autentifikuoti naudotoj\u0105 prie\u0161 parodant prisijungimo lang\u0105.
@@ -460,6 +472,8 @@ select-account.option=paskyros pasirinkimas
 prompt.tooltip=Nurodo, ar autorizacijos serveris galutini\u0173 naudotoj\u0173 reikalauja pakartotinai patvirtinti sutikim\u0105 ar prisijungti.
 validate-signatures=Para\u0161o tikrinimas
 identity-provider.validate-signatures.tooltip=\u012Egalinamas i\u0161orini\u0173 IDP para\u0161\u0173 tikrinimas.
+identity-provider.use-jwks-url.tooltip=Jei \u012Fgalinta, tuomet tapatyb\u0117s teik\u0117jo vie\u0161asis raktas atsiun\u010Diamas i\u0161 pateiktos JWKS URL. \u012Egalinimas suteikia lankstumo, nes tapatyb\u0117s teik\u0117jui pergeneravus raktus jie automati\u0161kai atsiun\u010Diami. Jei \u0161i nuostata i\u0161jungta, tuomet naudojamas Keycloak DB saugomas vie\u0161asis raktas (arba sertifikatas) ir klientui sugeneravus naujus raktus juos rankiniu b\u016Bdu reik\u0117s importuoti \u012F Keycloak DB.
+identity-provider.jwks-url.tooltip=URL, kuriuo pasiekiami tapatyb\u0117s teik\u0117jo JWK formatu saugomi raktai. \u017Di\u016Br\u0117kite JWK specifikacij\u0105 detalesnei informacijai. Jei naudojamas i\u0161orinis Keycloak tapatyb\u0117s teik\u0117jas, tuomet galite naudoti 'http://broker-keycloak:8180/auth/realms/test/protocol/openid-connect/certs' URL (pavyzdyje darome prielaida, kad Keycloak veikia 'http://broker-keycloak:8180' adresu ir naudojama 'test' sritis) 
 validating-public-key=Vie\u0161as raktas para\u0161o tikrinimui
 identity-provider.validating-public-key.tooltip=PEM formato vie\u0161asis raktas, kuris turi b\u016Bti naudojamas i\u0161orinio IDP para\u0161t\u0173 tikrinimui.
 import-external-idp-config=Importuoti i\u0161orinio IDP konfig\u016Bracij\u0105
@@ -839,6 +853,7 @@ include-representation=I\u0161saugoti reprezentacij\u0105
 include-representation.tooltip=I\u0161saugoti kur\u016Bmo ir redagavimo u\u017Eklaus\u0173 JSON reprezentacij\u0105.
 clear-admin-events.tooltip=I\u0161trina visus su administravimu susijusius veiksmus i\u0161 duomen\u0173 baz\u0117s.
 server-version=Serverio versija
+server-profile=Serverio profilis
 info=Informacija
 providers=Teik\u0117jai
 server-time=Serverio laikas
@@ -972,9 +987,9 @@ authz-select-user=Parinkite naudotoj\u0105
 authz-entitlements=Teis\u0117s
 authz-no-resources=Resurs\u0173 n\u0117ra
 authz-result=Rezultatas
-authz-authorization-services-enabled=Autorizacija \u012Fgalinta
+authz-authorization-services-enabled=\u012Egalinti autorizacij\u0105
 authz-authorization-services-enabled.tooltip=\u012Egalinti detal\u0173 kliento autorizacijos palaikym\u0105
-authz-required=Privalimas
+authz-required=Privalomas
 
 # Authz Settings
 authz-import-config.tooltip=Importuoti \u0161io resurs\u0173 serverio autorizacijos nustatym\u0173 JSON rinkmen\u0105.
diff --git a/themes/src/main/resources/theme/base/admin/messages/messages_lt.properties b/themes/src/main/resources/theme/base/admin/messages/messages_lt.properties
index 4908925..5cc51a2 100644
--- a/themes/src/main/resources/theme/base/admin/messages/messages_lt.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/messages_lt.properties
@@ -14,4 +14,11 @@ ldapErrorCantWriteOnlyForReadOnlyLdap=Negalima nustatyti ra\u0161ymo r\u0117\u01
 ldapErrorCantWriteOnlyAndReadOnly=Negalima nustatyti tik ra\u0161yti ir tik skaityti kartu
 
 clientRedirectURIsFragmentError=Nurodykite URI fragment\u0105, kurio negali b\u016Bti peradresuojamuose URI adresuose
-clientRootURLFragmentError=Nurodykite URL fragment\u0105, kurio negali b\u016Bti \u0161akniniame URL adrese
\ No newline at end of file
+clientRootURLFragmentError=Nurodykite URL fragment\u0105, kurio negali b\u016Bti \u0161akniniame URL adrese
+
+pairwiseMalformedClientRedirectURI=Klientas pateik\u0117 neteising\u0105 nukreipimo nuorod\u0105.
+pairwiseClientRedirectURIsMissingHost=Kliento nukreipimo nuorodos privalo b\u016Bti nurodytos su serverio vardo komponentu.
+pairwiseClientRedirectURIsMultipleHosts=Kuomet nesukonfig\u016Bruotas sektoriaus identifikatoriaus URL, kliento nukreipimo nuorodos privalo talpinti ne daugiau kaip vien\u0105 skirting\u0105 serverio vardo komponent\u0105.
+pairwiseMalformedSectorIdentifierURI=Neteisinga sektoriaus identifikatoriaus URI.
+pairwiseFailedToGetRedirectURIs=Nepavyko gauti nukreipimo nuorod\u0173 i\u0161 sektoriaus identifikatoriaus URI.
+pairwiseRedirectURIsMismatch=Kliento nukreipimo nuoroda neatitinka nukreipimo nuorodo\u0173 i\u0161 sektoriaus identifikatoriaus URI.
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index a7135f6..6ea2154 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -1955,7 +1955,7 @@ module.controller('CreateExecutionCtrl', function($scope, realm, parentFlow, for
 
 
 
-module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flows, selectedFlow, LastFlowSelected,
+module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flows, selectedFlow, LastFlowSelected, Dialog,
                                                       AuthenticationFlows, AuthenticationFlowsCopy, AuthenticationFlowExecutions,
                                                       AuthenticationExecution, AuthenticationExecutionRaisePriority, AuthenticationExecutionLowerPriority,
                                                       $modal, Notifications, CopyDialog, $location) {
@@ -2034,6 +2034,12 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo
         })
     };
 
+    $scope.deleteFlow = function() {
+        Dialog.confirmDelete($scope.flow.alias, 'flow', function() {
+            $scope.removeFlow();
+        });
+    };
+    
     $scope.removeFlow = function() {
         console.log('Remove flow:' + $scope.flow.alias);
         if (realm.browserFlow == $scope.flow.alias) {
@@ -2053,9 +2059,8 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo
 
         } else {
             AuthenticationFlows.remove({realm: realm.realm, flow: $scope.flow.id}, function () {
-                $location.url("/realms/" + realm.realm + '/authentication/flows');
+                $location.url("/realms/" + realm.realm + '/authentication/flows/' + flows[0].alias);
                 Notifications.success("Flow removed");
-
             })
         }
 
@@ -2100,10 +2105,14 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo
 
     $scope.removeExecution = function(execution) {
         console.log('removeExecution: ' + execution.id);
-        AuthenticationExecution.remove({realm: realm.realm, execution: execution.id}, function() {
-            Notifications.success("Execution removed");
-            setupForm();
-        })
+        var exeOrFlow = execution.authenticationFlow ? 'flow' : 'execution';
+        Dialog.confirmDelete(execution.displayName, exeOrFlow, function() {
+            AuthenticationExecution.remove({realm: realm.realm, execution: execution.id}, function() {
+                Notifications.success("The " + exeOrFlow + " was removed.");
+                setupForm();
+            });
+        });
+        
     }
 
     $scope.raisePriority = function(execution) {
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flows.html b/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flows.html
index da867b0..3ef54b3 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flows.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authentication-flows.html
@@ -17,7 +17,7 @@
                 <div class="pull-right" data-ng-show="access.manageRealm">
                     <button class="btn btn-default" data-ng-click="createFlow()">{{:: 'new' | translate}}</button>
                     <button class="btn btn-default" data-ng-click="copyFlow()">{{:: 'copy' | translate}}</button>
-                    <button class="btn btn-default" data-ng-hide="flow.builtIn" data-ng-click="removeFlow()">{{:: 'delete' | translate}}</button>
+                    <button class="btn btn-default" data-ng-hide="flow.builtIn" data-ng-click="deleteFlow()">{{:: 'delete' | translate}}</button>
                     <button class="btn btn-default" data-ng-hide="flow.builtIn" data-ng-click="addExecution()">{{:: 'add-execution' | translate}}</button>
                     <button class="btn btn-default" data-ng-hide="flow.builtIn || flow.providerId === 'client-flow'" data-ng-click="addFlow()">{{:: 'add-flow' | translate}}</button>
                 </div>
diff --git a/themes/src/main/resources/theme/base/email/messages/messages_lt.properties b/themes/src/main/resources/theme/base/email/messages/messages_lt.properties
index f0eb51c..a1b59ad 100644
--- a/themes/src/main/resources/theme/base/email/messages/messages_lt.properties
+++ b/themes/src/main/resources/theme/base/email/messages/messages_lt.properties
@@ -1,6 +1,6 @@
 emailVerificationSubject=El. pa\u0161to patvirtinimas
-emailVerificationBody=Paskyra {2} sukurta naudojant \u0161\u012F el. pa\u0161to adres\u0105. Jei tau buvote J\u016Bs, tuomet paspauskite \u017Eemiau esan\u010Di\u0105 nuorod\u0105\n\n{0}\n\n\u0160i nuoroda galioja {1} min.\n\nJei paskyros nek\u016Br\u0117te, tuomet ignuoruokite \u0161\u012F lai\u0161k\u0105. 
-emailVerificationBodyHtml=<p>Paskyra {2} sukurta naudojant \u0161\u012F el. pa\u0161to adres\u0105. Jei tau buvote J\u016Bs, tuomet paspauskite \u017Eemiau esan\u010Di\u0105 nuorod\u0105</p><p><a href=LT"{0}">{0}</a></p><p>\u0160i nuoroda galioja {1} min.</p><p>nJei paskyros nek\u016Br\u0117te, tuomet ignuoruokite \u0161\u012F lai\u0161k\u0105.</p>
+emailVerificationBody=Paskyra {2} sukurta naudojant \u0161\u012F el. pa\u0161to adres\u0105. Jei tai buvote J\u016Bs, tuomet paspauskite \u017Eemiau esan\u010Di\u0105 nuorod\u0105\n\n{0}\n\n\u0160i nuoroda galioja {1} min.\n\nJei paskyros nek\u016Br\u0117te, tuomet ignuoruokite \u0161\u012F lai\u0161k\u0105. 
+emailVerificationBodyHtml=<p>Paskyra {2} sukurta naudojant \u0161\u012F el. pa\u0161to adres\u0105. Jei tao buvote J\u016Bs, tuomet paspauskite \u017Eemiau esan\u010Di\u0105 nuorod\u0105</p><p><a href=LT"{0}">{0}</a></p><p>\u0160i nuoroda galioja {1} min.</p><p>nJei paskyros nek\u016Br\u0117te, tuomet ignuoruokite \u0161\u012F lai\u0161k\u0105.</p>
 identityProviderLinkSubject=S\u0105saja {0}
 identityProviderLinkBody=Ka\u017Eas pageidauja susieti J\u016Bs\u0173 "{1}" paskyr\u0105 su "{0}" {2} naudotojo paskyr\u0105. Jei tai buvote J\u016Bs, tuomet paspauskite \u017Eemiau esan\u010Di\u0105 nuorod\u0105 nor\u0117dami susieti paskyras\n\n{3}\n\n\u0160i nuoroda galioja {4} min.\n\nJei paskyr\u0173 susieti nenorite, tuomet ignoruokite \u0161\u012F lai\u0161k\u0105. Jei paskyras susiesite, tuomet prie {1} gal\u0117siste prisijungti per {0}.
 identityProviderLinkBodyHtml=<p>\u017Eas pageidauja susieti J\u016Bs\u0173 <b>{1}</b> paskyr\u0105 su <b>{0}</b> {2} naudotojo paskyr\u0105. Jei tai buvote J\u016Bs, tuomet paspauskite \u017Eemiau esan\u010Di\u0105 nuorod\u0105 nor\u0117dami susieti paskyras</p><p><a href=LT"{3}">{3}</a></p><p>\u0160i nuoroda galioja {4} min.</p><p>Jei paskyr\u0173 susieti nenorite, tuomet ignoruokite \u0161\u012F lai\u0161k\u0105. Jei paskyras susiesite, tuomet prie {1} gal\u0117siste prisijungti per {0}.</p>
@@ -10,15 +10,15 @@ passwordResetBodyHtml=<p>Ka\u017Ekas pageidauja pakeisti J\u016Bs\u0173 paskyros
 executeActionsSubject=Atnaujinkite savo paskyr\u0105
 executeActionsBody=Sistemos administratorius pageidauja, kad J\u016Bs atnaujintum\u0117te savo {2} paskyr\u0105. Paspauskite \u017Eemiau esan\u010Di\u0105 nuorod\u0105 paskyros duomen\u0173 atnaujinimui.\n\n{0}\n\n\u0160i nuoroda galioja {1} min.\n\nJei J\u016Bs neasate tikri, kad tai administratoriaus pageidavimas, tuomet ignoruokite \u0161\u012F lai\u0161k\u0105 ir niekas nebus pakeista.
 executeActionsBodyHtml=<p>Sistemos administratorius pageidauja, kad J\u016Bs atnaujintum\u0117te savo {2} paskyr\u0105. Paspauskite \u017Eemiau esan\u010Di\u0105 nuorod\u0105 paskyros duomen\u0173 atnaujinimui.</p><p><a href=LT"{0}">{0}</a></p><p>\u0160i nuoroda galioja {1} min.</p><p>Jei J\u016Bs neasate tikri, kad tai administratoriaus pageidavimas, tuomet ignoruokite \u0161\u012F lai\u0161k\u0105 ir niekas nebus pakeista.</p>
-eventLoginErrorSubject=Bandymas prisijungti prie J\u016Bs\u0173 paskyros
-eventLoginErrorBody=Bandymas prisijungti prie J\u016Bs\u0173 paskyros {0} i\u0161 {1} nes\u012Fkmingas. Jei tai nebuvote J\u016Bs, tuomet susisiekite su administratoriumi
-eventLoginErrorBodyHtml=<p>Bandymas prisijungti prie J\u016Bs\u0173 paskyros {0} i\u0161 {1} nes\u012Fkmingas. Jei tai nebuvote J\u016Bs, tuomet susisiekite su administratoriumi</p>
+eventLoginErrorSubject=Nes\u0117kmingas bandymas prisijungti prie j\u016Bs\u0173 paskyros
+eventLoginErrorBody=Bandymas prisijungti prie j\u016Bs\u0173 paskyros {0} i\u0161 {1} nes\u0117kmingas. Jei tai nebuvote j\u016Bs, tuomet susisiekite su administratoriumi
+eventLoginErrorBodyHtml=<p>Bandymas prisijungti prie j\u016Bs\u0173 paskyros {0} i\u0161 {1} nes\u0117kmingas. Jei tai nebuvote j\u016Bs, tuomet susisiekite su administratoriumi</p>
 eventRemoveTotpSubject=TOTP pa\u0161alinimas
 eventRemoveTotpBody=Ka\u017Ekas pageidauja atsieti TOPT J\u016Bs\u0173 {1} paskyroje su {0}. Jei tai nebuvote J\u016Bs, tuomet susisiekite su administratoriumi
 eventRemoveTotpBodyHtml=<p>Ka\u017Ekas pageidauja atsieti TOPT J\u016Bs\u0173 <b>{1}</b> paskyroje su <b>{0}</b>. Jei tai nebuvote J\u016Bs, tuomet susisiekite su administratoriumi</p>
 eventUpdatePasswordSubject=Slapta\u017Eod\u017Eio atnaujinimas
-eventUpdatePasswordBody=J\u016Bs\u0173 slapta\u017Eodis J\u016Bs\u0173 {1} paskyroje su {0} buvo pakeisas. Jei tai nebuvote J\u016Bs, tuomet susisiekite su administratoriumi
-eventUpdatePasswordBodyHtml=<p>J\u016Bs\u0173 {1} paskyroje su {0. Jei tai nebuvote J\u016Bs, tuomet susisiekite su administratoriumi</p>
+eventUpdatePasswordBody={1} paskyroje {0} pakeisas j\u016Bs\u0173 slapta\u017Eodis. Jei J\u016Bs nekeit\u0117te, tuomet susisiekite su administratoriumi
+eventUpdatePasswordBodyHtml=<p>{1} paskyroje {0} pakeisas j\u016Bs\u0173 slapta\u017Eodis. Jei J\u016Bs nekeit\u0117te, tuomet susisiekite su administratoriumi</p>
 eventUpdateTotpSubject=TOTP atnaujinimas
 eventUpdateTotpBody=TOTP J\u016Bs\u0173 {1} paskyroje su {0} buvo atnaujintas. Jei tai nebuvote J\u016Bs, tuomet susisiekite su administratoriumi
-eventUpdateTotpBodyHtml=<p>TOTP J\u016Bs\u0173 {1} paskyroje su {0} buvo atnaujintas. Jei tai nebuvote J\u016Bs, tuomet susisiekite su administratoriumi</p>
+eventUpdateTotpBodyHtml=<p>TOTP J\u016Bs\u0173 {1} paskyroje su {0} buvo atnaujintas. Jei tai nebuvote J\u016Bs, tuomet susisiekite su administratoriumi</p>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/keycloak/welcome/index.ftl b/themes/src/main/resources/theme/keycloak/welcome/index.ftl
index 43bec62..cbb08d6 100755
--- a/themes/src/main/resources/theme/keycloak/welcome/index.ftl
+++ b/themes/src/main/resources/theme/keycloak/welcome/index.ftl
@@ -73,7 +73,7 @@
             <#else>
                 <p>
                     You need local access to create the initial admin user. Open <a href="http://localhost:8080/auth">http://localhost:8080/auth</a>
-                    or use the add-user script.
+                    or use the add-user-keycloak script.
                 </p>
             </#if>
         </#if>
diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml
index cd7fc81..11d1240 100755
--- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml
+++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml
@@ -21,22 +21,22 @@
     <extension-module>org.jboss.as.connector</extension-module>
     <subsystem xmlns="urn:jboss:domain:datasources:4.0">
         <datasources>
-            <xa-datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
-                <xa-datasource-property name="URL">jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</xa-datasource-property>
+            <datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
+                <connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
                 <driver>h2</driver>
                 <security>
                     <user-name>sa</user-name>
                     <password>sa</password>
                 </security>
-            </xa-datasource>
-            <xa-datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true">
-                <xa-datasource-property name="URL"><?KEYCLOAK_DS_CONNECTION_URL?></xa-datasource-property>
+            </datasource>
+            <datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true">
+                <connection-url><?KEYCLOAK_DS_CONNECTION_URL?></connection-url>
                 <driver>h2</driver>
                 <security>
                     <user-name>sa</user-name>
                     <password>sa</password>
                 </security>
-            </xa-datasource>
+            </datasource>
             <drivers>
                 <driver name="h2" module="com.h2database.h2">
                     <xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>