Removing the extra two-minute Window for persistent user sessions (#32660)

Closes #28418

Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
Signed-off-by: Michal Hajas <mhajas@redhat.com>
Co-authored-by: Michal Hajas <mhajas@redhat.com>
This commit is contained in:
Alexander Schwartz 2024-09-09 09:28:48 +02:00 committed by GitHub
parent e1d5f0c871
commit b88ecc0237
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 29 additions and 11 deletions

View file

@ -93,6 +93,8 @@ image:images/tokens-tab.png[Tokens Tab]
[NOTE] [NOTE]
==== ====
The following logic is only applied if persistent user sessions are not active:
For idle timeouts, a two-minute window of time exists that the session is active. For example, when you have the timeout set to 30 minutes, it will be 32 minutes before the session expires. For idle timeouts, a two-minute window of time exists that the session is active. For example, when you have the timeout set to 30 minutes, it will be 32 minutes before the session expires.
This action is necessary for some scenarios in cluster and cross-data center environments where the token refreshes on one cluster node a short time before the expiration and the other cluster nodes incorrectly consider the session as expired because they have not yet received the message about a successful refresh from the refreshing node. This action is necessary for some scenarios in cluster and cross-data center environments where the token refreshes on one cluster node a short time before the expiration and the other cluster nodes incorrectly consider the session as expired because they have not yet received the message about a successful refresh from the refreshing node.

View file

@ -246,6 +246,14 @@ Update your custom embedded Infinispan cache configuration file with configurati
For more details proceed to the https://www.keycloak.org/server/caching[Configuring distributed caches] guide. For more details proceed to the https://www.keycloak.org/server/caching[Configuring distributed caches] guide.
= Grace period for idle sessions removed when persistent sessions are enabled
Previous versions of {project_name} added a grace period of two minutes to idle times of user and client sessions.
This was added due to a previous architecture where session refresh times were replicated asynchronously in a cluster.
With persistent user sessions, this is no longer necessary, and therefore the grace period is now removed.
To keep the old behavior, update your realm configuration and extend the session and client idle times by two minutes.
= Support for legacy `redirect_uri` parameter and SPI options has been removed = Support for legacy `redirect_uri` parameter and SPI options has been removed
Previous versions of {project_name} had supported automatic logout of the user and redirecting to the application by opening logout endpoint URL such as Previous versions of {project_name} had supported automatic logout of the user and redirecting to the application by opening logout endpoint URL such as

View file

@ -185,7 +185,8 @@ public class AuthenticationManager {
long idle = SessionExpirationUtils.calculateUserSessionIdleTimestamp(userSession.isOffline(), long idle = SessionExpirationUtils.calculateUserSessionIdleTimestamp(userSession.isOffline(),
userSession.isRememberMe(), TimeUnit.SECONDS.toMillis(userSession.getLastSessionRefresh()), realm); userSession.isRememberMe(), TimeUnit.SECONDS.toMillis(userSession.getLastSessionRefresh()), realm);
boolean sessionIdleOk = idle > currentTime - TimeUnit.SECONDS.toMillis(SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS); boolean sessionIdleOk = idle > currentTime -
((Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS) || Profile.isFeatureEnabled(Profile.Feature.REMOTE_CACHE)) ? 0 : TimeUnit.SECONDS.toMillis(SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS));
boolean sessionMaxOk = lifespan == -1L || lifespan > currentTime; boolean sessionMaxOk = lifespan == -1L || lifespan > currentTime;
return sessionIdleOk && sessionMaxOk; return sessionIdleOk && sessionMaxOk;
} }
@ -203,7 +204,8 @@ public class AuthenticationManager {
long idle = SessionExpirationUtils.calculateClientSessionIdleTimestamp(userSession.isOffline(), long idle = SessionExpirationUtils.calculateClientSessionIdleTimestamp(userSession.isOffline(),
userSession.isRememberMe(), TimeUnit.SECONDS.toMillis(clientSession.getTimestamp()), realm, client); userSession.isRememberMe(), TimeUnit.SECONDS.toMillis(clientSession.getTimestamp()), realm, client);
boolean sessionIdleOk = idle > currentTime - TimeUnit.SECONDS.toMillis(SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS); boolean sessionIdleOk = idle > currentTime -
((Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS) || Profile.isFeatureEnabled(Profile.Feature.REMOTE_CACHE)) ? 0 : TimeUnit.SECONDS.toMillis(SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS));
boolean sessionMaxOk = lifespan == -1L || lifespan > currentTime; boolean sessionMaxOk = lifespan == -1L || lifespan > currentTime;
return sessionIdleOk && sessionMaxOk; return sessionIdleOk && sessionMaxOk;
} }

View file

@ -4,6 +4,7 @@ import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.jboss.shrinkwrap.api.spec.WebArchive; import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test; import org.junit.Test;
import org.keycloak.common.Profile;
import org.keycloak.dom.saml.v2.SAML2Object; import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType; import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
import org.keycloak.dom.saml.v2.assertion.StatementAbstractType; import org.keycloak.dom.saml.v2.assertion.StatementAbstractType;
@ -11,6 +12,7 @@ import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.models.utils.SessionTimeoutHelper; import org.keycloak.models.utils.SessionTimeoutHelper;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil; import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter; import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter;
import org.keycloak.testsuite.adapter.page.Employee2Servlet; import org.keycloak.testsuite.adapter.page.Employee2Servlet;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
@ -46,7 +48,7 @@ public class SAMLServletSessionTimeoutTest extends AbstractSAMLServletAdapterTes
} }
private static final int SESSION_LENGTH_IN_SECONDS = 120; private static final int SESSION_LENGTH_IN_SECONDS = 120;
private static final int KEYCLOAK_SESSION_TIMEOUT = 1922; /** 1800 session max + 120 {@link SessionTimeoutHelper#IDLE_TIMEOUT_WINDOW_SECONDS} */ private static final int KEYCLOAK_SESSION_TIMEOUT = 1800 + (ProfileAssume.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS) ? 0 : SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS); /** 1800 session max + 120 {@link SessionTimeoutHelper#IDLE_TIMEOUT_WINDOW_SECONDS} */
private AtomicReference<String> sessionNotOnOrAfter = new AtomicReference<>(); private AtomicReference<String> sessionNotOnOrAfter = new AtomicReference<>();

