keycloak-aplcache

Merge pull request #4468 from hmlnarik/KEYCLOAK-4899-Optimize-client-session-writes KEYCLOAK-4899

9/12/2017 5:42:38 AM

Details

diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
index 3f09773..635d4f7 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
@@ -82,23 +82,26 @@ public class UserSessionAdapter implements UserSessionModel {
             });
         }
 
-        // Update user session
-        if (!removedClientUUIDS.isEmpty()) {
-            UserSessionUpdateTask task = new UserSessionUpdateTask() {
-
-                @Override
-                public void runUpdate(UserSessionEntity entity) {
-                    for (String clientUUID : removedClientUUIDS) {
-                        entity.getAuthenticatedClientSessions().remove(clientUUID);
-                    }
-                }
+        removeAuthenticatedClientSessions(removedClientUUIDS);
 
-            };
+        return Collections.unmodifiableMap(result);
+    }
 
-            update(task);
+    @Override
+    public void removeAuthenticatedClientSessions(Iterable<String> removedClientUUIDS) {
+        if (removedClientUUIDS == null || ! removedClientUUIDS.iterator().hasNext()) {
+            return;
         }
 
-        return Collections.unmodifiableMap(result);
+        // Update user session
+        UserSessionUpdateTask task = new UserSessionUpdateTask() {
+            @Override
+            public void runUpdate(UserSessionEntity entity) {
+                removedClientUUIDS.forEach(entity.getAuthenticatedClientSessions()::remove);
+            }
+        };
+
+        update(task);
     }
 
     public String getId() {
diff --git a/server-spi/src/main/java/org/keycloak/models/ClientModel.java b/server-spi/src/main/java/org/keycloak/models/ClientModel.java
index 072cda3..5f4403f 100755
--- a/server-spi/src/main/java/org/keycloak/models/ClientModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/ClientModel.java
@@ -34,8 +34,16 @@ public interface ClientModel extends RoleContainerModel,  ProtocolMapperContaine
 
     void updateClient();
 
+    /**
+     * Returns client internal ID (UUID).
+     * @return
+     */
     String getId();
 
+    /**
+     * Returns client ID as defined by the user.
+     * @return
+     */
     String getClientId();
 
     void setClientId(String clientId);
diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
index a6f1c35..ff7c864 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
@@ -52,7 +52,17 @@ public interface UserSessionModel {
 
     void setLastSessionRefresh(int seconds);
 
+    /**
+     * Returns map where key is ID of the client (its UUID) and value is the respective {@link AuthenticatedClientSessionModel} object.
+     * @return 
+     */
     Map<String, AuthenticatedClientSessionModel> getAuthenticatedClientSessions();
+    /**
+     * Removes authenticated client sessions for all clients whose UUID is present in {@code removedClientUUIDS} parameter.
+     * @param removedClientUUIDS
+     */
+    void removeAuthenticatedClientSessions(Iterable<String> removedClientUUIDS);
+
 
     public String getNote(String name);
     public void setNote(String name, String value);
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 99806d4..8cc4035 100644
--- a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
+++ b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
@@ -27,7 +27,10 @@ import java.util.Map;
  */
 public interface AuthenticationSessionProvider extends Provider {
 
-    // Generates random ID
+    /**
+     * 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);
 
     AuthenticationSessionModel createAuthenticationSession(String id, RealmModel realm, ClientModel client);
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 1598714..5f913ca 100644
--- a/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java
@@ -59,6 +59,7 @@ public interface CommonClientSessionModel {
         CODE_TO_TOKEN,
         AUTHENTICATE,
         LOGGED_OUT,
+        LOGGING_OUT,
         REQUIRED_ACTIONS
     }
 
diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
index 6bea75f..e3b5777 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
@@ -161,6 +161,15 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
     }
 
     @Override
+    public void removeAuthenticatedClientSessions(Iterable<String> removedClientUUIDS) {
+        if (removedClientUUIDS == null || ! removedClientUUIDS.iterator().hasNext()) {
+            return;
+        }
+
+        removedClientUUIDS.forEach(authenticatedClientSessions::remove);
+    }
+
+    @Override
     public String getNote(String name) {
         return getData().getNotes()==null ? null : getData().getNotes().get(name);
     }
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 4daee92..4f6f4ec 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -66,6 +66,7 @@ import javax.ws.rs.core.UriInfo;
 import java.net.URI;
 import java.security.PublicKey;
 import java.util.*;
+import java.util.stream.Collectors;
 
 /**
  * Stateless object that manages authentication
@@ -78,6 +79,11 @@ public class AuthenticationManager {
     public static final String END_AFTER_REQUIRED_ACTIONS = "END_AFTER_REQUIRED_ACTIONS";
     public static final String INVALIDATE_ACTION_TOKEN = "INVALIDATE_ACTION_TOKEN";
 
+    /**
+     * Auth session note on client logout state (when logging out)
+     */
+    public static final String CLIENT_LOGOUT_STATE = "logout.state.";
+
     // userSession note with authTime (time when authentication flow including requiredActions was finished)
     public static final String AUTH_TIME = "AUTH_TIME";
     // clientSession note with flag that clientSession was authenticated through SSO cookie
@@ -165,14 +171,49 @@ public class AuthenticationManager {
                                          boolean logoutBroker) {
         if (userSession == null) return;
         UserModel user = userSession.getUser();
-        userSession.setState(UserSessionModel.State.LOGGING_OUT);
+        if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
+            userSession.setState(UserSessionModel.State.LOGGING_OUT);
+        }
 
         logger.debugv("Logging out: {0} ({1})", user.getUsername(), userSession.getId());
         expireUserSessionCookie(session, userSession, realm, uriInfo, headers, connection);
 
-        for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
-            backchannelLogoutClientSession(session, realm, clientSession, userSession, uriInfo, headers);
+        final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
+        AuthenticationSessionModel logoutAuthSession = createOrJoinLogoutSession(realm, asm, false);
+
+        try {
+            backchannelLogoutAll(session, realm, userSession, logoutAuthSession, uriInfo, headers, logoutBroker);
+            checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
+        } finally {
+            asm.removeAuthenticationSession(realm, logoutAuthSession, false);
+        }
+
+        userSession.setState(UserSessionModel.State.LOGGED_OUT);
+        session.sessions().removeUserSession(realm, userSession);
+    }
+
+    private static AuthenticationSessionModel createOrJoinLogoutSession(RealmModel realm, final AuthenticationSessionManager asm, boolean browserCookie) {
+        ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
+        AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm);
+        // 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);
+        } else {
+            logoutAuthSession = asm.createAuthenticationSession(realm, client, browserCookie);
         }
+        logoutAuthSession.setAction(AuthenticationSessionModel.Action.LOGGING_OUT.name());
+        return logoutAuthSession;
+    }
+
+    private static void backchannelLogoutAll(KeycloakSession session, RealmModel realm,
+      UserSessionModel userSession, AuthenticationSessionModel logoutAuthSession, UriInfo uriInfo,
+      HttpHeaders headers, boolean logoutBroker) {
+        userSession.getAuthenticatedClientSessions().values().forEach(
+          clientSession -> backchannelLogoutClientSession(session, realm, clientSession, logoutAuthSession, uriInfo, headers)
+        );
         if (logoutBroker) {
             String brokerId = userSession.getNote(Details.IDENTITY_PROVIDER);
             if (brokerId != null) {
@@ -184,32 +225,180 @@ public class AuthenticationManager {
                 }
             }
         }
-        userSession.setState(UserSessionModel.State.LOGGED_OUT);
-        session.sessions().removeUserSession(realm, userSession);
     }
 
