Merge pull request #2784 from abstractj/RHSSO-121

RHSSO-121: Offline Tokens
This commit is contained in:
Stian Thorgersen 2016-05-05 06:35:58 +02:00
commit 8799405673
23 changed files with 1237 additions and 703 deletions

View file

@ -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;
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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());

View file

@ -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));
}
}

View file

@ -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;

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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>

View file

@ -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>

View file

@ -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"
}
}

View file

@ -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>

View file

@ -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"]
}

View file

@ -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 {
}

View file

@ -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 {
}

View file

@ -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 {
}

View file

@ -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 {
}

View file

@ -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 {
}

View file

@ -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 {
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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<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)
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("<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 = 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("</pre></body></html>");
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;
}
}
}