keycloak-memoizeit

Changes

pom.xml 9(+0 -9)

Details

diff --git a/authz/policy/drools/pom.xml b/authz/policy/drools/pom.xml
index 61e92de..d2484ad 100644
--- a/authz/policy/drools/pom.xml
+++ b/authz/policy/drools/pom.xml
@@ -17,6 +17,19 @@
     <name>KeyCloak AuthZ: Drools Policy Provider</name>
     <description>KeyCloak AuthZ: Drools Policy Provider</description>
 
+    <dependencyManagement>
+        <dependencies>
+            <!-- Authorization Drools Policy Provider -->
+            <dependency>
+                <groupId>org.drools</groupId>
+                <artifactId>drools-bom</artifactId>
+                <type>pom</type>
+                <version>${version.org.drools}</version>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
     <dependencies>
         <dependency>
             <groupId>org.keycloak</groupId>
diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml
index ebad67e..7ce0313 100755
--- a/dependencies/server-all/pom.xml
+++ b/dependencies/server-all/pom.xml
@@ -40,6 +40,15 @@
                 <scope>import</scope>
                 <version>${version.jboss-integration-platform}</version>
             </dependency>
+
+            <!-- Authorization Drools Policy Provider -->
+            <dependency>
+                <groupId>org.drools</groupId>
+                <artifactId>drools-bom</artifactId>
+                <type>pom</type>
+                <version>${version.org.drools}</version>
+                <scope>import</scope>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 
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 8c4235c..e445d57 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
@@ -78,8 +78,8 @@ public class Keycloak {
         return new Keycloak(serverUrl, realm, username, password, clientId, null, PASSWORD, null, null);
     }
 
