Invalidate user session when associated IdP is missing (previously removed)

Closes #31724

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen 2024-10-16 08:39:55 -03:00 committed by Pedro Igor
parent 731274f39e
commit 7d8ff710c2
2 changed files with 77 additions and 6 deletions

View file

@ -17,6 +17,7 @@
package org.keycloak.services.managers; package org.keycloak.services.managers;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.cookie.CookieProvider; import org.keycloak.cookie.CookieProvider;
import org.keycloak.cookie.CookieType; import org.keycloak.cookie.CookieType;
import org.keycloak.http.HttpRequest; import org.keycloak.http.HttpRequest;
@ -178,6 +179,13 @@ public class AuthenticationManager {
logger.debug("No user session"); logger.debug("No user session");
return false; return false;
} }
if (userSession.getNote(Details.IDENTITY_PROVIDER) != null) {
String brokerAlias = userSession.getNote(Details.IDENTITY_PROVIDER);
if (realm.getIdentityProviderByAlias(brokerAlias) == null) {
// associated idp was removed, invalidate the session.
return false;
}
}
long currentTime = Time.currentTimeMillis(); long currentTime = Time.currentTimeMillis();
long lifespan = SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(userSession.isOffline(), long lifespan = SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(userSession.isOffline(),
userSession.isRememberMe(), TimeUnit.SECONDS.toMillis(userSession.getStarted()), realm); userSession.isRememberMe(), TimeUnit.SECONDS.toMillis(userSession.getStarted()), realm);
@ -417,12 +425,19 @@ public class AuthenticationManager {
if (logoutBroker) { if (logoutBroker) {
String brokerId = userSession.getNote(Details.IDENTITY_PROVIDER); String brokerId = userSession.getNote(Details.IDENTITY_PROVIDER);
if (brokerId != null) { if (brokerId != null) {
IdentityProvider identityProvider = IdentityBrokerService.getIdentityProvider(session, brokerId); IdentityProvider identityProvider = null;
try { try {
identityProvider.backchannelLogout(session, userSession, uriInfo, realm); identityProvider = IdentityBrokerService.getIdentityProvider(session, brokerId);
} catch (Exception e) { } catch (IdentityBrokerException e) {
logger.warn("Exception at broker backchannel logout for broker " + brokerId, e); logger.warn("Skipping backchannel logout for broker " + brokerId + " - not found");
backchannelLogoutResponse.setLocalLogoutSucceeded(false); }
if (identityProvider != null) {
try {
identityProvider.backchannelLogout(session, userSession, uriInfo, realm);
} catch (Exception e) {
logger.warn("Exception at broker backchannel logout for broker " + brokerId, e);
backchannelLogoutResponse.setLocalLogoutSucceeded(false);
}
} }
} }
} }
@ -1516,7 +1531,18 @@ public class AuthenticationManager {
} }
} }
if (userSession != null) backchannelLogout(session, realm, userSession, uriInfo, connection, headers, true);
if (userSession != null) {
String userSessionId = userSession.getId();
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), session.getContext(), newSession -> {
RealmModel realmModel = newSession.realms().getRealm(realm.getId());
UserSessionModel userSessionModel = newSession.sessions().getUserSession(realmModel, userSessionId);
backchannelLogout(newSession, realmModel, userSessionModel, uriInfo, connection, headers, true);
});
// remove the user session here so that the external persistent session tx becomes aware of the removal that happened
// during the backchannel logout.
session.sessions().removeUserSession(realm, userSession);
}
logger.debug("User session not active"); logger.debug("User session not active");
return null; return null;
} }

View file

@ -20,10 +20,13 @@ import org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper;
import org.keycloak.broker.oidc.mappers.UserAttributeMapper; import org.keycloak.broker.oidc.mappers.UserAttributeMapper;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.Algorithm;
import org.keycloak.models.ClientModel;
import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderMapperSyncMode; import org.keycloak.models.IdentityProviderMapperSyncMode;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCConfigAttributes;
@ -40,6 +43,7 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.broker.util.SimpleHttpDefault; import org.keycloak.testsuite.broker.util.SimpleHttpDefault;
@ -49,6 +53,7 @@ import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.testsuite.util.WaitUtils;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -59,6 +64,7 @@ import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
@ -457,6 +463,45 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
errorPage.assertCurrent(); errorPage.assertCurrent();
} }
@Test
public void testIdpRemovedAfterLoginInvalidatesUserSession() {
loginUser();
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
assertThat(loginPage.isSocialButtonPresent(bc.getIDPAlias()), is(true));
logInWithBroker(bc);
// remove the IDP while the user is logged in
adminClient.realm(bc.consumerRealmName()).identityProviders().get(bc.getIDPAlias()).remove();
// user session should still be active, but checking if it is valid should fail as the associated IDP was removed
testingClient.server(bc.consumerRealmName()).run(session -> {
RealmModel realm = session.getContext().getRealm();
ClientModel client = session.clients().getClientByClientId(realm, "broker-app");
List<UserSessionModel> userSessions = session.sessions().getUserSessionsStream(realm, client).toList();
assertThat(userSessions, hasSize(1));
UserSessionModel userSession = userSessions.get(0);
assertThat(AuthenticationManager.isSessionValid(realm, userSession), is(false));
});
// logout should work even after the IDP was removed
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
// session should have been removed now
testingClient.server(bc.consumerRealmName()).run(session -> {
RealmModel realm = session.getContext().getRealm();
ClientModel client = session.clients().getClientByClientId(realm, "broker-app");
List<UserSessionModel> userSessions = session.sessions().getUserSessionsStream(realm, client).toList();
assertThat(userSessions, hasSize(0));
});
loginPage.open(bc.consumerRealmName());
assertThat(loginPage.isSocialButtonPresent(bc.getIDPAlias()), is(false));
}
@Test @Test
public void testInvalidAudience() { public void testInvalidAudience() {
loginUser(); loginUser();