Use SessionExpirationUtils for validate user and client sessions

Check client session is valid in TokenManager
Closes #24936

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2024-04-23 12:34:13 +02:00 committed by Alexander Schwartz
parent f32cd91792
commit f7044ba5c2
10 changed files with 367 additions and 65 deletions

View file

@ -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. `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. 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.

View file

@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -319,4 +320,34 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
return copy; 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);
}
} }

View file

@ -90,15 +90,8 @@ public class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<
result.childUpdates.add(child); result.childUpdates.add(child);
} else { } else {
// Merge the operations. REMOVE is special case as other operations are not needed then. // Merge the operations.
CacheOperation mergedOp = result.getOperation().merge(child.getOperation(), session); result.operation = 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;
// Check if we need to send message to other DCs and how critical it is // Check if we need to send message to other DCs and how critical it is
CrossDCMessageStatus currentDCStatus = result.getCrossDCMessageStatus(sessionWrapper); CrossDCMessageStatus currentDCStatus = result.getCrossDCMessageStatus(sessionWrapper);
@ -109,6 +102,13 @@ public class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<
result.crossDCMessageStatus = currentDCStatus.merge(childDCStatus); 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 // Finally add another update to the result
result.childUpdates.add(child); result.childUpdates.add(child);
} }

View file

@ -20,6 +20,7 @@ package org.keycloak.models;
import java.util.Map; import java.util.Map;
import org.keycloak.common.util.Time;
import org.keycloak.sessions.CommonClientSessionModel; import org.keycloak.sessions.CommonClientSessionModel;
/** /**
@ -72,4 +73,20 @@ public interface AuthenticatedClientSessionModel extends CommonClientSessionMode
void setNote(String name, String value); void setNote(String name, String value);
void removeNote(String name); void removeNote(String name);
Map<String, String> getNotes(); Map<String, String> 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()));
}
} }

View file

@ -153,7 +153,7 @@ public class TokenManager {
if (userSession != null) { if (userSession != null) {
// Revoke timeouted offline userSession // Revoke timeouted offline userSession
if (!AuthenticationManager.isOfflineSessionValid(realm, userSession)) { if (!AuthenticationManager.isSessionValid(realm, userSession)) {
sessionManager.revokeOfflineUserSession(userSession); sessionManager.revokeOfflineUserSession(userSession);
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline session not active", "Offline session not active"); 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())) { if (oldToken.isIssuedBeforeSessionStart(clientSession.getStarted())) {
logger.debug("refresh token issued before the client session started"); logger.debug("refresh token issued before the client session started");
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "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(); ClientModel client = authSession.getClient();
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); 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); clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession);
} }

View file

@ -65,6 +65,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.DefaultRequiredActions; import org.keycloak.models.utils.DefaultRequiredActions;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.SessionExpirationUtils;
import org.keycloak.models.utils.SessionTimeoutHelper; import org.keycloak.models.utils.SessionTimeoutHelper;
import org.keycloak.models.utils.SystemClientUtil; import org.keycloak.models.utils.SystemClientUtil;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
@ -107,10 +108,10 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; 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.models.UserSessionModel.CORRESPONDING_SESSION_ID;
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.isOAuth2DeviceVerificationFlow; import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.isOAuth2DeviceVerificationFlow;
@ -175,35 +176,33 @@ public class AuthenticationManager {
logger.debug("No user session"); logger.debug("No user session");
return false; 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 boolean sessionIdleOk = idle > currentTime - TimeUnit.SECONDS.toMillis(SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS);
int maxIdle = userSession.isRememberMe() && realm.getSsoSessionIdleTimeoutRememberMe() > 0 ? boolean sessionMaxOk = lifespan == -1L || lifespan > currentTime;
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();
return sessionIdleOk && sessionMaxOk; return sessionIdleOk && sessionMaxOk;
} }
public static boolean isOfflineSessionValid(RealmModel realm, UserSessionModel userSession) { public static boolean isClientSessionValid(RealmModel realm, ClientModel client,
if (userSession == null) { UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
logger.debug("No offline user session"); if (userSession == null || clientSession == null) {
logger.debug("No user session");
return false; return false;
} }
int currentTime = Time.currentTime(); long currentTime = Time.currentTimeMillis();
// Additional time window is added for the case when session was updated in different DC and the update to current DC was postponed long lifespan = SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(userSession.isOffline(),
int maxIdle = realm.getOfflineSessionIdleTimeout() + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS; 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 boolean sessionIdleOk = idle > currentTime - TimeUnit.SECONDS.toMillis(SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS);
if (realm.isOfflineSessionMaxLifespanEnabled()) { boolean sessionMaxOk = lifespan == -1L || lifespan > currentTime;
int max = userSession.getStarted() + realm.getOfflineSessionMaxLifespan(); return sessionIdleOk && sessionMaxOk;
return userSession.getLastSessionRefresh() + maxIdle > currentTime && max > currentTime;
} else {
return userSession.getLastSessionRefresh() + maxIdle > currentTime;
}
} }
public static boolean expireUserSessionCookie(KeycloakSession session, UserSessionModel userSession, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, ClientConnection connection) { 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. // Check if accessToken was for the offline session.
if (!isCookie) { if (!isCookie) {
UserSessionModel offlineUserSession = session.sessions().getOfflineUserSession(realm, token.getSessionState()); UserSessionModel offlineUserSession = session.sessions().getOfflineUserSession(realm, token.getSessionState());
if (isOfflineSessionValid(realm, offlineUserSession)) { if (isSessionValid(realm, offlineUserSession)) {
user = offlineUserSession.getUser(); user = offlineUserSession.getUser();
ClientModel client = realm.getClientByClientId(token.getIssuedFor()); ClientModel client = realm.getClientByClientId(token.getIssuedFor());
if (!isClientValid(offlineUserSession, client, token)) { if (!isClientValid(offlineUserSession, client, token)) {

View file

@ -54,7 +54,7 @@ public class UserSessionUtil {
return userSession; return userSession;
} else { } else {
offlineUserSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionId(), true, client.getId()); 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); checkTokenIssuedAt(realm, token, offlineUserSession, event, client);
event.session(offlineUserSession); event.session(offlineUserSession);
return offlineUserSession; return offlineUserSession;

View file

@ -918,6 +918,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
public void loginRememberMeExpiredIdle() throws Exception { public void loginRememberMeExpiredIdle() throws Exception {
try (Closeable c = new RealmAttributeUpdater(adminClient.realm("test")) try (Closeable c = new RealmAttributeUpdater(adminClient.realm("test"))
.setSsoSessionIdleTimeoutRememberMe(1) .setSsoSessionIdleTimeoutRememberMe(1)
.setSsoSessionIdleTimeout(1) // max of both values
.setRememberMe(true) .setRememberMe(true)
.update()) { .update()) {
// login form shown after redirect from app // login form shown after redirect from app

View file

@ -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 @Test
public void testShortOfflineSessionMax() throws Exception { public void testShortOfflineSessionMax() throws Exception {
int prevOfflineSession[] = null; int prevOfflineSession[] = null;

View file

@ -1163,14 +1163,13 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
@Test @Test
public void testUserSessionRefreshAndIdleRememberMe() throws Exception { public void testUserSessionRefreshAndIdleRememberMe() throws Exception {
RealmResource testRealm = adminClient.realm("test"); 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"); oauth.doRememberMeLogin("test-user@localhost", "password");
EventRepresentation loginEvent = events.expectLogin().assertEvent(); EventRepresentation loginEvent = events.expectLogin().assertEvent();
@ -1185,7 +1184,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(2); setTimeOffset(110 + 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());
@ -1194,12 +1193,9 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
int next = testingClient.testing().getLastSessionRefresh("test", sessionId, false); int next = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
Assert.assertNotEquals(last, next); Assert.assertNotEquals(last, next);
testRealmRep.setSsoSessionIdleTimeoutRememberMe(1);
testRealm.update(testRealmRep);
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(620 + 2 * 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
@ -1211,9 +1207,6 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
events.clear(); events.clear();
} finally { } finally {
testRealmRep.setSsoSessionIdleTimeoutRememberMe(originalIdleRememberMe);
testRealmRep.setRememberMe(previousRememberMe);
testRealm.update(testRealmRep);
resetTimeOffset(); 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 * KEYCLOAK-1267
* @throws Exception * @throws Exception
@ -1407,13 +1562,13 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
public void refreshTokenUserSessionMaxLifespanWithRememberMe() throws Exception { public void refreshTokenUserSessionMaxLifespanWithRememberMe() throws Exception {
RealmResource testRealm = adminClient.realm("test"); RealmResource testRealm = adminClient.realm("test");
RealmRepresentation testRealmRep = testRealm.toRepresentation();
Boolean previousRememberMe = testRealmRep.isRememberMe();
int previousSsoMaxLifespanRememberMe = testRealmRep.getSsoSessionMaxLifespanRememberMe();
try { try (Closeable realmUpdater = new RealmAttributeUpdater(testRealm)
testRealmRep.setRememberMe(true); .updateWith(r -> {
testRealm.update(testRealmRep); r.setRememberMe(true);
r.setSsoSessionMaxLifespanRememberMe(100);
r.setSsoSessionMaxLifespan(50);
}).update()) {
oauth.doRememberMeLogin("test-user@localhost", "password"); oauth.doRememberMeLogin("test-user@localhost", "password");
@ -1428,10 +1583,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId(); String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
testRealmRep.setSsoSessionMaxLifespanRememberMe(1); setTimeOffset(110);
testRealm.update(testRealmRep);
setTimeOffset(2);
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password"); tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
@ -1443,9 +1595,6 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
events.clear(); events.clear();
} finally { } finally {
testRealmRep.setSsoSessionMaxLifespanRememberMe(previousSsoMaxLifespanRememberMe);
testRealmRep.setRememberMe(previousRememberMe);
testRealm.update(testRealmRep);
resetTimeOffset(); resetTimeOffset();
} }
} }