keycloak-aplcache
Changes
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java 29(+16 -13)
server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java 9(+9 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java 2(+1 -1)
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());
}