keycloak-aplcache

[KEYCLOAK-3837] added session and account linking spring

10/19/2017 2:29:59 AM

Changes

Details

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 3b9ccc4..2857172 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<String, String> 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<String, String> 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 0000000..6c7d5bd
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/linking.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org/">
+<head>
+    <title>Linking page result</title>
+</head>
+<body>
+    <span id="error" th:text="${error}"/>
+</body>
+</html>
\ 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 0000000..9a7e52f
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/spring-boot-adapter/src/main/resources/templates/session.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html xmlns:th="http://www.thymeleaf.org/">
+<head>
+    <title>session counter page</title>
+</head>
+    <body>
+        <span id="counter" th:text="${counter}"></span>
+    </body>
+</html>
\ 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 0000000..620cc68
--- /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 0000000..f8c420c
--- /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 8ce5e75..30e2b52 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 5b15077..ad953b0 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<RealmRepresentation> 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);
-        if (userRep != null) {
-            providerRealm.users().get(userRep.getId()).remove();
-        }
-
-        RealmResource childRealm = adminClient.realm(REALM_NAME);
-        userRep = ApiUtil.findUserByUsername(childRealm, USER_LOGIN_2);
+        RealmResource realmResource = adminClient.realm(REALM_NAME);
+        UserRepresentation userRep = ApiUtil.findUserByUsername(realmResource, USER_LOGIN);
         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 0000000..64d62bd
--- /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<RealmRepresentation> 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<RoleRepresentation> 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<FederatedIdentityRepresentation> 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<RoleRepresentation> 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<FederatedIdentityRepresentation> 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<FederatedIdentityRepresentation> 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<String, String> 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<FederatedIdentityRepresentation> 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<FederatedIdentityRepresentation> 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 6aea719..7e812b0 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 5ac950f..9fdc0f7 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 0000000..a0a4982
--- /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("<spanid=\"counter\">" + counter + "</span>");
+    }
+
+    @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();
+    }
+}