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.
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.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);
}
}

View file

@ -90,15 +90,8 @@ public class MergedUpdate<S extends SessionEntity> 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<S extends SessionEntity> 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);
}

View file

@ -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<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) {
// 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);
}

View file

@ -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)) {

View file

@ -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;

View file

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

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

View file

@ -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();
}
}