View file

@ -937,7 +937,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
appPage.assertCurrent(); appPage.assertCurrent();
// expire idle timeout using the timeout window. // expire idle timeout using the timeout window.
setTimeOffset(2 + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS); setTimeOffset(2 + (ProfileAssume.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS) ? 0 : SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS));
// trying to open the account page with an expired idle timeout should redirect back to the login page. // trying to open the account page with an expired idle timeout should redirect back to the login page.
loginPage.open(); loginPage.open();

View file

@ -29,6 +29,7 @@ import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RoleResource; import org.keycloak.admin.client.resource.RoleResource;
import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.Profile;
import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.Algorithm;
import org.keycloak.events.Details; import org.keycloak.events.Details;
@ -56,6 +57,7 @@ import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.auth.page.AuthRealm; import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
@ -488,7 +490,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
testUser.roles().realmLevel().remove(Collections.singletonList(appRealm.roles().get("composite").toRepresentation())); testUser.roles().realmLevel().remove(Collections.singletonList(appRealm.roles().get("composite").toRepresentation()));
appRealm.roles().get("composite").remove(); appRealm.roles().get("composite").remove();
testUser.roles().realmLevel().add(Collections.singletonList(offlineAccess)); testUser.roles().realmLevel().add(Collections.singletonList(offlineAccess));
} }
/** /**
@ -648,7 +650,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
offlineRefresh = oauth.doRefreshTokenRequest(offlineResponse.getRefreshToken(), "secret1"); offlineRefresh = oauth.doRefreshTokenRequest(offlineResponse.getRefreshToken(), "secret1");
assertEquals(200, offlineRefresh.getStatusCode()); assertEquals(200, offlineRefresh.getStatusCode());
} }
@Test @Test
public void browserOfflineTokenLogoutFollowedByLoginSameSession() throws Exception { public void browserOfflineTokenLogoutFollowedByLoginSameSession() throws Exception {
oauth.scope(OAuth2Constants.OFFLINE_ACCESS); oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
@ -760,7 +762,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
final int MAX_LIFESPAN = 3000; final int MAX_LIFESPAN = 3000;
final int IDLE_LIFESPAN = 600; final int IDLE_LIFESPAN = 600;
// Additional time window is added for the case when session was updated in different DC and the update to current DC was postponed // Additional time window is added for the case when session was updated in different DC and the update to current DC was postponed
testOfflineSessionExpiration(IDLE_LIFESPAN, MAX_LIFESPAN, 0, IDLE_LIFESPAN + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS + 60); testOfflineSessionExpiration(IDLE_LIFESPAN, MAX_LIFESPAN, 0, IDLE_LIFESPAN + (ProfileAssume.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS) ? 0 : SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS) + 60);
} }
// Issue 13706 // Issue 13706
@ -927,7 +929,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
} }
setTimeOffset(0); setTimeOffset(0);
} finally { } finally {
getTestingClient().testing().revertTestingInfinispanTimeService(); getTestingClient().testing().revertTestingInfinispanTimeService();
changeOfflineSessionSettings(false, prev[0], prev[1], prev[2], prev[3]); changeOfflineSessionSettings(false, prev[0], prev[1], prev[2], prev[3]);

View file

@ -33,6 +33,7 @@ import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RealmsResource; import org.keycloak.admin.client.resource.RealmsResource;
import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.Profile;
import org.keycloak.common.enums.SslRequired; import org.keycloak.common.enums.SslRequired;
import org.keycloak.cookie.CookieType; import org.keycloak.cookie.CookieType;
import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.Algorithm;
@ -63,6 +64,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancer; import org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancer;
import org.keycloak.testsuite.oidc.AbstractOIDCScopeTest; import org.keycloak.testsuite.oidc.AbstractOIDCScopeTest;
@ -1211,7 +1213,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
events.clear(); events.clear();
// Needs to add some additional time due the tollerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS // Needs to add some additional time due the tollerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS
setTimeOffset(6 + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS); setTimeOffset(6 + (ProfileAssume.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS) ? 0 : SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS));
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password"); tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
// test idle timeout // test idle timeout
@ -1253,7 +1255,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId(); String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
int last = testingClient.testing().getLastSessionRefresh("test", sessionId, false); int last = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
setTimeOffset(110 + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS); setTimeOffset(110 + (ProfileAssume.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS) ? 0 : SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS));
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password"); tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
oauth.verifyToken(tokenResponse.getAccessToken()); oauth.verifyToken(tokenResponse.getAccessToken());
oauth.parseRefreshToken(tokenResponse.getRefreshToken()); oauth.parseRefreshToken(tokenResponse.getRefreshToken());
@ -1264,7 +1266,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
events.clear(); events.clear();
// Needs to add some additional time due the tollerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS // Needs to add some additional time due the tollerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS
setTimeOffset(620 + 2 * SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS); setTimeOffset(620 + 2 * (ProfileAssume.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS) ? 0 : SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS));
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password"); tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
// test idle remember me timeout // test idle remember me timeout