From f7044ba5c250eef129355db2fcd77fa2a2584fc9 Mon Sep 17 00:00:00 2001 From: rmartinc Date: Tue, 23 Apr 2024 12:34:13 +0200 Subject: [PATCH] Use SessionExpirationUtils for validate user and client sessions Check client session is valid in TokenManager Closes #24936 Signed-off-by: rmartinc --- .../topics/changes/changes-25_0_0.adoc | 8 +- .../AuthenticatedClientSessionAdapter.java | 31 +++ .../infinispan/changes/MergedUpdate.java | 18 +- .../AuthenticatedClientSessionModel.java | 17 ++ .../keycloak/protocol/oidc/TokenManager.java | 13 +- .../managers/AuthenticationManager.java | 47 ++-- .../services/util/UserSessionUtil.java | 2 +- .../keycloak/testsuite/forms/LoginTest.java | 1 + .../testsuite/oauth/OfflineTokenTest.java | 90 ++++++++ .../testsuite/oauth/RefreshTokenTest.java | 205 +++++++++++++++--- 10 files changed, 367 insertions(+), 65 deletions(-) diff --git a/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc index 5eef2f9a35..16b912095c 100644 --- a/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc @@ -328,4 +328,10 @@ one of the future {project_name} releases. It might be {project_name} 27 release `org.keycloak.common.util.Resteasy` has been deprecated. You should use the `org.keycloak.util.KeycloakSessionUtil` to obtain the `KeycloakSession` instead. -It is highly recommended to avoid obtaining the `KeycloakSession` by means other than when creating your custom provider. \ No newline at end of file +It is highly recommended to avoid obtaining the `KeycloakSession` by means other than when creating your custom provider. + += Small changes in session lifespan and idle calculations + +In previous versions the session max lifespan and idle timeout calculation was slightly different when validating if a session was still valid. Since now that validation uses the same code than the rest of the project. + +If the session is using the remember me feature, the idle timeout and max lifespan are the maximum value between the common SSO and the remember me configuration values. \ No newline at end of file diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java index af7246be47..bcbf7f9000 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.keycloak.common.util.Time; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; @@ -319,4 +320,34 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes return copy; } + @Override + public void restartClientSession() { + ClientSessionUpdateTask task = new ClientSessionUpdateTask() { + + @Override + public void runUpdate(AuthenticatedClientSessionEntity entity) { + UserSessionModel userSession = getUserSession(); + entity.setAction(null); + entity.setRedirectUri(null); + entity.setCurrentRefreshToken(null); + entity.setCurrentRefreshTokenUseCount(-1); + entity.setTimestamp(Time.currentTime()); + entity.getNotes().clear(); + entity.getNotes().put(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(entity.getTimestamp())); + entity.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE, String.valueOf(userSession.getStarted())); + entity.getNotes().put(AuthenticatedClientSessionEntity.CLIENT_ID_NOTE, getClient().getId()); + if (userSession.isRememberMe()) { + entity.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_REMEMBER_ME_NOTE, "true"); + } + } + + @Override + public boolean isOffline() { + return offline; + } + }; + + update(task); + } + } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java index 340a6308e7..4d9fadc931 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java @@ -90,15 +90,8 @@ public class MergedUpdate implements SessionUpdateTask< result.childUpdates.add(child); } else { - // Merge the operations. REMOVE is special case as other operations are not needed then. - CacheOperation mergedOp = result.getOperation().merge(child.getOperation(), session); - if (mergedOp == CacheOperation.REMOVE) { - result = new MergedUpdate<>(child.getOperation(), child.getCrossDCMessageStatus(sessionWrapper), lifespanMs, maxIdleTimeMs); - result.childUpdates.add(child); - return result; - } - - result.operation = mergedOp; + // Merge the operations. + result.operation = result.getOperation().merge(child.getOperation(), session); // Check if we need to send message to other DCs and how critical it is CrossDCMessageStatus currentDCStatus = result.getCrossDCMessageStatus(sessionWrapper); @@ -109,6 +102,13 @@ public class MergedUpdate implements SessionUpdateTask< result.crossDCMessageStatus = currentDCStatus.merge(childDCStatus); } + // REMOVE is special case as other operations are not needed then. + if (result.operation == CacheOperation.REMOVE) { + result = new MergedUpdate<>(result.operation, result.crossDCMessageStatus, lifespanMs, maxIdleTimeMs); + result.childUpdates.add(child); + return result; + } + // Finally add another update to the result result.childUpdates.add(child); } diff --git a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java index f3060fd337..b3b929bd3f 100644 --- a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java +++ b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java @@ -20,6 +20,7 @@ package org.keycloak.models; import java.util.Map; +import org.keycloak.common.util.Time; import org.keycloak.sessions.CommonClientSessionModel; /** @@ -72,4 +73,20 @@ public interface AuthenticatedClientSessionModel extends CommonClientSessionMode void setNote(String name, String value); void removeNote(String name); Map getNotes(); + + default void restartClientSession() { + setAction(null); + setRedirectUri(null); + setCurrentRefreshToken(null); + setCurrentRefreshTokenUseCount(-1); + setTimestamp(Time.currentTime()); + for (String note : getNotes().keySet()) { + if (!AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE.equals(note) + && !AuthenticatedClientSessionModel.STARTED_AT_NOTE.equals(note) + && !AuthenticatedClientSessionModel.USER_SESSION_REMEMBER_ME_NOTE.equals(note)) { + removeNote(note); + } + } + getNotes().put(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(getTimestamp())); + } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 1a4089f36f..f9af081cc1 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -153,7 +153,7 @@ public class TokenManager { if (userSession != null) { // Revoke timeouted offline userSession - if (!AuthenticationManager.isOfflineSessionValid(realm, userSession)) { + if (!AuthenticationManager.isSessionValid(realm, userSession)) { sessionManager.revokeOfflineUserSession(userSession); throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline session not active", "Offline session not active"); } @@ -198,6 +198,12 @@ public class TokenManager { } } + if (!AuthenticationManager.isClientSessionValid(realm, client, userSession, clientSession)) { + logger.debug("Client session not active"); + userSession.removeAuthenticatedClientSessions(Collections.singletonList(client.getId())); + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Client session not active"); + } + if (oldToken.isIssuedBeforeSessionStart(clientSession.getStarted())) { logger.debug("refresh token issued before the client session started"); throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "refresh token issued before the client session started"); @@ -567,7 +573,10 @@ public class TokenManager { ClientModel client = authSession.getClient(); AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); - if (clientSession == null) { + if (clientSession != null && !AuthenticationManager.isClientSessionValid(userSession.getRealm(), client, userSession, clientSession)) { + // session exists but not active so re-start it + clientSession.restartClientSession(); + } else if (clientSession == null) { clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession); } 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 ee555c5988..0fad68f081 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -65,6 +65,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.DefaultRequiredActions; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.SessionExpirationUtils; import org.keycloak.models.utils.SessionTimeoutHelper; import org.keycloak.models.utils.SystemClientUtil; import org.keycloak.protocol.LoginProtocol; @@ -107,10 +108,10 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.keycloak.models.light.LightweightUserAdapter.isLightweightUser; import static org.keycloak.models.UserSessionModel.CORRESPONDING_SESSION_ID; import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.isOAuth2DeviceVerificationFlow; @@ -175,35 +176,33 @@ public class AuthenticationManager { logger.debug("No user session"); return false; } - int currentTime = Time.currentTime(); + long currentTime = Time.currentTimeMillis(); + long lifespan = SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(userSession.isOffline(), + userSession.isRememberMe(), TimeUnit.SECONDS.toMillis(userSession.getStarted()), realm); + long idle = SessionExpirationUtils.calculateUserSessionIdleTimestamp(userSession.isOffline(), + userSession.isRememberMe(), TimeUnit.SECONDS.toMillis(userSession.getLastSessionRefresh()), realm); - // Additional time window is added for the case when session was updated in different DC and the update to current DC was postponed - int maxIdle = userSession.isRememberMe() && realm.getSsoSessionIdleTimeoutRememberMe() > 0 ? - realm.getSsoSessionIdleTimeoutRememberMe() : realm.getSsoSessionIdleTimeout(); - int maxLifespan = userSession.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0 ? - realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan(); - - boolean sessionIdleOk = maxIdle > currentTime - userSession.getLastSessionRefresh() - SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS; - boolean sessionMaxOk = maxLifespan > currentTime - userSession.getStarted(); + boolean sessionIdleOk = idle > currentTime - TimeUnit.SECONDS.toMillis(SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS); + boolean sessionMaxOk = lifespan == -1L || lifespan > currentTime; return sessionIdleOk && sessionMaxOk; } - public static boolean isOfflineSessionValid(RealmModel realm, UserSessionModel userSession) { - if (userSession == null) { - logger.debug("No offline user session"); + public static boolean isClientSessionValid(RealmModel realm, ClientModel client, + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + if (userSession == null || clientSession == null) { + logger.debug("No user session"); return false; } - int currentTime = Time.currentTime(); - // Additional time window is added for the case when session was updated in different DC and the update to current DC was postponed - int maxIdle = realm.getOfflineSessionIdleTimeout() + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS; + long currentTime = Time.currentTimeMillis(); + long lifespan = SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(userSession.isOffline(), + userSession.isRememberMe(), TimeUnit.SECONDS.toMillis(clientSession.getStarted()), + TimeUnit.SECONDS.toMillis(userSession.getStarted()), realm, client); + long idle = SessionExpirationUtils.calculateClientSessionIdleTimestamp(userSession.isOffline(), + userSession.isRememberMe(), TimeUnit.SECONDS.toMillis(clientSession.getTimestamp()), realm, client); - // KEYCLOAK-7688 Offline Session Max for Offline Token - if (realm.isOfflineSessionMaxLifespanEnabled()) { - int max = userSession.getStarted() + realm.getOfflineSessionMaxLifespan(); - return userSession.getLastSessionRefresh() + maxIdle > currentTime && max > currentTime; - } else { - return userSession.getLastSessionRefresh() + maxIdle > currentTime; - } + boolean sessionIdleOk = idle > currentTime - TimeUnit.SECONDS.toMillis(SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS); + boolean sessionMaxOk = lifespan == -1L || lifespan > currentTime; + return sessionIdleOk && sessionMaxOk; } public static boolean expireUserSessionCookie(KeycloakSession session, UserSessionModel userSession, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, ClientConnection connection) { @@ -1427,7 +1426,7 @@ public class AuthenticationManager { // Check if accessToken was for the offline session. if (!isCookie) { UserSessionModel offlineUserSession = session.sessions().getOfflineUserSession(realm, token.getSessionState()); - if (isOfflineSessionValid(realm, offlineUserSession)) { + if (isSessionValid(realm, offlineUserSession)) { user = offlineUserSession.getUser(); ClientModel client = realm.getClientByClientId(token.getIssuedFor()); if (!isClientValid(offlineUserSession, client, token)) { diff --git a/services/src/main/java/org/keycloak/services/util/UserSessionUtil.java b/services/src/main/java/org/keycloak/services/util/UserSessionUtil.java index 71c3359b8e..917c205b87 100644 --- a/services/src/main/java/org/keycloak/services/util/UserSessionUtil.java +++ b/services/src/main/java/org/keycloak/services/util/UserSessionUtil.java @@ -54,7 +54,7 @@ public class UserSessionUtil { return userSession; } else { offlineUserSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionId(), true, client.getId()); - if (AuthenticationManager.isOfflineSessionValid(realm, offlineUserSession)) { + if (AuthenticationManager.isSessionValid(realm, offlineUserSession)) { checkTokenIssuedAt(realm, token, offlineUserSession, event, client); event.session(offlineUserSession); return offlineUserSession; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index 454505e257..e31db36151 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -918,6 +918,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { public void loginRememberMeExpiredIdle() throws Exception { try (Closeable c = new RealmAttributeUpdater(adminClient.realm("test")) .setSsoSessionIdleTimeoutRememberMe(1) + .setSsoSessionIdleTimeout(1) // max of both values .setRememberMe(true) .update()) { // login form shown after redirect from app diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java index 42ace9c1c5..22c71ce110 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java @@ -1146,6 +1146,96 @@ public class OfflineTokenTest extends AbstractKeycloakTest { } } + @Test + public void refreshTokenUserSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception { + oauth.scope(OAuth2Constants.OFFLINE_ACCESS); + oauth.clientId("offline-client"); + oauth.redirectUri(offlineClientAppUri); + + RealmResource realmResource = adminClient.realm("test"); + getTestingClient().testing().setTestingInfinispanTimeService(); + + int[] prev = changeOfflineSessionSettings(true, 7200, 7200, 7200, 7200); + try { + oauth.doLogin("test-user@localhost", "password"); + EventRepresentation loginEvent = events.expectLogin().client("offline-client") + .detail(Details.REDIRECT_URI, offlineClientAppUri).assertEvent(); + + String sessionId = loginEvent.getSessionId(); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1"); + assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getType()); + assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200); + String clientSessionId = getOfflineClientSessionUuid(sessionId, loginEvent.getClientId()); + assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId)); + + events.poll(); + + RealmRepresentation rep = realmResource.toRepresentation(); + rep.setOfflineSessionMaxLifespan(3600); + realmResource.update(rep); + + setTimeOffset(3700); + tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "secret1"); + assertEquals(400, tokenResponse.getStatusCode()); + assertNull(tokenResponse.getAccessToken()); + assertNull(tokenResponse.getRefreshToken()); + events.expect(EventType.REFRESH_TOKEN).session(sessionId).client("offline-client").error(Errors.INVALID_TOKEN).user((String) null).assertEvent(); + assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId)); + } finally { + changeOfflineSessionSettings(false, prev[0], prev[1], prev[2], prev[3]); + getTestingClient().testing().revertTestingInfinispanTimeService(); + events.clear(); + resetTimeOffset(); + } + } + + @Test + public void refreshTokenClientSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception { + oauth.scope(OAuth2Constants.OFFLINE_ACCESS); + oauth.clientId("offline-client"); + oauth.redirectUri(offlineClientAppUri); + + RealmResource realmResource = adminClient.realm("test"); + getTestingClient().testing().setTestingInfinispanTimeService(); + + int[] prev = changeOfflineSessionSettings(true, 7200, 7200, 7200, 7200); + try { + oauth.doLogin("test-user@localhost", "password"); + EventRepresentation loginEvent = events.expectLogin().client("offline-client") + .detail(Details.REDIRECT_URI, offlineClientAppUri).assertEvent(); + + String sessionId = loginEvent.getSessionId(); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1"); + assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getType()); + assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200); + String clientSessionId = getOfflineClientSessionUuid(sessionId, loginEvent.getClientId()); + assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId)); + + events.poll(); + + RealmRepresentation rep = realmResource.toRepresentation(); + rep.setClientOfflineSessionMaxLifespan(3600); + realmResource.update(rep); + + setTimeOffset(3700); + tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "secret1"); + assertEquals(400, tokenResponse.getStatusCode()); + assertNull(tokenResponse.getAccessToken()); + assertNull(tokenResponse.getRefreshToken()); + events.expect(EventType.REFRESH_TOKEN).client("offline-client").error(Errors.INVALID_TOKEN).session(sessionId).user((String) null).assertEvent(); + assertEquals(1, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId)); + } finally { + changeOfflineSessionSettings(false, prev[0], prev[1], prev[2], prev[3]); + getTestingClient().testing().revertTestingInfinispanTimeService(); + events.clear(); + resetTimeOffset(); + } + } + @Test public void testShortOfflineSessionMax() throws Exception { int prevOfflineSession[] = null; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java index e25474d965..e508c9749d 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java @@ -1163,14 +1163,13 @@ public class RefreshTokenTest extends AbstractKeycloakTest { @Test public void testUserSessionRefreshAndIdleRememberMe() throws Exception { RealmResource testRealm = adminClient.realm("test"); - RealmRepresentation testRealmRep = testRealm.toRepresentation(); - Boolean previousRememberMe = testRealmRep.isRememberMe(); - int originalIdleRememberMe = testRealmRep.getSsoSessionIdleTimeoutRememberMe(); - - try { - testRealmRep.setRememberMe(true); - testRealm.update(testRealmRep); + try (Closeable realmUpdater = new RealmAttributeUpdater(testRealm) + .updateWith(r -> { + r.setRememberMe(true); + r.setSsoSessionIdleTimeoutRememberMe(500); + r.setSsoSessionIdleTimeout(100); + }).update()) { oauth.doRememberMeLogin("test-user@localhost", "password"); EventRepresentation loginEvent = events.expectLogin().assertEvent(); @@ -1185,7 +1184,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest { String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId(); int last = testingClient.testing().getLastSessionRefresh("test", sessionId, false); - setTimeOffset(2); + setTimeOffset(110 + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS); tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password"); oauth.verifyToken(tokenResponse.getAccessToken()); oauth.parseRefreshToken(tokenResponse.getRefreshToken()); @@ -1194,12 +1193,9 @@ public class RefreshTokenTest extends AbstractKeycloakTest { int next = testingClient.testing().getLastSessionRefresh("test", sessionId, false); Assert.assertNotEquals(last, next); - testRealmRep.setSsoSessionIdleTimeoutRememberMe(1); - testRealm.update(testRealmRep); - events.clear(); // Needs to add some additional time due the tollerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS - setTimeOffset(6 + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS); + setTimeOffset(620 + 2 * SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS); tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password"); // test idle remember me timeout @@ -1211,9 +1207,6 @@ public class RefreshTokenTest extends AbstractKeycloakTest { events.clear(); } finally { - testRealmRep.setSsoSessionIdleTimeoutRememberMe(originalIdleRememberMe); - testRealmRep.setRememberMe(previousRememberMe); - testRealm.update(testRealmRep); resetTimeOffset(); } } @@ -1399,6 +1392,168 @@ public class RefreshTokenTest extends AbstractKeycloakTest { } } + @Test + public void refreshTokenUserSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception { + RealmResource realmResource = adminClient.realm("test"); + getTestingClient().testing().setTestingInfinispanTimeService(); + + try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource) + .updateWith(r -> { + r.setSsoSessionMaxLifespan(7200); + r.setSsoSessionIdleTimeout(7200); + r.setClientSessionMaxLifespan(7200); + r.setClientSessionIdleTimeout(7200); + }).update()) { + oauth.doLogin("test-user@localhost", "password"); + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200); + final String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId()); + assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId)); + + events.poll(); + + RealmRepresentation rep = realmResource.toRepresentation(); + rep.setSsoSessionMaxLifespan(3600); + realmResource.update(rep); + + setTimeOffset(3700); + tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password"); + assertEquals(400, tokenResponse.getStatusCode()); + assertNull(tokenResponse.getAccessToken()); + assertNull(tokenResponse.getRefreshToken()); + events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).session(sessionId).user((String) null).assertEvent(); + assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId)); + } finally { + getTestingClient().testing().revertTestingInfinispanTimeService(); + events.clear(); + resetTimeOffset(); + } + } + + @Test + public void refreshTokenClientSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception { + RealmResource realmResource = adminClient.realm("test"); + getTestingClient().testing().setTestingInfinispanTimeService(); + + try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource) + .updateWith(r -> { + r.setSsoSessionMaxLifespan(7200); + r.setSsoSessionIdleTimeout(7200); + r.setClientSessionMaxLifespan(7200); + r.setClientSessionIdleTimeout(7200); + }).update()) { + oauth.doLogin("test-user@localhost", "password"); + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200); + String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId()); + assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId)); + + events.poll(); + + RealmRepresentation rep = realmResource.toRepresentation(); + rep.setClientSessionMaxLifespan(3600); + realmResource.update(rep); + + setTimeOffset(3700); + tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password"); + assertEquals(400, tokenResponse.getStatusCode()); + assertNull(tokenResponse.getAccessToken()); + assertNull(tokenResponse.getRefreshToken()); + events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).session(sessionId).user((String) null).assertEvent(); + assertEquals(1, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId)); + + setTimeOffset(4200); + oauth.doSilentLogin(); + loginEvent = events.expectLogin().assertEvent(); + sessionId = loginEvent.getSessionId(); + code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + tokenResponse = oauth.doAccessTokenRequest(code, "password"); + assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 3000); + events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), sessionId).assertEvent(); + + clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId()); + assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId)); + + setTimeOffset(7300); + tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password"); + assertEquals(400, tokenResponse.getStatusCode()); + assertNull(tokenResponse.getAccessToken()); + assertNull(tokenResponse.getRefreshToken()); + events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent(); + assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId)); + } finally { + getTestingClient().testing().revertTestingInfinispanTimeService(); + events.clear(); + resetTimeOffset(); + } + } + + @Test + public void silentLoginClientSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception { + RealmResource realmResource = adminClient.realm("test"); + getTestingClient().testing().setTestingInfinispanTimeService(); + + try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource) + .updateWith(r -> { + r.setSsoSessionMaxLifespan(7200); + r.setSsoSessionIdleTimeout(7200); + r.setClientSessionMaxLifespan(7200); + r.setClientSessionIdleTimeout(7200); + }).update()) { + + oauth.doLogin("test-user@localhost", "password"); + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200); + String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId()); + assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId)); + + events.poll(); + + RealmRepresentation rep = realmResource.toRepresentation(); + rep.setClientSessionMaxLifespan(3600); + realmResource.update(rep); + + setTimeOffset(4200); + oauth.doSilentLogin(); + loginEvent = events.expectLogin().assertEvent(); + sessionId = loginEvent.getSessionId(); + code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + tokenResponse = oauth.doAccessTokenRequest(code, "password"); + assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 3000); + events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), sessionId).assertEvent(); + + clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId()); + assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId)); + + setTimeOffset(7300); + tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password"); + assertEquals(400, tokenResponse.getStatusCode()); + assertNull(tokenResponse.getAccessToken()); + assertNull(tokenResponse.getRefreshToken()); + events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent(); + assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId)); + } finally { + getTestingClient().testing().revertTestingInfinispanTimeService(); + events.clear(); + resetTimeOffset(); + } + } + /** * KEYCLOAK-1267 * @throws Exception @@ -1407,13 +1562,13 @@ public class RefreshTokenTest extends AbstractKeycloakTest { public void refreshTokenUserSessionMaxLifespanWithRememberMe() throws Exception { RealmResource testRealm = adminClient.realm("test"); - RealmRepresentation testRealmRep = testRealm.toRepresentation(); - Boolean previousRememberMe = testRealmRep.isRememberMe(); - int previousSsoMaxLifespanRememberMe = testRealmRep.getSsoSessionMaxLifespanRememberMe(); - try { - testRealmRep.setRememberMe(true); - testRealm.update(testRealmRep); + try (Closeable realmUpdater = new RealmAttributeUpdater(testRealm) + .updateWith(r -> { + r.setRememberMe(true); + r.setSsoSessionMaxLifespanRememberMe(100); + r.setSsoSessionMaxLifespan(50); + }).update()) { oauth.doRememberMeLogin("test-user@localhost", "password"); @@ -1428,10 +1583,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest { String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId(); - testRealmRep.setSsoSessionMaxLifespanRememberMe(1); - testRealm.update(testRealmRep); - - setTimeOffset(2); + setTimeOffset(110); tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password"); @@ -1443,9 +1595,6 @@ public class RefreshTokenTest extends AbstractKeycloakTest { events.clear(); } finally { - testRealmRep.setSsoSessionMaxLifespanRememberMe(previousSsoMaxLifespanRememberMe); - testRealmRep.setRememberMe(previousRememberMe); - testRealm.update(testRealmRep); resetTimeOffset(); } }