-    public static Keycloak getInstance(String serverUrl, String realm, String clientId, String authtoken) {
-        return new Keycloak(serverUrl, realm, null, null, clientId, null, PASSWORD, null, null);
+    public static Keycloak getInstance(String serverUrl, String realm, String clientId, String authToken) {
+        return new Keycloak(serverUrl, realm, null, null, clientId, null, PASSWORD, null, authToken);
     }
 
     public RealmsResource realms() {
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleMappingResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleMappingResource.java
index 7621eac..bed37e6 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleMappingResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleMappingResource.java
@@ -39,7 +39,7 @@ public interface RoleMappingResource {
     @Path("realm")
     public RoleScopeResource realmLevel();
 
-    @Path("clients/{clientId}")
-    public RoleScopeResource clientLevel(@PathParam("clientId") String clientId);
+    @Path("clients/{clientUUID}")
+    public RoleScopeResource clientLevel(@PathParam("clientUUID") String clientUUID);
 
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java
index af47a13..94aae1e 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java
@@ -17,6 +17,8 @@
 
 package org.keycloak.models.jpa.session;
 
+import org.keycloak.storage.jpa.KeyUtils;
+
 import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.Id;
@@ -84,6 +86,7 @@ public class PersistentUserSessionEntity {
     }
 
     public void setUserId(String userId) {
+        KeyUtils.assertValidKey(userId);
         this.userId = userId;
     }
 
diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/BrokerLinkEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/BrokerLinkEntity.java
index 24cddf4..a32acb6 100755
--- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/BrokerLinkEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/BrokerLinkEntity.java
@@ -17,6 +17,8 @@
 
 package org.keycloak.storage.jpa.entity;
 
+import org.keycloak.storage.jpa.KeyUtils;
+
 import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.Id;
@@ -71,6 +73,7 @@ public class BrokerLinkEntity {
     }
 
     public void setUserId(String userId) {
+        KeyUtils.assertValidKey(userId);
         this.userId = userId;
     }
 
diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUser.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUser.java
index c74c630..ebc1db0 100644
--- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUser.java
+++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUser.java
@@ -16,6 +16,8 @@
  */
 package org.keycloak.storage.jpa.entity;
 
+import org.keycloak.storage.jpa.KeyUtils;
+
 import javax.persistence.Access;
 import javax.persistence.AccessType;
 import javax.persistence.Column;
@@ -58,6 +60,7 @@ public class FederatedUser {
     }
 
     public void setId(String id) {
+        KeyUtils.assertValidKey(id);
         this.id = id;
     }
 
diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserGroupMembershipEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserGroupMembershipEntity.java
index ffe130a..2cf6857 100755
--- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserGroupMembershipEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserGroupMembershipEntity.java
@@ -17,6 +17,8 @@
 
 package org.keycloak.storage.jpa.entity;
 
+import org.keycloak.storage.jpa.KeyUtils;
+
 import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.Id;
@@ -74,6 +76,7 @@ public class FederatedUserGroupMembershipEntity {
     }
 
     public void setUserId(String userId) {
+        KeyUtils.assertValidKey(userId);
         this.userId = userId;
     }
 
diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRequiredActionEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRequiredActionEntity.java
index cea6908..d4b4879 100755
--- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRequiredActionEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRequiredActionEntity.java
@@ -17,6 +17,8 @@
 
 package org.keycloak.storage.jpa.entity;
 
+import org.keycloak.storage.jpa.KeyUtils;
+
 import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.Id;
@@ -69,6 +71,7 @@ public class FederatedUserRequiredActionEntity {
     }
 
     public void setUserId(String userId) {
+        KeyUtils.assertValidKey(userId);
         this.userId = userId;
     }
 
diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRoleMappingEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRoleMappingEntity.java
index a7c4099..1a2580f 100755
--- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRoleMappingEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRoleMappingEntity.java
@@ -17,6 +17,8 @@
 
 package org.keycloak.storage.jpa.entity;
 
+import org.keycloak.storage.jpa.KeyUtils;
+
 import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.Id;
@@ -64,6 +66,7 @@ public class FederatedUserRoleMappingEntity {
     }
 
     public void setUserId(String userId) {
+        KeyUtils.assertValidKey(userId);
         this.userId = userId;
     }
 
diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/KeyUtils.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/KeyUtils.java
new file mode 100644
index 0000000..ac2bc25
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/KeyUtils.java
@@ -0,0 +1,56 @@
+/*
+ * 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.storage.jpa;
+
+import java.util.regex.Pattern;
+import org.jboss.logging.Logger;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class KeyUtils {
+
+    private static final Logger LOG = Logger.getLogger(KeyUtils.class);
+
+    public static final Pattern UUID_PATTERN = Pattern.compile("[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}");
+
+    public static final Pattern EXPECTED_KEY_PATTERN = Pattern.compile(
+      UUID_PATTERN.pattern()
+      + "|"
+      + "f:" + UUID_PATTERN.pattern() + ":.*"
+    );
+
+    /**
+     * Returns {@code} true when the key is {@code null} or either a plain UUID or a key formatted as "f:[UUID]:any_string"
+     * @param key String representation of the key
+     * @return 
+     */
+    public static boolean isValidKey(String key) {
+        return key == null || EXPECTED_KEY_PATTERN.matcher(key).matches();
+    }
+
+    /**
+     * Logs an warning when the key is not a valid key
+     * @param key String representation of the key
+     */
+    public static void assertValidKey(String key) throws IllegalArgumentException {
+        if (! isValidKey(key)) {
+            LOG.warnf("The given key is not a valid key per specification, future migration might fail: %s", key);
+        }
+    }
+}
diff --git a/model/jpa/src/test/java/org/keycloak/storage/jpa/KeyUtilsTest.java b/model/jpa/src/test/java/org/keycloak/storage/jpa/KeyUtilsTest.java
new file mode 100644
index 0000000..8dce67b
--- /dev/null
+++ b/model/jpa/src/test/java/org/keycloak/storage/jpa/KeyUtilsTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.storage.jpa;
+
+import java.util.UUID;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class KeyUtilsTest {
+
+    @Test
+    public void testValidKeys() {
+        assertTrue(KeyUtils.isValidKey(UUID.randomUUID().toString()));
+        assertTrue(KeyUtils.isValidKey("01234567-1234-1234-aAAa-123456789012"));
+        assertTrue(KeyUtils.isValidKey("01234567-1234-1234-aAAf-123456789012"));
+
+        assertTrue(KeyUtils.isValidKey("f:" + UUID.randomUUID() + ":dsadsada"));
+        assertTrue(KeyUtils.isValidKey("f:01234567-1234-1234-aAAa-123456789012:dsadsada"));
+        assertTrue(KeyUtils.isValidKey("f:a1234567-1234-1234-aAAa-123456789012:dsadsada"));
+    }
+
+    @Test
+    public void testInvalidKeys() {
+        assertFalse(KeyUtils.isValidKey("any string"));
+        assertFalse(KeyUtils.isValidKey("0"));
+        assertFalse(KeyUtils.isValidKey("01234567-1234-1234-aAAg-123456789012a"));
+        assertFalse(KeyUtils.isValidKey("z1234567-1234-1234-aAAa-123456789012"));
+
+        assertFalse(KeyUtils.isValidKey("f:g1234567-1234-1234-aAAa-123456789012:dsadsada"));
+        assertFalse(KeyUtils.isValidKey("g:a1234567-1234-1234-aAAa-123456789012:dsadsada"));
+        assertFalse(KeyUtils.isValidKey("f:a1234567-1234-1234-aAAa-123456789012"));
+    }
+
+}

pom.xml 9(+0 -9)

diff --git a/pom.xml b/pom.xml
index 5d9d3f0..6eae54b 100755
--- a/pom.xml
+++ b/pom.xml
@@ -428,15 +428,6 @@
                 <version>${google.zxing.version}</version>
             </dependency>
 
-            <!-- Authorization Drools Policy Provider -->
-            <dependency>
-                <groupId>org.drools</groupId>
-                <artifactId>drools-bom</artifactId>
-                <type>pom</type>
-                <version>${version.org.drools}</version>
-                <scope>import</scope>
-            </dependency>
-
             <!-- Email Test Servers -->
             <dependency>
                 <groupId>com.icegreen</groupId>
diff --git a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
index d9a57cf..50972f4 100755
--- a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
@@ -61,7 +61,7 @@ public class AppAuthManager extends AuthenticationManager {
     public AuthResult authenticateBearerToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
         String tokenString = extractAuthorizationHeaderToken(headers);
         if (tokenString == null) return null;
-        AuthResult authResult = verifyIdentityToken(session, realm, uriInfo, connection, true, true, tokenString, headers);
+        AuthResult authResult = verifyIdentityToken(session, realm, uriInfo, connection, true, true, false, tokenString, headers);
         return authResult;
     }
 
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 1392328..5d467da 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -108,6 +108,15 @@ public class AuthenticationManager {
         return userSession.getLastSessionRefresh() + realm.getSsoSessionIdleTimeout() > currentTime && max > currentTime;
     }
 
+    public static boolean isOfflineSessionValid(RealmModel realm, UserSessionModel userSession) {
+        if (userSession == null) {
+            logger.debug("No offline user session");
+            return false;
+        }
+        int currentTime = Time.currentTime();
+        return userSession.getLastSessionRefresh() + realm.getOfflineSessionIdleTimeout() > currentTime;
+    }
+
     public static void expireUserSessionCookie(KeycloakSession session, UserSessionModel userSession, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, ClientConnection connection) {
         try {
             // check to see if any identity cookie is set with the same session and expire it if necessary
@@ -390,7 +399,7 @@ public class AuthenticationManager {
         }
 
         String tokenString = cookie.getValue();
-        AuthResult authResult = verifyIdentityToken(session, realm, session.getContext().getUri(), session.getContext().getConnection(), checkActive, false, tokenString, session.getContext().getRequestHeaders());
+        AuthResult authResult = verifyIdentityToken(session, realm, session.getContext().getUri(), session.getContext().getConnection(), checkActive, false, true, tokenString, session.getContext().getRequestHeaders());
         if (authResult == null) {
             expireIdentityCookie(realm, session.getContext().getUri(), session.getContext().getConnection());
             return null;
@@ -691,7 +700,7 @@ public class AuthenticationManager {
 
 
     protected static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, boolean checkTokenType,
-                                                    String tokenString, HttpHeaders headers) {
+                                                    boolean isCookie, String tokenString, HttpHeaders headers) {
         try {
             TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType);
             String kid = verifier.getHeader().getKeyId();
@@ -729,6 +738,14 @@ public class AuthenticationManager {
 
             UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
             if (!isSessionValid(realm, userSession)) {
+                // Check if accessToken was for the offline session.
+                if (!isCookie) {
+                    UserSessionModel offlineUserSession = session.sessions().getUserSession(realm, token.getSessionState());
+                    if (isOfflineSessionValid(realm, offlineUserSession)) {
+                        return new AuthResult(user, offlineUserSession, token);
+                    }
+                }
+
                 if (userSession != null) backchannelLogout(session, realm, userSession, uriInfo, connection, headers, true);
                 logger.debug("User session not active");
                 return null;
diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/common/install-features.sh b/testsuite/integration-arquillian/servers/app-server/karaf/common/install-features.sh
index d094ff4..b8cba84 100755
--- a/testsuite/integration-arquillian/servers/app-server/karaf/common/install-features.sh
+++ b/testsuite/integration-arquillian/servers/app-server/karaf/common/install-features.sh
@@ -23,7 +23,7 @@ do
         fi
 
         if "$UPDATE_CONFIG" == "true"; then
-            echo "Updating Config - org.ops4j.pax.url.mvn"
+            echo "Updating Config"
             ./client $CLIENT_AUTH -f update-config.cli
             if [ $? -ne 0 ]; then 
                 RESULT=1; 
@@ -36,6 +36,14 @@ do
         ./client $CLIENT_AUTH -f install-features.cli
         if [ $? -ne 0 ]; then RESULT=1; fi
 
+        if "$UPDATE_CONFIG" == "true"; then
+            echo "Updating Config - Keycloak authentication"
+            ./client $CLIENT_AUTH -f update-config-auth.cli
+            if [ $? -ne 0 ]; then
+                RESULT=1;
+            fi
+        fi
+
         ./stop
         rm -rf ../data/log
         rm -rf ../data/tmp
diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml
index 80dfc56..a048eea 100644
--- a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/pom.xml
@@ -32,7 +32,7 @@
     <properties>
         <app.server.karaf>fuse63</app.server.karaf>
         <app.server.karaf.groupId>org.jboss.fuse</app.server.karaf.groupId>
-        <app.server.karaf.artifactId>jboss-fuse-full</app.server.karaf.artifactId>
+        <app.server.karaf.artifactId>jboss-fuse-karaf</app.server.karaf.artifactId>
         <app.server.karaf.version>${fuse63.version}</app.server.karaf.version>
         <app.server.karaf.unpacked.folder.name>jboss-fuse-${fuse63.version}</app.server.karaf.unpacked.folder.name>
         <app.server.karaf.client.auth>-u admin -p admin</app.server.karaf.client.auth>
diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/keycloak-direct-access.json b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/keycloak-direct-access.json
new file mode 100644
index 0000000..2441134
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/keycloak-direct-access.json
@@ -0,0 +1,9 @@
+{
+    "realm": "demo",
+    "resource": "ssh-jmx-admin-client",
+    "ssl-required" : "external",
+    "auth-server-url" : "http://localhost:8080/auth",
+    "credentials": {
+        "secret": "password"
+    }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/keycloak-hawtio.json b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/keycloak-hawtio.json
new file mode 100644
index 0000000..36daac0
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/keycloak-hawtio.json
@@ -0,0 +1,9 @@
+{
+  "realm" : "demo",
+  "resource" : "jaas",
+  "bearer-only" : true,
+  "auth-server-url" : "http://localhost:8080/auth",
+  "ssl-required" : "external",
+  "use-resource-role-mappings": false,
+  "principal-attribute": "preferred_username"
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/keycloak-hawtio-client.json b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/keycloak-hawtio-client.json
new file mode 100644
index 0000000..64d0d57
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/keycloak-hawtio-client.json
@@ -0,0 +1,7 @@
+{
+  "realm" : "demo",
+  "resource" : "hawtio-client",
+  "auth-server-url" : "http://localhost:8080/auth",
+  "ssl-required" : "external",
+  "public-client" : true
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config.cli b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config.cli
index d6a425a..9a31940 100644
--- a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config.cli
+++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config.cli
@@ -3,3 +3,8 @@ config:propset org.ops4j.pax.url.mvn.localRepository ${maven.repo.local}
 config:propset org.ops4j.pax.url.mvn.settings ${maven.local.settings}
 config:propappend org.ops4j.pax.url.mvn.repositories ${repositories}
 config:update
+config:edit jmx.acl.org.apache.karaf.security.jmx
+config:propappend list* viewer
+config:propappend set* jmxAdmin
+config:propappend * jmxAdmin,admin
+config:update
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config-auth.cli b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config-auth.cli
new file mode 100644
index 0000000..2c3bc66
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/app-server/karaf/fuse63/src/main/resources/update-config-auth.cli
@@ -0,0 +1,8 @@
+config:edit org.apache.karaf.shell
+config:propset sshRealm keycloak
+config:update
+system-property -p hawtio.roles admin,user
+system-property -p hawtio.keycloakEnabled true
+system-property -p hawtio.realm keycloak
+system-property -p hawtio.keycloakClientConfig \$\{karaf.base\}/etc/keycloak-hawtio-client.json
+system-property -p hawtio.rolePrincipalClasses org.keycloak.adapters.jaas.RolePrincipal,org.apache.karaf.jaas.boot.principal.RolePrincipal
diff --git a/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml b/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml
index ecff21a..b717b3d 100644
--- a/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/karaf/pom.xml
@@ -117,6 +117,7 @@
                                             <includes>
                                                 <include>install-features.cli</include>
                                                 <include>update-config.cli</include>
+                                                <include>update-config-auth.cli</include>
                                             </includes>
                                             <filtering>true</filtering>
                                         </resource>
@@ -124,7 +125,7 @@
                                 </configuration>
                             </execution>
                             <execution>
-                                <id>copy-users-properties</id>
+                                <id>copy-configs</id>
                                 <phase>process-resources</phase>
                                 <goals>
                                     <goal>copy-resources</goal>
@@ -137,6 +138,9 @@
                                             <directory>src/main/resources</directory>
                                             <includes>
                                                 <include>users.properties</include>
+                                                <include>keycloak-direct-access.json</include>
+                                                <include>keycloak-hawtio-client.json</include>
+                                                <include>keycloak-hawtio.json</include>
                                             </includes>
                                         </resource>
                                     </resources>
diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index 198c0aa..58050dd 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -81,6 +81,26 @@
             <artifactId>greenmail</artifactId>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>org.jboss.arquillian.container</groupId>
+            <artifactId>arquillian-container-karaf-managed</artifactId>
+            <version>2.1.0.CR18</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.arquillian.container</groupId>
+            <artifactId>arquillian-container-osgi</artifactId>
+            <version>2.1.0.CR18</version>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.enterprise</artifactId>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
     
     <build>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/karaf/CustomKarafContainer.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/karaf/CustomKarafContainer.java
new file mode 100644
index 0000000..46f43a3
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/karaf/CustomKarafContainer.java
@@ -0,0 +1,231 @@
+package org.keycloak.testsuite.arquillian.karaf;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectName;
+
+import org.jboss.arquillian.container.osgi.jmx.JMXDeployableContainer;
+import org.jboss.arquillian.container.osgi.jmx.ObjectNameFactory;
+import org.jboss.arquillian.container.osgi.karaf.managed.KarafManagedContainerConfiguration;
+import org.jboss.arquillian.container.spi.client.container.LifecycleException;
+import org.osgi.jmx.framework.BundleStateMBean;
+import org.osgi.jmx.framework.FrameworkMBean;
+import org.osgi.jmx.framework.ServiceStateMBean;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * KarafManagedDeployableContainer
+ *
+ * @author thomas.diesler@jboss.com
+ */
+public class CustomKarafContainer<T extends KarafManagedContainerConfiguration> extends JMXDeployableContainer<T> {
+
+    static final Logger _logger = LoggerFactory.getLogger(CustomKarafContainer.class.getPackage().getName());
+
+    private KarafManagedContainerConfiguration config;
+    private Process process;
+
+    @Override
+    public Class<T> getConfigurationClass() {
+        @SuppressWarnings("unchecked")
+        Class<T> clazz = (Class<T>) KarafManagedContainerConfiguration.class;
+        return clazz;
+    }
+
+    @Override
+    public void setup(T config) {
+        super.setup(config);
+        this.config = config;
+    }
+
+    @Override
+    public void start() throws LifecycleException {
+
+        // Try to connect to an already running server
+        MBeanServerConnection mbeanServer = null;
+        try {
+            mbeanServer = getMBeanServerConnection(500, TimeUnit.MILLISECONDS);
+        } catch (TimeoutException ex) {
+            // ignore
+        }
+
+        if (mbeanServer != null && !config.isAllowConnectingToRunningServer()) {
+            throw new LifecycleException(
+                    "The server is already running! Managed containers does not support connecting to running server instances due to the " +
+                    "possible harmful effect of connecting to the wrong server. Please stop server before running or change to another type of container.\n" +
+                    "To disable this check and allow Arquillian to connect to a running server, set allowConnectingToRunningServer to true in the container configuration");
+        }
+
+        // Start the Karaf process
+        if (mbeanServer == null) {
+            String karafHome = config.getKarafHome();
+            if (karafHome == null)
+                throw new IllegalStateException("karafHome cannot be null");
+
+            File karafHomeDir = new File(karafHome).getAbsoluteFile();
+            if (!karafHomeDir.isDirectory())
+                throw new IllegalStateException("Not a valid Karaf home dir: " + karafHomeDir);
+
+            String java = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
+            _logger.info(String.format("Using java: %s", java));
+
+            List<String> cmd = new ArrayList<String>();
+            cmd.add(java);
+
+            // JavaVM args
+            String javaArgs = config.getJavaVmArguments();
+            if (!javaArgs.contains("-Xmx")) {
+                javaArgs = KarafManagedContainerConfiguration.DEFAULT_JAVAVM_ARGUMENTS + " " + javaArgs;
+            }
+            cmd.addAll(Arrays.asList(javaArgs.split("\\s")));
+
+            // Karaf properties
+            cmd.add("-Dkaraf.home=" + karafHomeDir);
+            cmd.add("-Dkaraf.base=" + karafHomeDir);
+            cmd.add("-Dkaraf.etc=" + karafHomeDir + "/etc");
+            cmd.add("-Dkaraf.data=" + karafHomeDir + "/data");
+            cmd.add("-Dkaraf.instances=" + karafHomeDir + "/instances");
+            cmd.add("-Dkaraf.startLocalConsole=false");
+            cmd.add("-Dkaraf.startRemoteShell=true");
+
+            // Java properties
+            cmd.add("-Djava.io.tmpdir=" + new File(karafHomeDir, "data/tmp"));
+            cmd.add("-Djava.util.logging.config.file=" + new File(karafHomeDir, "etc/java.util.logging.properties"));
+            cmd.add("-Djava.endorsed.dirs=" + new File(karafHomeDir, "lib/endorsed"));
+
+            // Classpath
+            StringBuilder classPath = new StringBuilder();
+            File karafLibDir = new File(karafHomeDir, "lib");
+            String[] libs = karafLibDir.list(new FilenameFilter() {
+                @Override
+                public boolean accept(File dir, String name) {
+                    return name.startsWith("karaf");
+                }
+            });
+            for (String lib : libs) {
+                String separator = classPath.length() > 0 ? File.pathSeparator : "";
+                classPath.append(separator).append(new File(karafHomeDir, "lib/" + lib));
+            }
+            cmd.add("-classpath");
+            cmd.add(classPath.toString());
+
+            // Main class
+            cmd.add("org.apache.karaf.main.Main");
+
+            // Output the startup command
+            StringBuffer cmdstr = new StringBuffer();
+            for (String tok : cmd) {
+                cmdstr.append(tok).append(" ");
+            }
+            _logger.debug("Starting Karaf with: {}", cmdstr);
+
+            try {
+                ProcessBuilder processBuilder = new ProcessBuilder(cmd);
+                processBuilder.directory(karafHomeDir);
+                processBuilder.redirectErrorStream(true);
+                process = processBuilder.start();
+                new Thread(new ConsoleConsumer()).start();
+            } catch (Exception ex) {
+                throw new LifecycleException("Cannot start managed Karaf container", ex);
+            }
+
+            // Get the MBeanServerConnection
+            try {
+                mbeanServer = getMBeanServerConnection(30, TimeUnit.SECONDS);
+            } catch (Exception ex) {
+                destroyKarafProcess();
+                throw new LifecycleException("Cannot obtain MBean server connection", ex);
+            }
+        }
+
+        mbeanServerInstance.set(mbeanServer);
+
+        try {
+            // Get the FrameworkMBean
+            ObjectName oname = ObjectNameFactory.create("osgi.core:type=framework,*");
+            frameworkMBean = getMBeanProxy(mbeanServer, oname, FrameworkMBean.class, 30, TimeUnit.SECONDS);
+
+            // Get the BundleStateMBean
+            oname = ObjectNameFactory.create("osgi.core:type=bundleState,*");
+            bundleStateMBean = getMBeanProxy(mbeanServer, oname, BundleStateMBean.class, 30, TimeUnit.SECONDS);
+
+            // Get the BundleStateMBean
+            oname = ObjectNameFactory.create("osgi.core:type=serviceState,*");
+            serviceStateMBean = getMBeanProxy(mbeanServer, oname, ServiceStateMBean.class, 30, TimeUnit.SECONDS);
+
+            // Install the arquillian bundle to become active
+            installArquillianBundle();
+
+            // Await the arquillian bundle to become active
+            awaitArquillianBundleActive(30, TimeUnit.SECONDS);
+
+            // Await the beginning start level
+            Integer beginningStartLevel = config.getKarafBeginningStartLevel();
+            if (beginningStartLevel != null)
+                awaitBeginningStartLevel(beginningStartLevel, 30, TimeUnit.SECONDS);
+
+            // Await bootsrap complete services
+            awaitBootstrapCompleteServices();
+
+        } catch (RuntimeException rte) {
+            destroyKarafProcess();
+            throw rte;
+        } catch (Exception ex) {
+            destroyKarafProcess();
+            throw new LifecycleException("Cannot start Karaf container", ex);
+        }
+    }
+
+    @Override
+    public void stop() throws LifecycleException {
+        super.stop();
+        destroyKarafProcess();
+    }
+
+    private void destroyKarafProcess() throws LifecycleException {
+        if (process != null) {
+            process.destroy();            
+            try {
+                process.waitFor();
+            } catch (InterruptedException e) {
+                throw new LifecycleException("Cannot start Karaf container", e);
+            }
+        }
+    }
+
+    /**
+     * Runnable that consumes the output of the process. If nothing consumes the output the AS will hang on some platforms
+     *
+     * @author Stuart Douglas
+     */
+    private class ConsoleConsumer implements Runnable {
+
+        @Override
+        public void run() {
+            final InputStream stream = process.getInputStream();
+            final boolean writeOutput = config.isOutputToConsole();
+            try {
+                byte[] buf = new byte[32];
+                int num;
+                // Do not try reading a line cos it considers '\r' end of line
+                while ((num = stream.read(buf)) != -1) {
+                    if (writeOutput)
+                        System.out.write(buf, 0, num);
+                }
+            } catch (IOException ignored) {
+            }
+        }
+    }
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
index c9a9d89..310a3bc 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
@@ -17,6 +17,7 @@
 
 package org.keycloak.testsuite.arquillian;
 
+import org.jboss.arquillian.container.spi.client.container.DeployableContainer;
 import org.jboss.arquillian.container.test.impl.enricher.resource.URLResourceProvider;
 import org.jboss.arquillian.container.test.spi.client.deployment.ApplicationArchiveProcessor;
 import org.jboss.arquillian.container.test.spi.client.deployment.DeploymentScenarioGenerator;
@@ -26,6 +27,7 @@ import org.jboss.arquillian.graphene.location.CustomizableURLResourceProvider;
 import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider;
 import org.jboss.arquillian.test.spi.execution.TestExecutionDecider;
 import org.keycloak.testsuite.arquillian.h2.H2TestEnricher;
+import org.keycloak.testsuite.arquillian.karaf.CustomKarafContainer;
 import org.keycloak.testsuite.arquillian.migration.MigrationTestExecutionDecider;
 import org.keycloak.testsuite.arquillian.provider.AdminClientProvider;
 import org.keycloak.testsuite.arquillian.provider.OAuthClientProvider;
@@ -51,10 +53,10 @@ public class KeycloakArquillianExtension implements LoadableExtension {
         builder
                 .service(DeploymentScenarioGenerator.class, DeploymentTargetModifier.class)
                 .service(ApplicationArchiveProcessor.class, DeploymentArchiveProcessor.class)
+                .service(DeployableContainer.class, CustomKarafContainer.class)
                 .observer(AuthServerTestEnricher.class)
                 .observer(AppServerTestEnricher.class)
                 .observer(H2TestEnricher.class);
-
         builder
                 .service(TestExecutionDecider.class, MigrationTestExecutionDecider.class);
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseAdminAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseAdminAdapterTest.java
new file mode 100644
index 0000000..3cc9b38
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseAdminAdapterTest.java
@@ -0,0 +1,258 @@
+package org.keycloak.testsuite.adapter.example;
+
+import static org.junit.Assert.assertTrue;
+import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
+import static org.keycloak.testsuite.util.IOUtil.loadRealm;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.management.InstanceNotFoundException;
+import javax.management.MBeanException;
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectName;
+import javax.management.ReflectionException;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXServiceURL;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.channel.ClientChannel;
+import org.apache.sshd.client.channel.ClientChannelEvent;
+import org.apache.sshd.client.future.ConnectFuture;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.session.ClientSession.ClientSessionEvent;
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
+import org.keycloak.testsuite.adapter.page.HawtioPage;
+
+public abstract class AbstractFuseAdminAdapterTest extends AbstractExampleAdapterTest {
+    
+    @Page
+    private HawtioPage hawtioPage;
+    
+    private SshClient client;
+    
+    private ClientChannel channel;
+    
+    private ClientSession session;
+    
+    enum Result { OK, NOT_FOUND, NO_CREDENTIALS, NO_ROLES };
+    
+    @Override
+    public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
+        RealmRepresentation fuseRealm = loadRealm(new File(EXAMPLES_HOME_DIR + "/fuse/demorealm.json"));
+        testRealms.add(fuseRealm);
+    }
+
+    @Override
+    public void setDefaultPageUriParameters() {
+        super.setDefaultPageUriParameters();
+        testRealmPage.setAuthRealm(DEMO);
+        testRealmLoginPage.setAuthRealm(DEMO);
+    }
+        
+    @Test
+    public void hawtioLoginTest() throws Exception {
+        hawtioPage.navigateTo();
+        testRealmLoginPage.form().login("user", "invalid-password");
+        assertCurrentUrlDoesntStartWith(hawtioPage);
+
+        testRealmLoginPage.form().login("invalid-user", "password");
+        assertCurrentUrlDoesntStartWith(hawtioPage);
+
+        testRealmLoginPage.form().login("root", "password");
+        assertCurrentUrlStartsWith(hawtioPage.getDriver(), hawtioPage.toString() + "/welcome");
+        hawtioPage.logout();
+        assertCurrentUrlStartsWith(testRealmLoginPage);
+        
+        hawtioPage.navigateTo();
+        testRealmLoginPage.form().login("mary", "password");
+        assertTrue(!driver.getPageSource().contains("welcome"));
+    }
+    
+    
+    
+    @Test
+    public void sshLoginTest() throws Exception {
+        assertCommand("mary", "password", "shell:date", Result.NO_ROLES);
+        assertCommand("john", "password", "shell:info", Result.NO_CREDENTIALS);
+        assertCommand("john", "password", "shell:date", Result.OK);
+        assertCommand("root", "password", "shell:info", Result.OK);
+    }
+
+    @Test
+    public void jmxLoginTest() throws Exception {
+        setJMXAuthentication("keycloak", "password");
+        ObjectName mbean = new ObjectName("org.apache.karaf:type=config,name=root");
+        //invalid credentials
+        try {
+            getJMXConnector("mary", "password1").getMBeanServerConnection();
+            Assert.fail();
+        } catch (SecurityException se) {}
+        //no role
+        MBeanServerConnection connection = getJMXConnector("mary", "password").getMBeanServerConnection();
+        assertJmxInvoke(false, connection, mbean, "listProperties", new Object [] {""}, new String [] {String.class.getName()});
+        assertJmxInvoke(false, connection, mbean, "setProperty", new Object [] {"", "x", "y"}, new String [] {String.class.getName(), String.class.getName(), String.class.getName()});
+        //read only role  
+        connection = getJMXConnector("john", "password").getMBeanServerConnection();
+        assertJmxInvoke(true, connection, mbean, "listProperties", new Object [] {""}, new String [] {String.class.getName()});
+        assertJmxInvoke(false, connection, mbean, "setProperty", new Object [] {"", "x", "y"}, new String [] {String.class.getName(), String.class.getName(), String.class.getName()});
+        //read write role
+        connection = getJMXConnector("root", "password").getMBeanServerConnection();
+        assertJmxInvoke(true, connection, mbean, "listProperties", new Object [] {""}, new String [] {String.class.getName()});
+        assertJmxInvoke(true, connection, mbean, "setProperty", new Object [] {"", "x", "y"}, new String [] {String.class.getName(), String.class.getName(), String.class.getName()});
+        setJMXAuthentication("karaf", "admin");
+    }
+
+    private String assertCommand(String user, String password,  String command, Result result) throws Exception, IOException {
+        if (!command.endsWith("\n"))
+            command += "\n";
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        OutputStream pipe = openSshChannel(user, password, out, out);
+        pipe.write(command.getBytes());
+        pipe.flush();
+
+        closeSshChannel(pipe);
+        String output = new String(out.toByteArray());
+
+        switch(result) {
+        case OK:
+            Assert.assertFalse("Should not contain 'Insufficient credentials' or 'Command not found': " + output,
+                    output.contains("Insufficient credentials") || output.contains("Command not found"));
+            break;
+        case NOT_FOUND:
+            Assert.assertTrue("Should contain 'Command not found': " + output,
+                    output.contains("Command not found"));
+            break;
+        case NO_CREDENTIALS:
+            Assert.assertTrue("Should contain 'Insufficient credentials': " + output,
+                    output.contains("Insufficient credentials"));
+            break;
+        case NO_ROLES:
+            Assert.assertTrue("Should contain 'Current user has no associated roles': " + output,
+                    output.contains("Current user has no associated roles"));
+            break;
+        default:
+            Assert.fail("Unexpected enum value: " + result);
+        }
+        return output;
+    }
+    
+    private OutputStream openSshChannel(String username, String password, OutputStream ... outputs) throws Exception {
+        client = SshClient.setUpDefaultClient();
+        client.start();
+        ConnectFuture future = client.connect(username, "localhost", 8101);
+        future.await();
+        session = future.getSession();
+
+        Set<ClientSessionEvent> ret = EnumSet.of(ClientSessionEvent.WAIT_AUTH);
+        while (ret.contains(ClientSessionEvent.WAIT_AUTH)) {
+            session.addPasswordIdentity(password);
+            session.auth().verify();
+            ret = session.waitFor(EnumSet.of(ClientSessionEvent.WAIT_AUTH, ClientSessionEvent.CLOSED, ClientSessionEvent.AUTHED), 0);
+        }
+        if (ret.contains(ClientSessionEvent.CLOSED)) {
+            throw new Exception("Could not open SSH channel");
+        }
+        channel = session.createChannel("shell");
+        PipedOutputStream pipe = new PipedOutputStream();
+        channel.setIn(new PipedInputStream(pipe));
+
+        OutputStream out;
+        if (outputs.length >= 1) {
+            out = outputs[0];
+        } else {
+            out = new ByteArrayOutputStream();
+        }
+        channel.setOut(out);
+
+        OutputStream err;
+        if (outputs.length >= 2) {
+            err = outputs[1];
+        } else {
+            err = new ByteArrayOutputStream();
+        }
+        channel.setErr(err);
+        channel.open();
+
+        return pipe;
+    }
+    
+    private void setJMXAuthentication(String realm, String password) throws Exception {
+       assertCommand("admin", "password", "config:edit org.apache.karaf.management; config:propset jmxRealm " + realm + "; config:update", Result.OK);
+       getMBeanServerConnection(10000, TimeUnit.MILLISECONDS, "admin", password);
+    }
+    
+    private void closeSshChannel(OutputStream pipe) throws IOException {
+        pipe.write("logout\n".getBytes());
+        pipe.flush();
+
+        channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0);
+        session.close(true);
+        client.stop();
+
+        client = null;
+        channel = null;
+        session = null;
+    }
+    
+    private Object assertJmxInvoke(boolean expectSuccess, MBeanServerConnection connection, ObjectName mbean, String method,
+            Object[] params, String[] signature) throws InstanceNotFoundException, MBeanException, ReflectionException, IOException {
+        try {
+            Object result = connection.invoke(mbean, method, params, signature);
+            assertTrue(expectSuccess);
+            return result;
+        } catch (SecurityException se) {
+            assertTrue(!expectSuccess);
+            return null;
+        }
+    }
+    
+    private MBeanServerConnection getMBeanServerConnection(long timeout, final TimeUnit unit, String username, String password) throws Exception {
+        Exception lastException = null;
+        long timeoutMillis = System.currentTimeMillis() + unit.toMillis(timeout);
+        while (System.currentTimeMillis() < timeoutMillis) {
+            try {
+                return getJMXConnector(username, password).getMBeanServerConnection();
+            } catch (Exception ex) {
+                lastException = ex;
+                Thread.sleep(500);
+                ex.printStackTrace();
+            }
+        }
+        TimeoutException timeoutException = new TimeoutException();
+        timeoutException.initCause(lastException);
+        throw timeoutException;
+    }
+    
+    private JMXConnector getJMXConnector(String userName, String password) throws Exception {
+        JMXServiceURL url = new JMXServiceURL(getJmxServiceUrl());
+        String[] credentials = new String[] { userName, password };
+        Map<String, ?> env = Collections.singletonMap(JMXConnector.CREDENTIALS, credentials);
+        JMXConnector connector = JMXConnectorFactory.connect(url, env);
+        return connector;
+    }
+
+    private String getJmxServiceUrl() throws Exception {
+        return "service:jmx:rmi://localhost:44444/jndi/rmi://localhost:1099/karaf-root";
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseExampleAdapterTest.java
index b0583fb..f21a48c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseExampleAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractFuseExampleAdapterTest.java
@@ -59,7 +59,7 @@ public abstract class AbstractFuseExampleAdapterTest extends AbstractExampleAdap
 
     @Override
     public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
-        RealmRepresentation fuseRealm = loadRealm(new File(EXAMPLES_HOME_DIR + "/fuse/testrealm.json"));
+        RealmRepresentation fuseRealm = loadRealm(new File(EXAMPLES_HOME_DIR + "/fuse/demorealm.json"));
         testRealms.add(fuseRealm);
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
index 223a3f6..f4cf19c 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
@@ -23,12 +23,15 @@ import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.ClientResource;
 import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.admin.client.resource.RoleResource;
 import org.keycloak.admin.client.resource.UserResource;
 import org.keycloak.common.constants.ServiceAccountConstants;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
+import org.keycloak.models.AdminRoles;
 import org.keycloak.models.Constants;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.representations.AccessToken;
@@ -40,6 +43,9 @@ import org.keycloak.representations.idm.RoleRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.testsuite.AbstractKeycloakTest;
 import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.keycloak.testsuite.auth.page.AuthRealm;
 import org.keycloak.testsuite.pages.LoginPage;
 import org.keycloak.testsuite.util.ClientBuilder;
 import org.keycloak.testsuite.util.ClientManager;
@@ -432,4 +438,40 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
         testUser.roles().realmLevel().add(Collections.singletonList(offlineAccess));
         
     }
+
+    /**
+     * KEYCLOAK-4201
+     *
+     * @throws Exception
+     */
+    @Test
+    public void offlineTokenAdminRESTAccess() throws Exception {
+        // Grant "view-realm" role to user
+        RealmResource appRealm = adminClient.realm("test");
+        ClientResource realmMgmt = ApiUtil.findClientByClientId(appRealm, Constants.REALM_MANAGEMENT_CLIENT_ID);
+        String realmMgmtUuid = realmMgmt.toRepresentation().getId();
+        RoleRepresentation roleRep = realmMgmt.roles().get(AdminRoles.VIEW_REALM).toRepresentation();
+
+        UserResource testUser = findUserByUsernameId(appRealm, "test-user@localhost");
+        testUser.roles().clientLevel(realmMgmtUuid).add(Collections.singletonList(roleRep));
+
+        // Login with offline token now
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.clientId("offline-client");
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("secret1", "test-user@localhost", "password");
+
+        events.clear();
+
+        // Set the time offset, so that "normal" userSession expires
+        setTimeOffset(86400);
+
+        // Refresh with the offline token
+        tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "secret1");
+
+        // Use accessToken to admin REST request
+        Keycloak offlineTokenAdmin = Keycloak.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth",
+                AuthRealm.MASTER, Constants.ADMIN_CLI_CLIENT_ID, tokenResponse.getAccessToken());
+        RealmRepresentation testRealm = offlineTokenAdmin.realm("test").toRepresentation();
+        Assert.assertNotNull(testRealm);
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/common/xslt/arquillian.xsl b/testsuite/integration-arquillian/tests/other/adapters/karaf/common/xslt/arquillian.xsl
index 9c8f2a6..9c1b6eb 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/karaf/common/xslt/arquillian.xsl
+++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/common/xslt/arquillian.xsl
@@ -31,21 +31,19 @@
             <container qualifier="app-server-${{app.server}}" mode="manual" >
                 <configuration>
                     <property name="enabled">true</property>
-                    <property name="adapterImplClass">org.jboss.arquillian.container.osgi.karaf.managed.KarafManagedDeployableContainer</property>
-                    <!--<property name="adapterImplClass">org.keycloak.testsuite.arquillian.karaf.CustomKarafContainer</property>-->
+<!--                     <property name="adapterImplClass">org.jboss.arquillian.container.osgi.karaf.managed.KarafManagedDeployableContainer</property> -->
+                    <property name="adapterImplClass">org.keycloak.testsuite.arquillian.karaf.CustomKarafContainer</property>
                     <property name="autostartBundle">false</property>
                     <property name="karafHome">${app.server.home}</property>
                     <property name="javaHome">${app.server.java.home}</property>
                     <property name="javaVmArguments">
-                        -agentlib:jdwp=transport=dt_socket,address=5005,server=y,suspend=n 
                         ${adapter.test.props}
                     </property>
                     <property name="jmxServiceURL">service:jmx:rmi://127.0.0.1:44444/jndi/rmi://127.0.0.1:1099/karaf-root</property>
                     <property name="jmxUsername">${app.server.management.user}</property>
-                    <property name="jmxPassword">${app.server.management.password}</property>          
+                    <property name="jmxPassword">${app.server.management.password}</property>
                 </configuration>
             </container>
-    
         </xsl:copy>
     </xsl:template>
     
diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse63/src/test/java/org/keycloak/testsuite/adapter/example/Fuse63AdminAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse63/src/test/java/org/keycloak/testsuite/adapter/example/Fuse63AdminAdapterTest.java
new file mode 100644
index 0000000..90f80d4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/fuse63/src/test/java/org/keycloak/testsuite/adapter/example/Fuse63AdminAdapterTest.java
@@ -0,0 +1,8 @@
+package org.keycloak.testsuite.adapter.example;
+
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+@AppServerContainer("app-server-fuse63")
+public class Fuse63AdminAdapterTest extends AbstractFuseAdminAdapterTest {
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml
index 70e4a2c..c930dec 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/karaf/pom.xml
@@ -86,7 +86,12 @@
                     <groupId>org.apache.karaf</groupId>
                     <artifactId>org.apache.karaf.client</artifactId>
                     <version>3.0.3</version>
-                </dependency>  
+                </dependency>
+                <dependency>
+                    <groupId>org.apache.sshd</groupId>
+                    <artifactId>sshd-core</artifactId>
+                    <version>1.2.0</version>
+                </dependency>
             </dependencies>
             <build>
                 <plugins>
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 642087d..5c6a03f 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -719,6 +719,12 @@
                     <scope>compile</scope>
                 </dependency>
 
+                <dependency>
+                    <groupId>org.apache.sshd</groupId>
+                    <artifactId>sshd-core</artifactId>
+                    <version>1.2.0</version>
+                </dependency>
+
                 <!-- Email Test Server -->
                 <dependency>
                     <groupId>com.icegreen</groupId>
diff --git a/themes/src/main/resources/theme/base/login/login-config-totp.ftl b/themes/src/main/resources/theme/base/login/login-config-totp.ftl
index 914ecb5..0515c27 100755
--- a/themes/src/main/resources/theme/base/login/login-config-totp.ftl
+++ b/themes/src/main/resources/theme/base/login/login-config-totp.ftl
@@ -11,7 +11,7 @@
         </li>
     <li>
         <p>${msg("loginTotpStep2")}</p>
-        <img src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Figure: Barcode"><br/>
+        <img id="kc-totp-secret-qr-code" src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Figure: Barcode"><br/>
         <span class="code">${totp.totpSecretEncoded}</span>
         </li>
     <li>
diff --git a/themes/src/main/resources/theme/keycloak/login/resources/css/login.css b/themes/src/main/resources/theme/keycloak/login/resources/css/login.css
index 7b202e1..bbf08af 100644
--- a/themes/src/main/resources/theme/keycloak/login/resources/css/login.css
+++ b/themes/src/main/resources/theme/keycloak/login/resources/css/login.css
@@ -152,6 +152,11 @@ ol#kc-totp-settings li:first-of-type {
     margin-top: 0;
 }
 
+#kc-totp-secret-qr-code {
+    max-width:150px;
+    max-height:150px;
+}
+
 /* OAuth */
 
 #kc-oauth h3 {
@@ -244,18 +249,6 @@ ol#kc-totp-settings li:first-of-type {
 .zocial.microsoft {background-color: #0052a4; color: #fff;}
 .zocial.microsoft:before { content: "\f15d"; }
 
-@media (min-width: 1281px) {
-    #kc-container-wrapper {
-        bottom: 13%;        
-    }
-    
-    #kc-logo-wrapper {
-        position: absolute;
-        top: 50px;
-        right: 50px;
-    }
-}
-
 @media (min-width: 768px) {
     #kc-container-wrapper {
         position: absolute;
@@ -326,7 +319,13 @@ ol#kc-totp-settings li:first-of-type {
     }
 }
 
-@media (max-height: 500px) {
+@media (min-height: 621px) {
+    #kc-container-wrapper {
+        bottom: 12%;
+    }
+}
+
+@media (max-height: 620px) {
     #kc-container-wrapper {
     }
 }