From 9a3bea7062bc7a7f79bc04dbe1d0b57e582c6b27 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 25 Apr 2016 19:09:30 -0300 Subject: [PATCH] RHSSO-121: Offline Tokens --- .../testsuite/adapter/page/OfflineToken.java | 59 ++ .../adapter/servlet/OfflineTokenServlet.java | 69 ++ .../AbstractOfflineServletsAdapterTest.java | 186 +++++ .../org/keycloak/testsuite/admin/ApiUtil.java | 6 +- .../testsuite/oauth/AccessTokenTest.java | 2 +- .../testsuite/oauth/OfflineTokenTest.java | 436 +++++++++++ .../testsuite/util/ClientBuilder.java | 5 + .../testsuite/util/ClientManager.java | 12 + .../keycloak/testsuite/util/RealmBuilder.java | 9 + .../keycloak/testsuite/util/RealmManager.java | 31 +- .../keycloak/testsuite/util/UserBuilder.java | 6 +- .../offline-client/META-INF/context.xml | 20 + .../offline-client/WEB-INF/jetty-web.xml | 46 ++ .../offline-client/WEB-INF/keycloak.json | 10 + .../offline-client/WEB-INF/web.xml | 77 ++ .../offline-client/offlinerealm.json | 204 +++++ .../AS7OfflineServletsAdapterTest.java | 11 + .../EAPOfflineServletsAdapterTest.java | 11 + .../EAP6OfflineServletsAdapterTest.java | 11 + .../WildflyOfflineServletsAdapterTest.java | 11 + .../Wildfly8OfflineServletsAdapterTest.java | 11 + .../Wildfly9OfflineServletsAdapterTest.java | 11 + .../testsuite/oauth/OfflineTokenTest.java | 696 ------------------ 23 files changed, 1237 insertions(+), 703 deletions(-) create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/OfflineToken.java create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/OfflineTokenServlet.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOfflineServletsAdapterTest.java create mode 100755 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/META-INF/context.xml create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/jetty-web.xml create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/keycloak.json create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/web.xml create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/offlinerealm.json create mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/as7/src/test/java/org/keycloak/testsuite/adapter/AS7OfflineServletsAdapterTest.java create mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPOfflineServletsAdapterTest.java create mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6OfflineServletsAdapterTest.java create mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflyOfflineServletsAdapterTest.java create mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly8/src/test/java/org/keycloak/testsuite/adapter/Wildfly8OfflineServletsAdapterTest.java create mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/src/test/java/org/keycloak/testsuite/adapter/Wildfly9OfflineServletsAdapterTest.java delete mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java 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 0000000000..7116e659a6 --- /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 Bruno Oliveira. + */ +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 0000000000..6f7d954880 --- /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 Marek Posolda + */ +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("Offline token servlet
");
+        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("" + accessTokenPretty + "")
+                .append("" + refreshTokenPretty + "")
+                .append("
"); + 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 0000000000..b00549c166 --- /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 Bruno Oliveira. + */ +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 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 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 aae4f5331e..73946ff2a3 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 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 fc8c8fab1c..887aa26447 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 0000000000..ac9c5a7808 --- /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 Marek Posolda + */ +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 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 7c5da16e12..e3c5f94041 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 @@ -94,6 +94,11 @@ public class ClientBuilder { 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 5654814b33..f8a9a73cb1 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 3d322b3732..4576c2fecb 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 030f79845b..40ad33d585 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 26968337e8..f769d057a6 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()); } - 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 0000000000..b4ddcce386 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/META-INF/context.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file 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 0000000000..8c59313878 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/jetty-web.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + \ 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 0000000000..0e703e9360 --- /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 0000000000..a329f9afc6 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/offline-client/WEB-INF/web.xml @@ -0,0 +1,77 @@ + + + + + + offline-client + + + Servlet + org.keycloak.testsuite.adapter.servlet.OfflineTokenServlet + + + + Error Servlet + org.keycloak.testsuite.adapter.servlet.ErrorServlet + + + + Servlet + /* + + + + Error Servlet + /error.html + + + + + Users + /* + + + user + + + + + Errors + /error.html + + + + + KEYCLOAK + test + + /error.html + /error.html + + + + + admin + + + user + + 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 0000000000..7155790579 --- /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/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 0000000000..d28be58b75 --- /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 Bruno Oliveira. + */ +@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 0000000000..48cb3ef9b9 --- /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 Bruno Oliveira. + */ +@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 0000000000..76b180f45d --- /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 Bruno Oliveira. + */ +@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 0000000000..0f645cc2e0 --- /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 Bruno Oliveira. + */ +@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 0000000000..0de01ded77 --- /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 Bruno Oliveira. + */ +@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 0000000000..83ae9cfb84 --- /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 Bruno Oliveira. + */ +@AppServerContainer("app-server-wildfly9") +public class Wildfly9OfflineServletsAdapterTest extends AbstractOfflineServletsAdapterTest { +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java deleted file mode 100755 index 5beb5b6f10..0000000000 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java +++ /dev/null @@ -1,696 +0,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. - */ - -package org.keycloak.testsuite.oauth; - -import java.io.IOException; -import java.net.URL; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; - -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 org.junit.Assert; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.OAuth2Constants; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.common.constants.ServiceAccountConstants; -import org.keycloak.events.Details; -import org.keycloak.events.Errors; -import org.keycloak.events.Event; -import org.keycloak.events.EventType; -import org.keycloak.jose.jws.JWSInput; -import org.keycloak.jose.jws.JWSInputException; -import org.keycloak.models.ClientModel; -import org.keycloak.models.Constants; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.protocol.oidc.TokenManager; -import org.keycloak.representations.AccessToken; -import org.keycloak.representations.RefreshToken; -import org.keycloak.services.managers.ClientManager; -import org.keycloak.services.managers.RealmManager; -import org.keycloak.testsuite.AssertEvents; -import org.keycloak.testsuite.OAuthClient; -import org.keycloak.testsuite.pages.AccountApplicationsPage; -import org.keycloak.testsuite.pages.LoginPage; -import org.keycloak.testsuite.pages.OAuthGrantPage; -import org.keycloak.testsuite.rule.KeycloakRule; -import org.keycloak.testsuite.rule.WebResource; -import org.keycloak.testsuite.rule.WebRule; -import org.keycloak.util.JsonSerialization; -import org.keycloak.util.TokenUtil; -import org.keycloak.common.util.Time; -import org.keycloak.common.util.UriUtils; -import org.openqa.selenium.WebDriver; - -import static org.junit.Assert.assertEquals; - -/** - * @author Marek Posolda - */ -public class OfflineTokenTest { - - @ClassRule - public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { - - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - // For testing - appRealm.setAccessTokenLifespan(10); - appRealm.setSsoSessionIdleTimeout(30); - - ClientModel app = KeycloakModelUtils.createClient(appRealm, "offline-client"); - app.setDirectAccessGrantsEnabled(true); - app.setSecret("secret1"); - String testAppRedirectUri = appRealm.getClientByClientId("test-app").getRedirectUris().iterator().next(); - offlineClientAppUri = UriUtils.getOrigin(testAppRedirectUri) + "/offline-client"; - app.setRedirectUris(new HashSet<>(Arrays.asList(offlineClientAppUri))); - app.setManagementUrl(offlineClientAppUri); - - new ClientManager(manager).enableServiceAccount(app); - UserModel serviceAccountUser = manager.getSession().users().getUserByUsername(ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client", appRealm); - RoleModel customerUserRole = appRealm.getClientByClientId("test-app").getRole("customer-user"); - serviceAccountUser.grantRole(customerUserRole); - - userId = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm).getId(); - - URL url = getClass().getResource("/oidc/offline-client-keycloak.json"); - keycloakRule.createApplicationDeployment() - .name("offline-client").contextPath("/offline-client") - .servletClass(OfflineTokenServlet.class).adapterConfigPath(url.getPath()) - .role("user").deployApplication(); - } - - }); - - private static String userId; - private static String offlineClientAppUri; - - @Rule - public WebRule webRule = new WebRule(this); - - @WebResource - protected WebDriver driver; - - @WebResource - protected OAuthClient oauth; - - @WebResource - protected LoginPage loginPage; - - @WebResource - protected OAuthGrantPage oauthGrantPage; - - @WebResource - protected AccountApplicationsPage accountAppPage; - - @Rule - public AssertEvents events = new AssertEvents(keycloakRule); - - -// @Test -// public void testSleep() throws Exception { -// Thread.sleep(9999000); -// } - - @Test - public void offlineTokenDisabledForClient() throws Exception { - keycloakRule.update(new KeycloakRule.KeycloakSetup() { - - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - appRealm.getClientByClientId("offline-client").setFullScopeAllowed(false); - } - - }); - - oauth.scope(OAuth2Constants.OFFLINE_ACCESS); - oauth.clientId("offline-client"); - oauth.redirectUri(offlineClientAppUri); - oauth.doLogin("test-user@localhost", "password"); - - Event 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(); - - keycloakRule.update(new KeycloakRule.KeycloakSetup() { - - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - appRealm.getClientByClientId("offline-client").setFullScopeAllowed(true); - } - - }); - } - - @Test - public void offlineTokenUserNotAllowed() throws Exception { - String userId = keycloakRule.getUser("test", "keycloak-user@localhost").getId(); - - oauth.scope(OAuth2Constants.OFFLINE_ACCESS); - oauth.clientId("offline-client"); - oauth.redirectUri(offlineClientAppUri); - oauth.doLogin("keycloak-user@localhost", "password"); - - Event 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"); - - Event 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(); - - Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType()); - Assert.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 - keycloakRule.update(new KeycloakRule.KeycloakSetup() { - - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - manager.getSession().sessions().removeExpired(appRealm); - } - - }); - keycloakRule.update(new KeycloakRule.KeycloakSetup() { - - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - Assert.assertNull(manager.getSession().sessions().getUserSession(appRealm, 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")); - - Event 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"); - - 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 { - keycloakRule.configure(new KeycloakRule.KeycloakSetup() { - - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - appRealm.setRevokeRefreshToken(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); - - keycloakRule.configure(new KeycloakRule.KeycloakSetup() { - - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - appRealm.setRevokeRefreshToken(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); - - String serviceAccountUserId = keycloakRule.getUser("test", ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client").getId(); - - 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 { - keycloakRule.update(new KeycloakRule.KeycloakSetup() { - - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - ClientModel offlineClient = appRealm.getClientByClientId("offline-client"); - UserModel testUser = session.users().getUserByUsername("test-user@localhost", appRealm); - RoleModel offlineAccess = appRealm.getRole(Constants.OFFLINE_ACCESS_ROLE); - - // Test access - Assert.assertFalse(TokenManager.getAccess(null, true, offlineClient, testUser).contains(offlineAccess)); - Assert.assertTrue(TokenManager.getAccess(OAuth2Constants.OFFLINE_ACCESS, true, offlineClient, testUser).contains(offlineAccess)); - - // Grant offline_access role indirectly through composite role - RoleModel composite = appRealm.addRole("composite"); - composite.addCompositeRole(offlineAccess); - - testUser.deleteRoleMapping(offlineAccess); - testUser.grantRole(composite); - - // Test access - Assert.assertFalse(TokenManager.getAccess(null, true, offlineClient, testUser).contains(offlineAccess)); - Assert.assertTrue(TokenManager.getAccess(OAuth2Constants.OFFLINE_ACCESS, true, offlineClient, testUser).contains(offlineAccess)); - } - - }); - - // Integration test - offlineTokenDirectGrantFlow(); - - // Revert changes - keycloakRule.update(new KeycloakRule.KeycloakSetup() { - - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - RoleModel composite = appRealm.getRole("composite"); - RoleModel offlineAccess = appRealm.getRole(Constants.OFFLINE_ACCESS_ROLE); - UserModel testUser = session.users().getUserByUsername("test-user@localhost", appRealm); - - testUser.deleteRoleMapping(composite); - appRealm.removeRole(composite); - testUser.grantRole(offlineAccess); - } - - }); - } - - @Test - public void testServlet() { - OfflineTokenServlet.tokenInfo = null; - - String servletUri = UriBuilder.fromUri(offlineClientAppUri) - .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS) - .build().toString(); - driver.navigate().to(servletUri); - loginPage.login("test-user@localhost", "password"); - Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri)); - - Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getType(), TokenUtil.TOKEN_TYPE_OFFLINE); - Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getExpiration(), 0); - - String accessTokenId = OfflineTokenServlet.tokenInfo.accessToken.getId(); - String refreshTokenId = OfflineTokenServlet.tokenInfo.refreshToken.getId(); - - // Assert access token and offline token are refreshed - Time.setOffset(9999); - driver.navigate().to(offlineClientAppUri); - Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri)); - Assert.assertNotEquals(OfflineTokenServlet.tokenInfo.refreshToken.getId(), refreshTokenId); - Assert.assertNotEquals(OfflineTokenServlet.tokenInfo.accessToken.getId(), accessTokenId); - - // Ensure that logout works for webapp (even if offline token will be still valid in Keycloak DB) - driver.navigate().to(offlineClientAppUri + "/logout"); - loginPage.assertCurrent(); - driver.navigate().to(offlineClientAppUri); - loginPage.assertCurrent(); - - Time.setOffset(0); - events.clear(); - } - - @Test - public void testServletWithRevoke() { - // Login to servlet first with offline token - String servletUri = UriBuilder.fromUri(offlineClientAppUri) - .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS) - .build().toString(); - driver.navigate().to(servletUri); - loginPage.login("test-user@localhost", "password"); - Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri)); - - Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getType(), TokenUtil.TOKEN_TYPE_OFFLINE); - - // Assert refresh works with increased time - Time.setOffset(9999); - driver.navigate().to(offlineClientAppUri); - Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri)); - Time.setOffset(0); - - events.clear(); - - // Go to account service and revoke grant - accountAppPage.open(); - List 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) - Time.setOffset(9999); - driver.navigate().to(offlineClientAppUri); - Assert.assertFalse(driver.getCurrentUrl().startsWith(offlineClientAppUri)); - loginPage.assertCurrent(); - Time.setOffset(0); - } - - @Test - public void testServletWithConsent() { - keycloakRule.update(new KeycloakRule.KeycloakSetup() { - - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - appRealm.getClientByClientId("offline-client").setConsentRequired(true); - } - - }); - - // Assert grant page doesn't have 'Offline Access' role when offline token is not requested - driver.navigate().to(offlineClientAppUri); - 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(offlineClientAppUri) - .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(offlineClientAppUri)); - Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.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")); - - events.clear(); - - // Revert change - keycloakRule.update(new KeycloakRule.KeycloakSetup() { - - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - appRealm.getClientByClientId("offline-client").setConsentRequired(false); - } - - }); - } - - public static class OfflineTokenServlet extends HttpServlet { - - private static TokenInfo tokenInfo; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - if (req.getRequestURI().endsWith("logout")) { - - UriBuilder redirectUriBuilder = UriBuilder.fromUri(offlineClientAppUri); - if (req.getParameter(OAuth2Constants.SCOPE) != null) { - redirectUriBuilder.queryParam(OAuth2Constants.SCOPE, req.getParameter(OAuth2Constants.SCOPE)); - } - String redirectUri = redirectUriBuilder.build().toString(); - - String origin = UriUtils.getOrigin(req.getRequestURL().toString()); - String serverLogoutRedirect = UriBuilder.fromUri(origin + "/auth/realms/test/protocol/openid-connect/logout") - .queryParam("redirect_uri", redirectUri) - .build().toString(); - - resp.sendRedirect(serverLogoutRedirect); - return; - } - - StringBuilder response = new StringBuilder("Offline token servlet
");
-            RefreshableKeycloakSecurityContext ctx = (RefreshableKeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
-            String accessTokenPretty = JsonSerialization.writeValueAsPrettyString(ctx.getToken());
-            RefreshToken refreshToken = null;
-            try {
-                refreshToken = new JWSInput(ctx.getRefreshToken()).readJsonContent(RefreshToken.class);
-            } catch (JWSInputException e) {
-                throw new IOException(e);
-            }
-            String refreshTokenPretty = JsonSerialization.writeValueAsPrettyString(refreshToken);
-
-            response = response.append(accessTokenPretty)
-                    .append(refreshTokenPretty)
-                    .append("
"); - resp.getWriter().println(response.toString()); - - tokenInfo = new TokenInfo(ctx.getToken(), refreshToken); - } - - } - - private static class TokenInfo { - - private final AccessToken accessToken; - private final RefreshToken refreshToken; - - public TokenInfo(AccessToken accessToken, RefreshToken refreshToken) { - this.accessToken = accessToken; - this.refreshToken = refreshToken; - } - } -}