-    public static void backchannelLogoutClientSession(KeycloakSession session, RealmModel realm, AuthenticatedClientSessionModel clientSession, UserSessionModel userSession, UriInfo uriInfo, HttpHeaders headers) {
+    /**
+     * Checks that all sessions have been removed from the user session. The list of logged out clients is determined from
+     * the {@code logoutAuthSession} auth session notes.
+     * @param realm
+     * @param userSession
+     * @param logoutAuthSession
+     * @return {@code true} when all clients have been logged out, {@code false} otherwise
+     */
+    private static boolean checkUserSessionOnlyHasLoggedOutClients(RealmModel realm,
+      UserSessionModel userSession, AuthenticationSessionModel logoutAuthSession) {
+        final Map<String, AuthenticatedClientSessionModel> acs = userSession.getAuthenticatedClientSessions();
+        Set<AuthenticatedClientSessionModel> notLoggedOutSessions = acs.entrySet().stream()
+          .filter(me -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT, getClientLogoutAction(logoutAuthSession, me.getKey())))
+          .filter(me -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT.name(), me.getValue().getAction()))
+          .filter(me -> Objects.nonNull(me.getValue().getProtocol()))   // Keycloak service-like accounts
+          .map(Map.Entry::getValue)
+          .collect(Collectors.toSet());
+
+        boolean allClientsLoggedOut = notLoggedOutSessions.isEmpty();
+
+        if (! allClientsLoggedOut) {
+            logger.warnf("Some clients have been not been logged out for user %s in %s realm: %s",
+              userSession.getUser().getUsername(), realm.getName(),
+              notLoggedOutSessions.stream()
+                .map(AuthenticatedClientSessionModel::getClient)
+                .map(ClientModel::getClientId)
+                .sorted()
+                .collect(Collectors.joining(", "))
+            );
+        } else if (logger.isDebugEnabled()) {
+            logger.debugf("All clients have been logged out for user %s in %s realm, session %s",
+              userSession.getUser().getUsername(), realm.getName(), userSession.getId());
+        }
+
+        return allClientsLoggedOut;
+    }
+
+    /**
+     * Logs out the given client session and records the result into {@code logoutAuthSession} if set.
+     * @param session
+     * @param realm
+     * @param clientSession
+     * @param logoutAuthSession auth session used for recording result of logout. May be {@code null}
+     * @param uriInfo
+     * @param headers
+     * @return {@code true} if the client was or is already being logged out, {@code false} if logout failed or it is not known how to log it out.
+     */
+    private static boolean backchannelLogoutClientSession(KeycloakSession session, RealmModel realm,
+      AuthenticatedClientSessionModel clientSession, AuthenticationSessionModel logoutAuthSession,
+      UriInfo uriInfo, HttpHeaders headers) {
+        UserSessionModel userSession = clientSession.getUserSession();
         ClientModel client = clientSession.getClient();
-        if (!client.isFrontchannelLogout() && !AuthenticatedClientSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) {
+
+        if (client.isFrontchannelLogout() || AuthenticationSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) {
+            return false;
+        }
+
+        final AuthenticationSessionModel.Action logoutState = getClientLogoutAction(logoutAuthSession, client.getId());
+
+        if (logoutState == AuthenticationSessionModel.Action.LOGGED_OUT || logoutState == AuthenticationSessionModel.Action.LOGGING_OUT) {
+            return true;
+        }
+
+        try {
+            setClientLogoutAction(logoutAuthSession, client.getId(), AuthenticationSessionModel.Action.LOGGING_OUT);
+
             String authMethod = clientSession.getProtocol();
-            if (authMethod == null) return; // must be a keycloak service like account
+            if (authMethod == null) return true; // must be a keycloak service like account
+
+            logger.debugv("backchannel logout to: {0}", client.getClientId());
             LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
             protocol.setRealm(realm)
                     .setHttpHeaders(headers)
                     .setUriInfo(uriInfo);
             protocol.backchannelLogout(userSession, clientSession);
-            clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name());
+
+            setClientLogoutAction(logoutAuthSession, client.getId(), AuthenticationSessionModel.Action.LOGGED_OUT);
+
+            return true;
+        } catch (Exception ex) {
+            ServicesLogger.LOGGER.failedToLogoutClient(ex);
+            return false;
+        }
+    }
+
+    private static Response frontchannelLogoutClientSession(KeycloakSession session, RealmModel realm,
+      AuthenticatedClientSessionModel clientSession, AuthenticationSessionModel logoutAuthSession,
+      UriInfo uriInfo, HttpHeaders headers) {
+        UserSessionModel userSession = clientSession.getUserSession();
+        ClientModel client = clientSession.getClient();
+
+        if (! client.isFrontchannelLogout() || AuthenticationSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) {
+            return null;
         }
 
+        final AuthenticationSessionModel.Action logoutState = getClientLogoutAction(logoutAuthSession, client.getId());
+
+        if (logoutState == AuthenticationSessionModel.Action.LOGGED_OUT || logoutState == AuthenticationSessionModel.Action.LOGGING_OUT) {
+            return null;
+        }
+
+        try {
+            setClientLogoutAction(logoutAuthSession, client.getId(), AuthenticationSessionModel.Action.LOGGING_OUT);
+
+            String authMethod = clientSession.getProtocol();
+            if (authMethod == null) return null; // must be a keycloak service like account
+
+            logger.debugv("frontchannel logout to: {0}", client.getClientId());
+            LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
+            protocol.setRealm(realm)
+                    .setHttpHeaders(headers)
+                    .setUriInfo(uriInfo);
+
+            Response response = protocol.frontchannelLogout(userSession, clientSession);
+            if (response != null) {
+                logger.debug("returning frontchannel logout request to client");
+                // setting this to logged out cuz I'm not sure protocols can always verify that the client was logged out or not
+
+                setClientLogoutAction(logoutAuthSession, client.getId(), AuthenticationSessionModel.Action.LOGGED_OUT);
+
+                return response;
+            }
+        } catch (Exception e) {
+            ServicesLogger.LOGGER.failedToLogoutClient(e);
+        }
+
+        return null;
     }
 
