keycloak-aplcache

Changes

testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java 696(+0 -696)

Details

diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/OfflineToken.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/OfflineToken.java
new file mode 100644
index 0000000..7116e65
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/OfflineToken.java
@@ -0,0 +1,59 @@
+package org.keycloak.testsuite.adapter.page;
+
+import org.jboss.arquillian.container.test.api.OperateOnDeployment;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
+import org.keycloak.util.JsonSerialization;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+import java.io.IOException;
+import java.net.URL;
+
+/**
+ * @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>.
+ */
+public class OfflineToken extends AbstractPageWithInjectedUrl {
+
+    public static final String DEPLOYMENT_NAME = "offline-client";
+
+    @ArquillianResource
+    @OperateOnDeployment(DEPLOYMENT_NAME)
+    private URL url;
+
+    @Override
+    public URL getInjectedUrl() {
+        return url;
+    }
+
+    @FindBy(id = "accessToken")
+    private WebElement accessToken;
+
+    @FindBy(id = "refreshToken")
+    private WebElement refreshToken;
+
+    @FindBy(id = "prettyToken")
+    private WebElement prettyToken;
+
+
+    public AccessToken getAccessToken() {
+        try {
+            return JsonSerialization.readValue(accessToken.getText(), AccessToken.class);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+
+    public RefreshToken getRefreshToken() {
+        try {
+            return JsonSerialization.readValue(refreshToken.getText(), RefreshToken.class);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/OfflineTokenServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/OfflineTokenServlet.java
new file mode 100644
index 0000000..6f7d954
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/OfflineTokenServlet.java
@@ -0,0 +1,69 @@
+package org.keycloak.testsuite.adapter.servlet;
+
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
+import org.keycloak.common.util.Time;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.util.JsonSerialization;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.UriBuilder;
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineTokenServlet extends HttpServlet {
+
+    private static final String OFFLINE_CLIENT_APP_URI = "http://localhost:8280/offline-client";
+    private static final String ADAPTER_ROOT_URL = "http://localhost:8180";
+
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+
+        //Accept timeOffset as argument to enforce timeouts
+        String timeOffsetParam = req.getParameter("timeOffset");
+        if (timeOffsetParam != null && !timeOffsetParam.isEmpty()) {
+            Time.setOffset(Integer.parseInt(timeOffsetParam));
+        }
+
+        if (req.getRequestURI().endsWith("logout")) {
+
+            UriBuilder redirectUriBuilder = UriBuilder.fromUri(OFFLINE_CLIENT_APP_URI);
+            if (req.getParameter(OAuth2Constants.SCOPE) != null) {
+                redirectUriBuilder.queryParam(OAuth2Constants.SCOPE, req.getParameter(OAuth2Constants.SCOPE));
+            }
+            String redirectUri = redirectUriBuilder.build().toString();
+
+            String serverLogoutRedirect = UriBuilder.fromUri(ADAPTER_ROOT_URL + "/auth/realms/test/protocol/openid-connect/logout")
+                    .queryParam("redirect_uri", redirectUri)
+                    .build().toString();
+
+            resp.sendRedirect(serverLogoutRedirect);
+            return;
+        }
+
+        StringBuilder response = new StringBuilder("<html><head><title>Offline token servlet</title></head><body><pre>");
+        RefreshableKeycloakSecurityContext ctx = (RefreshableKeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
+        String accessTokenPretty = JsonSerialization.writeValueAsPrettyString(ctx.getToken());
+        RefreshToken refreshToken;
+        try {
+            refreshToken = new JWSInput(ctx.getRefreshToken()).readJsonContent(RefreshToken.class);
+        } catch (JWSInputException e) {
+            throw new IOException(e);
+        }
+        String refreshTokenPretty = JsonSerialization.writeValueAsPrettyString(refreshToken);
+
+        response = response.append("<span id=\"accessToken\">" + accessTokenPretty + "</span>")
+                .append("<span id=\"refreshToken\">" + refreshTokenPretty + "</span>")
+                .append("</pre></body></html>");
+        resp.getWriter().println(response.toString());
+    }
+}
+
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOfflineServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOfflineServletsAdapterTest.java
new file mode 100644
index 0000000..b00549c
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOfflineServletsAdapterTest.java
@@ -0,0 +1,186 @@
+package org.keycloak.testsuite.adapter.servlet;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.common.util.Time;
+import org.keycloak.events.Details;
+import org.keycloak.events.EventType;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
+import org.keycloak.testsuite.adapter.page.OfflineToken;
+import org.keycloak.testsuite.pages.AccountApplicationsPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.OAuthGrantPage;
+import org.keycloak.testsuite.util.ClientManager;
+import org.keycloak.util.TokenUtil;
+
+import javax.ws.rs.core.UriBuilder;
+import java.util.List;
+
+import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
+import static org.keycloak.testsuite.util.IOUtil.loadRealm;
+
+/**
+ * @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>.
+ */
+public abstract class AbstractOfflineServletsAdapterTest extends AbstractServletsAdapterTest {
+
+    private static final String OFFLINE_CLIENT_APP_URI = "http://localhost:8280/offline-client";
+
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+    @Page
+    protected OfflineToken offlineToken;
+    @Page
+    protected LoginPage loginPage;
+    @Page
+    protected AccountApplicationsPage accountAppPage;
+    @Page
+    protected OAuthGrantPage oauthGrantPage;
+
+    @Deployment(name = OfflineToken.DEPLOYMENT_NAME)
+    protected static WebArchive offlineClient() {
+        return servletDeployment(OfflineToken.DEPLOYMENT_NAME, OfflineTokenServlet.class, ErrorServlet.class);
+    }
+
+    @Override
+    public void setDefaultPageUriParameters() {
+        super.setDefaultPageUriParameters();
+        testRealmPage.setAuthRealm(TEST);
+        testRealmLoginPage.setAuthRealm(TEST);
+    }
+
+
+    @Override
+    public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
+        testRealms.add(loadRealm("/adapter-test/offline-client/offlinerealm.json"));
+    }
+
+    @Test
+    public void testServlet() throws Exception {
+        String servletUri = UriBuilder.fromUri(OFFLINE_CLIENT_APP_URI)
+                .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS)
+                .build().toString();
+        oauth.doLogin("test-user@localhost", "password");
+
+        driver.navigate().to(servletUri);
+
+        Assert.assertTrue(driver.getCurrentUrl().startsWith(OFFLINE_CLIENT_APP_URI));
+
+        Assert.assertEquals(offlineToken.getRefreshToken().getType(), TokenUtil.TOKEN_TYPE_OFFLINE);
+        Assert.assertEquals(offlineToken.getRefreshToken().getExpiration(), 0);
+
+        String accessTokenId = offlineToken.getAccessToken().getId();
+        String refreshTokenId = offlineToken.getRefreshToken().getId();
+
+        setAdapterTimeOffset(9999);
+
+        Assert.assertTrue(driver.getCurrentUrl().startsWith(OFFLINE_CLIENT_APP_URI));
+        Assert.assertNotEquals(offlineToken.getRefreshToken().getId(), refreshTokenId);
+        Assert.assertNotEquals(offlineToken.getAccessToken().getId(), accessTokenId);
+
+        // Ensure that logout works for webapp (even if offline token will be still valid in Keycloak DB)
+        driver.navigate().to(OFFLINE_CLIENT_APP_URI + "/logout");
+        loginPage.assertCurrent();
+        driver.navigate().to(OFFLINE_CLIENT_APP_URI);
+        loginPage.assertCurrent();
+
+        setAdapterTimeOffset(0);
+        events.clear();
+    }
+
+    @Test
+    public void testServletWithRevoke() {
+        // Login to servlet first with offline token
+        String servletUri = UriBuilder.fromUri(OFFLINE_CLIENT_APP_URI)
+                .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS)
+                .build().toString();
+        driver.navigate().to(servletUri);
+        loginPage.login("test-user@localhost", "password");
+        Assert.assertTrue(driver.getCurrentUrl().startsWith(OFFLINE_CLIENT_APP_URI));
+
+        Assert.assertEquals(offlineToken.getRefreshToken().getType(), TokenUtil.TOKEN_TYPE_OFFLINE);
+
+        // Assert refresh works with increased time
+        setAdapterTimeOffset(9999);
+        driver.navigate().to(OFFLINE_CLIENT_APP_URI);
+        Assert.assertTrue(driver.getCurrentUrl().startsWith(OFFLINE_CLIENT_APP_URI));
+        setAdapterTimeOffset(0);
+
+        events.clear();
+
+        // Go to account service and revoke grant
+        accountAppPage.open();
+        List<String> additionalGrants = accountAppPage.getApplications().get("offline-client").getAdditionalGrants();
+        Assert.assertEquals(additionalGrants.size(), 1);
+        Assert.assertEquals(additionalGrants.get(0), "Offline Token");
+        accountAppPage.revokeGrant("offline-client");
+        Assert.assertEquals(accountAppPage.getApplications().get("offline-client").getAdditionalGrants().size(), 0);
+
+        events.expect(EventType.REVOKE_GRANT)
+                .client("account").detail(Details.REVOKED_CLIENT, "offline-client").assertEvent();
+
+        // Assert refresh doesn't work now (increase time one more time)
+        setAdapterTimeOffset(9999);
+        driver.navigate().to(OFFLINE_CLIENT_APP_URI);
+        Assert.assertFalse(driver.getCurrentUrl().startsWith(OFFLINE_CLIENT_APP_URI));
+        loginPage.assertCurrent();
+        setAdapterTimeOffset(0);
+    }
+
+    @Test
+    public void testServletWithConsent() {
+        ClientManager.realm(adminClient.realm("test")).clientId("offline-client").consentRequired(true);
+
+        // Assert grant page doesn't have 'Offline Access' role when offline token is not requested
+        driver.navigate().to(OFFLINE_CLIENT_APP_URI);
+        loginPage.login("test-user@localhost", "password");
+        oauthGrantPage.assertCurrent();
+        Assert.assertFalse(driver.getPageSource().contains("Offline access"));
+        oauthGrantPage.cancel();
+
+        // Assert grant page has 'Offline Access' role now
+        String servletUri = UriBuilder.fromUri(OFFLINE_CLIENT_APP_URI)
+                .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS)
+                .build().toString();
+        driver.navigate().to(servletUri);
+        loginPage.login("test-user@localhost", "password");
+        oauthGrantPage.assertCurrent();
+        Assert.assertTrue(driver.getPageSource().contains("Offline access"));
+        oauthGrantPage.accept();
+
+        Assert.assertTrue(driver.getCurrentUrl().startsWith(OFFLINE_CLIENT_APP_URI));
+        Assert.assertEquals(offlineToken.getRefreshToken().getType(), TokenUtil.TOKEN_TYPE_OFFLINE);
+
+        accountAppPage.open();
+        AccountApplicationsPage.AppEntry offlineClient = accountAppPage.getApplications().get("offline-client");
+        Assert.assertTrue(offlineClient.getRolesGranted().contains("Offline access"));
+        Assert.assertTrue(offlineClient.getAdditionalGrants().contains("Offline Token"));
+
+        //This was necessary to be introduced, otherwise other testcases will fail
+        driver.navigate().to(OFFLINE_CLIENT_APP_URI + "/logout");
+        loginPage.assertCurrent();
+
+        events.clear();
+
+        // Revert change
+        ClientManager.realm(adminClient.realm("test")).clientId("offline-client").consentRequired(false);
+
+    }
+
+    private void setAdapterTimeOffset(int timeOffset) {
+        Time.setOffset(timeOffset);
+        String timeOffsetUri = UriBuilder.fromUri(OFFLINE_CLIENT_APP_URI)
+                .queryParam("timeOffset", timeOffset)
+                .build().toString();
+
+        driver.navigate().to(timeOffsetUri);
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ApiUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ApiUtil.java
index aae4f53..73946ff 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ApiUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ApiUtil.java
@@ -90,6 +90,10 @@ public class ApiUtil {
         return client.roles().get(role);
     }
 
+    public static RoleResource findRealmRoleByName(RealmResource realm, String role) {
+        return realm.roles().get(role);
+    }
+
     public static UserRepresentation findUserByUsername(RealmResource realm, String username) {
         UserRepresentation user = null;
         List<UserRepresentation> ur = realm.users().search(username, null, null);
@@ -143,7 +147,7 @@ public class ApiUtil {
             }
 
             UserResource userResource = realm.users().get(userId);
-            log.debug("assigning roles: " + Arrays.toString(roles) + " to user: \""
+            log.debug("assigning role: " + Arrays.toString(roles) + " to user: \""
                     + userResource.toRepresentation().getUsername() + "\" of client: \""
                     + clientName + "\" in realm: \"" + realmName + "\"");
             userResource.roles().clientLevel(clientId).add(roleRepresentations);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index fc8c8fa..887aa26 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -132,7 +132,7 @@ public class AccessTokenTest extends AbstractKeycloakTest {
         UserBuilder user = UserBuilder.create()
                 .id(KeycloakModelUtils.generateId())
                 .username("no-permissions")
-                .role("user")
+                .addRoles("user")
                 .password("password");
         realm.getUsers().add(user.build());
 
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
new file mode 100755
index 0000000..ac9c5a7
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.oauth;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+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.common.util.Time;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.models.Constants;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+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.pages.LoginPage;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.ClientManager;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.RealmManager;
+import org.keycloak.testsuite.util.RoleBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.TokenUtil;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+import static org.keycloak.testsuite.admin.ApiUtil.findRealmRoleByName;
+import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
+import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsernameId;
+import static org.keycloak.testsuite.util.OAuthClient.APP_ROOT;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineTokenTest extends AbstractKeycloakTest {
+
+    private static String userId;
+    private static String offlineClientAppUri;
+    private static String serviceAccountUserId;
+
+    @Page
+    protected LoginPage loginPage;
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+
+    @Override
+    public void beforeAbstractKeycloakTest() throws Exception {
+        super.beforeAbstractKeycloakTest();
+    }
+
+    @Before
+    public void clientConfiguration() {
+        userId = findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId();
+        oauth.clientId("test-app");
+    }
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+
+        RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
+
+        RealmBuilder realm = RealmBuilder.edit(realmRepresentation)
+                .accessTokenLifespan(10)
+                .ssoSessionIdleTimeout(30)
+                .testEventListener();
+
+        offlineClientAppUri = APP_ROOT + "/offline-client";
+
+        ClientRepresentation app = ClientBuilder.create().clientId("offline-client")
+                .id(KeycloakModelUtils.generateId())
+                .adminUrl(offlineClientAppUri)
+                .redirectUris(offlineClientAppUri)
+                .directAccessGrants()
+                .serviceAccountsEnabled(true)
+                .secret("secret1").build();
+
+        realm.client(app);
+
+        serviceAccountUserId = KeycloakModelUtils.generateId();
+        UserRepresentation serviceAccountUser = UserBuilder.create()
+                .id(serviceAccountUserId)
+                .addRoles("user", "offline_access")
+                .role("test-app", "customer-user")
+                .username(ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + app.getClientId())
+                .serviceAccountId(app.getClientId()).build();
+
+        realm.user(serviceAccountUser);
+
+        testRealms.add(realm.build());
+
+    }
+
+    @Test
+    public void offlineTokenDisabledForClient() throws Exception {
+        ClientManager.realm(adminClient.realm("test")).clientId("offline-client").fullScopeAllowed(false);
+
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.clientId("offline-client");
+        oauth.redirectUri(offlineClientAppUri);
+        oauth.doLogin("test-user@localhost", "password");
+
+        EventRepresentation loginEvent = events.expectLogin()
+                .client("offline-client")
+                .detail(Details.REDIRECT_URI, offlineClientAppUri)
+                .assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
+
+        assertEquals(400, tokenResponse.getStatusCode());
+        assertEquals("not_allowed", tokenResponse.getError());
+
+        events.expectCodeToToken(codeId, sessionId)
+                .client("offline-client")
+                .error("not_allowed")
+                .clearDetails()
+                .assertEvent();
+
+        ClientManager.realm(adminClient.realm("test")).clientId("offline-client").fullScopeAllowed(true);
+
+    }
+
+    @Test
+    public void offlineTokenUserNotAllowed() throws Exception {
+        String userId = findUserByUsername(adminClient.realm("test"), "keycloak-user@localhost").getId();
+
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.clientId("offline-client");
+        oauth.redirectUri(offlineClientAppUri);
+        oauth.doLogin("keycloak-user@localhost", "password");
+
+        EventRepresentation loginEvent = events.expectLogin()
+                .client("offline-client")
+                .user(userId)
+                .detail(Details.REDIRECT_URI, offlineClientAppUri)
+                .assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
+
+        assertEquals(400, tokenResponse.getStatusCode());
+        assertEquals("not_allowed", tokenResponse.getError());
+
+        events.expectCodeToToken(codeId, sessionId)
+                .client("offline-client")
+                .user(userId)
+                .error("not_allowed")
+                .clearDetails()
+                .assertEvent();
+    }
+
+    @Test
+    public void offlineTokenBrowserFlow() throws Exception {
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.clientId("offline-client");
+        oauth.redirectUri(offlineClientAppUri);
+        oauth.doLogin("test-user@localhost", "password");
+
+        EventRepresentation loginEvent = events.expectLogin()
+                .client("offline-client")
+                .detail(Details.REDIRECT_URI, offlineClientAppUri)
+                .assertEvent();
+
+        final String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
+        AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
+        String offlineTokenString = tokenResponse.getRefreshToken();
+        RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
+
+        events.expectCodeToToken(codeId, sessionId)
+                .client("offline-client")
+                .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
+                .assertEvent();
+
+        assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
+        assertEquals(0, offlineToken.getExpiration());
+
+        String newRefreshTokenString = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, sessionId, userId);
+
+        // Change offset to very big value to ensure offline session expires
+        Time.setOffset(3000000);
+
+        OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(newRefreshTokenString, "secret1");
+        Assert.assertEquals(400, response.getStatusCode());
+        assertEquals("invalid_grant", response.getError());
+
+        events.expectRefresh(offlineToken.getId(), sessionId)
+                .client("offline-client")
+                .error(Errors.INVALID_TOKEN)
+                .user(userId)
+                .clearDetails()
+                .assertEvent();
+
+
+        Time.setOffset(0);
+    }
+
+    private String testRefreshWithOfflineToken(AccessToken oldToken, RefreshToken offlineToken, String offlineTokenString,
+                                               final String sessionId, String userId) {
+        // Change offset to big value to ensure userSession expired
+        Time.setOffset(99999);
+        Assert.assertFalse(oldToken.isActive());
+        Assert.assertTrue(offlineToken.isActive());
+
+        // Assert userSession expired
+        testingClient.testing().removeExpired("test");
+
+        testingClient.testing().removeUserSession("test", sessionId);
+
+        OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "secret1");
+        AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
+        Assert.assertEquals(200, response.getStatusCode());
+        Assert.assertEquals(sessionId, refreshedToken.getSessionState());
+
+        // Assert new refreshToken in the response
+        String newRefreshToken = response.getRefreshToken();
+        Assert.assertNotNull(newRefreshToken);
+        Assert.assertNotEquals(oldToken.getId(), refreshedToken.getId());
+
+        Assert.assertEquals(userId, refreshedToken.getSubject());
+
+        Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole("user"));
+        Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole(Constants.OFFLINE_ACCESS_ROLE));
+
+        Assert.assertEquals(1, refreshedToken.getResourceAccess("test-app").getRoles().size());
+        Assert.assertTrue(refreshedToken.getResourceAccess("test-app").isUserInRole("customer-user"));
+
+        EventRepresentation refreshEvent = events.expectRefresh(offlineToken.getId(), sessionId)
+                .client("offline-client")
+                .user(userId)
+                .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
+                .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
+                .assertEvent();
+        Assert.assertNotEquals(oldToken.getId(), refreshEvent.getDetails().get(Details.TOKEN_ID));
+
+        Time.setOffset(0);
+        return newRefreshToken;
+    }
+
+    @Test
+    public void offlineTokenDirectGrantFlow() throws Exception {
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.clientId("offline-client");
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("secret1", "test-user@localhost", "password");
+        tokenResponse.getErrorDescription();
+        AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
+        String offlineTokenString = tokenResponse.getRefreshToken();
+        RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
+
+        events.expectLogin()
+                .client("offline-client")
+                .user(userId)
+                .session(token.getSessionState())
+                .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
+                .detail(Details.TOKEN_ID, token.getId())
+                .detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
+                .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
+                .detail(Details.USERNAME, "test-user@localhost")
+                .removeDetail(Details.CODE_ID)
+                .removeDetail(Details.REDIRECT_URI)
+                .removeDetail(Details.CONSENT)
+                .assertEvent();
+
+        Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
+        Assert.assertEquals(0, offlineToken.getExpiration());
+
+        testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
+
+        // Assert same token can be refreshed again
+        testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
+    }
+
+    @Test
+    public void offlineTokenDirectGrantFlowWithRefreshTokensRevoked() throws Exception {
+        RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(true);
+
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.clientId("offline-client");
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("secret1", "test-user@localhost", "password");
+
+        AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
+        String offlineTokenString = tokenResponse.getRefreshToken();
+        RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
+
+        events.expectLogin()
+                .client("offline-client")
+                .user(userId)
+                .session(token.getSessionState())
+                .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
+                .detail(Details.TOKEN_ID, token.getId())
+                .detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
+                .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
+                .detail(Details.USERNAME, "test-user@localhost")
+                .removeDetail(Details.CODE_ID)
+                .removeDetail(Details.REDIRECT_URI)
+                .removeDetail(Details.CONSENT)
+                .assertEvent();
+
+        Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
+        Assert.assertEquals(0, offlineToken.getExpiration());
+
+        String offlineTokenString2 = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
+        RefreshToken offlineToken2 = oauth.verifyRefreshToken(offlineTokenString2);
+
+        // Assert second refresh with same refresh token will fail
+        OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "secret1");
+        Assert.assertEquals(400, response.getStatusCode());
+        events.expectRefresh(offlineToken.getId(), token.getSessionState())
+                .client("offline-client")
+                .error(Errors.INVALID_TOKEN)
+                .user(userId)
+                .clearDetails()
+                .assertEvent();
+
+        // Refresh with new refreshToken is successful now
+        testRefreshWithOfflineToken(token, offlineToken2, offlineTokenString2, token.getSessionState(), userId);
+
+        RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(false);
+    }
+
+    @Test
+    public void offlineTokenServiceAccountFlow() throws Exception {
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.clientId("offline-client");
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
+
+        AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
+        String offlineTokenString = tokenResponse.getRefreshToken();
+        RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
+
+        events.expectClientLogin()
+                .client("offline-client")
+                .user(serviceAccountUserId)
+                .session(token.getSessionState())
+                .detail(Details.TOKEN_ID, token.getId())
+                .detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
+                .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
+                .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client")
+                .assertEvent();
+
+        Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
+        Assert.assertEquals(0, offlineToken.getExpiration());
+
+        testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
+
+        // Now retrieve another offline token and verify that previous offline token is still valid
+        tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
+
+        AccessToken token2 = oauth.verifyToken(tokenResponse.getAccessToken());
+        String offlineTokenString2 = tokenResponse.getRefreshToken();
+        RefreshToken offlineToken2 = oauth.verifyRefreshToken(offlineTokenString2);
+
+        events.expectClientLogin()
+                .client("offline-client")
+                .user(serviceAccountUserId)
+                .session(token2.getSessionState())
+                .detail(Details.TOKEN_ID, token2.getId())
+                .detail(Details.REFRESH_TOKEN_ID, offlineToken2.getId())
+                .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
+                .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client")
+                .assertEvent();
+
+        // Refresh with both offline tokens is fine
+        testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
+        testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId);
+    }
+
+    @Test
+    public void offlineTokenAllowedWithCompositeRole() throws Exception {
+        RealmResource appRealm = adminClient.realm("test");
+        UserResource testUser = findUserByUsernameId(appRealm, "test-user@localhost");
+        RoleRepresentation offlineAccess = findRealmRoleByName(adminClient.realm("test"),
+                Constants.OFFLINE_ACCESS_ROLE).toRepresentation();
+
+        // Grant offline_access role indirectly through composite role
+        appRealm.roles().create(RoleBuilder.create().name("composite").build());
+        RoleResource roleResource = appRealm.roles().get("composite");
+        roleResource.addComposites(Collections.singletonList(offlineAccess));
+
+        testUser.roles().realmLevel().remove(Collections.singletonList(offlineAccess));
+        testUser.roles().realmLevel().add(Collections.singletonList(roleResource.toRepresentation()));
+
+        // Integration test
+        offlineTokenDirectGrantFlow();
+
+        // Revert changes
+        testUser.roles().realmLevel().remove(Collections.singletonList(appRealm.roles().get("composite").toRepresentation()));
+        appRealm.roles().get("composite").remove();
+        testUser.roles().realmLevel().add(Collections.singletonList(offlineAccess));
+        
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
index 24395ab..e3c5f94 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
@@ -88,4 +88,19 @@ public class ClientBuilder {
         rep.setRedirectUris(Arrays.asList(redirectUris));
         return this;
     }
+
+    public ClientBuilder baseUrl(String baseUrl) {
+        rep.setBaseUrl(baseUrl);
+        return this;
+    }
+
+    public ClientBuilder adminUrl(String adminUrl) {
+        rep.setAdminUrl(adminUrl);
+        return this;
+    }
+
+    public ClientBuilder rootUrl(String rootUrl) {
+        rep.setRootUrl(rootUrl);
+        return this;
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientManager.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientManager.java
index 5654814..f8a9a73 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientManager.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientManager.java
@@ -61,5 +61,17 @@ public class ClientManager {
             app.setDirectAccessGrantsEnabled(enable);
             clientResource.update(app);
         }
+
+        public void fullScopeAllowed(boolean enable) {
+            ClientRepresentation app = clientResource.toRepresentation();
+            app.setFullScopeAllowed(enable);
+            clientResource.update(app);
+        }
+
+        public void consentRequired(boolean enable) {
+            ClientRepresentation app = clientResource.toRepresentation();
+            app.setConsentRequired(enable);
+            clientResource.update(app);
+        }
     }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java
index 3d322b3..4576c2f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java
@@ -142,4 +142,13 @@ public class RealmBuilder {
         return rep;
     }
 
+    public RealmBuilder accessTokenLifespan(int accessTokenLifespan) {
+        rep.setAccessTokenLifespan(accessTokenLifespan);
+        return this;
+    }
+
+    public RealmBuilder ssoSessionIdleTimeout(int sessionIdleTimeout) {
+        rep.setSsoSessionIdleTimeout(sessionIdleTimeout);
+        return this;
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmManager.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmManager.java
index 030f798..40ad33d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmManager.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmManager.java
@@ -18,21 +18,46 @@ public class RealmManager {
         return new RealmManager();
     }
 
-    public void accessCodeLifeSpan(Integer accessCodeLifespan) {
+    public RealmManager accessCodeLifeSpan(Integer accessCodeLifespan) {
         RealmRepresentation realmRepresentation = realm.toRepresentation();
         realmRepresentation.setAccessCodeLifespan(accessCodeLifespan);
         realm.update(realmRepresentation);
+        return this;
     }
 
-    public void verifyEmail(Boolean enabled) {
+    public RealmManager verifyEmail(Boolean enabled) {
         RealmRepresentation rep = realm.toRepresentation();
         rep.setVerifyEmail(enabled);
         realm.update(rep);
+        return this;
     }
 
-    public void passwordPolicy(String passwordPolicy) {
+    public RealmManager passwordPolicy(String passwordPolicy) {
         RealmRepresentation rep = realm.toRepresentation();
         rep.setPasswordPolicy(passwordPolicy);
         realm.update(rep);
+        return this;
+    }
+
+    public RealmManager accessTokenLifespan(int accessTokenLifespan) {
+        RealmRepresentation rep = realm.toRepresentation();
+        rep.setAccessTokenLifespan(accessTokenLifespan);
+        realm.update(rep);
+        return this;
+    }
+
+    public RealmManager ssoSessionIdleTimeout(int sessionIdleTimeout) {
+        RealmRepresentation rep = realm.toRepresentation();
+        rep.setSsoSessionIdleTimeout(sessionIdleTimeout);
+        realm.update(rep);
+        return this;
+
+    }
+
+    public RealmManager revokeRefreshToken(boolean enable) {
+        RealmRepresentation rep = realm.toRepresentation();
+        rep.setRevokeRefreshToken(enable);
+        realm.update(rep);
+        return this;
     }
 }
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserBuilder.java
index 2696833..f769d05 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserBuilder.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserBuilder.java
@@ -90,11 +90,13 @@ public class UserBuilder {
         return this;
     }
 
-    public UserBuilder role(String role) {
+    public UserBuilder addRoles(String... roles) {
         if (rep.getRealmRoles() == null) {
             rep.setRealmRoles(new ArrayList<String>());
         }
-        rep.getRealmRoles().add(role);
+        for (String role : roles) {
+            rep.getRealmRoles().add(role);
+        }
         return this;
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/META-INF/context.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/META-INF/context.xml
new file mode 100644
index 0000000..b4ddcce
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/META-INF/context.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ 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.
+  -->
+
+<Context path="/customer-portal">
+    <Valve className="org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve"/>
+</Context>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/offlinerealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/offlinerealm.json
new file mode 100644
index 0000000..7155790
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/offlinerealm.json
@@ -0,0 +1,204 @@
+{
+  "id": "test",
+  "realm": "test",
+  "enabled": true,
+  "accessTokenLifespan": 10,
+  "ssoSessionIdleTimeout": 30,
+  "sslRequired": "external",
+  "registrationAllowed": true,
+  "resetPasswordAllowed": true,
+  "editUsernameAllowed" : true,
+  "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+  "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+  "requiredCredentials": [ "password" ],
+  "defaultRoles": [ "user" ],
+  "smtpServer": {
+    "from": "auto@keycloak.org",
+    "host": "localhost",
+    "port":"3025"
+  },
+  "users" : [
+    {
+      "username" : "test-user@localhost",
+      "enabled": true,
+      "email" : "test-user@localhost",
+      "firstName": "Tom",
+      "lastName": "Brady",
+      "credentials" : [
+        { "type" : "password",
+          "value" : "password" }
+      ],
+      "realmRoles": ["user", "offline_access"],
+      "clientRoles": {
+        "test-app": [ "customer-user" ],
+        "account": [ "view-profile", "manage-account" ]
+      }
+    },
+    {
+      "username" : "john-doh@localhost",
+      "enabled": true,
+      "email" : "john-doh@localhost",
+      "firstName": "John",
+      "lastName": "Doh",
+      "credentials" : [
+        { "type" : "password",
+          "value" : "password" }
+      ],
+      "realmRoles": ["user"],
+      "clientRoles": {
+        "test-app": [ "customer-user" ],
+        "account": [ "view-profile", "manage-account" ]
+      }
+    },
+    {
+      "username" : "keycloak-user@localhost",
+      "enabled": true,
+      "email" : "keycloak-user@localhost",
+      "credentials" : [
+        { "type" : "password",
+          "value" : "password" }
+      ],
+      "realmRoles": ["user"],
+      "clientRoles": {
+        "test-app": [ "customer-user" ],
+        "account": [ "view-profile", "manage-account" ]
+      }
+    },
+    {
+      "username" : "topGroupUser",
+      "enabled": true,
+      "email" : "top@redhat.com",
+      "credentials" : [
+        { "type" : "password",
+          "value" : "password" }
+      ],
+      "groups": [
+        "/topGroup"
+      ]
+    },
+    {
+      "username" : "level2GroupUser",
+      "enabled": true,
+      "email" : "level2@redhat.com",
+      "credentials" : [
+        { "type" : "password",
+          "value" : "password" }
+      ],
+      "groups": [
+        "/topGroup/level2group"
+      ]
+    }
+  ],
+  "scopeMappings": [
+    {
+      "client": "third-party",
+      "roles": ["user"]
+    },
+    {
+      "client": "test-app",
+      "roles": ["user"]
+    },
+    {
+      "client": "offline-client",
+      "roles": ["user","offline_access"]
+    }
+  ],
+  "clients": [
+    {
+      "clientId": "test-app",
+      "enabled": true,
+      "baseUrl": "http://localhost:8180/auth/realms/master/app",
+      "redirectUris": [
+        "http://localhost:8180/auth/realms/master/app/*"
+      ],
+      "adminUrl": "http://localhost:8180/auth/realms/master/app/logout",
+      "secret": "password"
+    },
+    {
+      "clientId" : "third-party",
+      "enabled": true,
+      "consentRequired": true,
+
+      "redirectUris": [
+        "http://localhost:8180/app/*"
+      ],
+      "secret": "password"
+    },
+    {
+      "clientId": "offline-client",
+      "enabled": true,
+      "adminUrl": "/offline-client/logout",
+      "baseUrl": "/offline-client",
+      "directAccessGrantsEnabled": true,
+      "redirectUris": [
+        "/offline-client/*"
+      ],
+      "secret": "secret1"
+    }
+
+  ],
+  "roles" : {
+    "realm" : [
+      {
+        "name": "user",
+        "description": "Have User privileges"
+      },
+      {
+        "name": "admin",
+        "description": "Have Administrator privileges"
+      }
+    ],
+    "client" : {
+      "test-app" : [
+        {
+          "name": "customer-user",
+          "description": "Have Customer User privileges"
+        },
+        {
+          "name": "customer-admin",
+          "description": "Have Customer Admin privileges"
+        }
+      ]
+    }
+
+  },
+  "groups" : [
+    {
+      "name": "topGroup",
+      "attributes": {
+        "topAttribute": ["true"]
+
+      },
+      "realmRoles": ["user"],
+
+      "subGroups": [
+        {
+          "name": "level2group",
+          "realmRoles": ["admin"],
+          "clientRoles": {
+            "test-app": ["customer-user"]
+          },
+          "attributes": {
+            "level2Attribute": ["true"]
+
+          }
+        }
+      ]
+    }
+  ],
+
+
+  "clientScopeMappings": {
+    "test-app": [
+      {
+        "client": "third-party",
+        "roles": ["customer-user"]
+      }
+    ]
+  },
+
+  "internationalizationEnabled": true,
+  "supportedLocales": ["en", "de"],
+  "defaultLocale": "en",
+  "eventsListeners": ["jboss-logging", "event-queue"]
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/jetty-web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/jetty-web.xml
new file mode 100644
index 0000000..8c59313
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/jetty-web.xml
@@ -0,0 +1,46 @@
+<?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.
+  -->
+
+<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
+<Configure class="org.eclipse.jetty.webapp.WebAppContext">
+    <Get name="securityHandler">
+        <Set name="authenticator">
+            <New class="org.keycloak.adapters.jetty.KeycloakJettyAuthenticator">
+                <!--
+                <Set name="adapterConfig">
+                    <New class="org.keycloak.representations.adapters.config.AdapterConfig">
+                        <Set name="realm">tomcat</Set>
+                        <Set name="resource">customer-portal</Set>
+                        <Set name="authServerUrl">http://localhost:8180/auth</Set>
+                        <Set name="sslRequired">external</Set>
+                        <Set name="credentials">
+                            <Map>
+                                <Entry>
+                                    <Item>secret</Item>
+                                    <Item>password</Item>
+                                </Entry>
+                            </Map>
+                        </Set>
+                        <Set name="realmKey">MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</Set>
+                    </New>
+                </Set>
+                -->
+            </New>
+        </Set>
+    </Get>
+</Configure>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/keycloak.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/keycloak.json
new file mode 100644
index 0000000..0e703e9
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/keycloak.json
@@ -0,0 +1,10 @@
+{
+  "realm": "test",
+  "resource": "offline-client",
+  "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+  "auth-server-url": "http://localhost:8081/auth",
+  "ssl-required" : "external",
+  "credentials": {
+    "secret": "secret1"
+  }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/web.xml
new file mode 100644
index 0000000..a329f9a
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/web.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+         version="3.0">
+
+    <module-name>offline-client</module-name>
+
+    <servlet>
+        <servlet-name>Servlet</servlet-name>
+        <servlet-class>org.keycloak.testsuite.adapter.servlet.OfflineTokenServlet</servlet-class>
+    </servlet>
+
+    <servlet>
+        <servlet-name>Error Servlet</servlet-name>
+        <servlet-class>org.keycloak.testsuite.adapter.servlet.ErrorServlet</servlet-class>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>Servlet</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+
+    <servlet-mapping>
+        <servlet-name>Error Servlet</servlet-name>
+        <url-pattern>/error.html</url-pattern>
+    </servlet-mapping>
+
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Users</web-resource-name>
+            <url-pattern>/*</url-pattern>
+        </web-resource-collection>
+        <auth-constraint>
+            <role-name>user</role-name>
+        </auth-constraint>
+    </security-constraint>
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Errors</web-resource-name>
+            <url-pattern>/error.html</url-pattern>
+        </web-resource-collection>
+    </security-constraint>
+
+    <login-config>
+        <auth-method>KEYCLOAK</auth-method>
+        <realm-name>test</realm-name>
+        <form-login-config>
+            <form-login-page>/error.html</form-login-page>
+            <form-error-page>/error.html</form-error-page>
+        </form-login-config>
+    </login-config>
+
+    <security-role>
+        <role-name>admin</role-name>
+    </security-role>
+    <security-role>
+        <role-name>user</role-name>
+    </security-role>
+</web-app>
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/as7/src/test/java/org/keycloak/testsuite/adapter/AS7OfflineServletsAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/as7/src/test/java/org/keycloak/testsuite/adapter/AS7OfflineServletsAdapterTest.java
new file mode 100644
index 0000000..d28be58
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/as7/src/test/java/org/keycloak/testsuite/adapter/AS7OfflineServletsAdapterTest.java
@@ -0,0 +1,11 @@
+package org.keycloak.testsuite.adapter;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractOfflineServletsAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ * @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>.
+ */
+@AppServerContainer("app-server-as7")
+public class AS7OfflineServletsAdapterTest extends AbstractOfflineServletsAdapterTest {
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPOfflineServletsAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPOfflineServletsAdapterTest.java
new file mode 100644
index 0000000..48cb3ef
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPOfflineServletsAdapterTest.java
@@ -0,0 +1,11 @@
+package org.keycloak.testsuite.adapter;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractOfflineServletsAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ * @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>.
+ */
+@AppServerContainer("app-server-eap")
+public class EAPOfflineServletsAdapterTest extends AbstractOfflineServletsAdapterTest {
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6OfflineServletsAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6OfflineServletsAdapterTest.java
new file mode 100644
index 0000000..76b180f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6OfflineServletsAdapterTest.java
@@ -0,0 +1,11 @@
+package org.keycloak.testsuite.adapter;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractOfflineServletsAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ * @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>.
+ */
+@AppServerContainer("app-server-eap6")
+public class EAP6OfflineServletsAdapterTest extends AbstractOfflineServletsAdapterTest {
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflyOfflineServletsAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflyOfflineServletsAdapterTest.java
new file mode 100644
index 0000000..0f645cc
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflyOfflineServletsAdapterTest.java
@@ -0,0 +1,11 @@
+package org.keycloak.testsuite.adapter;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractOfflineServletsAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ * @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>.
+ */
+@AppServerContainer("app-server-wildfly")
+public class WildflyOfflineServletsAdapterTest extends AbstractOfflineServletsAdapterTest {
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/src/test/java/org/keycloak/testsuite/adapter/Wildfly8OfflineServletsAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/src/test/java/org/keycloak/testsuite/adapter/Wildfly8OfflineServletsAdapterTest.java
new file mode 100644
index 0000000..0de01de
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/src/test/java/org/keycloak/testsuite/adapter/Wildfly8OfflineServletsAdapterTest.java
@@ -0,0 +1,11 @@
+package org.keycloak.testsuite.adapter;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractOfflineServletsAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ * @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>.
+ */
+@AppServerContainer("app-server-wildfly8")
+public class Wildfly8OfflineServletsAdapterTest extends AbstractOfflineServletsAdapterTest {
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/src/test/java/org/keycloak/testsuite/adapter/Wildfly9OfflineServletsAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/src/test/java/org/keycloak/testsuite/adapter/Wildfly9OfflineServletsAdapterTest.java
new file mode 100644
index 0000000..83ae9cf
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/src/test/java/org/keycloak/testsuite/adapter/Wildfly9OfflineServletsAdapterTest.java
@@ -0,0 +1,11 @@
+package org.keycloak.testsuite.adapter;
+
+import org.keycloak.testsuite.adapter.servlet.AbstractOfflineServletsAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ * @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>.
+ */
+@AppServerContainer("app-server-wildfly9")
+public class Wildfly9OfflineServletsAdapterTest extends AbstractOfflineServletsAdapterTest {
+}