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 3f09773c11..635d4f7a0c 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,25 +82,28 @@ 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); - } - } - - }; - - update(task); - } + removeAuthenticatedClientSessions(removedClientUUIDS); return Collections.unmodifiableMap(result); } + @Override + public void removeAuthenticatedClientSessions(Iterable removedClientUUIDS) { + if (removedClientUUIDS == null || ! removedClientUUIDS.iterator().hasNext()) { + return; + } + + // Update user session + UserSessionUpdateTask task = new UserSessionUpdateTask() { + @Override + public void runUpdate(UserSessionEntity entity) { + removedClientUUIDS.forEach(entity.getAuthenticatedClientSessions()::remove); + } + }; + + update(task); + } + public String getId() { return entity.getId(); } 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 6bea75f7fb..e3b577766d 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 @@ -160,6 +160,15 @@ public class PersistentUserSessionAdapter implements UserSessionModel { return authenticatedClientSessions; } + @Override + public void removeAuthenticatedClientSessions(Iterable 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/server-spi/src/main/java/org/keycloak/models/ClientModel.java b/server-spi/src/main/java/org/keycloak/models/ClientModel.java index 072cda3a9c..5f4403f9f6 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 a6f1c355ae..ff7c86485c 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 getAuthenticatedClientSessions(); + /** + * Removes authenticated client sessions for all clients whose UUID is present in {@code removedClientUUIDS} parameter. + * @param removedClientUUIDS + */ + void removeAuthenticatedClientSessions(Iterable 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 99806d45b2..8cc40350d0 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 1598714c37..5f913caf41 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/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 4daee92f54..4f6f4eca80 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 acs = userSession.getAuthenticatedClientSessions(); + Set 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; + } } - // Logout all clientSessions of this user and client - public static void backchannelUserFromClient(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client, UriInfo uriInfo, HttpHeaders headers) { + 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; + } + + /** + * 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 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 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); - } - } + + 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; } - 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); - } - - } 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> 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 backendLogoutSessions = acss.get(false) == null ? Collections.emptyList() : acss.get(false); + backendLogoutSessions.forEach(acs -> backchannelLogoutClientSession(session, realm, acs, logoutAuthSession, uriInfo, headers)); + + final List 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 1cba9dcf3a..ac6259eede 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 e5da4ac3ac..04c6f04f64 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 98b7e752eb..34bbcea9ce 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 da0bc2bbcc..3f2d1d7a46 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 a3a03cbe68..1b1a857e63 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 781714aa76..0831053911 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()); }