-    // Logout all clientSessions of this user and client
-    public static void backchannelUserFromClient(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client, UriInfo uriInfo, HttpHeaders headers) {
+    /**
+     * 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
+     */
+    public static void setClientLogoutAction(AuthenticationSessionModel logoutAuthSession, String clientUuid, AuthenticationSessionModel.Action action) {
+        if (logoutAuthSession != null && clientUuid != null) {
+            logoutAuthSession.setAuthNote(CLIENT_LOGOUT_STATE + clientUuid, action.name());
+        }
+    }
+
+    /**
+     * Returns the logout state of the particular client as per the {@code logoutAuthSession}
+     * @param logoutAuthSession logoutAuthSession. May be {@code null} in which case this is a no-op.
+     * @param clientUuid Internal ID of the client. Must not be {@code null}
+     * @return State if it can be determined, {@code null} otherwise.
+     */
+    public static AuthenticationSessionModel.Action getClientLogoutAction(AuthenticationSessionModel logoutAuthSession, String clientUuid) {
+        if (logoutAuthSession == null || clientUuid == null) {
+            return null;
+        }
+
+        String state = logoutAuthSession.getAuthNote(CLIENT_LOGOUT_STATE + clientUuid);
+        return state == null ? null : AuthenticationSessionModel.Action.valueOf(state);
+    }
+
+    /**
+     * Logout all clientSessions of this user and client
+     * @param session
+     * @param realm
+     * @param user
+     * @param client
+     * @param uriInfo
+     * @param headers
+     */
+    public static void backchannelLogoutUserFromClient(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client, UriInfo uriInfo, HttpHeaders headers) {
         List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
         for (UserSessionModel userSession : userSessions) {
             AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
             if (clientSession != null) {
-                AuthenticationManager.backchannelLogoutClientSession(session, realm, clientSession, userSession, uriInfo, headers);
+                AuthenticationManager.backchannelLogoutClientSession(session, realm, clientSession, null, uriInfo, headers);
+                clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
                 TokenManager.dettachClientSession(session.sessions(), realm, clientSession);
             }
         }
@@ -217,67 +406,61 @@ public class AuthenticationManager {
 
     public static Response browserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
         if (userSession == null) return null;
-        UserModel user = userSession.getUser();
 
-        logger.debugv("Logging out: {0} ({1})", user.getUsername(), userSession.getId());
+        if (logger.isDebugEnabled()) {
+            UserModel user = userSession.getUser();
+            logger.debugv("Logging out: {0} ({1})", user.getUsername(), userSession.getId());
+        }
+        
         if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
             userSession.setState(UserSessionModel.State.LOGGING_OUT);
         }
-        List<AuthenticatedClientSessionModel> redirectClients = new LinkedList<>();
-        for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
-            ClientModel client = clientSession.getClient();
-            if (AuthenticatedClientSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) continue;
-            if (client.isFrontchannelLogout()) {
-                String authMethod = clientSession.getProtocol();
-                if (authMethod == null) continue; // must be a keycloak service like account
-                redirectClients.add(clientSession);
-            } else {
-                String authMethod = clientSession.getProtocol();
-                if (authMethod == null) continue; // must be a keycloak service like account
-                LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
-                protocol.setRealm(realm)
-                        .setHttpHeaders(headers)
-                        .setUriInfo(uriInfo);
-                try {
-                    logger.debugv("backchannel logout to: {0}", client.getClientId());
-                    protocol.backchannelLogout(userSession, clientSession);
-                    clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name());
-                } catch (Exception e) {
-                    ServicesLogger.LOGGER.failedToLogoutClient(e);
-                }
-            }
-        }
 
