keycloak-uncached

KEYCLOAK-5797 Refactoring authenticationSessions to support

11/28/2017 2:21:58 PM

Changes

Details

diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java
index 34e22e3..b29922b 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java
@@ -36,6 +36,8 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
 
     private String authSessionId;
 
+    private String clientUUID;
+
     private Map<String, String> authNotesFragment;
 
     /**
@@ -44,9 +46,10 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
      * @param authNotesFragment
      * @return Event. Note that {@code authNotesFragment} property is not thread safe which is fine for now.
      */
-    public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, Map<String, String> authNotesFragment) {
+    public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, String clientUUID, Map<String, String> authNotesFragment) {
         AuthenticationSessionAuthNoteUpdateEvent event = new AuthenticationSessionAuthNoteUpdateEvent();
         event.authSessionId = authSessionId;
+        event.clientUUID = clientUUID;
         event.authNotesFragment = new LinkedHashMap<>(authNotesFragment);
         return event;
     }
@@ -55,13 +58,18 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
         return authSessionId;
     }
 
+    public String getClientUUID() {
+        return clientUUID;
+    }
+
     public Map<String, String> getAuthNotesFragment() {
         return authNotesFragment;
     }
 
     @Override
     public String toString() {
-        return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, authNotesFragment=%s ]", authSessionId, authNotesFragment);
+        return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, clientUUID=%s, authNotesFragment=%s ]",
+                authSessionId, clientUUID, authNotesFragment);
     }
 
     public static class ExternalizerImpl implements Externalizer<AuthenticationSessionAuthNoteUpdateEvent> {
@@ -73,6 +81,7 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
             output.writeByte(VERSION_1);
 
             MarshallUtil.marshallString(value.authSessionId, output);
+            MarshallUtil.marshallString(value.clientUUID, output);
             MarshallUtil.marshallMap(value.authNotesFragment, output);
         }
 
@@ -89,6 +98,7 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
         public AuthenticationSessionAuthNoteUpdateEvent readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException {
             return create(
               MarshallUtil.unmarshallString(input),
+              MarshallUtil.unmarshallString(input),
               MarshallUtil.unmarshallMap(input, HashMap::new)
             );
         }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java
