diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/AdminController.java b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/AdminController.java index 3b9ccc4108..28571729d4 100644 --- a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/AdminController.java +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/java/org/keycloak/AdminController.java @@ -1,28 +1,41 @@ package org.keycloak; -import java.io.IOException; -import java.util.Map; - -import javax.servlet.http.HttpServletRequest; - import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.Time; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; +import org.keycloak.representations.AccessToken; import org.keycloak.representations.RefreshToken; import org.keycloak.util.JsonSerialization; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.util.NumberUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.WebRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.UUID; + @Controller @RequestMapping(path = "/admin") public class AdminController { + + private static Logger logger = LoggerFactory.getLogger(AdminController.class); @RequestMapping(path = "/TokenServlet", method = RequestMethod.GET) public String showTokens(WebRequest req, Model model, @RequestParam Map attributes) throws IOException { @@ -56,4 +69,74 @@ public class AdminController { return "tokens"; } + + @RequestMapping(path = "/SessionServlet", method = RequestMethod.GET) + public String sessionServlet(WebRequest webRequest, Model model) { + String counterString = (String) webRequest.getAttribute("counter", RequestAttributes.SCOPE_SESSION); + int counter = 0; + try { + counter = Integer.parseInt(counterString, 10); + } + catch (NumberFormatException ignored) { + } + + model.addAttribute("counter", counter); + + webRequest.setAttribute("counter", Integer.toString(counter+1), RequestAttributes.SCOPE_SESSION); + + return "session"; + } + + @RequestMapping(path = "/LinkServlet", method = RequestMethod.GET) + public String tokenController(WebRequest webRequest, + @RequestParam Map attributes, + Model model) { + + ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + HttpSession httpSession = attr.getRequest().getSession(true); + +// response.addHeader("Cache-Control", "no-cache"); + + String responseAttr = attributes.get("response"); + + if (StringUtils.isEmpty(responseAttr)) { + String provider = attributes.get("provider"); + String realm = attributes.get("realm"); + KeycloakSecurityContext keycloakSession = + (KeycloakSecurityContext) webRequest.getAttribute( + KeycloakSecurityContext.class.getName(), + RequestAttributes.SCOPE_REQUEST); + AccessToken token = keycloakSession.getToken(); + String clientId = token.getAudience()[0]; + String nonce = UUID.randomUUID().toString(); + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + String input = nonce + token.getSessionState() + clientId + provider; + byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8)); + String hash = Base64Url.encode(check); + httpSession.setAttribute("hash", hash); + String redirectUri = KeycloakUriBuilder.fromUri("http://localhost:8280/admin/LinkServlet") + .queryParam("response", "true").build().toString(); + String accountLinkUrl = KeycloakUriBuilder.fromUri("http://localhost:8180/") + .path("/auth/realms/{realm}/broker/{provider}/link") + .queryParam("nonce", nonce) + .queryParam("hash", hash) + .queryParam("client_id", token.getIssuedFor()) + .queryParam("redirect_uri", redirectUri).build(realm, provider).toString(); + + return "redirect:" + accountLinkUrl; + } else { + String error = attributes.get("link_error"); + if (StringUtils.isEmpty(error)) + model.addAttribute("error", "Account linked"); + else + model.addAttribute("error", error); + + return "linking"; + } + } } diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/linking.html b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/linking.html new file mode 100644 index 0000000000..6c7d5bd2b6 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/linking.html @@ -0,0 +1,9 @@ + + + + Linking page result + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/session.html b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/session.html new file mode 100644 index 0000000000..9a7e52f027 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/session.html @@ -0,0 +1,9 @@ + + + + session counter page + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/LinkingPage.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/LinkingPage.java new file mode 100644 index 0000000000..620cc688d3 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/LinkingPage.java @@ -0,0 +1,24 @@ +package org.keycloak.testsuite.springboot; + +import org.keycloak.testsuite.pages.AbstractPage; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +public class LinkingPage extends AbstractPage { + + @FindBy(id = "error") + private WebElement errorMessage; + + @Override + public boolean isCurrent() { + return driver.getTitle().equalsIgnoreCase("linking page result"); + } + + @Override + public void open() throws Exception { + } + + public String getErrorMessage() { + return errorMessage.getText(); + } +} diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SessionPage.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SessionPage.java new file mode 100644 index 0000000000..f8c420ccca --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SessionPage.java @@ -0,0 +1,29 @@ +package org.keycloak.testsuite.springboot; + +import org.apache.commons.lang3.math.NumberUtils; +import org.keycloak.testsuite.pages.AbstractPage; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +public class SessionPage extends AbstractPage { + + public static final String PAGE_TITLE = "session counter page"; + + @FindBy(id = "counter") + private WebElement counterElement; + + @Override + public boolean isCurrent() { + return driver.getTitle().equalsIgnoreCase(PAGE_TITLE); + } + + @Override + public void open() throws Exception { + } + + public int getCounter() { + String counterString = counterElement.getText(); + + return NumberUtils.toInt(counterString, 0); + } +} diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringAdminPage.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringAdminPage.java index 8ce5e75688..30e2b52f63 100644 --- a/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringAdminPage.java +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/main/java/org/keycloak/testsuite/springboot/SpringAdminPage.java @@ -19,4 +19,8 @@ public class SpringAdminPage extends AbstractPage { public void open() throws Exception { } + + public String getTestDivString() { + return testDiv.getText(); + } } diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java index 5b15077719..ad953b0fe5 100644 --- a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AbstractSpringBootTest.java @@ -36,32 +36,27 @@ import org.openqa.selenium.By; public abstract class AbstractSpringBootTest extends AbstractKeycloakTest { - protected static final String REALM_ID = "cd8ee421-5100-41ba-95dd-b27c8e5cf042"; + static final String REALM_ID = "cd8ee421-5100-41ba-95dd-b27c8e5cf042"; - protected static final String REALM_NAME = "test"; + static final String REALM_NAME = "test"; - protected static final String CLIENT_ID = "spring-boot-app"; - protected static final String SECRET = "e3789ac5-bde6-4957-a7b0-612823dac101"; + static final String CLIENT_ID = "spring-boot-app"; + static final String SECRET = "e3789ac5-bde6-4957-a7b0-612823dac101"; - protected static final String APPLICATION_URL = "http://localhost:8280"; - protected static final String BASE_URL = APPLICATION_URL + "/admin"; + static final String APPLICATION_URL = "http://localhost:8280"; + static final String BASE_URL = APPLICATION_URL + "/admin"; - protected static final String USER_LOGIN = "testuser"; - protected static final String USER_EMAIL = "user@email.test"; - protected static final String USER_PASSWORD = "user-password"; + static final String USER_LOGIN = "testuser"; + static final String USER_EMAIL = "user@email.test"; + static final String USER_PASSWORD = "user-password"; - protected static final String USER_LOGIN_2 = "testuser2"; - protected static final String USER_EMAIL_2 = "user2@email.test"; - protected static final String USER_PASSWORD_2 = "user2-password"; + static final String CORRECT_ROLE = "admin"; - protected static final String CORRECT_ROLE = "admin"; - protected static final String INCORRECT_ROLE = "wrong-admin"; - - protected static final String REALM_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5" + + static final String REALM_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5" + "mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi7" + "9NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"; - protected static final String REALM_PRIVATE_KEY = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3Bj" + + static final String REALM_PRIVATE_KEY = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3Bj" + "LGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vj" + "O2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jY" + "lQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn" + @@ -72,16 +67,16 @@ public abstract class AbstractSpringBootTest extends AbstractKeycloakTest { "N39fOYAlo+nTixgeW7X8Y="; @Page - protected LoginPage loginPage; + LoginPage loginPage; @Page - protected SpringApplicationPage applicationPage; + SpringApplicationPage applicationPage; @Page - protected SpringAdminPage adminPage; + SpringAdminPage adminPage; @Page - protected TokenPage tokenPage; + TokenPage tokenPage; @Override public void addTestRealms(List testRealms) { @@ -117,7 +112,7 @@ public abstract class AbstractSpringBootTest extends AbstractKeycloakTest { return clientRepresentation; } - private void addUser(String login, String email, String password, String... roles) { + void addUser(String login, String email, String password, String... roles) { UserRepresentation userRepresentation = new UserRepresentation(); userRepresentation.setUsername(login); @@ -149,14 +144,14 @@ public abstract class AbstractSpringBootTest extends AbstractKeycloakTest { return result; } - protected String logoutPage(String redirectUrl) { + String logoutPage(String redirectUrl) { return getAuthRoot(suiteContext) + "/auth/realms/" + REALM_NAME + "/protocol/" + "openid-connect" + "/logout?redirect_uri=" + encodeUrl(redirectUrl); } - protected void setAdapterAndServerTimeOffset(int timeOffset, String url) { + void setAdapterAndServerTimeOffset(int timeOffset, String url) { setTimeOffset(timeOffset); String timeOffsetUri = UriBuilder.fromUri(url) @@ -167,51 +162,41 @@ public abstract class AbstractSpringBootTest extends AbstractKeycloakTest { WaitUtils.waitUntilElement(By.tagName("body")).is().visible(); } - protected String getCorrectUserId() { - return adminClient.realms().realm(REALM_NAME).users().search(USER_LOGIN) - .get(0).getId(); - } - - @Before public void createRoles() { RealmResource realm = realmsResouce().realm(REALM_NAME); RoleRepresentation correct = new RoleRepresentation(CORRECT_ROLE, CORRECT_ROLE, false); realm.roles().create(correct); - - RoleRepresentation incorrect = new RoleRepresentation(INCORRECT_ROLE, INCORRECT_ROLE, false); - realm.roles().create(incorrect); } - @Before public void addUsers() { addUser(USER_LOGIN, USER_EMAIL, USER_PASSWORD, CORRECT_ROLE); - addUser(USER_LOGIN_2, USER_EMAIL_2, USER_PASSWORD_2, INCORRECT_ROLE); } - @After public void cleanupUsers() { - RealmResource providerRealm = adminClient.realm(REALM_NAME); - UserRepresentation userRep = ApiUtil.findUserByUsername(providerRealm, USER_LOGIN); + RealmResource realmResource = adminClient.realm(REALM_NAME); + UserRepresentation userRep = ApiUtil.findUserByUsername(realmResource, USER_LOGIN); if (userRep != null) { - providerRealm.users().get(userRep.getId()).remove(); - } - - RealmResource childRealm = adminClient.realm(REALM_NAME); - userRep = ApiUtil.findUserByUsername(childRealm, USER_LOGIN_2); - if (userRep != null) { - childRealm.users().get(userRep.getId()).remove(); + realmResource.users().get(userRep.getId()).remove(); } } - @After public void cleanupRoles() { RealmResource realm = realmsResouce().realm(REALM_NAME); RoleResource correctRole = realm.roles().get(CORRECT_ROLE); correctRole.remove(); + } - RoleResource incorrectRole = realm.roles().get(INCORRECT_ROLE); - incorrectRole.remove(); + @Before + public void setUp() { + createRoles(); + addUsers(); + } + + @After + public void tearDown() { + cleanupUsers(); + cleanupRoles(); } } diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AccountLinkSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AccountLinkSpringBootTest.java new file mode 100644 index 0000000000..64d62bd7d6 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/AccountLinkSpringBootTest.java @@ -0,0 +1,560 @@ +package org.keycloak.testsuite.springboot; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.common.util.Base64Url; +import org.keycloak.models.Constants; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocolService; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.idm.*; +import org.keycloak.testsuite.ActionURIUtils; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; +import org.keycloak.testsuite.broker.BrokerTestTools; +import org.keycloak.testsuite.pages.AccountUpdateProfilePage; +import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.LoginUpdateProfilePage; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.testsuite.util.WaitUtils; +import org.keycloak.util.JsonSerialization; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.UriBuilder; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT; +import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS; +import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient; + +public class AccountLinkSpringBootTest extends AbstractSpringBootTest { + + private static final String PARENT_REALM = "parent-realm"; + + private static final String LINKING_URL = BASE_URL + "/LinkServlet"; + + private static final String PARENT_USERNAME = "parent-username"; + private static final String PARENT_PASSWORD = "parent-password"; + + private static final String CHILD_USERNAME_1 = "child-username-1"; + private static final String CHILD_PASSWORD_1 = "child-password-1"; + + private static final String CHILD_USERNAME_2 = "child-username-2"; + private static final String CHILD_PASSWORD_2 = "child-password-2"; + + @Page + private LinkingPage linkingPage; + + @Page + private AccountUpdateProfilePage profilePage; + + @Page + private LoginUpdateProfilePage loginUpdateProfilePage; + + @Page + private ErrorPage errorPage; + + @Override + public void addTestRealms(List testRealms) { + RealmRepresentation realm = new RealmRepresentation(); + realm.setRealm(REALM_NAME); + realm.setEnabled(true); + realm.setPublicKey(REALM_PUBLIC_KEY); + realm.setPrivateKey(REALM_PRIVATE_KEY); + realm.setAccessTokenLifespan(600); + realm.setAccessCodeLifespan(10); + realm.setAccessCodeLifespanUserAction(6000); + realm.setSslRequired("external"); + ClientRepresentation servlet = new ClientRepresentation(); + servlet.setClientId(CLIENT_ID); + servlet.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + servlet.setAdminUrl(LINKING_URL); + servlet.setDirectAccessGrantsEnabled(true); + servlet.setBaseUrl(LINKING_URL); + servlet.setRedirectUris(new LinkedList<>()); + servlet.getRedirectUris().add(LINKING_URL + "/*"); + servlet.setSecret(SECRET); + servlet.setFullScopeAllowed(true); + realm.setClients(new LinkedList<>()); + realm.getClients().add(servlet); + testRealms.add(realm); + + realm = new RealmRepresentation(); + realm.setRealm(PARENT_REALM); + realm.setEnabled(true); + + testRealms.add(realm); + } + + @Override + public void addUsers() { + addIdpUser(); + addChildUser(); + } + + @Override + public void cleanupUsers() { + } + + @Override + public void createRoles() { + } + + @Override + protected boolean isImportAfterEachMethod() { + return true; + } + + public void addIdpUser() { + RealmResource realm = adminClient.realms().realm(PARENT_REALM); + UserRepresentation user = new UserRepresentation(); + user.setUsername(PARENT_USERNAME); + user.setEnabled(true); + createUserAndResetPasswordWithAdminClient(realm, user, PARENT_PASSWORD); + } + + private String childUserId = null; + + public void addChildUser() { + RealmResource realm = adminClient.realms().realm(REALM_NAME); + UserRepresentation user = new UserRepresentation(); + user.setUsername(CHILD_USERNAME_1); + user.setEnabled(true); + childUserId = createUserAndResetPasswordWithAdminClient(realm, user, CHILD_PASSWORD_1); + UserRepresentation user2 = new UserRepresentation(); + user2.setUsername(CHILD_USERNAME_2); + user2.setEnabled(true); + String user2Id = createUserAndResetPasswordWithAdminClient(realm, user2, CHILD_PASSWORD_2); + + // have to add a role as undertow default auth manager doesn't like "*". todo we can remove this eventually as undertow fixes this in later versions + realm.roles().create(new RoleRepresentation(CORRECT_ROLE, null, false)); + RoleRepresentation role = realm.roles().get(CORRECT_ROLE).toRepresentation(); + List roles = new LinkedList<>(); + roles.add(role); + realm.users().get(childUserId).roles().realmLevel().add(roles); + realm.users().get(user2Id).roles().realmLevel().add(roles); + ClientRepresentation brokerService = realm.clients().findByClientId(Constants.BROKER_SERVICE_CLIENT_ID).get(0); + role = realm.clients().get(brokerService.getId()).roles().get(Constants.READ_TOKEN_ROLE).toRepresentation(); + roles.clear(); + roles.add(role); + realm.users().get(childUserId).roles().clientLevel(brokerService.getId()).add(roles); + realm.users().get(user2Id).roles().clientLevel(brokerService.getId()).add(roles); + } + + @Before + public void createParentChild() { + BrokerTestTools.createKcOidcBroker(adminClient, REALM_NAME, PARENT_REALM, suiteContext); + } + + + @Test + public void testErrorConditions() throws Exception { + RealmResource realm = adminClient.realms().realm(REALM_NAME); + List links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + ClientRepresentation client = adminClient.realms().realm(REALM_NAME).clients().findByClientId(CLIENT_ID).get(0); + + UriBuilder redirectUri = UriBuilder.fromUri(LINKING_URL).queryParam("response", "true"); + + UriBuilder directLinking = UriBuilder.fromUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth") + .path("realms/{child-realm}/broker/{provider}/link") + .queryParam("client_id", CLIENT_ID) + .queryParam("redirect_uri", redirectUri.build()) + .queryParam("hash", Base64Url.encode("crap".getBytes())) + .queryParam("nonce", UUID.randomUUID().toString()); + + String linkUrl = directLinking + .build(REALM_NAME, PARENT_REALM).toString(); + + // test that child user cannot log into parent realm + + navigateTo(linkUrl); + Assert.assertTrue(loginPage.isCurrent(REALM_NAME)); + loginPage.login(CHILD_USERNAME_1, CHILD_PASSWORD_1); + + Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_logged_in")); + + logoutAll(); + + // now log in + + navigateTo(LINKING_URL + "?response=true"); + Assert.assertTrue(loginPage.isCurrent(REALM_NAME)); + loginPage.login(CHILD_USERNAME_1, CHILD_PASSWORD_1); + Assert.assertTrue("Must be on linking page", linkingPage.isCurrent()); + Assert.assertEquals("account linked", linkingPage.getErrorMessage().toLowerCase()); + + // now test CSRF with bad hash. + + navigateTo(linkUrl); + + Assert.assertTrue(driver.getPageSource().contains("We're sorry...")); + + logoutAll(); + + // now log in again with client that does not have scope + + String accountId = adminClient.realms().realm(REALM_NAME).clients().findByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).get(0).getId(); + RoleRepresentation manageAccount = adminClient.realms().realm(REALM_NAME).clients().get(accountId).roles().get(MANAGE_ACCOUNT).toRepresentation(); + RoleRepresentation manageLinks = adminClient.realms().realm(REALM_NAME).clients().get(accountId).roles().get(MANAGE_ACCOUNT_LINKS).toRepresentation(); + RoleRepresentation userRole = adminClient.realms().realm(REALM_NAME).roles().get(CORRECT_ROLE).toRepresentation(); + + client.setFullScopeAllowed(false); + ClientResource clientResource = adminClient.realms().realm(REALM_NAME).clients().get(client.getId()); + clientResource.update(client); + + List roles = new LinkedList<>(); + roles.add(userRole); + clientResource.getScopeMappings().realmLevel().add(roles); + + navigateTo(LINKING_URL + "?response=true"); + Assert.assertTrue(loginPage.isCurrent(REALM_NAME)); + loginPage.login(CHILD_USERNAME_1, CHILD_PASSWORD_1); + Assert.assertTrue(linkingPage.isCurrent()); + Assert.assertEquals("account linked", linkingPage.getErrorMessage().toLowerCase()); + + UriBuilder linkBuilder = UriBuilder.fromUri(LINKING_URL); + String clientLinkUrl = linkBuilder.clone() + .queryParam("realm", REALM_NAME) + .queryParam("provider", PARENT_REALM).build().toString(); + + navigateTo(clientLinkUrl); + + Assert.assertTrue(driver.getCurrentUrl().contains("error=not_allowed")); + + logoutAll(); + + // add MANAGE_ACCOUNT_LINKS scope should pass. + + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + roles = new LinkedList<>(); + roles.add(manageLinks); + clientResource.getScopeMappings().clientLevel(accountId).add(roles); + + navigateTo(clientLinkUrl); + Assert.assertTrue(loginPage.isCurrent(REALM_NAME)); + loginPage.login(CHILD_USERNAME_1, CHILD_PASSWORD_1); + Assert.assertTrue(loginPage.isCurrent(PARENT_REALM)); + loginPage.login(PARENT_USERNAME, PARENT_PASSWORD); + + Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate())); + Assert.assertTrue(driver.getPageSource().contains("Account linked")); + + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertFalse(links.isEmpty()); + + realm.users().get(childUserId).removeFederatedIdentity(PARENT_REALM); + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + clientResource.getScopeMappings().clientLevel(accountId).remove(roles); + + logoutAll(); + + navigateTo(clientLinkUrl); + Assert.assertTrue(loginPage.isCurrent(REALM_NAME)); + loginPage.login(CHILD_USERNAME_1, CHILD_PASSWORD_1); + + Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_allowed")); + + logoutAll(); + + // add MANAGE_ACCOUNT scope should pass + + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + roles = new LinkedList<>(); + roles.add(manageAccount); + clientResource.getScopeMappings().clientLevel(accountId).add(roles); + + navigateTo(clientLinkUrl); + Assert.assertTrue(loginPage.isCurrent(REALM_NAME)); + loginPage.login(CHILD_USERNAME_1, CHILD_PASSWORD_1); + Assert.assertTrue(loginPage.isCurrent(PARENT_REALM)); + loginPage.login(PARENT_USERNAME, PARENT_PASSWORD); + + Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate())); + Assert.assertTrue(driver.getPageSource().contains("Account linked")); + + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertFalse(links.isEmpty()); + + realm.users().get(childUserId).removeFederatedIdentity(PARENT_REALM); + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + clientResource.getScopeMappings().clientLevel(accountId).remove(roles); + + logoutAll(); + + navigateTo(clientLinkUrl); + Assert.assertTrue(loginPage.isCurrent(REALM_NAME)); + loginPage.login(CHILD_USERNAME_1, CHILD_PASSWORD_1); + + Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_allowed")); + + logoutAll(); + + + // undo fullScopeAllowed + + client = adminClient.realms().realm(REALM_NAME).clients().findByClientId(CLIENT_ID).get(0); + client.setFullScopeAllowed(true); + clientResource.update(client); + + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + logoutAll(); + } + + @Test + public void testAccountLink() throws Exception { + RealmResource realm = adminClient.realms().realm(REALM_NAME); + List links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + UriBuilder linkBuilder = UriBuilder.fromUri(LINKING_URL); + String linkUrl = linkBuilder.clone() + .queryParam("realm", REALM_NAME) + .queryParam("provider", PARENT_REALM).build().toString(); + log.info("linkUrl: " + linkUrl); + navigateTo(linkUrl); + Assert.assertTrue(loginPage.isCurrent(REALM_NAME)); + Assert.assertTrue(driver.getPageSource().contains(PARENT_REALM)); + loginPage.login(CHILD_USERNAME_1, CHILD_PASSWORD_1); + Assert.assertTrue(loginPage.isCurrent(PARENT_REALM)); + loginPage.login(PARENT_USERNAME, PARENT_PASSWORD); + log.info("After linking: " + driver.getCurrentUrl()); + log.info(driver.getPageSource()); + Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate())); + Assert.assertTrue(driver.getPageSource().contains("Account linked")); + + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest( + REALM_NAME, + CHILD_USERNAME_1, + CHILD_PASSWORD_1, + null, + CLIENT_ID, + SECRET); + Assert.assertNotNull(response.getAccessToken()); + Assert.assertNull(response.getError()); + Client httpClient = ClientBuilder.newClient(); + String firstToken = getToken(response, httpClient); + Assert.assertNotNull(firstToken); + + navigateTo(linkUrl); + Assert.assertTrue(driver.getPageSource().contains("Account linked")); + String nextToken = getToken(response, httpClient); + Assert.assertNotNull(nextToken); + Assert.assertNotEquals(firstToken, nextToken); + + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertFalse(links.isEmpty()); + + realm.users().get(childUserId).removeFederatedIdentity(PARENT_REALM); + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + logoutAll(); + } + + @Test + public void testLinkOnlyProvider() throws Exception { + RealmResource realm = adminClient.realms().realm(REALM_NAME); + IdentityProviderRepresentation rep = realm.identityProviders().get(PARENT_REALM).toRepresentation(); + rep.setLinkOnly(true); + realm.identityProviders().get(PARENT_REALM).update(rep); + + try { + List links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + UriBuilder linkBuilder = UriBuilder.fromUri(LINKING_URL); + String linkUrl = linkBuilder.clone() + .queryParam("realm", REALM_NAME) + .queryParam("provider", PARENT_REALM).build().toString(); + navigateTo(linkUrl); + Assert.assertTrue(loginPage.isCurrent(REALM_NAME)); + + // should not be on login page. This is what we are testing + Assert.assertFalse(driver.getPageSource().contains(PARENT_REALM)); + + // now test that we can still link. + loginPage.login(CHILD_USERNAME_1, CHILD_PASSWORD_1); + Assert.assertTrue(loginPage.isCurrent(PARENT_REALM)); + loginPage.login(PARENT_USERNAME, PARENT_PASSWORD); + log.info("After linking: " + driver.getCurrentUrl()); + log.info(driver.getPageSource()); + Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate())); + Assert.assertTrue(driver.getPageSource().contains("Account linked")); + + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertFalse(links.isEmpty()); + + realm.users().get(childUserId).removeFederatedIdentity(PARENT_REALM); + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + logoutAll(); + + log.info("testing link-only attack"); + + navigateTo(linkUrl); + Assert.assertTrue(loginPage.isCurrent(REALM_NAME)); + + log.info("login page uri is: " + driver.getCurrentUrl()); + + // ok, now scrape the code from page + String pageSource = driver.getPageSource(); + String action = ActionURIUtils.getActionURIFromPageSource(pageSource); + System.out.println("action uri: " + action); + + Map queryParams = ActionURIUtils.parseQueryParamsFromActionURI(action); + System.out.println("query params: " + queryParams); + + // now try and use the code to login to remote link-only idp + + String uri = "/auth/realms/" + REALM_NAME + "/broker/" + PARENT_REALM + "/login"; + + uri = UriBuilder.fromUri(AuthServerTestEnricher.getAuthServerContextRoot()) + .path(uri) + .queryParam(OAuth2Constants.CODE, queryParams.get(OAuth2Constants.CODE)) + .queryParam(Constants.CLIENT_ID, queryParams.get(Constants.CLIENT_ID)) + .build().toString(); + + log.info("hack uri: " + uri); + + navigateTo(uri); + + Assert.assertTrue(driver.getPageSource().contains("Could not send authentication request to identity provider.")); + } finally { + rep.setLinkOnly(false); + realm.identityProviders().get(PARENT_REALM).update(rep); + } + } + + @Test + public void testAccountNotLinkedAutomatically() throws Exception { + RealmResource realm = adminClient.realms().realm(REALM_NAME); + List links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + // Login to account mgmt first + profilePage.open(REALM_NAME); + WaitUtils.waitForPageToLoad(); + + Assert.assertTrue(loginPage.isCurrent(REALM_NAME)); + loginPage.login(CHILD_USERNAME_1, CHILD_PASSWORD_1); + profilePage.assertCurrent(); + + // Now in another tab, open login screen with "prompt=login" . Login screen will be displayed even if I have SSO cookie + UriBuilder linkBuilder = UriBuilder.fromUri(LINKING_URL); + String linkUrl = linkBuilder.clone() + .queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN) + .build().toString(); + + navigateTo(linkUrl); + Assert.assertTrue(loginPage.isCurrent(REALM_NAME)); + loginPage.clickSocial(PARENT_REALM); + Assert.assertTrue(loginPage.isCurrent(PARENT_REALM)); + loginPage.login(PARENT_USERNAME, PARENT_PASSWORD); + + // Test I was not automatically linked. + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + loginUpdateProfilePage.assertCurrent(); + loginUpdateProfilePage.update("Joe", "Doe", "joe@parent.com"); + + errorPage.assertCurrent(); + Assert.assertEquals("You are already authenticated as different user '" + + CHILD_USERNAME_1 + + "' in this session. Please logout first.", errorPage.getError()); + + logoutAll(); + + // Remove newly created user + String newUserId = ApiUtil.findUserByUsername(realm, PARENT_USERNAME).getId(); + getCleanup(REALM_NAME).addUserId(newUserId); + } + + @Test + public void testAccountLinkingExpired() throws Exception { + RealmResource realm = adminClient.realms().realm(REALM_NAME); + List links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + // Login to account mgmt first + profilePage.open(REALM_NAME); + WaitUtils.waitForPageToLoad(); + + Assert.assertTrue(loginPage.isCurrent(REALM_NAME)); + loginPage.login(CHILD_USERNAME_1, CHILD_PASSWORD_1); + profilePage.assertCurrent(); + + // Now in another tab, request account linking + UriBuilder linkBuilder = UriBuilder.fromUri(LINKING_URL); + String linkUrl = linkBuilder.clone() + .queryParam("realm", REALM_NAME) + .queryParam("provider", PARENT_REALM).build().toString(); + navigateTo(linkUrl); + + Assert.assertTrue(loginPage.isCurrent(PARENT_REALM)); + + // Logout "child" userSession in the meantime (for example through admin request) + realm.logoutAll(); + + // Finish login on parent. + loginPage.login(PARENT_USERNAME, PARENT_PASSWORD); + + // Test I was not automatically linked + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + errorPage.assertCurrent(); + Assert.assertEquals("Requested broker account linking, but current session is no longer valid.", errorPage.getError()); + + logoutAll(); + } + + private void navigateTo(String uri) { + driver.navigate().to(uri); + WaitUtils.waitForPageToLoad(); + } + + public void logoutAll() { + String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(REALM_NAME).toString(); + navigateTo(logoutUri); + logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(PARENT_REALM).toString(); + navigateTo(logoutUri); + } + + private String getToken(OAuthClient.AccessTokenResponse response, Client httpClient) throws Exception { + log.info("target here is " + OAuthClient.AUTH_SERVER_ROOT); + String idpToken = httpClient.target(OAuthClient.AUTH_SERVER_ROOT) + .path("realms") + .path(REALM_NAME) + .path("broker") + .path(PARENT_REALM) + .path("token") + .request() + .header("Authorization", "Bearer " + response.getAccessToken()) + .get(String.class); + AccessTokenResponse res = JsonSerialization.readValue(idpToken, AccessTokenResponse.class); + return res.getToken(); + } +} diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java index 6aea719f18..7e812b0943 100644 --- a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/BasicSpringBootTest.java @@ -1,9 +1,44 @@ package org.keycloak.testsuite.springboot; +import org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import org.keycloak.admin.client.resource.RolesResource; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.admin.ApiUtil; public class BasicSpringBootTest extends AbstractSpringBootTest { + + private static final String USER_LOGIN_2 = "testuser2"; + private static final String USER_EMAIL_2 = "user2@email.test"; + private static final String USER_PASSWORD_2 = "user2-password"; + + private static final String INCORRECT_ROLE = "wrong-admin"; + + @Before + public void addIncorrectUser() { + RolesResource rolesResource = adminClient.realm(REALM_NAME).roles(); + + RoleRepresentation role = new RoleRepresentation(INCORRECT_ROLE, INCORRECT_ROLE, false); + + rolesResource.create(role); + + addUser(USER_LOGIN_2, USER_EMAIL_2, USER_PASSWORD_2, INCORRECT_ROLE); + } + + @After + public void removeUser() { + UserRepresentation user = ApiUtil.findUserByUsername(adminClient.realm(REALM_NAME), USER_LOGIN_2); + + if (user != null) { + adminClient.realm(REALM_NAME).users().delete(user.getId()); + } + + adminClient.realm(REALM_NAME).roles().deleteRole(INCORRECT_ROLE); + } + @Test public void testCorrectUser() { driver.navigate().to(APPLICATION_URL + "/index.html"); diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java index 5ac950f767..9fdc0f76b5 100644 --- a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/OfflineTokenSpringBootTest.java @@ -7,8 +7,10 @@ import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.events.Details; import org.keycloak.events.EventType; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.Urls; import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.AccountApplicationsPage; import org.keycloak.testsuite.pages.OAuthGrantPage; import org.keycloak.testsuite.util.ClientManager; @@ -22,7 +24,7 @@ import java.util.List; import static org.keycloak.testsuite.util.WaitUtils.pause; public class OfflineTokenSpringBootTest extends AbstractSpringBootTest { - private static final String SERVLET_URI = APPLICATION_URL + "/admin/TokenServlet"; + private static final String SERVLET_URL = BASE_URL + "/TokenServlet"; @Rule public AssertEvents events = new AssertEvents(this); @@ -35,7 +37,7 @@ public class OfflineTokenSpringBootTest extends AbstractSpringBootTest { @Test public void testTokens() { - String servletUri = UriBuilder.fromUri(SERVLET_URI) + String servletUri = UriBuilder.fromUri(SERVLET_URL) .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS) .build().toString(); driver.navigate().to(servletUri); @@ -45,31 +47,31 @@ public class OfflineTokenSpringBootTest extends AbstractSpringBootTest { WaitUtils.waitUntilElement(By.tagName("body")).is().visible(); - Assert.assertTrue(tokenPage.isCurrent()); + Assert.assertTrue("Must be on tokens page", tokenPage.isCurrent()); - Assert.assertEquals(tokenPage.getRefreshToken().getType(), TokenUtil.TOKEN_TYPE_OFFLINE); - Assert.assertEquals(tokenPage.getRefreshToken().getExpiration(), 0); + Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, tokenPage.getRefreshToken().getType()); + Assert.assertEquals(0, tokenPage.getRefreshToken().getExpiration()); String accessTokenId = tokenPage.getAccessToken().getId(); String refreshTokenId = tokenPage.getRefreshToken().getId(); - setAdapterAndServerTimeOffset(9999, SERVLET_URI); + setAdapterAndServerTimeOffset(9999, SERVLET_URL); - driver.navigate().to(SERVLET_URI); + driver.navigate().to(SERVLET_URL); Assert.assertTrue("Must be on tokens page", tokenPage.isCurrent()); - Assert.assertNotEquals(tokenPage.getRefreshToken().getId(), refreshTokenId); - Assert.assertNotEquals(tokenPage.getAccessToken().getId(), accessTokenId); + Assert.assertNotEquals(refreshTokenId, tokenPage.getRefreshToken().getId()); + Assert.assertNotEquals(accessTokenId, tokenPage.getAccessToken().getId()); - setAdapterAndServerTimeOffset(0, SERVLET_URI); + setAdapterAndServerTimeOffset(0, SERVLET_URL); - driver.navigate().to(logoutPage(SERVLET_URI)); + driver.navigate().to(logoutPage(SERVLET_URL)); Assert.assertTrue("Must be on login page", loginPage.isCurrent()); } @Test public void testRevoke() { // Login to servlet first with offline token - String servletUri = UriBuilder.fromUri(SERVLET_URI) + String servletUri = UriBuilder.fromUri(SERVLET_URL) .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS) .build().toString(); driver.navigate().to(servletUri); @@ -81,10 +83,10 @@ public class OfflineTokenSpringBootTest extends AbstractSpringBootTest { Assert.assertEquals(tokenPage.getRefreshToken().getType(), TokenUtil.TOKEN_TYPE_OFFLINE); // Assert refresh works with increased time - setAdapterAndServerTimeOffset(9999, SERVLET_URI); - driver.navigate().to(SERVLET_URI); + setAdapterAndServerTimeOffset(9999, SERVLET_URL); + driver.navigate().to(SERVLET_URL); Assert.assertTrue("Must be on token page", tokenPage.isCurrent()); - setAdapterAndServerTimeOffset(0, SERVLET_URI); + setAdapterAndServerTimeOffset(0, SERVLET_URL); events.clear(); @@ -98,14 +100,18 @@ public class OfflineTokenSpringBootTest extends AbstractSpringBootTest { pause(500); Assert.assertEquals(accountAppPage.getApplications().get(CLIENT_ID).getAdditionalGrants().size(), 0); - events.expect(EventType.REVOKE_GRANT).realm(REALM_ID).user(getCorrectUserId()) + UserRepresentation userRepresentation = + ApiUtil.findUserByUsername(realmsResouce().realm(REALM_NAME), USER_LOGIN); + Assert.assertNotNull("User should exist", userRepresentation); + + events.expect(EventType.REVOKE_GRANT).realm(REALM_ID).user(userRepresentation.getId()) .client("account").detail(Details.REVOKED_CLIENT, CLIENT_ID).assertEvent(); // Assert refresh doesn't work now (increase time one more time) - setAdapterAndServerTimeOffset(9999, SERVLET_URI); - driver.navigate().to(SERVLET_URI); + setAdapterAndServerTimeOffset(9999, SERVLET_URL); + driver.navigate().to(SERVLET_URL); loginPage.assertCurrent(); - setAdapterAndServerTimeOffset(0, SERVLET_URI); + setAdapterAndServerTimeOffset(0, SERVLET_URL); } @Test @@ -113,17 +119,15 @@ public class OfflineTokenSpringBootTest extends AbstractSpringBootTest { ClientManager.realm(adminClient.realm(REALM_NAME)).clientId(CLIENT_ID).consentRequired(true); // Assert grant page doesn't have 'Offline Access' role when offline token is not requested - driver.navigate().to(SERVLET_URI); + driver.navigate().to(SERVLET_URL); loginPage.login(USER_LOGIN, USER_PASSWORD); oauthGrantPage.assertCurrent(); WaitUtils.waitUntilElement(By.xpath("//body")).text().not().contains("Offline access"); oauthGrantPage.cancel(); - // Assert grant page has 'Offline Access' role now - String servletUri = UriBuilder.fromUri(SERVLET_URI) + driver.navigate().to(UriBuilder.fromUri(SERVLET_URL) .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS) - .build().toString(); - driver.navigate().to(servletUri); + .build().toString()); WaitUtils.waitUntilElement(By.tagName("body")).is().visible(); loginPage.login(USER_LOGIN, USER_PASSWORD); @@ -143,7 +147,7 @@ public class OfflineTokenSpringBootTest extends AbstractSpringBootTest { Assert.assertTrue(offlineClient.getAdditionalGrants().contains("Offline Token")); //This was necessary to be introduced, otherwise other testcases will fail - driver.navigate().to(logoutPage(SERVLET_URI)); + driver.navigate().to(logoutPage(SERVLET_URL)); loginPage.assertCurrent(); events.clear(); diff --git a/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/SessionSpringBootTest.java b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/SessionSpringBootTest.java new file mode 100644 index 0000000000..a0a4982eb9 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/springboot-tests/src/test/java/org/keycloak/testsuite/springboot/SessionSpringBootTest.java @@ -0,0 +1,169 @@ +package org.keycloak.testsuite.springboot; + +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.jboss.arquillian.graphene.page.Page; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.auth.page.account.Sessions; +import org.keycloak.testsuite.util.SecondBrowser; +import org.keycloak.testsuite.util.WaitUtils; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +public class SessionSpringBootTest extends AbstractSpringBootTest { + + private static final String SERVLET_URL = BASE_URL + "/SessionServlet"; + + static final String USER_LOGIN_CORRECT_2 = "testcorrectuser2"; + static final String USER_EMAIL_CORRECT_2 = "usercorrect2@email.test"; + static final String USER_PASSWORD_CORRECT_2 = "testcorrectpassword2"; + + @Page + private SessionPage sessionPage; + + @Drone + @SecondBrowser + private WebDriver driver2; + + @Page + private Sessions realmSessions; + + @Override + public void setDefaultPageUriParameters() { + super.setDefaultPageUriParameters(); + realmSessions.setAuthRealm(REALM_NAME); + } + + private void loginAndCheckSession() { + driver.navigate().to(SERVLET_URL); + Assert.assertTrue("Must be on login page", loginPage.isCurrent()); + loginPage.login(USER_LOGIN, USER_PASSWORD); + WaitUtils.waitUntilElement(By.tagName("body")).is().visible(); + Assert.assertTrue("Must be on servlet page", sessionPage.isCurrent()); + Assert.assertEquals("Counter must be 0", 0, sessionPage.getCounter()); + + driver.navigate().to(SERVLET_URL); + Assert.assertEquals("Counter now must be 1", 1, sessionPage.getCounter()); + } + + private boolean checkCounterInSource(WebDriver driver, int counter) { + return driver.getPageSource().replaceAll("\\s", "") + .contains("" + counter + ""); + } + + @Before + public void addUserCorrect2() { + addUser(USER_LOGIN_CORRECT_2, USER_EMAIL_CORRECT_2, USER_PASSWORD_CORRECT_2, CORRECT_ROLE); + } + + @After + public void removeUserCorrect2() { + UserRepresentation userRep = ApiUtil.findUserByUsername(realmsResouce().realm(REALM_NAME), USER_LOGIN_CORRECT_2); + if (userRep != null) { + realmsResouce().realm(REALM_NAME).users().get(userRep.getId()).remove(); + } + } + + @Test + public void testSingleSessionInvalidated() { + + loginAndCheckSession(); + + // cannot pass to loginAndCheckSession becayse loginPage is not working together with driver2, therefore copypasta + driver2.navigate().to(SERVLET_URL); + log.info("current title is " + driver2.getTitle()); + Assert.assertTrue("Must be on login page", driver2.getTitle().toLowerCase().startsWith("log in to")); + driver2.findElement(By.id("username")).sendKeys(USER_LOGIN); + driver2.findElement(By.id("password")).sendKeys(USER_PASSWORD); + driver2.findElement(By.id("password")).submit(); + Assert.assertTrue("Must be on session page", driver2.getTitle().equals(SessionPage.PAGE_TITLE)); + Assert.assertTrue("Counter must be 0", checkCounterInSource(driver2, 0)); + // Counter increased now + driver2.navigate().to(SERVLET_URL); + Assert.assertTrue("Counter must be 1", checkCounterInSource(driver2, 1)); + + // Logout in browser1 + driver.navigate().to(logoutPage(SERVLET_URL)); + + // Assert that I am logged out in browser1 + driver.navigate().to(SERVLET_URL); + Assert.assertTrue("Must be on login page", loginPage.isCurrent()); + + // Assert that I am still logged in browser2 and same session is still preserved + driver2.navigate().to(SERVLET_URL); + Assert.assertTrue("Must be on session page", driver2.getTitle().equals(SessionPage.PAGE_TITLE)); + Assert.assertTrue("Counter must be 2", checkCounterInSource(driver2, 2)); + + driver2.navigate().to(logoutPage(SERVLET_URL)); + Assert.assertTrue("Must be on login page", driver2.getTitle().toLowerCase().startsWith("log in to")); + + } + + @Test + public void testSessionInvalidatedAfterFailedRefresh() { + RealmResource realmResource = adminClient.realm(REALM_NAME); + RealmRepresentation realmRep = realmResource.toRepresentation(); + ClientResource clientResource = null; + for (ClientRepresentation clientRep : realmResource.clients().findAll()) { + if (CLIENT_ID.equals(clientRep.getClientId())) { + clientResource = realmResource.clients().get(clientRep.getId()); + } + } + Assert.assertNotNull(clientResource); + clientResource.toRepresentation().setAdminUrl(""); + int origTokenLifespan = realmRep.getAccessCodeLifespan(); + realmRep.setAccessCodeLifespan(1); + realmResource.update(realmRep); + + // Login + loginAndCheckSession(); + + // Logout + String logoutUri = logoutPage(SERVLET_URL); + driver.navigate().to(logoutUri); + + // Assert that http session was invalidated + driver.navigate().to(SERVLET_URL); + Assert.assertTrue("Must be on login page", loginPage.isCurrent()); + loginPage.login(USER_LOGIN, USER_PASSWORD); + Assert.assertTrue("Must be on session page", sessionPage.isCurrent()); + Assert.assertEquals("Counter must be 0", 0, sessionPage.getCounter()); + + clientResource.toRepresentation().setAdminUrl(BASE_URL); + realmRep.setAccessCodeLifespan(origTokenLifespan); + realmResource.update(realmRep); + } + + @Test + public void testAdminApplicationLogout() { + loginAndCheckSession(); + + // logout user2 with admin client + UserRepresentation correct2 = realmsResouce().realm(REALM_NAME) + .users().search(USER_LOGIN_CORRECT_2, null, null, null, null, null).get(0); + realmsResouce().realm(REALM_NAME).users().get(correct2.getId()).logout(); + + // user1 should be still logged with original httpSession in our browser window + driver.navigate().to(SERVLET_URL); + Assert.assertTrue("Must be on session page", sessionPage.isCurrent()); + Assert.assertEquals("Counter must be 2", 2, sessionPage.getCounter()); + driver.navigate().to(logoutPage(SERVLET_URL)); + } + + @Test + public void testAccountManagementSessionsLogout() { + loginAndCheckSession(); + realmSessions.navigateTo(); + realmSessions.logoutAll(); + // Assert I need to login again (logout was propagated to the app) + loginAndCheckSession(); + } +}