-        for (AuthenticatedClientSessionModel nextRedirectClient : redirectClients) {
-            String authMethod = nextRedirectClient.getProtocol();
-            LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
-            protocol.setRealm(realm)
-                    .setHttpHeaders(headers)
-                    .setUriInfo(uriInfo);
-            // setting this to logged out cuz I"m not sure protocols can always verify that the client was logged out or not
-            nextRedirectClient.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name());
-            try {
-                logger.debugv("frontchannel logout to: {0}", nextRedirectClient.getClient().getClientId());
-                Response response = protocol.frontchannelLogout(userSession, nextRedirectClient);
-                if (response != null) {
-                    logger.debug("returning frontchannel logout request to client");
-                    return response;
-                }
-            } catch (Exception e) {
-                ServicesLogger.LOGGER.failedToLogoutClient(e);
-            }
+        final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
+        AuthenticationSessionModel logoutAuthSession = createOrJoinLogoutSession(realm, asm, true);
 
+        Response response = browserLogoutAllClients(userSession, session, realm, headers, uriInfo, logoutAuthSession);
+        if (response != null) {
+            return response;
         }
+
         String brokerId = userSession.getNote(Details.IDENTITY_PROVIDER);
         if (brokerId != null) {
             IdentityProvider identityProvider = IdentityBrokerService.getIdentityProvider(session, realm, brokerId);
-            Response response = identityProvider.keycloakInitiatedBrowserLogout(session, userSession, uriInfo, realm);
-            if (response != null) return response;
+            response = identityProvider.keycloakInitiatedBrowserLogout(session, userSession, uriInfo, realm);
+            if (response != null) {
+                return response;
+            }
         }
