8000 Use SessionExpirationUtils for validate user and client sessions. Check client session is valid in TokenManager. by rmartinc · Pull Request #29013 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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;
Expand Down Expand Up @@ -319,4 +320,34 @@ public Map<String, String> getNotes() {
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);
}

}
8000
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,8 @@ public static <S extends SessionEntity> MergedUpdate<S> computeUpdate(List<Sessi
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);
Expand All @@ -109,6 +102,13 @@ public static <S extends SessionEntity> MergedUpdate<S> computeUpdate(List<Sessi
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.util.Map;

import org.keycloak.common.util.Time;
import org.keycloak.sessions.CommonClientSessionModel;

/**
Expand Down Expand Up @@ -72,4 +73,20 @@ default boolean isUserSessionRememberMe() {
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()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, C
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");
}
Expand Down Expand Up @@ -198,6 +198,12 @@ public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, C
}
}

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");
Expand Down Expand Up @@ -567,7 +573,10 @@ public static ClientSessionContext attachAuthenticationSession(KeycloakSession s
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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
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;
Expand Down Expand Up @@ -107,10 +108,10 @@
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;

Expand Down Expand Up @@ -175,35 +176,33 @@ public static boolean isSessionValid(RealmModel realm, UserSessionModel userSess
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) {
Expand Down Expand Up @@ -1427,7 +1426,7 @@ public static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel
// 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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public static UserSessionModel findValidSession(KeycloakSession session, RealmMo
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,7 @@ public void testAuthenticationSessionExpiresEarlyAfterAuthentication() throws Ex
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,96 @@ public void refreshTokenUserClientMaxLifespanGreaterThanSession() throws Excepti
}
}

@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;
Expand Down
Loading
0