index 5736188..9ab5945 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java
@@ -23,14 +23,13 @@ import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
-import org.infinispan.Cache;
-import org.keycloak.common.util.Time;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
 import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
 
 /**
  * NOTE: Calling setter doesn't automatically enlist for update
@@ -39,39 +38,37 @@ import org.keycloak.sessions.AuthenticationSessionModel;
  */
 public class AuthenticationSessionAdapter implements AuthenticationSessionModel {
 
-    private KeycloakSession session;
-    private InfinispanAuthenticationSessionProvider provider;
-    private Cache<String, AuthenticationSessionEntity> cache;
-    private RealmModel realm;
+    private final KeycloakSession session;
+    private final RootAuthenticationSessionAdapter parent;
+    private final String clientUUID;
     private AuthenticationSessionEntity entity;
 
-    public AuthenticationSessionAdapter(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, Cache<String, AuthenticationSessionEntity> cache, RealmModel realm,
-                                        AuthenticationSessionEntity entity) {
+    public AuthenticationSessionAdapter(KeycloakSession session, RootAuthenticationSessionAdapter parent, String clientUUID, AuthenticationSessionEntity entity) {
         this.session = session;
-        this.provider = provider;
-        this.cache = cache;
-        this.realm = realm;
+        this.parent = parent;
+        this.clientUUID = clientUUID;
         this.entity = entity;
     }
 
-    void update() {
-        provider.tx.replace(cache, entity.getId(), entity);
+    private void update() {
+        parent.update();
     }
 
 
     @Override
-    public String getId() {
-        return entity.getId();
+    public RootAuthenticationSessionModel getParentSession() {
+        return parent;
     }
 
+
     @Override
     public RealmModel getRealm() {
-        return realm;
+        return parent.getRealm();
     }
 
     @Override
     public ClientModel getClient() {
-        return realm.getClientById(entity.getClientUuid());
+        return getRealm().getClientById(clientUUID);
     }
 
     @Override
@@ -85,16 +82,6 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel 
         update();
     }
 
-    @Override
-    public int getTimestamp() {
-        return entity.getTimestamp();
-    }
-
-    @Override
-    public void setTimestamp(int timestamp) {
-        entity.setTimestamp(timestamp);
-        update();
-    }
 
     @Override
     public String getAction() {
@@ -303,7 +290,7 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel 
 
     @Override
     public UserModel getAuthenticatedUser() {
-        return entity.getAuthUserId() == null ? null : session.users().getUserById(entity.getAuthUserId(), realm);    }
+        return entity.getAuthUserId() == null ? null : session.users().getUserById(entity.getAuthUserId(), getRealm());    }
 
     @Override
     public void setAuthenticatedUser(UserModel user) {
@@ -312,20 +299,4 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel 
         update();
     }
 
-    @Override
-    public void updateClient(ClientModel client) {
-        entity.setClientUuid(client.getId());
-        update();
-    }
-
-    @Override
-    public void restartSession(RealmModel realm, ClientModel client) {
-        String id = entity.getId();
-        entity = new AuthenticationSessionEntity();
-        entity.setId(id);
-        entity.setRealmId(realm.getId());
-        entity.setClientUuid(client.getId());
-        entity.setTimestamp(Time.currentTime());
-        update();
-    }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java
index 3d7d9af..710f17a 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java
@@ -17,53 +17,34 @@
 
 package org.keycloak.models.sessions.infinispan.entities;
 
-import java.util.HashMap;
-import java.util.HashSet;
+import java.io.Serializable;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
+import org.infinispan.util.concurrent.ConcurrentHashSet;
 import org.keycloak.sessions.AuthenticationSessionModel;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
-public class AuthenticationSessionEntity extends SessionEntity {
+public class AuthenticationSessionEntity implements Serializable {
 
-    private String id;
-
-    private String clientUuid;
     private String authUserId;
 
     private String redirectUri;
-    private int timestamp;
     private String action;
     private Set<String> roles;
     private Set<String> protocolMappers;
 
-    private Map<String, AuthenticationSessionModel.ExecutionStatus> executionStatus  = new HashMap<>();
+    private Map<String, AuthenticationSessionModel.ExecutionStatus> executionStatus = new ConcurrentHashMap<>();
     private String protocol;
 
     private Map<String, String> clientNotes;
     private Map<String, String> authNotes;
-    private Set<String> requiredActions  = new HashSet<>();
+    private Set<String> requiredActions  = new ConcurrentHashSet<>();
     private Map<String, String> userSessionNotes;
 
-    public String getId() {
-        return id;
-    }
-
-    public void setId(String id) {
-        this.id = id;
-    }
-
-    public String getClientUuid() {
-        return clientUuid;
-    }
-
-    public void setClientUuid(String clientUuid) {
-        this.clientUuid = clientUuid;
-    }
-
     public String getAuthUserId() {
         return authUserId;
     }
@@ -80,14 +61,6 @@ public class AuthenticationSessionEntity extends SessionEntity {
         this.redirectUri = redirectUri;
     }
 
-    public int getTimestamp() {
-        return timestamp;
-    }
-
-    public void setTimestamp(int timestamp) {
-        this.timestamp = timestamp;
-    }
-
     public String getAction() {
         return action;
     }
@@ -160,25 +133,4 @@ public class AuthenticationSessionEntity extends SessionEntity {
         this.authNotes = authNotes;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (!(o instanceof AuthenticationSessionEntity)) return false;
-
-        AuthenticationSessionEntity that = (AuthenticationSessionEntity) o;
-
-        if (id != null ? !id.equals(that.id) : that.id != null) return false;
-
-        return true;
-    }
-
-    @Override
-    public int hashCode() {
-        return id != null ? id.hashCode() : 0;
-    }
-
-    @Override
-    public String toString() {
-        return String.format("AuthenticationSessionEntity [id=%s, realm=%s, clientUuid=%s ]", getId(), getRealmId(), getClientUuid());
-    }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/RootAuthenticationSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/RootAuthenticationSessionEntity.java
new file mode 100644
index 0000000..751b6f0
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/RootAuthenticationSessionEntity.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.entities;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RootAuthenticationSessionEntity extends SessionEntity {
+
+    private String id;
+    private int timestamp;
+    private Map<String, AuthenticationSessionEntity> authenticationSessions = new ConcurrentHashMap<>();
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public int getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(int timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    public Map<String, AuthenticationSessionEntity> getAuthenticationSessions() {
+        return authenticationSessions;
+    }
+
+    public void setAuthenticationSessions(Map<String, AuthenticationSessionEntity> authenticationSessions) {
+        this.authenticationSessions = authenticationSessions;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof RootAuthenticationSessionEntity)) return false;
+
+        RootAuthenticationSessionEntity that = (RootAuthenticationSessionEntity) o;
+
+        if (id != null ? !id.equals(that.id) : that.id != null) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return id != null ? id.hashCode() : 0;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("RootAuthenticationSessionEntity [ id=%s, realm=%s ]", getId(), getRealmId());
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
index 5ae8416..47174f8 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
@@ -28,15 +28,14 @@ import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
-import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
-import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
+import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
 import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
 import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
-import org.keycloak.models.sessions.infinispan.stream.AuthenticationSessionPredicate;
+import org.keycloak.models.sessions.infinispan.stream.RootAuthenticationSessionPredicate;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.models.utils.RealmInfoUtil;
-import org.keycloak.sessions.AuthenticationSessionModel;
 import org.keycloak.sessions.AuthenticationSessionProvider;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -46,11 +45,11 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
     private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProvider.class);
 
     private final KeycloakSession session;
-    private final Cache<String, AuthenticationSessionEntity> cache;
+    private final Cache<String, RootAuthenticationSessionEntity> cache;
     protected final InfinispanKeycloakTransaction tx;
     protected final SessionEventsSenderTransaction clusterEventsSenderTx;
 
-    public InfinispanAuthenticationSessionProvider(KeycloakSession session, Cache<String, AuthenticationSessionEntity> cache) {
+    public InfinispanAuthenticationSessionProvider(KeycloakSession session, Cache<String, RootAuthenticationSessionEntity> cache) {
         this.session = session;
         this.cache = cache;
 
@@ -62,38 +61,33 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
     }
 
     @Override
-    public AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client) {
+    public RootAuthenticationSessionModel createRootAuthenticationSession(RealmModel realm) {
         String id = KeycloakModelUtils.generateId();
-        return createAuthenticationSession(id, realm, client);
+        return createRootAuthenticationSession(id, realm);
     }
 
+
     @Override
-    public AuthenticationSessionModel createAuthenticationSession(String id, RealmModel realm, ClientModel client) {
-        AuthenticationSessionEntity entity = new AuthenticationSessionEntity();
+    public RootAuthenticationSessionModel createRootAuthenticationSession(String id, RealmModel realm) {
+        RootAuthenticationSessionEntity entity = new RootAuthenticationSessionEntity();
         entity.setId(id);
         entity.setRealmId(realm.getId());
         entity.setTimestamp(Time.currentTime());
-        entity.setClientUuid(client.getId());
 
         tx.put(cache, id, entity);
 
-        AuthenticationSessionAdapter wrap = wrap(realm, entity);
-        return wrap;
+        return wrap(realm, entity);
     }
 
-    private AuthenticationSessionAdapter wrap(RealmModel realm, AuthenticationSessionEntity entity) {
-        return entity==null ? null : new AuthenticationSessionAdapter(session, this, cache, realm, entity);
-    }
 
-    @Override
-    public AuthenticationSessionModel getAuthenticationSession(RealmModel realm, String authenticationSessionId) {
-        AuthenticationSessionEntity entity = getAuthenticationSessionEntity(realm, authenticationSessionId);
-        return wrap(realm, entity);
+    private RootAuthenticationSessionAdapter wrap(RealmModel realm, RootAuthenticationSessionEntity entity) {
+        return entity==null ? null : new RootAuthenticationSessionAdapter(session, this, cache, realm, entity);
     }
 
-    private AuthenticationSessionEntity getAuthenticationSessionEntity(RealmModel realm, String authSessionId) {
+
+    private RootAuthenticationSessionEntity getRootAuthenticationSessionEntity(RealmModel realm, String authSessionId) {
         // Chance created in this transaction
-        AuthenticationSessionEntity entity = tx.get(cache, authSessionId);
+        RootAuthenticationSessionEntity entity = tx.get(cache, authSessionId);
 
         if (entity == null) {
             entity = cache.get(authSessionId);
@@ -102,10 +96,6 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
         return entity;
     }
 
-    @Override
-    public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authenticationSession) {
-        tx.remove(cache, authenticationSession.getId());
-    }
 
     @Override
     public void removeExpired(RealmModel realm) {
@@ -115,16 +105,16 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
 
 
         // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
-        Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
+        Iterator<Map.Entry<String, RootAuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
                 .entrySet()
                 .stream()
-                .filter(AuthenticationSessionPredicate.create(realm.getId()).expired(expired))
+                .filter(RootAuthenticationSessionPredicate.create(realm.getId()).expired(expired))
                 .iterator();
 
         int counter = 0;
         while (itr.hasNext()) {
             counter++;
-            AuthenticationSessionEntity entity = itr.next().getValue();
+            RootAuthenticationSessionEntity entity = itr.next().getValue();
             tx.remove(CacheDecorators.localCache(cache), entity.getId());
         }
 
@@ -141,10 +131,10 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
     }
 
     protected void onRealmRemovedEvent(String realmId) {
-        Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
+        Iterator<Map.Entry<String, RootAuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
                 .entrySet()
                 .stream()
-                .filter(AuthenticationSessionPredicate.create(realmId))
+                .filter(RootAuthenticationSessionPredicate.create(realmId))
                 .iterator();
 
         while (itr.hasNext()) {
@@ -156,28 +146,20 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
 
     @Override
     public void onClientRemoved(RealmModel realm, ClientModel client) {
-        // Send message to all DCs. The remoteCache will notify client listeners on all DCs for remove authentication sessions of this client
-        clusterEventsSenderTx.addEvent(
-                ClientRemovedSessionEvent.create(session, InfinispanAuthenticationSessionProviderFactory.CLIENT_REMOVED_AUTHSESSION_EVENT, realm.getId(), false, client.getId()),
-                ClusterProvider.DCNotify.ALL_DCS);
+        // No update anything on clientRemove for now. AuthenticationSessions of removed client will be handled at runtime if needed.
+
+//        clusterEventsSenderTx.addEvent(
+//                ClientRemovedSessionEvent.create(session, InfinispanAuthenticationSessionProviderFactory.CLIENT_REMOVED_AUTHSESSION_EVENT, realm.getId(), false, client.getId()),
+//                ClusterProvider.DCNotify.ALL_DCS);
     }
 
     protected void onClientRemovedEvent(String realmId, String clientUuid) {
-        Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
-                .entrySet()
-                .stream()
-                .filter(AuthenticationSessionPredicate.create(realmId).client(clientUuid))
-                .iterator();
 
-        while (itr.hasNext()) {
-            CacheDecorators.localCache(cache)
-                    .remove(itr.next().getKey());
-        }
     }
 
 
     @Override
-    public void updateNonlocalSessionAuthNotes(String authSessionId, Map<String, String> authNotesFragment) {
+    public void updateNonlocalSessionAuthNotes(String authSessionId, ClientModel client, Map<String, String> authNotesFragment) {
         if (authSessionId == null) {
             return;
         }
@@ -185,18 +167,31 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
         ClusterProvider cluster = session.getProvider(ClusterProvider.class);
         cluster.notify(
           InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS,
-          AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, authNotesFragment),
+          AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, client.getId(), authNotesFragment),
           true,
           ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC
         );
     }
 
+
+    @Override
+    public RootAuthenticationSessionModel getRootAuthenticationSession(RealmModel realm, String authenticationSessionId) {
+        RootAuthenticationSessionEntity entity = getRootAuthenticationSessionEntity(realm, authenticationSessionId);
+        return wrap(realm, entity);
+    }
+
+
+    @Override
+    public void removeRootAuthenticationSession(RealmModel realm, RootAuthenticationSessionModel authenticationSession) {
+        tx.remove(cache, authenticationSession.getId());
+    }
+
     @Override
     public void close() {
 
     }
 
-    public Cache<String, AuthenticationSessionEntity> getCache() {
+    public Cache<String, RootAuthenticationSessionEntity> getCache() {
         return cache;
     }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
index 04e1dc8..4055885 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
@@ -26,6 +26,7 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
 import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
 import org.keycloak.models.sessions.infinispan.events.AbstractAuthSessionClusterListener;
 import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
 import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
@@ -46,7 +47,7 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
 
     private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProviderFactory.class);
 
-    private volatile Cache<String, AuthenticationSessionEntity> authSessionsCache;
+    private volatile Cache<String, RootAuthenticationSessionEntity> authSessionsCache;
 
     public static final String PROVIDER_ID = "infinispan";
 
@@ -113,11 +114,18 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
         }
 
         AuthenticationSessionAuthNoteUpdateEvent event = (AuthenticationSessionAuthNoteUpdateEvent) clEvent;
-        AuthenticationSessionEntity authSession = this.authSessionsCache.get(event.getAuthSessionId());
-        updateAuthSession(authSession, event.getAuthNotesFragment());
+        RootAuthenticationSessionEntity authSession = this.authSessionsCache.get(event.getAuthSessionId());
+        updateAuthSession(authSession, event.getClientUUID(), event.getAuthNotesFragment());
     }
 
-    private static void updateAuthSession(AuthenticationSessionEntity authSession, Map<String, String> authNotesFragment) {
+
+    private static void updateAuthSession(RootAuthenticationSessionEntity rootAuthSession, String clientUUID, Map<String, String> authNotesFragment) {
+        if (rootAuthSession == null) {
+            return;
+        }
+
+        AuthenticationSessionEntity authSession = rootAuthSession.getAuthenticationSessions().get(clientUUID);
+
         if (authSession != null) {
             if (authSession.getAuthNotes() == null) {
                 authSession.setAuthNotes(new ConcurrentHashMap<>());
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/RootAuthenticationSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/RootAuthenticationSessionAdapter.java
new file mode 100644
index 0000000..e7cd727
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/RootAuthenticationSessionAdapter.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.infinispan.Cache;
+import org.keycloak.common.util.Time;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RootAuthenticationSessionAdapter implements RootAuthenticationSessionModel {
+
+    private KeycloakSession session;
+    private InfinispanAuthenticationSessionProvider provider;
+    private Cache<String, RootAuthenticationSessionEntity> cache;
+    private RealmModel realm;
+    private RootAuthenticationSessionEntity entity;
+
+    public RootAuthenticationSessionAdapter(KeycloakSession session, InfinispanAuthenticationSessionProvider provider,
+                                            Cache<String, RootAuthenticationSessionEntity> cache, RealmModel realm,
+                                            RootAuthenticationSessionEntity entity) {
+        this.session = session;
+        this.provider = provider;
+        this.cache = cache;
+        this.realm = realm;
+        this.entity = entity;
+    }
+
+    void update() {
+        provider.tx.replace(cache, entity.getId(), entity);
+    }
+
+
+    @Override
+    public String getId() {
+        return entity.getId();
+    }
+
+    @Override
+    public RealmModel getRealm() {
+        return realm;
+    }
+
+    @Override
+    public int getTimestamp() {
+        return entity.getTimestamp();
+    }
+
+    @Override
+    public void setTimestamp(int timestamp) {
+        entity.setTimestamp(timestamp);
+        update();
+    }
+
+    @Override
+    public Map<String, AuthenticationSessionModel> getAuthenticationSessions() {
+        Map<String, AuthenticationSessionModel> result = new HashMap<>();
+
+        for (Map.Entry<String, AuthenticationSessionEntity> entry : entity.getAuthenticationSessions().entrySet()) {
+            String clientUUID = entry.getKey();
+            result.put(clientUUID , new AuthenticationSessionAdapter(session, this, clientUUID, entry.getValue()));
+        }
+
+        return result;
+    }
+
+    @Override
+    public AuthenticationSessionModel getAuthenticationSession(ClientModel client) {
+        return client==null ? null : getAuthenticationSessions().get(client.getId());
+    }
+
+    @Override
+    public AuthenticationSessionModel createAuthenticationSession(ClientModel client) {
+        AuthenticationSessionEntity authSessionEntity = new AuthenticationSessionEntity();
+        entity.getAuthenticationSessions().put(client.getId(), authSessionEntity);
+        update();
+
+        return new AuthenticationSessionAdapter(session, this, client.getId(), authSessionEntity);
+    }
+
+    @Override
+    public void restartSession(RealmModel realm) {
+        entity.getAuthenticationSessions().clear();
+        entity.setTimestamp(Time.currentTime());
+        update();
+    }
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java
index c54533c..55628cb 100644
--- a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java
@@ -27,6 +27,11 @@ import org.keycloak.sessions.CommonClientSessionModel;
  */
 public interface AuthenticatedClientSessionModel extends CommonClientSessionModel {
 
+    String getId();
+
+    int getTimestamp();
+    void setTimestamp(int timestamp);
+
     /**
      * Detaches the client session from its user session.
      */
diff --git a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java
index 8e84f1a..56b83ac 100644
--- a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java
@@ -21,7 +21,6 @@ import java.util.Map;
 import java.util.Set;
 
 import org.keycloak.models.ClientModel;
-import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 
 /**
@@ -31,10 +30,8 @@ import org.keycloak.models.UserModel;
  */
 public interface AuthenticationSessionModel extends CommonClientSessionModel {
 
-//
-//    public UserSessionModel getUserSession();
-//    public void setUserSession(UserSessionModel userSession);
 
+    RootAuthenticationSessionModel getParentSession();
 
     Map<String, ExecutionStatus> getExecutionStatus();
     void setExecutionStatus(String authenticator, ExecutionStatus status);
@@ -125,8 +122,4 @@ public interface AuthenticationSessionModel extends CommonClientSessionModel {
      */
     void clearClientNotes();
 
-    void updateClient(ClientModel client);
-
-    // Will completely restart whole state of authentication session. It will just keep same ID. It will setup it with provided realm and client.
-    void restartSession(RealmModel realm, ClientModel client);
 }
diff --git a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
index 8cc4035..9a01446 100644
--- a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
+++ b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
@@ -31,26 +31,26 @@ public interface AuthenticationSessionProvider extends Provider {
      * Creates and registers a new authentication session with random ID. Authentication session
      * entity will be prefilled with current timestamp, the given realm and client.
      */
-    AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client);
+    RootAuthenticationSessionModel createRootAuthenticationSession(RealmModel realm);
 
-    AuthenticationSessionModel createAuthenticationSession(String id, RealmModel realm, ClientModel client);
+    RootAuthenticationSessionModel createRootAuthenticationSession(String id, RealmModel realm);
 
-    AuthenticationSessionModel getAuthenticationSession(RealmModel realm, String authenticationSessionId);
+    RootAuthenticationSessionModel getRootAuthenticationSession(RealmModel realm, String authenticationSessionId);
 
-    void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authenticationSession);
+    void removeRootAuthenticationSession(RealmModel realm, RootAuthenticationSessionModel authenticationSession);
 
     void removeExpired(RealmModel realm);
     void onRealmRemoved(RealmModel realm);
     void onClientRemoved(RealmModel realm, ClientModel client);
 
     /**
-     * Requests update of authNotes of an authentication session that is not owned
+     * Requests update of authNotes of a root authentication session that is not owned
      * by this instance but might exist somewhere in the cluster.
      * 
      * @param authSessionId
      * @param authNotesFragment Map with authNote values. Auth note is removed if the corresponding value in the map is {@code null}.
      */
-    void updateNonlocalSessionAuthNotes(String authSessionId, Map<String, String> authNotesFragment);
+    void updateNonlocalSessionAuthNotes(String authSessionId, ClientModel client, Map<String, String> authNotesFragment);
 
 
 }
diff --git a/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java
index a87309c..b59ffa1 100644
--- a/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java
@@ -33,13 +33,9 @@ public interface CommonClientSessionModel {
     public String getRedirectUri();
     public void setRedirectUri(String uri);
 
-    public String getId();
     public RealmModel getRealm();
     public ClientModel getClient();
 
-    public int getTimestamp();
-    public void setTimestamp(int timestamp);
-
     public String getAction();
     public void setAction(String action);
 
diff --git a/server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java
new file mode 100644
index 0000000..fb656cf
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.sessions;
+
+import java.util.Map;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+
+/**
+ * Represents usually one browser session with potentially many browser tabs. Every browser tab is represented by {@link AuthenticationSessionModel}
+ * of different client.
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface RootAuthenticationSessionModel {
+
+    String getId();
+    RealmModel getRealm();
+
+    int getTimestamp();
+    void setTimestamp(int timestamp);
+
+
+    /**
+     * Key is client UUID, Value is AuthenticationSessionModel for particular client
+     * @return authentication sessions or empty map if no authenticationSessions presents. Never return null.
+     */
+    Map<String, AuthenticationSessionModel> getAuthenticationSessions();
+
+
+    /**
+     * @return authentication session for particular client or null if it doesn't yet exists.
+     */
+    AuthenticationSessionModel getAuthenticationSession(ClientModel client);
+
+
+    /**
+     * Create new authentication session and returns it. Overwrites existing session for particular client if already exists.
+     *
+     * @param client
+     * @return non-null fresh authentication session
+     */
+    AuthenticationSessionModel createAuthenticationSession(ClientModel client);
+
+
+    /**
+     * Will completely restart whole state of authentication session. It will just keep same ID. It will setup it with provided realm.
+     */
+    void restartSession(RealmModel realm);
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
index 1550a8d..29ce6c4 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
@@ -30,6 +30,7 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilderException;
 import javax.ws.rs.core.UriInfo;
 import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
 
 /**
  *
@@ -111,7 +112,9 @@ public class ActionTokenContext<T extends JsonWebToken> {
         // set up the account service as the endpoint to call.
         ClientModel client = realm.getClientByClientId(clientId == null ? Constants.ACCOUNT_MANAGEMENT_CLIENT_ID : clientId);
         
-        authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true);
+        RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, true);
+        authSession = rootAuthSession.createAuthenticationSession(client);
+
         authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
         authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
         String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
index 43e4f90..f21cf68 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
@@ -75,8 +75,8 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander<
         final KeycloakSession session = tokenContext.getSession();
         if (tokenContext.isAuthenticationSessionFresh()) {
             // Update the authentication session in the token
-            token.setAuthenticationSessionId(authSession.getId());
-            UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+            token.setAuthenticationSessionId(authSession.getParentSession().getId());
+            UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
             String confirmUri = builder.build(realm.getName()).toString();
 
             return session.getProvider(LoginFormsProvider.class)
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
index 39c6f9a..64c61dd 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
@@ -31,6 +31,7 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
     private static final String JSON_FIELD_IDENTITY_PROVIDER_USERNAME = "idpu";
     private static final String JSON_FIELD_IDENTITY_PROVIDER_ALIAS = "idpa";
     private static final String JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID = "oasid";
+    private static final String JSON_FIELD_ORIGINAL_CLIENT_UUID = "ocid";
 
     @JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_USERNAME)
     private String identityProviderUsername;
@@ -41,9 +42,13 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
     @JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID)
     private String originalAuthenticationSessionId;
 
-    public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId,
+    @JsonProperty(value = JSON_FIELD_ORIGINAL_CLIENT_UUID)
+    private String originalClientUUID;
+
+    public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, String clientUUID,
       String identityProviderUsername, String identityProviderAlias) {
         super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
+        this.originalClientUUID = clientUUID;
         this.identityProviderUsername = identityProviderUsername;
         this.identityProviderAlias = identityProviderAlias;
     }
@@ -74,4 +79,12 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
     public void setOriginalAuthenticationSessionId(String originalAuthenticationSessionId) {
         this.originalAuthenticationSessionId = originalAuthenticationSessionId;
     }
+
+    public String getOriginalClientUUID() {
+        return originalClientUUID;
+    }
+
+    public void setOriginalClientUUID(String originalClientUUID) {
+        this.originalClientUUID = originalClientUUID;
+    }
 }
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
index 7f9d58c..514ca1a 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
@@ -23,6 +23,7 @@ import org.keycloak.authentication.actiontoken.*;
 import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticator;
 import org.keycloak.events.*;
 import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
@@ -31,7 +32,7 @@ import org.keycloak.services.Urls;
 import org.keycloak.services.managers.AuthenticationSessionManager;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.sessions.AuthenticationSessionModel;
-import org.keycloak.sessions.AuthenticationSessionProvider;
+
 import java.util.Collections;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
@@ -76,8 +77,8 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
         AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
         if (tokenContext.isAuthenticationSessionFresh()) {
             token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId());
-            token.setAuthenticationSessionId(authSession.getId());
-            UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+            token.setAuthenticationSessionId(authSession.getParentSession().getId());
+            UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
             String confirmUri = builder.build(realm.getName()).toString();
 
             return session.getProvider(LoginFormsProvider.class)
@@ -94,14 +95,16 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
             AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
             asm.removeAuthenticationSession(realm, authSession, true);
 
-            AuthenticationSessionProvider authSessProvider = session.authenticationSessions();
-            authSession = authSessProvider.getAuthenticationSession(realm, token.getOriginalAuthenticationSessionId());
+            ClientModel originalClient = realm.getClientById(token.getOriginalClientUUID());
+            authSession = asm.getAuthenticationSessionByIdAndClient(realm, token.getOriginalAuthenticationSessionId(), originalClient);
 
             if (authSession != null) {
                 authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
             } else {
-                authSessProvider.updateNonlocalSessionAuthNotes(
+
+                session.authenticationSessions().updateNonlocalSessionAuthNotes(
                   token.getAuthenticationSessionId(),
+                  originalClient,
                   Collections.singletonMap(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername())
                 );
             }
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java
index 98ad8b4..91ceb2a 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java
@@ -77,8 +77,8 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
         if (tokenContext.isAuthenticationSessionFresh()) {
             // Update the authentication session in the token
             token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId());
-            token.setAuthenticationSessionId(authSession.getId());
-            UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+            token.setAuthenticationSessionId(authSession.getParentSession().getId());
+            UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
             String confirmUri = builder.build(realm.getName()).toString();
 
             return session.getProvider(LoginFormsProvider.class)
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index d033143..4c0db68 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -56,6 +56,7 @@ import org.keycloak.services.util.CacheControlUtil;
 import org.keycloak.services.util.AuthenticationFlowURLHelper;
 import org.keycloak.sessions.AuthenticationSessionModel;
 import org.keycloak.sessions.CommonClientSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
 
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
@@ -222,7 +223,7 @@ public class AuthenticationProcessor {
 
     public String generateCode() {
         ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getAuthenticationSession());
-        authenticationSession.setTimestamp(Time.currentTime());
+        authenticationSession.getParentSession().setTimestamp(Time.currentTime());
         return accessCode.getOrGenerateCode();
     }
 
@@ -632,7 +633,10 @@ public class AuthenticationProcessor {
 
             } else if (e.getError() == AuthenticationFlowError.FORK_FLOW) {
                 ForkFlowException reset = (ForkFlowException)e;
-                AuthenticationSessionModel clone = clone(session, authenticationSession);
+
+                RootAuthenticationSessionModel rootClone = clone(session, authenticationSession.getClient(), authenticationSession.getParentSession());
+                AuthenticationSessionModel clone = rootClone.getAuthenticationSession(authenticationSession.getClient());
+
                 clone.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
                 setAuthenticationSession(clone);
 
@@ -748,7 +752,7 @@ public class AuthenticationProcessor {
 
     public static void resetFlow(AuthenticationSessionModel authSession, String flowPath) {
         logger.debug("RESET FLOW");
-        authSession.setTimestamp(Time.currentTime());
+        authSession.getParentSession().setTimestamp(Time.currentTime());
         authSession.setAuthenticatedUser(null);
         authSession.clearExecutionStatus();
         authSession.clearUserSessionNotes();
@@ -759,20 +763,26 @@ public class AuthenticationProcessor {
         authSession.setAuthNote(CURRENT_FLOW_PATH, flowPath);
     }
 
-    public static AuthenticationSessionModel clone(KeycloakSession session, AuthenticationSessionModel authSession) {
-        AuthenticationSessionModel clone = new AuthenticationSessionManager(session).createAuthenticationSession(authSession.getRealm(), authSession.getClient(), true);
+    public static RootAuthenticationSessionModel clone(KeycloakSession session, ClientModel client, RootAuthenticationSessionModel authSession) {
+        RootAuthenticationSessionModel clone = new AuthenticationSessionManager(session).createAuthenticationSession(authSession.getRealm(), true);
 
         // Transfer just the client "notes", but not "authNotes"
-        for (Map.Entry<String, String> entry : authSession.getClientNotes().entrySet()) {
-            clone.setClientNote(entry.getKey(), entry.getValue());
+        for (Map.Entry<String, AuthenticationSessionModel> entry : authSession.getAuthenticationSessions().entrySet()) {
+            AuthenticationSessionModel asmOrig = entry.getValue();
+            AuthenticationSessionModel asmClone = clone.createAuthenticationSession(asmOrig.getClient());
+
+            asmClone.setRedirectUri(asmOrig.getRedirectUri());
+            asmClone.setProtocol(asmOrig.getProtocol());
+
+            for (Map.Entry<String, String> clientNote : asmOrig.getClientNotes().entrySet()) {
+                asmClone.setClientNote(clientNote.getKey(), clientNote.getValue());
+            }
         }
 
-        clone.setRedirectUri(authSession.getRedirectUri());
-        clone.setProtocol(authSession.getProtocol());
         clone.setTimestamp(Time.currentTime());
 
-        clone.setAuthNote(FORKED_FROM, authSession.getId());
-        logger.debugf("Forked authSession %s from authSession %s", clone.getId(), authSession.getId());
+        clone.getAuthenticationSession(client).setAuthNote(FORKED_FROM, authSession.getId());
+        logger.debugf("Forked authSession %s from authSession %s . Client: '%s'", clone.getId(), authSession.getId(), client.getClientId());
 
         return clone;
 
@@ -825,7 +835,8 @@ public class AuthenticationProcessor {
         if (!code.isActionActive(ClientSessionCode.ActionType.LOGIN)) {
             throw new AuthenticationFlowException(AuthenticationFlowError.EXPIRED_CODE);
         }
-        authenticationSession.setTimestamp(Time.currentTime());
+
+        authenticationSession.getParentSession().setTimestamp(Time.currentTime());
     }
 
     public Response authenticateOnly() throws AuthenticationFlowException {
@@ -872,9 +883,9 @@ public class AuthenticationProcessor {
 
         if (userSession == null) { // if no authenticator attached a usersession
 
-            userSession = session.sessions().getUserSession(realm, authSession.getId());
+            userSession = session.sessions().getUserSession(realm, authSession.getParentSession().getId());
             if (userSession == null) {
-                userSession = session.sessions().createUserSession(authSession.getId(), realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol()
+                userSession = session.sessions().createUserSession(authSession.getParentSession().getId(), realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol()
                         , remember, brokerSessionId, brokerUserId);
             } else if (userSession.getUser() == null || !AuthenticationManager.isSessionValid(realm, userSession)) {
                 userSession.restartSession(realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol()
@@ -936,7 +947,7 @@ public class AuthenticationProcessor {
         if (nextRequiredAction != null) {
             return AuthenticationManager.redirectToRequiredActions(session, realm, authenticationSession, uriInfo, nextRequiredAction);
         } else {
-            event.detail(Details.CODE_ID, authenticationSession.getId());  // todo This should be set elsewhere.  find out why tests fail.  Don't know where this is supposed to be set
+            event.detail(Details.CODE_ID, authenticationSession.getParentSession().getId());  // todo This should be set elsewhere.  find out why tests fail.  Don't know where this is supposed to be set
             // the user has successfully logged in and we can clear his/her previous login failure attempts.
             logSuccess();
             return AuthenticationManager.finishedRequiredActions(session, authenticationSession, userSession, connection, request, uriInfo, event);
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
index 35854d0..a0bf4d3 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
@@ -123,18 +123,17 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator 
                 .user(existingUser)
                 .detail(Details.USERNAME, existingUser.getUsername())
                 .detail(Details.EMAIL, existingUser.getEmail())
-                .detail(Details.CODE_ID, authSession.getId())
+                .detail(Details.CODE_ID, authSession.getParentSession().getId())
                 .removeDetail(Details.AUTH_METHOD)
                 .removeDetail(Details.AUTH_TYPE);
 
         IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
-          existingUser.getId(), absoluteExpirationInSecs, authSession.getId(),
+          existingUser.getId(), absoluteExpirationInSecs, authSession.getParentSession().getId(), authSession.getClient().getId(),
           brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias()
         );
-        UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+        UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
         String link = builder
                 .queryParam(Constants.EXECUTION, context.getExecution().getId())
-                .queryParam(Constants.CLIENT_ID, context.getExecution().getId())
                 .build(realm.getName()).toString();
         long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
 
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java
index d48d3b8..5c87908 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java
@@ -89,7 +89,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
         int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
 
         // We send the secret in the email in a link as a query param.
-        ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, authenticationSession.getId());
+        ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, authenticationSession.getParentSession().getId());
         String link = UriBuilder
           .fromUri(context.getActionTokenUrl(token.serialize(context.getSession(), context.getRealm(), context.getUriInfo())))
           .build()
@@ -101,7 +101,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
             event.clone().event(EventType.SEND_RESET_PASSWORD)
                          .user(user)
                          .detail(Details.USERNAME, username)
-                         .detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, authenticationSession.getId()).success();
+                         .detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, authenticationSession.getParentSession().getId()).success();
             context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT));
         } catch (EmailException e) {
             event.clone().event(EventType.SEND_RESET_PASSWORD)
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
index e87652a..4b001df 100755
--- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
@@ -149,7 +149,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
     @Override
     public String generateCode() {
         ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, getRealm(), getAuthenticationSession());
-        authenticationSession.setTimestamp(Time.currentTime());
+        authenticationSession.getParentSession().setTimestamp(Time.currentTime());
         return accessCode.getOrGenerateCode();
     }
 
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
index 4208308..e061a3d 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
@@ -134,8 +134,8 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
         int validityInSecs = realm.getActionTokenGeneratedByUserLifespan(VerifyEmailActionToken.TOKEN_TYPE);
         int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
 
-        VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSession.getId(), user.getEmail());
-        UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+        VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSession.getParentSession().getId(), user.getEmail());
+        UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
         String link = builder.build(realm.getName()).toString();
         long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
 
diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
index e3903a8..e74bc11 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
@@ -236,7 +236,8 @@ public class PolicyEvaluationService {
                     ClientModel clientModel = realm.getClientById(clientId);
                     String id = KeycloakModelUtils.generateId();
 
-                    AuthenticationSessionModel authSession = keycloakSession.authenticationSessions().createAuthenticationSession(id, realm, clientModel);
+                    AuthenticationSessionModel authSession = keycloakSession.authenticationSessions().createRootAuthenticationSession(id, realm)
+                            .createAuthenticationSession(clientModel);
                     authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
                     authSession.setAuthenticatedUser(userModel);
                     userSession = keycloakSession.sessions().createUserSession(id, realm, userModel, userModel.getUsername(), "127.0.0.1", "passwd", false, null, null);
diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
index abae0b8..edeb68d 100755
--- a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
+++ b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
@@ -40,6 +40,7 @@ import org.keycloak.services.resources.LoginActionsService;
 import org.keycloak.services.util.CacheControlUtil;
 import org.keycloak.services.util.AuthenticationFlowURLHelper;
 import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
 
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.HttpHeaders;
@@ -108,7 +109,7 @@ public abstract class AuthorizationEndpointBase {
         AuthenticationFlowModel flow = getAuthenticationFlow();
         String flowId = flow.getId();
         AuthenticationProcessor processor = createProcessor(authSession, flowId, LoginActionsService.AUTHENTICATE_PATH);
-        event.detail(Details.CODE_ID, authSession.getId());
+        event.detail(Details.CODE_ID, authSession.getParentSession().getId());
         if (isPassive) {
             // OIDC prompt == NONE or SAML 2 IsPassive flag
             // This means that client is just checking if the user is already completely logged in.
@@ -168,45 +169,61 @@ public abstract class AuthorizationEndpointBase {
     protected AuthorizationEndpointChecks getOrCreateAuthenticationSession(ClientModel client, String requestState) {
         AuthenticationSessionManager manager = new AuthenticationSessionManager(session);
         String authSessionId = manager.getCurrentAuthenticationSessionId(realm);
-        AuthenticationSessionModel authSession = authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
+        RootAuthenticationSessionModel rootAuthSession = authSessionId==null ? null : session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
+        AuthenticationSessionModel authSession;
 
-        if (authSession != null) {
+        if (rootAuthSession != null) {
 
-            ClientSessionCode<AuthenticationSessionModel> check = new ClientSessionCode<>(session, realm, authSession);
-            if (!check.isActionActive(ClientSessionCode.ActionType.LOGIN)) {
+            authSession = rootAuthSession.getAuthenticationSession(client);
 
-                logger.debugf("Authentication session '%s' exists, but is expired. Restart existing authentication session", authSession.getId());
-                authSession.restartSession(realm, client);
-                return new AuthorizationEndpointChecks(authSession);
+            if (authSession != null) {
+                ClientSessionCode<AuthenticationSessionModel> check = new ClientSessionCode<>(session, realm, authSession);
+                if (!check.isActionActive(ClientSessionCode.ActionType.LOGIN)) {
 
-            } else if (isNewRequest(authSession, client, requestState)) {
-                // Check if we have lastProcessedExecution note or if some request parameter beside state (eg. prompt, kc_idp_hint) changed. Restart the session just if yes.
-                // Otherwise update just client information from the AuthorizationEndpoint request.
-                // This difference is needed, because of logout from JS applications in multiple browser tabs.
-                if (shouldRestartAuthSession(authSession)) {
-                    logger.debug("New request from application received, but authentication session already exists. Restart existing authentication session");
-                    authSession.restartSession(realm, client);
-                } else {
-                    logger.debug("New request from application received, but authentication session already exists. Update client information in existing authentication session");
-                    authSession.clearClientNotes(); // update client data
-                    authSession.updateClient(client);
-                }
+                    logger.debugf("Authentication session '%s' exists, but is expired. Restart existing authentication session", rootAuthSession.getId());
+                    rootAuthSession.restartSession(realm);
+                    authSession = rootAuthSession.createAuthenticationSession(client);
 
-                return new AuthorizationEndpointChecks(authSession);
+                    return new AuthorizationEndpointChecks(authSession);
 
-            } else {
-                logger.debug("Re-sent some previous request to Authorization endpoint. Likely browser 'back' or 'refresh' button.");
+                } else if (isNewRequest(authSession, client, requestState)) {
+                    // Check if we have lastProcessedExecution note or if some request parameter beside state (eg. prompt, kc_idp_hint) changed. Restart the session just if yes.
+                    // Otherwise update just client information from the AuthorizationEndpoint request.
+                    // This difference is needed, because of logout from JS applications in multiple browser tabs.
+                    if (shouldRestartAuthSession(authSession)) {
+                        logger.debugf("New request from application received, but authentication session '%s' already exists and has client '%s'. Restart child authentication session for client.",
+                                rootAuthSession.getId(), client.getClientId());
+
+                        authSession = rootAuthSession.createAuthenticationSession(client);
+
+                    } else {
+                        logger.debugf("New request from application received, but authentication session '%s' already exists and has client '%s'. Update client information in existing authentication session.",
+                                rootAuthSession.getId(), client.getClientId());
+                        authSession.clearClientNotes();
+                    }
 
-                // See if we have lastProcessedExecution note. If yes, we are expired. Also if we are in different flow than initial one. Otherwise it is browser refresh of initial username/password form
-                if (!shouldShowExpirePage(authSession)) {
                     return new AuthorizationEndpointChecks(authSession);
-                } else {
-                    CacheControlUtil.noBackButtonCacheControlHeader();
 
-                    Response response = new AuthenticationFlowURLHelper(session, realm, uriInfo)
-                            .showPageExpired(authSession);
-                    return new AuthorizationEndpointChecks(response);
+                } else {
+                    logger.debug("Re-sent some previous request to Authorization endpoint. Likely browser 'back' or 'refresh' button.");
+
+                    // See if we have lastProcessedExecution note. If yes, we are expired. Also if we are in different flow than initial one. Otherwise it is browser refresh of initial username/password form
+                    if (!shouldShowExpirePage(authSession)) {
+                        return new AuthorizationEndpointChecks(authSession);
+                    } else {
+                        CacheControlUtil.noBackButtonCacheControlHeader();
+
+                        Response response = new AuthenticationFlowURLHelper(session, realm, uriInfo)
+                                .showPageExpired(authSession);
+                        return new AuthorizationEndpointChecks(response);
+                    }
                 }
+            } else {
+                logger.debugf("Sent request to authz endpoint. Authentication session with ID '%s' exists, but doesn't have client: '%s' . Adding client to authentication session",
+                        rootAuthSession.getId(), client.getClientId());
+
+                authSession = rootAuthSession.createAuthenticationSession(client);
+                return new AuthorizationEndpointChecks(authSession);
             }
         }
 
@@ -214,10 +231,12 @@ public abstract class AuthorizationEndpointBase {
 
         if (userSession != null) {
             logger.debugf("Sent request to authz endpoint. We don't have authentication session with ID '%s' but we have userSession. Will re-create authentication session with same ID", authSessionId);
-            authSession = session.authenticationSessions().createAuthenticationSession(authSessionId, realm, client);
+            rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(authSessionId, realm);
+            authSession = rootAuthSession.createAuthenticationSession(client);
         } else {
-            authSession = manager.createAuthenticationSession(realm, client, true);
-            logger.debugf("Sent request to authz endpoint. Created new authentication session with ID '%s'", authSession.getId());
+            rootAuthSession = manager.createAuthenticationSession(realm, true);
+            authSession = rootAuthSession.createAuthenticationSession(client);
+            logger.debugf("Sent request to authz endpoint. Created new authentication session with ID '%s'", rootAuthSession.getId());
         }
 
         return new AuthorizationEndpointChecks(authSession);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 94c4d24..0e8d36e 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -80,6 +80,7 @@ import org.keycloak.services.resources.admin.AdminAuth;
 import org.keycloak.services.resources.admin.permissions.AdminPermissions;
 import org.keycloak.services.validation.Validation;
 import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
 import org.keycloak.util.TokenUtil;
 import org.keycloak.utils.ProfileHelper;
 
@@ -255,7 +256,7 @@ public class TokenEndpoint {
             throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST);
         }
 
-        ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, session, realm, event, AuthenticatedClientSessionModel.class);
+        ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, session, realm, client, event, AuthenticatedClientSessionModel.class);
         if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) {
             AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
 
@@ -469,7 +470,9 @@ public class TokenEndpoint {
         }
         String scope = formParams.getFirst(OAuth2Constants.SCOPE);
 
-        AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, false);
+        RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false);
+        AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client);
+
         authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
         authSession.setAction(AuthenticatedClientSessionModel.Action.AUTHENTICATE.name());
         authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
@@ -553,13 +556,16 @@ public class TokenEndpoint {
 
         String scope = formParams.getFirst(OAuth2Constants.SCOPE);
 
-        AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, false);
+        RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false);
+        AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client);
+
         authSession.setAuthenticatedUser(clientUser);
         authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
         authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
         authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
 
-        UserSessionModel userSession = session.sessions().createUserSession(authSession.getId(), realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
+        UserSessionModel userSession = session.sessions().createUserSession(authSession.getParentSession().getId(), realm, clientUser, clientUsername,
+                clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
         event.session(userSession);
 
         AuthenticationManager.setRolesAndMappersInSession(authSession);
@@ -763,7 +769,9 @@ public class TokenEndpoint {
 
         String scope = formParams.getFirst(OAuth2Constants.SCOPE);
 
-        AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, targetClient, false);
+        RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false);
+        AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(targetClient);
+
         authSession.setAuthenticatedUser(targetUser);
         authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
         authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
diff --git a/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java b/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java
index b4e037b..59cd0b9 100644
--- a/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java
+++ b/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java
@@ -31,6 +31,7 @@ import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.AuthenticationSessionManager;
 import org.keycloak.services.util.CookieHelper;
 import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
 
 import javax.crypto.SecretKey;
 import javax.ws.rs.core.Cookie;
@@ -143,7 +144,8 @@ public class RestartLoginCookie {
     }
 
 
-    public static AuthenticationSessionModel restartSession(KeycloakSession session, RealmModel realm) throws Exception {
+    public static AuthenticationSessionModel restartSession(KeycloakSession session, RealmModel realm,
+                                                            RootAuthenticationSessionModel rootSession, String expectedClientId) throws Exception {
         Cookie cook = session.getContext().getRequestHeaders().getCookies().get(KC_RESTART);
         if (cook ==  null) {
             logger.debug("KC_RESTART cookie doesn't exist");
@@ -161,7 +163,18 @@ public class RestartLoginCookie {
         ClientModel client = realm.getClientByClientId(cookie.getClientId());
         if (client == null) return null;
 
-        AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true);
+        // Restart just if client from cookie matches client from the URL.
+        if (!client.getClientId().equals(expectedClientId)) {
+            logger.debugf("Skip restarting from the KC_RESTART. Clients doesn't match: Cookie client: %s, Requested client: %s", client.getClientId(), expectedClientId);
+            return null;
+        }
+
+        // Need to create brand new session and setup cookie
+        if (rootSession == null) {
+            rootSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, true);
+        }
+
+        AuthenticationSessionModel authSession = rootSession.createAuthenticationSession(client);
         authSession.setProtocol(cookie.getAuthMethod());
         authSession.setRedirectUri(cookie.getRedirectUri());
         authSession.setAction(cookie.getAction());
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 8791010..649c77d 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -56,6 +56,7 @@ import org.keycloak.services.util.CookieHelper;
 import org.keycloak.services.util.P3PHelper;
 import org.keycloak.sessions.AuthenticationSessionModel;
 import org.keycloak.sessions.CommonClientSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
 
 import javax.crypto.SecretKey;
 import javax.ws.rs.core.Cookie;
@@ -215,16 +216,20 @@ public class AuthenticationManager {
     }
 
     private static AuthenticationSessionModel createOrJoinLogoutSession(RealmModel realm, final AuthenticationSessionManager asm, boolean browserCookie) {
+        // Account management client is used as a placeholder
         ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
-        AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm);
+
+        AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm, client);
         // Try to join existing logout session if it exists and browser session is required
         if (browserCookie && logoutAuthSession != null) {
             if (Objects.equals(AuthenticationSessionModel.Action.LOGGING_OUT.name(), logoutAuthSession.getAction())) {
                 return logoutAuthSession;
             }
-            logoutAuthSession.restartSession(realm, client);
+            // Re-create the authentication session for logout
+            logoutAuthSession = logoutAuthSession.getParentSession().createAuthenticationSession(client);
         } else {
-            logoutAuthSession = asm.createAuthenticationSession(realm, client, browserCookie);
+            RootAuthenticationSessionModel rootLogoutSession = asm.createAuthenticationSession(realm, browserCookie);
+            logoutAuthSession = rootLogoutSession.createAuthenticationSession(client);
         }
         logoutAuthSession.setAction(AuthenticationSessionModel.Action.LOGGING_OUT.name());
         return logoutAuthSession;
@@ -381,8 +386,8 @@ public class AuthenticationManager {
     /**
      * Sets logout state of the particular client into the {@code logoutAuthSession}
      * @param logoutAuthSession logoutAuthSession. May be {@code null} in which case this is a no-op.
-     * @param client Client. Must not be {@code null}
-     * @param state
+     * @param clientUuid Client. Must not be {@code null}
+     * @param action
      */
     public static void setClientLogoutAction(AuthenticationSessionModel logoutAuthSession, String clientUuid, AuthenticationSessionModel.Action action) {
         if (logoutAuthSession != null && clientUuid != null) {
@@ -479,8 +484,11 @@ public class AuthenticationManager {
     }
 
     public static Response finishBrowserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
+        // Account management client is used as a placeholder
+        ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
+
         final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
-        AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm);
+        AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm, client);
         checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
 
         expireIdentityCookie(realm, uriInfo, connection);
@@ -832,7 +840,7 @@ public class AuthenticationManager {
 
         logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired());
 
-        event.detail(Details.CODE_ID, authSession.getId());
+        event.detail(Details.CODE_ID, authSession.getParentSession().getId());
 
         Set<String> requiredActions = user.getRequiredActions();
         Response action = executionActions(session, authSession, request, event, realm, user, requiredActions);
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java
index ac6259e..8a625b3 100644
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java
@@ -28,6 +28,7 @@ import org.keycloak.models.UserSessionModel;
 import org.keycloak.protocol.RestartLoginCookie;
 import org.keycloak.services.util.CookieHelper;
 import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
 import org.keycloak.sessions.StickySessionEncoderProvider;
 
 /**
@@ -45,22 +46,22 @@ public class AuthenticationSessionManager {
         this.session = session;
     }
 
+
     /**
-     * Creates a fresh authentication session for the given realm and client. Optionally sets the browser
+     * Creates a fresh authentication session for the given realm . Optionally sets the browser
      * authentication session cookie {@link #AUTH_SESSION_ID} with the ID of the new session.
      * @param realm
-     * @param client
      * @param browserCookie Set the cookie in the browser for the
      * @return
      */
-    public AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client, boolean browserCookie) {
-        AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client);
+    public RootAuthenticationSessionModel createAuthenticationSession(RealmModel realm, boolean browserCookie) {
+        RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(realm);
 
         if (browserCookie) {
-            setAuthSessionCookie(authSession.getId(), realm);
+            setAuthSessionCookie(rootAuthSession.getId(), realm);
         }
 
-        return authSession;
+        return rootAuthSession;
     }
 
 
@@ -73,14 +74,20 @@ public class AuthenticationSessionManager {
         return getAuthSessionCookieDecoded(realm);
     }
 
+
     /**
      * Returns current authentication session if it exists, otherwise returns {@code null}.
      * @param realm
      * @return
      */
-    public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm) {
+    public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm, ClientModel client) {
         String authSessionId = getAuthSessionCookieDecoded(realm);
-        return authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
+
+        if (authSessionId == null) {
+            return null;
+        }
+
+        return getAuthenticationSessionByIdAndClient(realm, authSessionId, client);
     }
 
 
@@ -124,8 +131,10 @@ public class AuthenticationSessionManager {
 
 
     public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authSession, boolean expireRestartCookie) {
-        log.debugf("Removing authSession '%s'. Expire restart cookie: %b", authSession.getId(), expireRestartCookie);
-        session.authenticationSessions().removeAuthenticationSession(realm, authSession);
+        RootAuthenticationSessionModel rootAuthSession = authSession.getParentSession();
+
+        log.debugf("Removing authSession '%s'. Expire restart cookie: %b", rootAuthSession.getId(), expireRestartCookie);
+        session.authenticationSessions().removeRootAuthenticationSession(realm, rootAuthSession);
 
         // expire restart cookie
         if (expireRestartCookie) {
@@ -138,7 +147,14 @@ public class AuthenticationSessionManager {
 
     // Check to see if we already have authenticationSession with same ID
     public UserSessionModel getUserSession(AuthenticationSessionModel authSession) {
-        return session.sessions().getUserSession(authSession.getRealm(), authSession.getId());
+        return session.sessions().getUserSession(authSession.getRealm(), authSession.getParentSession().getId());
+    }
+
+
+    // Don't look at cookie. Just lookup authentication session based on the ID and client. Return null if not found
+    public AuthenticationSessionModel getAuthenticationSessionByIdAndClient(RealmModel realm, String authSessionId, ClientModel client) {
+        RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
+        return rootAuthSession==null ? null : rootAuthSession.getAuthenticationSession(client);
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
index 503973e..2acf683 100755
--- a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
+++ b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
@@ -80,7 +80,8 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel> 
         }
     }
 
-    public static <CLIENT_SESSION extends CommonClientSessionModel> ParseResult<CLIENT_SESSION> parseResult(String code, KeycloakSession session, RealmModel realm, EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
+    public static <CLIENT_SESSION extends CommonClientSessionModel> ParseResult<CLIENT_SESSION> parseResult(String code, KeycloakSession session, RealmModel realm, ClientModel client,
+                                                                                                            EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
         ParseResult<CLIENT_SESSION> result = new ParseResult<>();
         if (code == null) {
             result.illegalHash = true;
@@ -88,7 +89,7 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel> 
         }
         try {
             CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser(sessionClass);
-            result.clientSession = getClientSession(code, session, realm, event, clientSessionParser);
+            result.clientSession = getClientSession(code, session, realm, client, event, clientSessionParser);
             if (result.clientSession == null) {
                 result.authSessionNotFound = true;
                 return result;
@@ -113,15 +114,16 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel> 
     }
 
 
-    public static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
+    public static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, ClientModel client,
+                                                                                                    EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
         CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser(sessionClass);
-        return getClientSession(code, session, realm, event, clientSessionParser);
+        return getClientSession(code, session, realm, client, event, clientSessionParser);
     }
 
 
-    private static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event,
+    private static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event,
                                                                                                      CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser) {
-        return clientSessionParser.parseSession(code, session, realm, event);
+        return clientSessionParser.parseSession(code, session, realm, client, event);
     }
 
 
@@ -135,7 +137,8 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel> 
     }
 
     public boolean isActionActive(ActionType actionType) {
-        int timestamp = commonLoginSession.getTimestamp();
+        CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = (CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION>) CodeGenerateUtil.getParser(commonLoginSession.getClass());
+        int timestamp = clientSessionParser.getTimestamp(commonLoginSession);
 
         int lifespan;
         switch (actionType) {
@@ -210,7 +213,9 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel> 
 
     public void setAction(String action) {
         commonLoginSession.setAction(action);
-        commonLoginSession.setTimestamp(Time.currentTime());
+
+        CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = (CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION>) CodeGenerateUtil.getParser(commonLoginSession.getClass());
+        clientSessionParser.setTimestamp(commonLoginSession, Time.currentTime());
     }
 
     public String getOrGenerateCode() {
diff --git a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java
index de141ff..8ea2450 100644
--- a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java
+++ b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java
@@ -32,6 +32,7 @@ import org.keycloak.events.Details;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.jose.jwe.JWEException;
 import org.keycloak.models.AuthenticatedClientSessionModel;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.CodeToTokenStoreProvider;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
@@ -78,7 +79,7 @@ class CodeGenerateUtil {
 
     interface ClientSessionParser<CS extends CommonClientSessionModel> {
 
-        CS parseSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event);
+        CS parseSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event);
 
         String retrieveCode(KeycloakSession session, CS clientSession);
 
@@ -88,6 +89,9 @@ class CodeGenerateUtil {
 
         boolean isExpired(KeycloakSession session, String code, CS clientSession);
 
+        int getTimestamp(CS clientSession);
+        void setTimestamp(CS clientSession, int timestamp);
+
     }
 
 
@@ -97,9 +101,9 @@ class CodeGenerateUtil {
     private static class AuthenticationSessionModelParser implements ClientSessionParser<AuthenticationSessionModel> {
 
         @Override
-        public AuthenticationSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event) {
+        public AuthenticationSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event) {
             // Read authSessionID from cookie. Code is ignored for now
-            return new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm);
+            return new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client);
         }
 
         @Override
@@ -141,6 +145,16 @@ class CodeGenerateUtil {
         public boolean isExpired(KeycloakSession session, String code, AuthenticationSessionModel clientSession) {
             return false;
         }
+
+        @Override
+        public int getTimestamp(AuthenticationSessionModel clientSession) {
+            return clientSession.getParentSession().getTimestamp();
+        }
+
+        @Override
+        public void setTimestamp(AuthenticationSessionModel clientSession, int timestamp) {
+            clientSession.getParentSession().setTimestamp(timestamp);
+        }
     }
 
 
@@ -149,7 +163,7 @@ class CodeGenerateUtil {
         private CodeJWT codeJWT;
 
         @Override
-        public AuthenticatedClientSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event) {
+        public AuthenticatedClientSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event) {
             SecretKey aesKey = session.keys().getActiveAesKey(realm).getSecretKey();
             SecretKey hmacKey = session.keys().getActiveHmacKey(realm).getSecretKey();
 
@@ -241,6 +255,16 @@ class CodeGenerateUtil {
         public boolean isExpired(KeycloakSession session, String code, AuthenticatedClientSessionModel clientSession) {
             return !codeJWT.isActive();
         }
+
+        @Override
+        public int getTimestamp(AuthenticatedClientSessionModel clientSession) {
+            return clientSession.getTimestamp();
+        }
+
+        @Override
+        public void setTimestamp(AuthenticatedClientSessionModel clientSession, int timestamp) {
+            clientSession.setTimestamp(timestamp);
+        }
     }
 
 
diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
index 99e6ea3..d40dc61 100755
--- a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
+++ b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
@@ -52,6 +52,7 @@ import org.keycloak.services.Urls;
 import org.keycloak.services.managers.AppAuthManager;
 import org.keycloak.services.managers.Auth;
 import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.AuthenticationSessionManager;
 import org.keycloak.services.managers.UserSessionManager;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.services.resources.AbstractSecuredLocalService;
@@ -60,6 +61,7 @@ import org.keycloak.services.resources.RealmsResource;
 import org.keycloak.services.util.ResolveRelative;
 import org.keycloak.services.validation.Validation;
 import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
 import org.keycloak.storage.ReadOnlyException;
 import org.keycloak.util.JsonSerialization;
 
@@ -179,7 +181,7 @@ public class AccountFormService extends AbstractSecuredLocalService {
             setReferrerOnPage();
 
             UserSessionModel userSession = auth.getSession();
-            AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, userSession.getId());
+            AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getAuthenticationSessionByIdAndClient(realm, userSession.getId(), client);
             if (authSession != null) {
                 String forwardedError = authSession.getAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
                 if (forwardedError != null) {
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index 0f3d307..611f05d 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -292,7 +292,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
 
         // Create AuthenticationSessionModel with same ID like userSession and refresh cookie
         UserSessionModel userSession = cookieResult.getSession();
-        AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(userSession.getId(), realmModel, client);
+        AuthenticationSessionModel authSession = session.authenticationSessions().createRootAuthenticationSession(userSession.getId(), realmModel).createAuthenticationSession(client);
         new AuthenticationSessionManager(session).setAuthSessionCookie(userSession.getId(), realmModel);
 
         ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession);
@@ -588,7 +588,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
         AuthenticationSessionModel authSession = clientSessionCode.getClientSession();
 
         try {
-            this.event.detail(Details.CODE_ID, authSession.getId())
+            this.event.detail(Details.CODE_ID, authSession.getParentSession().getId())
                     .removeDetail("auth_method");
 
             SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
@@ -685,7 +685,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
 
             logger.debugf("Redirect to postBrokerLogin flow after authentication with identityProvider '%s'.", context.getIdpConfig().getAlias());
 
-            authSession.setTimestamp(Time.currentTime());
+            authSession.getParentSession().setTimestamp(Time.currentTime());
 
             SerializedBrokeredIdentityContext ctx = SerializedBrokeredIdentityContext.serialize(context);
             ctx.saveToAuthenticationSession(authSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
@@ -789,7 +789,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
         if (nextRequiredAction != null) {
             return AuthenticationManager.redirectToRequiredActions(session, realmModel, authSession, uriInfo, nextRequiredAction);
         } else {
-            event.detail(Details.CODE_ID, authSession.getId());  // todo This should be set elsewhere.  find out why tests fail.  Don't know where this is supposed to be set
+            event.detail(Details.CODE_ID, authSession.getParentSession().getId());  // todo This should be set elsewhere.  find out why tests fail.  Don't know where this is supposed to be set
             return AuthenticationManager.finishedRequiredActions(session, authSession, null, clientConnection, request, uriInfo, event);
         }
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index 8aaca57..d707e03 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -72,6 +72,7 @@ import org.keycloak.services.util.CacheControlUtil;
 import org.keycloak.services.util.AuthenticationFlowURLHelper;
 import org.keycloak.services.util.BrowserHistoryHelper;
 import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
 
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
@@ -333,7 +334,8 @@ public class LoginActionsService {
     public Response resetCredentialsGET(@QueryParam("code") String code,
                                         @QueryParam("execution") String execution,
                                         @QueryParam("client_id") String clientId) {
-        AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm);
+        ClientModel client = realm.getClientByClientId(clientId);
+        AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client);
 
         // we allow applications to link to reset credentials without going through OAuth or SAML handshakes
         if (authSession == null && code == null) {
@@ -357,7 +359,10 @@ public class LoginActionsService {
 
         // set up the account service as the endpoint to call.
         ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
-        authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true);
+
+        RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, true);
+        authSession = rootAuthSession.createAuthenticationSession(client);
+
         authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
         //authSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
         authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
@@ -413,9 +418,8 @@ public class LoginActionsService {
         ActionTokenContext<T> tokenContext;
         String eventError = null;
         String defaultErrorMessage = null;
-        AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm);
 
-        event.event(EventType.EXECUTE_ACTION_TOKEN);
+        AuthenticationSessionModel authSession = null;
 
         // Setup client, so error page will contain "back to application" link
         ClientModel client = null;
@@ -424,8 +428,11 @@ public class LoginActionsService {
         }
         if (client != null) {
             session.getContext().setClient(client);
+            authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client);
         }
 
+        event.event(EventType.EXECUTE_ACTION_TOKEN);
+
         // First resolve action token handler
         try {
             if (tokenString == null) {
@@ -500,7 +507,7 @@ public class LoginActionsService {
                 authSession = handler.startFreshAuthenticationSession(token, tokenContext);
                 tokenContext.setAuthenticationSession(authSession, true);
             } else if (tokenAuthSessionId == null ||
-              ! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, tokenAuthSessionId)) {
+              ! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, tokenAuthSessionId, client)) {
                 // There exists an authentication session but no auth session ID was received in the action token
                 logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId());
                 new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false);
@@ -737,7 +744,7 @@ public class LoginActionsService {
 
     public static Response redirectToAfterBrokerLoginEndpoint(KeycloakSession session, RealmModel realm, UriInfo uriInfo, AuthenticationSessionModel authSession, boolean firstBrokerLogin) {
         ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, realm, authSession);
-        authSession.setTimestamp(Time.currentTime());
+        authSession.getParentSession().setTimestamp(Time.currentTime());
 
         String clientId = authSession.getClient().getClientId();
         URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId) :
@@ -816,7 +823,7 @@ public class LoginActionsService {
         OIDCResponseMode responseMode = OIDCResponseMode.parse(respMode, OIDCResponseType.parse(responseType));
 
         event.event(EventType.LOGIN).client(authSession.getClient())
-                .detail(Details.CODE_ID, authSession.getId())
+                .detail(Details.CODE_ID, authSession.getParentSession().getId())
                 .detail(Details.REDIRECT_URI, authSession.getRedirectUri())
                 .detail(Details.AUTH_METHOD, authSession.getProtocol())
                 .detail(Details.RESPONSE_TYPE, responseType)
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
index 345f64a..f0e7ef0 100644
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
@@ -35,6 +35,8 @@ import org.keycloak.sessions.CommonClientSessionModel.Action;
 import java.util.Objects;
 import java.util.function.Consumer;
 import org.jboss.logging.Logger;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
+
 /**
  *
  * @author hmlnarik
@@ -75,7 +77,6 @@ public class LoginActionsServiceChecks {
      * If there is an action required in the session, furthermore it is not the expected one, and the required
      * action is redirection to "required actions", it throws with response performing the redirect to required
      * actions.
-     * @param <T>
      */
     public static class IsActionRequired implements Predicate<JsonWebToken> {
 
@@ -250,7 +251,7 @@ public class LoginActionsServiceChecks {
      *
      *  @param <T>
      */
-    public static <T extends JsonWebToken> boolean doesAuthenticationSessionFromCookieMatchOneFromToken(ActionTokenContext<T> context, String authSessionIdFromToken) throws VerificationException {
+    public static <T extends JsonWebToken> boolean doesAuthenticationSessionFromCookieMatchOneFromToken(ActionTokenContext<T> context, String authSessionIdFromToken, ClientModel client) throws VerificationException {
         if (authSessionIdFromToken == null) {
             return false;
         }
@@ -262,9 +263,8 @@ public class LoginActionsServiceChecks {
             return false;
         }
 
-        AuthenticationSessionModel authSessionFromCookie = context.getSession()
-          .authenticationSessions().getAuthenticationSession(context.getRealm(), authSessionIdFromCookie);
-        if (authSessionFromCookie == null) {    // Cookie contains ID of expired auth session
+        AuthenticationSessionModel authSessionFromCookie = asm.getAuthenticationSessionByIdAndClient(context.getRealm(), authSessionIdFromCookie, client);
+        if (authSessionFromCookie == null) {    // Not our client in root session
             return false;
         }
 
@@ -278,14 +278,16 @@ public class LoginActionsServiceChecks {
             return false;
         }
 
-        AuthenticationSessionModel authSessionFromParent = context.getSession()
-          .authenticationSessions().getAuthenticationSession(context.getRealm(), parentSessionId);
+        AuthenticationSessionModel authSessionFromParent = asm.getAuthenticationSessionByIdAndClient(context.getRealm(), parentSessionId, client);
+        if (authSessionFromParent == null) {
+            return false;
+        }
 
         // It's the correct browser. Let's remove forked session as we won't continue
         // from the login form (browser flow) but from the token's flow
         // Don't expire KC_RESTART cookie at this point
         asm.removeAuthenticationSession(context.getRealm(), authSessionFromCookie, false);
-        LOG.debugf("Removed forked session: %s", authSessionFromCookie.getId());
+        LOG.debugf("Removed forked session: %s", authSessionFromCookie.getParentSession().getId());
 
         // Refresh browser cookie
         asm.setAuthSessionCookie(parentSessionId, context.getRealm());
diff --git a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
index 3624b53..90dd186 100644
--- a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
+++ b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
@@ -47,6 +47,7 @@ import org.keycloak.services.messages.Messages;
 import org.keycloak.services.util.BrowserHistoryHelper;
 import org.keycloak.services.util.AuthenticationFlowURLHelper;
 import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
 
 
 public class SessionCodeChecks {
@@ -132,12 +133,6 @@ public class SessionCodeChecks {
             return null;
         }
 
-        // object retrieve
-        AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(code, session, realm, event, AuthenticationSessionModel.class);
-        if (authSession != null) {
-            return authSession;
-        }
-
         // Setup client to be shown on error/info page based on "client_id" parameter
         logger.debugf("Will use client '%s' in back-to-application link", clientId);
         ClientModel client = null;
@@ -148,8 +143,16 @@ public class SessionCodeChecks {
             session.getContext().setClient(client);
         }
 
+        // object retrieve
+        AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(session);
+        AuthenticationSessionModel authSession = authSessionManager.getCurrentAuthenticationSession(realm, client);
+        if (authSession != null) {
+            return authSession;
+        }
+
         // See if we are already authenticated and userSession with same ID exists.
-        String sessionId = new AuthenticationSessionManager(session).getCurrentAuthenticationSessionId(realm);
+        String sessionId = authSessionManager.getCurrentAuthenticationSessionId(realm);
+        RootAuthenticationSessionModel existingRootAuthSession = null;
         if (sessionId != null) {
             UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
             if (userSession != null) {
@@ -164,10 +167,13 @@ public class SessionCodeChecks {
                 response = loginForm.createInfoPage();
                 return null;
             }
+
+
+            existingRootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, sessionId);
         }
 
         // Otherwise just try to restart from the cookie
-        response = restartAuthenticationSessionFromCookie();
+        response = restartAuthenticationSessionFromCookie(existingRootAuthSession);
         return null;
     }
 
@@ -186,7 +192,7 @@ public class SessionCodeChecks {
         }
 
         // Client checks
-        event.detail(Details.CODE_ID, authSession.getId());
+        event.detail(Details.CODE_ID, authSession.getParentSession().getId());
         ClientModel client = authSession.getClient();
         if (client == null) {
             event.error(Errors.CLIENT_NOT_FOUND);
@@ -240,7 +246,7 @@ public class SessionCodeChecks {
                 return false;
             }
         } else {
-            ClientSessionCode.ParseResult<AuthenticationSessionModel> result = ClientSessionCode.parseResult(code, session, realm, event, AuthenticationSessionModel.class);
+            ClientSessionCode.ParseResult<AuthenticationSessionModel> result = ClientSessionCode.parseResult(code, session, realm, client, event, AuthenticationSessionModel.class);
             clientCode = result.getCode();
             if (clientCode == null) {
 
@@ -341,11 +347,12 @@ public class SessionCodeChecks {
     }
 
 
-    private Response restartAuthenticationSessionFromCookie() {
+    private Response restartAuthenticationSessionFromCookie(RootAuthenticationSessionModel existingRootSession) {
         logger.debug("Authentication session not found. Trying to restart from cookie.");
         AuthenticationSessionModel authSession = null;
+
         try {
-            authSession = RestartLoginCookie.restartSession(session, realm);
+            authSession = RestartLoginCookie.restartSession(session, realm, existingRootSession, clientId);
         } catch (Exception e) {
             ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
         }
diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java
index 6e954f9..b14ef06 100755
--- a/services/src/main/java/org/keycloak/services/Urls.java
+++ b/services/src/main/java/org/keycloak/services/Urls.java
@@ -182,9 +182,11 @@ public class Urls {
         return loginResetCredentialsBuilder(baseUri).build(realmName);
     }
 
-    public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString) {
+    public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString, String clientId) {
         return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken")
-          .queryParam("key", tokenString);
+                .queryParam("key", tokenString)
+                .queryParam(Constants.CLIENT_ID, clientId);
+
     }
 
     public static UriBuilder loginResetCredentialsBuilder(URI baseUri) {
diff --git a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
index d4c67a7..432bc7b 100755
--- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
@@ -25,6 +25,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
 import org.keycloak.broker.provider.IdentityProvider;
+import org.keycloak.broker.provider.util.IdentityBrokerState;
 import org.keycloak.broker.social.SocialIdentityProvider;
 import org.keycloak.common.ClientConnection;
 import org.keycloak.events.Details;
@@ -197,7 +198,9 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
 
                 twitter.setOAuthConsumer(getConfig().getClientId(), getConfig().getClientSecret());
 
-                authSession = ClientSessionCode.getClientSession(state, session, realm, event, AuthenticationSessionModel.class);
+                String clientId = IdentityBrokerState.encoded(state).getClientId();
+                ClientModel client = realm.getClientByClientId(clientId);
+                authSession = ClientSessionCode.getClientSession(state, session, realm, client, event, AuthenticationSessionModel.class);
 
                 String twitterToken = authSession.getAuthNote(TWITTER_TOKEN);
                 String twitterSecret = authSession.getAuthNote(TWITTER_TOKENSECRET);
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/MailUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/MailUtils.java
index add2a89..415cfd1 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/MailUtils.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/MailUtils.java
@@ -56,13 +56,14 @@ public class MailUtils {
         assertEquals("text/html; charset=UTF-8", htmlContentType);
 
         final String htmlBody = (String) multipart.getBodyPart(1).getContent();
+        final String htmlChangePwdUrl = MailUtils.getLink(htmlBody);
         // .replace() accounts for escaping the ampersand
         // It's not escaped in the html version because html retrieved from a
         // message bundle is considered safe and it must be unescaped to display
         // properly.
-        final String htmlChangePwdUrl = MailUtils.getLink(htmlBody).replace("&", "&amp;");
+        final String htmlChangePwdUrlToCompare = htmlChangePwdUrl.replace("&", "&amp;");
 
-        assertEquals(htmlChangePwdUrl, textChangePwdUrl);
+        assertEquals(htmlChangePwdUrlToCompare, textChangePwdUrl);
 
         return htmlChangePwdUrl;
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
index 55dd453..e95d500 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
@@ -578,25 +578,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
 
 
     public static String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
-    	Multipart multipart = (Multipart) message.getContent();
-
-        final String textContentType = multipart.getBodyPart(0).getContentType();
-
-        assertEquals("text/plain; charset=UTF-8", textContentType);
-
-        final String textBody = (String) multipart.getBodyPart(0).getContent();
-        final String textChangePwdUrl = MailUtils.getLink(textBody);
-
-        final String htmlContentType = multipart.getBodyPart(1).getContentType();
-
-        assertEquals("text/html; charset=UTF-8", htmlContentType);
-
-        final String htmlBody = (String) multipart.getBodyPart(1).getContent();
-        final String htmlChangePwdUrl = MailUtils.getLink(htmlBody);
-
-        assertEquals(htmlChangePwdUrl, textChangePwdUrl);
-
-        return htmlChangePwdUrl;
+        return MailUtils.getPasswordResetEmailLink(message);
     }
 
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java
index 714c8a3..c72932f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java
@@ -159,8 +159,6 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
 
     ) {
 
-        // TODO:mposolda Disable periodic cleaner now on all Keycloak nodes. Make sure it's re-enabled after finish
-
         // Ensure to remove all current sessions and offline sessions
         setTimeOffset(10000000);
         getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
@@ -281,8 +279,6 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
 
     ) throws Exception {
 
-        // TODO:mposolda Disable periodic cleaner now on all Keycloak nodes. Make sure it's re-enabled after finish
-
         // Ensure to remove all current sessions and offline sessions
         setTimeOffset(10000000);
         getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java
index a81f30f..ac71037 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java
@@ -630,23 +630,4 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
     }
 
 
-    @Test
-    public void testClientRemoveAuthSessions(
-            @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
-
-        createInitialAuthSessions();
-
-        channelStatisticsCrossDc.reset();
-
-        // Remove test-app client
-        ApiUtil.findClientByClientId(getAdminClient().realm(REALM_NAME), "test-app").remove();
-
-        // Assert sessions removed on node1 and node2 and on remote caches.
-        assertAuthSessionsStatisticsExpected("After client removed", channelStatisticsCrossDc,
-                0);
-    }
-
-
-
-
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java
index 9d914bd..a276cdc 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java
@@ -17,6 +17,7 @@
 
 package org.keycloak.testsuite.forms;
 
+import org.hamcrest.Matchers;
 import org.jboss.arquillian.graphene.page.Page;
 import org.junit.Before;
 import org.junit.Rule;
@@ -281,5 +282,36 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
     }
 
 
+    // KEYCLOAK-5797
+    @Test
+    public void loginWithDifferentClients() throws Exception {
+        // Open tab1 and start login here
+        oauth.openLoginForm();
+        loginPage.assertCurrent();
+        loginPage.login("login-test", "bad-password");
+        String tab1Url = driver.getCurrentUrl();
+
+        // Go to tab2 and start login with different client "root-url-client"
+        oauth.clientId("root-url-client");
+        oauth.redirectUri("http://localhost:8180/foo/bar/baz");
+        oauth.openLoginForm();
+        loginPage.assertCurrent();
+        String tab2Url = driver.getCurrentUrl();
+
+        // Go back to tab1 and finish login here
+        driver.navigate().to(tab1Url);
+        loginPage.login("login-test", "password");
+        updatePasswordPage.changePassword("password", "password");
+        updateProfilePage.update("John", "Doe3", "john@doe3.com");
+
+        // Assert I am redirected to the appPage in tab1
+        appPage.assertCurrent();
+
+        // Go back to tab2 and finish login here. Should be on the root-url-client page
+        driver.navigate().to(tab2Url);
+        String currentUrl = driver.getCurrentUrl();
+        Assert.assertThat(currentUrl, Matchers.startsWith("http://localhost:8180/foo/bar/baz"));
+    }
+
 
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RestartCookieTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RestartCookieTest.java
index 6135c56..375070b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RestartCookieTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RestartCookieTest.java
@@ -90,7 +90,7 @@ public class RestartCookieTest extends AbstractTestRealmKeycloakTest {
 
     // KEYCLOAK-5440
     @Test
-    public void invalidLoginAndBackButton() throws IOException, MessagingException {
+    public void testRestartCookieBackwardsCompatible() throws IOException, MessagingException {
         String oldRestartCookie = testingClient.server().fetchString((KeycloakSession session) -> {
             try {
                 String cookieVal = OLD_RESTART_COOKIE_JSON.replace("\n", "").replace(" ", "");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/MailAssert.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/MailAssert.java
index 463c32f..0575317 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/MailAssert.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/MailAssert.java
@@ -50,6 +50,7 @@ public class MailAssert {
             if (message.getContent() instanceof MimeMultipart) {
                 MimeMultipart mimeMultipart = (MimeMultipart) message.getContent();
 
+                // TEXT content is on index 0
                 messageContent = String.valueOf(mimeMultipart.getBodyPart(0).getContent());
             } else {
                 messageContent = String.valueOf(message.getContent());
@@ -61,6 +62,9 @@ public class MailAssert {
             assertTrue(errorMessage, messageContent.contains(content));
             for (String string : messageContent.split("\n")) {
                 if (string.contains("http://")) {
+
+                    // Ampersand escaped in the text version. Needs to be replaced to have correct URL
+                    string = string.replace("&amp;", "&");
                     return string;
                 }
             }
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
index e977aef..49b037b 100755
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
@@ -356,15 +356,16 @@ public abstract class AbstractIdentityProviderTest {
         assertEquals("text/html; charset=UTF-8", htmlContentType);
 
         final String htmlBody = (String) multipart.getBodyPart(1).getContent();
-        
+
+        final String htmlChangePwdUrl = MailUtil.getLink(htmlBody);
         // .replace() accounts for escaping the ampersand
         // It's not escaped in the html version because html retrieved from a
         // message bundle is considered safe and it must be unescaped to display
         // properly.
-        final String htmlVerificationUrl = MailUtil.getLink(htmlBody).replace("&", "&amp;");
+        final String htmlChangePwdUrlToCompare = htmlChangePwdUrl.replace("&", "&amp;");
 
-        assertEquals(htmlVerificationUrl, textVerificationUrl);
+        assertEquals(htmlChangePwdUrlToCompare, textVerificationUrl);
 
-        return htmlVerificationUrl;
+        return htmlChangePwdUrl;
     }
 }
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java
index db08e81..e28a976 100644
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java
@@ -32,6 +32,7 @@ import org.keycloak.services.managers.ClientManager;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.sessions.AuthenticationSessionModel;
 import org.keycloak.sessions.CommonClientSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
 import org.keycloak.testsuite.rule.KeycloakRule;
 
 import static org.junit.Assert.assertNotNull;
@@ -83,37 +84,40 @@ public class AuthenticationSessionProviderTest {
         ClientModel client1 = realm.getClientByClientId("test-app");
         UserModel user1 = session.users().getUserByUsername("user1", realm);
 
-        AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client1);
+        RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(realm);
+        AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client1);
 
         authSession.setAction("foo");
-        authSession.setTimestamp(100);
+        rootAuthSession.setTimestamp(100);
 
         resetSession();
 
         // Ensure session is here
-        authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
+        rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId());
         testAuthenticationSession(authSession, client1.getId(), null, "foo");
-        Assert.assertEquals(100, authSession.getTimestamp());
+        Assert.assertEquals(100, rootAuthSession.getTimestamp());
 
         // Update and commit
         authSession.setAction("foo-updated");
-        authSession.setTimestamp(200);
+        rootAuthSession.setTimestamp(200);
         authSession.setAuthenticatedUser(session.users().getUserByUsername("user1", realm));
 
         resetSession();
 
         // Ensure session was updated
-        authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
+        rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId());
+        client1 = realm.getClientByClientId("test-app");
+        authSession = rootAuthSession.getAuthenticationSession(client1);
         testAuthenticationSession(authSession, client1.getId(), user1.getId(), "foo-updated");
-        Assert.assertEquals(200, authSession.getTimestamp());
+        Assert.assertEquals(200, rootAuthSession.getTimestamp());
 
         // Remove and commit
-        session.authenticationSessions().removeAuthenticationSession(realm, authSession);
+        session.authenticationSessions().removeRootAuthenticationSession(realm, rootAuthSession);
 
         resetSession();
 
         // Ensure session was removed
-        Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSession.getId()));
+        Assert.assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId()));
 
     }
 
@@ -122,10 +126,10 @@ public class AuthenticationSessionProviderTest {
         ClientModel client1 = realm.getClientByClientId("test-app");
         UserModel user1 = session.users().getUserByUsername("user1", realm);
 
-        AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client1);
+        AuthenticationSessionModel authSession = session.authenticationSessions().createRootAuthenticationSession(realm).createAuthenticationSession(client1);
 
         authSession.setAction("foo");
-        authSession.setTimestamp(100);
+        authSession.getParentSession().setTimestamp(100);
 
         authSession.setAuthenticatedUser(user1);
         authSession.setAuthNote("foo", "bar");
@@ -134,20 +138,17 @@ public class AuthenticationSessionProviderTest {
 
         resetSession();
 
+        // Test restart root authentication session
         client1 = realm.getClientByClientId("test-app");
-        authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
-        authSession.restartSession(realm, client1);
+        authSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSession.getParentSession().getId())
+                .getAuthenticationSession(client1);
+        authSession.getParentSession().restartSession(realm);
 
         resetSession();
 
-        authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
-        testAuthenticationSession(authSession, client1.getId(), null, null);
-        Assert.assertTrue(authSession.getTimestamp() > 0);
-
-        Assert.assertTrue(authSession.getClientNotes().isEmpty());
-        Assert.assertNull(authSession.getAuthNote("foo2"));
-        Assert.assertTrue(authSession.getExecutionStatus().isEmpty());
-
+        RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSession.getParentSession().getId());
+        Assert.assertNull(rootAuthSession.getAuthenticationSession(client1));
+        Assert.assertTrue(rootAuthSession.getTimestamp() > 0);
     }
 
 
@@ -159,58 +160,59 @@ public class AuthenticationSessionProviderTest {
             realm.setAccessCodeLifespanLogin(30);
 
             // Login lifespan is largest
-            String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
+            String authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId();
+
             resetSession();
 
             Time.setOffset(25);
             session.authenticationSessions().removeExpired(realm);
             resetSession();
 
-            assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
+            assertNotNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId));
 
             Time.setOffset(35);
             session.authenticationSessions().removeExpired(realm);
             resetSession();
 
-            assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
+            assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId));
 
             // User action is largest
             realm.setAccessCodeLifespanUserAction(40);
 
             Time.setOffset(0);
-            authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
+            authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId();
             resetSession();
 
             Time.setOffset(35);
             session.authenticationSessions().removeExpired(realm);
             resetSession();
 
-            assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
+            assertNotNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId));
 
             Time.setOffset(45);
             session.authenticationSessions().removeExpired(realm);
             resetSession();
 
-            assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
+            assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId));
 
             // Access code is largest
             realm.setAccessCodeLifespan(50);
 
             Time.setOffset(0);
-            authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
+            authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId();
             resetSession();
 
             Time.setOffset(45);
             session.authenticationSessions().removeExpired(realm);
             resetSession();
 
-            assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
+            assertNotNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId));
 
             Time.setOffset(55);
             session.authenticationSessions().removeExpired(realm);
             resetSession();
 
-            assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
+            assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId));
         } finally {
             Time.setOffset(0);
 
@@ -227,8 +229,8 @@ public class AuthenticationSessionProviderTest {
         RealmModel fooRealm = session.realms().createRealm("foo-realm");
         ClientModel fooClient = fooRealm.addClient("foo-client");
 
-        String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
-        String authSessionId2 = session.authenticationSessions().createAuthenticationSession(fooRealm, fooClient).getId();
+        String authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId();
+        String authSessionId2 = session.authenticationSessions().createRootAuthenticationSession(fooRealm).getId();
 
         resetSession();
 
@@ -236,27 +238,36 @@ public class AuthenticationSessionProviderTest {
 
         resetSession();
 
-        AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
-        testAuthenticationSession(authSession, realm.getClientByClientId("test-app").getId(), null, null);
-        Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId2));
+        RootAuthenticationSessionModel authSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
+        Assert.assertNotNull(authSession);
+        Assert.assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId2));
     }
 
     @Test
     public void testOnClientRemoved() {
-        String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
-        String authSessionId2 = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("third-party")).getId();
+        String authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId();
+        AuthenticationSessionModel authSession1 = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId).createAuthenticationSession(realm.getClientByClientId("test-app"));
+        AuthenticationSessionModel authSession2 = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId).createAuthenticationSession(realm.getClientByClientId("third-party"));
+
+        authSession1.setAuthNote("foo", "bar");
+        authSession2.setAuthNote("foo", "baz");
 
         String testAppClientUUID = realm.getClientByClientId("test-app").getId();
 
         resetSession();
 
+        RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
+        Assert.assertEquals(2, rootAuthSession.getAuthenticationSessions().size());
+        Assert.assertEquals("bar", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("test-app")).getAuthNote("foo"));
+        Assert.assertEquals("baz", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("third-party")).getAuthNote("foo"));
+
         new ClientManager(new RealmManager(session)).removeClient(realm, realm.getClientByClientId("third-party"));
 
         resetSession();
 
-        AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
-        testAuthenticationSession(authSession, testAppClientUUID, null, null);
-        Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId2));
+        rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
+        Assert.assertEquals("bar", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("test-app")).getAuthNote("foo"));
+        Assert.assertNull(rootAuthSession.getAuthenticationSession(realm.getClientByClientId("third-party")));
 
         // Revert client
         realm.addClient("third-party");