+
         return finishBrowserLogout(session, realm, userSession, uriInfo, connection, headers);
     }
 
+    private static Response browserLogoutAllClients(UserSessionModel userSession, KeycloakSession session, RealmModel realm, HttpHeaders headers, UriInfo uriInfo, AuthenticationSessionModel logoutAuthSession) {
+        Map<Boolean, List<AuthenticatedClientSessionModel>> acss = userSession.getAuthenticatedClientSessions().values().stream()
+          .filter(clientSession -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT.name(), clientSession.getAction()))
+          .filter(clientSession -> clientSession.getProtocol() != null)
+          .collect(Collectors.partitioningBy(clientSession -> clientSession.getClient().isFrontchannelLogout()));
+
+        final List<AuthenticatedClientSessionModel> backendLogoutSessions = acss.get(false) == null ? Collections.emptyList() : acss.get(false);
+        backendLogoutSessions.forEach(acs -> backchannelLogoutClientSession(session, realm, acs, logoutAuthSession, uriInfo, headers));
+
+        final List<AuthenticatedClientSessionModel> redirectClients = acss.get(true) == null ? Collections.emptyList() : acss.get(true);
+        for (AuthenticatedClientSessionModel nextRedirectClient : redirectClients) {
+            Response response = frontchannelLogoutClientSession(session, realm, nextRedirectClient, logoutAuthSession, uriInfo, headers);
+            if (response != null) {
+                return response;
+            }
+        }
+
+        return null;
+    }
+
     public static Response finishBrowserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
+        final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
+        AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm);
+        checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
+
         expireIdentityCookie(realm, uriInfo, connection);
         expireRememberMeCookie(realm, uriInfo, connection);
         userSession.setState(UserSessionModel.State.LOGGED_OUT);
