Merge pull request #2784 from abstractj/RHSSO-121
RHSSO-121: Offline Tokens
This commit is contained in:
commit
8799405673
23 changed files with 1237 additions and 703 deletions
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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"]
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue