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);
|
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) {
|
public static UserRepresentation findUserByUsername(RealmResource realm, String username) {
|
||||||
UserRepresentation user = null;
|
UserRepresentation user = null;
|
||||||
List<UserRepresentation> ur = realm.users().search(username, null, null);
|
List<UserRepresentation> ur = realm.users().search(username, null, null);
|
||||||
|
@ -143,7 +147,7 @@ public class ApiUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
UserResource userResource = realm.users().get(userId);
|
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: \""
|
+ userResource.toRepresentation().getUsername() + "\" of client: \""
|
||||||
+ clientName + "\" in realm: \"" + realmName + "\"");
|
+ clientName + "\" in realm: \"" + realmName + "\"");
|
||||||
userResource.roles().clientLevel(clientId).add(roleRepresentations);
|
userResource.roles().clientLevel(clientId).add(roleRepresentations);
|
||||||
|
|
|
@ -132,7 +132,7 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
||||||
UserBuilder user = UserBuilder.create()
|
UserBuilder user = UserBuilder.create()
|
||||||
.id(KeycloakModelUtils.generateId())
|
.id(KeycloakModelUtils.generateId())
|
||||||
.username("no-permissions")
|
.username("no-permissions")
|
||||||
.role("user")
|
.addRoles("user")
|
||||||
.password("password");
|
.password("password");
|
||||||
realm.getUsers().add(user.build());
|
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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ClientBuilder adminUrl(String adminUrl) {
|
||||||
|
rep.setAdminUrl(adminUrl);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public ClientBuilder rootUrl(String rootUrl) {
|
public ClientBuilder rootUrl(String rootUrl) {
|
||||||
rep.setRootUrl(rootUrl);
|
rep.setRootUrl(rootUrl);
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -61,5 +61,17 @@ public class ClientManager {
|
||||||
app.setDirectAccessGrantsEnabled(enable);
|
app.setDirectAccessGrantsEnabled(enable);
|
||||||
clientResource.update(app);
|
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;
|
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();
|
return new RealmManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void accessCodeLifeSpan(Integer accessCodeLifespan) {
|
public RealmManager accessCodeLifeSpan(Integer accessCodeLifespan) {
|
||||||
RealmRepresentation realmRepresentation = realm.toRepresentation();
|
RealmRepresentation realmRepresentation = realm.toRepresentation();
|
||||||
realmRepresentation.setAccessCodeLifespan(accessCodeLifespan);
|
realmRepresentation.setAccessCodeLifespan(accessCodeLifespan);
|
||||||
realm.update(realmRepresentation);
|
realm.update(realmRepresentation);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void verifyEmail(Boolean enabled) {
|
public RealmManager verifyEmail(Boolean enabled) {
|
||||||
RealmRepresentation rep = realm.toRepresentation();
|
RealmRepresentation rep = realm.toRepresentation();
|
||||||
rep.setVerifyEmail(enabled);
|
rep.setVerifyEmail(enabled);
|
||||||
realm.update(rep);
|
realm.update(rep);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void passwordPolicy(String passwordPolicy) {
|
public RealmManager passwordPolicy(String passwordPolicy) {
|
||||||
RealmRepresentation rep = realm.toRepresentation();
|
RealmRepresentation rep = realm.toRepresentation();
|
||||||
rep.setPasswordPolicy(passwordPolicy);
|
rep.setPasswordPolicy(passwordPolicy);
|
||||||
realm.update(rep);
|
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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserBuilder role(String role) {
|
public UserBuilder addRoles(String... roles) {
|
||||||
if (rep.getRealmRoles() == null) {
|
if (rep.getRealmRoles() == null) {
|
||||||
rep.setRealmRoles(new ArrayList<String>());
|
rep.setRealmRoles(new ArrayList<String>());
|
||||||
}
|
}
|
||||||
rep.getRealmRoles().add(role);
|
for (String role : roles) {
|
||||||
|
rep.getRealmRoles().add(role);
|
||||||
|
}
|
||||||
return this;
|
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