@@ -316,7 +499,7 @@ public class AuthenticationManager {
         String encoded = encodeToken(keycloakSession, realm, identityToken);
         boolean secureOnly = realm.getSslRequired().isRequired(connection);
         int maxAge = NewCookie.DEFAULT_MAX_AGE;
-        if (session.isRememberMe()) {
+        if (session != null && session.isRememberMe()) {
             maxAge = realm.getSsoSessionMaxLifespan();
         }
         logger.debugv("Create login cookie - name: {0}, path: {1}, max-age: {2}", KEYCLOAK_IDENTITY_COOKIE, cookiePath, maxAge);
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 1cba9dc..ac6259e 100644
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java
@@ -45,7 +45,14 @@ public class AuthenticationSessionManager {
         this.session = session;
     }
 
-
+    /**
+     * Creates a fresh authentication session for the given realm and client. 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);
 
@@ -57,11 +64,20 @@ public class AuthenticationSessionManager {
     }
 
 
+    /**
+     * Returns ID of current authentication session if it exists, otherwise returns {@code null}.
+     * @param realm
+     * @return
+     */
     public String getCurrentAuthenticationSessionId(RealmModel realm) {
         return getAuthSessionCookieDecoded(realm);
     }
 
-
+    /**
+     * Returns current authentication session if it exists, otherwise returns {@code null}.
+     * @param realm
+     * @return
+     */
     public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm) {
         String authSessionId = getAuthSessionCookieDecoded(realm);
         return authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
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 e5da4ac..04c6f04 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
@@ -420,7 +420,7 @@ public class AccountFormService extends AbstractSecuredLocalService {
         new UserSessionManager(session).revokeOfflineToken(user, client);
 
         // Logout clientSessions for this user and client
-        AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);
+        AuthenticationManager.backchannelLogoutUserFromClient(session, realm, user, client, uriInfo, headers);
 
         event.event(EventType.REVOKE_GRANT).client(auth.getClient()).user(auth.getUser()).detail(Details.REVOKED_CLIENT, client.getClientId()).success();
         setReferrerOnPage();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
index 98b7e75..34bbcea 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
@@ -490,7 +490,7 @@ public class UserResource {
 
         if (revokedConsent) {
             // Logout clientSessions for this user and client
-            AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);
+            AuthenticationManager.backchannelLogoutUserFromClient(session, realm, user, client, uriInfo, headers);
         }
 
         if (!revokedConsent && !revokedOfflineToken) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
index da0bc2b..3f2d1d7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
@@ -143,7 +143,7 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
         config.put(POST_BINDING_AUTHN_REQUEST, "true");
         config.put(VALIDATE_SIGNATURE, "false");
         config.put(WANT_AUTHN_REQUESTS_SIGNED, "false");
-        config.put(BACKCHANNEL_SUPPORTED, "true");
+        config.put(BACKCHANNEL_SUPPORTED, "false");
 
         return idp;
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
index a3a03cb..1b1a857 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
@@ -25,6 +25,7 @@ import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.testsuite.Assert;
 import org.keycloak.testsuite.AssertEvents;
 import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.Retry;
 import org.keycloak.testsuite.admin.ApiUtil;
 import org.keycloak.testsuite.pages.AppPage;
 import org.keycloak.testsuite.pages.LoginPage;
@@ -225,11 +226,13 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest {
 
         adminClient.realm("test").users().get(user.getId()).logout();
 
-        user = adminClient.realm("test").users().get(user.getId()).toRepresentation();
-        Assert.assertTrue(user.getNotBefore() > 0);
+        Retry.execute(() -> {
+            UserRepresentation u = adminClient.realm("test").users().get(user.getId()).toRepresentation();
+            Assert.assertTrue(u.getNotBefore() > 0);
 
-        loginPage.open();
-        loginPage.assertCurrent();
+            loginPage.open();
+            loginPage.assertCurrent();
+        }, 10, 200);
     }
 
 }
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java
index 781714a..0831053 100755
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java
@@ -48,11 +48,9 @@ import java.io.IOException;
 import java.net.URI;
 import java.util.Set;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.Assert.*;
 
 /**
  * @author pedroigor
@@ -470,19 +468,19 @@ public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdent
         // Login as pedroigor to account management
         accountFederatedIdentityPage.realm("realm-with-broker");
         accountFederatedIdentityPage.open();
-        assertTrue(driver.getTitle().equals("Log in to realm-with-broker"));
+        assertThat(driver.getTitle(), is("Log in to realm-with-broker"));
         loginPage.login("pedroigor", "password");
-        assertTrue(accountFederatedIdentityPage.isCurrent());
+        accountFederatedIdentityPage.assertCurrent();
 
         // Try to link my "pedroigor" identity with "test-user" from brokered Keycloak.
         accountFederatedIdentityPage.clickAddProvider(identityProvider.getAlias());
 
-        assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/"));
+        assertThat(this.driver.getCurrentUrl(), startsWith("http://localhost:8082/auth/"));
         this.loginPage.login("test-user", "password");
         doAfterProviderAuthentication();
 
         // Error is displayed in account management because federated identity"test-user" already linked to local account "test-user"
-        assertTrue(accountFederatedIdentityPage.isCurrent());
+        accountFederatedIdentityPage.assertCurrent();
         assertEquals("Federated identity returned by " + getProviderId() + " is already linked to another user.", accountFederatedIdentityPage.getError());
     }