keycloak-aplcache
Changes
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/OfflineToken.java 59(+59 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/OfflineTokenServlet.java 69(+69 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOfflineServletsAdapterTest.java 186(+186 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ApiUtil.java 6(+5 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java 436(+436 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java 15(+15 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientManager.java 12(+12 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java 9(+9 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmManager.java 31(+28 -3)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/UserBuilder.java 6(+4 -2)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/META-INF/context.xml 20(+20 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/offlinerealm.json 204(+204 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/jetty-web.xml 46(+46 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/keycloak.json 10(+10 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/web.xml 77(+77 -0)
testsuite/integration-arquillian/tests/other/adapters/jboss/as7/src/test/java/org/keycloak/testsuite/adapter/AS7OfflineServletsAdapterTest.java 11(+11 -0)
testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPOfflineServletsAdapterTest.java 11(+11 -0)
testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6OfflineServletsAdapterTest.java 11(+11 -0)
testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflyOfflineServletsAdapterTest.java 11(+11 -0)
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 {
+}