diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml
index 07a187a519..3d5b99f1ab 100644
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml
@@ -4,6 +4,9 @@
+
+
+
@@ -47,16 +50,11 @@
+
-
-
-
-
-
-
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 36da3e89e8..f13d6e8e65 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -339,6 +339,7 @@ show-offline-tokens=Show Offline Tokens
show-offline-tokens.tooltip=Warning, this is a potentially expensive operation depending on number of offline tokens.
token-issued=Token Issued
last-access=Last Access
+last-refresh=Last Refresh
key-export=Key Export
key-import=Key Import
export-saml-key=Export SAML Key
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html
index 82f5562e06..3d2aaf7a1b 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html
@@ -31,7 +31,7 @@
{{:: 'user' | translate}} |
{{:: 'from-ip' | translate}} |
{{:: 'token-issued' | translate}} |
- {{:: 'last-access' | translate}} |
+ {{:: 'last-refresh' | translate}} |
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-offline-sessions.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-offline-sessions.html
index b06f32625b..bc7ad501bd 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-offline-sessions.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-offline-sessions.html
@@ -13,7 +13,7 @@
IP Address |
Started |
- Last Access |
+ Last Refresh |
diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
index 836cc75769..1a59f4ffef 100755
--- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
@@ -59,6 +59,10 @@ public interface UserSessionProvider extends Provider {
int getOfflineSessionsCount(RealmModel realm, ClientModel client);
List getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max);
+ // Triggered by persister during pre-load
+ UserSessionModel importUserSession(UserSessionModel persistentUserSession, boolean offline);
+ ClientSessionModel importClientSession(ClientSessionModel persistentClientSession, boolean offline);
+
void close();
}
diff --git a/model/api/src/main/java/org/keycloak/models/entities/PersistentClientSessionEntity.java b/model/api/src/main/java/org/keycloak/models/entities/PersistentClientSessionEntity.java
index a03edd31d5..1c1802ef6e 100644
--- a/model/api/src/main/java/org/keycloak/models/entities/PersistentClientSessionEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/PersistentClientSessionEntity.java
@@ -7,6 +7,7 @@ public class PersistentClientSessionEntity {
private String clientSessionId;
private String clientId;
+ private int timestamp;
private String data;
public String getClientSessionId() {
@@ -25,6 +26,14 @@ public class PersistentClientSessionEntity {
this.clientId = clientId;
}
+ public int getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(int timestamp) {
+ this.timestamp = timestamp;
+ }
+
public String getData() {
return data;
}
diff --git a/model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java b/model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java
index 6daf7c749c..809fbd29e0 100644
--- a/model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java
@@ -92,6 +92,11 @@ public class DisabledUserSessionPersisterProvider implements UserSessionPersiste
}
+ @Override
+ public void updateAllTimestamps(int time) {
+
+ }
+
@Override
public List loadUserSessions(int firstResult, int maxResults, boolean offline) {
return Collections.emptyList();
diff --git a/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java b/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java
index 1fced88bbe..8465269e85 100644
--- a/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java
+++ b/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java
@@ -37,7 +37,6 @@ public class PersistentClientSessionAdapter implements ClientSessionModel {
data.setProtocolMappers(clientSession.getProtocolMappers());
data.setRedirectUri(clientSession.getRedirectUri());
data.setRoles(clientSession.getRoles());
- data.setTimestamp(clientSession.getTimestamp());
data.setUserSessionNotes(clientSession.getUserSessionNotes());
model = new PersistentClientSessionModel();
@@ -47,6 +46,7 @@ public class PersistentClientSessionAdapter implements ClientSessionModel {
model.setUserId(clientSession.getAuthenticatedUser().getId());
}
model.setUserSessionId(clientSession.getUserSession().getId());
+ model.setTimestamp(clientSession.getTimestamp());
realm = clientSession.getRealm();
client = clientSession.getClient();
@@ -122,12 +122,12 @@ public class PersistentClientSessionAdapter implements ClientSessionModel {
@Override
public int getTimestamp() {
- return getData().getTimestamp();
+ return model.getTimestamp();
}
@Override
public void setTimestamp(int timestamp) {
- getData().setTimestamp(timestamp);
+ model.setTimestamp(timestamp);
}
@Override
@@ -309,9 +309,6 @@ public class PersistentClientSessionAdapter implements ClientSessionModel {
@JsonProperty("executionStatus")
private Map executionStatus = new HashMap<>();
- @JsonProperty("timestamp")
- private int timestamp;
-
@JsonProperty("action")
private String action;
@@ -374,14 +371,6 @@ public class PersistentClientSessionAdapter implements ClientSessionModel {
this.executionStatus = executionStatus;
}
- public int getTimestamp() {
- return timestamp;
- }
-
- public void setTimestamp(int timestamp) {
- this.timestamp = timestamp;
- }
-
public String getAction() {
return action;
}
diff --git a/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java b/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java
index 96e900fb7b..b1a388b82e 100644
--- a/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java
+++ b/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java
@@ -9,6 +9,7 @@ public class PersistentClientSessionModel {
private String userSessionId;
private String clientId;
private String userId;
+ private int timestamp;
private String data;
public String getClientSessionId() {
@@ -43,6 +44,14 @@ public class PersistentClientSessionModel {
this.userId = userId;
}
+ public int getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(int timestamp) {
+ this.timestamp = timestamp;
+ }
+
public String getData() {
return data;
}
diff --git a/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java
index 4b3355e9d3..5863fdb4a3 100644
--- a/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java
@@ -35,6 +35,9 @@ public interface UserSessionPersisterProvider extends Provider {
// Called at startup to remove userSessions without any clientSession
void clearDetachedUserSessions();
+ // Update "lastSessionRefresh" of all userSessions and "timestamp" of all clientSessions to specified time
+ void updateAllTimestamps(int time);
+
// Called during startup. For each userSession, it loads also clientSessions
List loadUserSessions(int firstResult, int maxResults, boolean offline);
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
index ffc04557b1..bf19d96980 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
@@ -58,6 +58,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
PersistentClientSessionEntity entity = new PersistentClientSessionEntity();
entity.setClientSessionId(clientSession.getId());
entity.setClientId(clientSession.getClient().getId());
+ entity.setTimestamp(clientSession.getTimestamp());
entity.setOffline(offline);
entity.setUserSessionId(clientSession.getUserSession().getId());
entity.setData(model.getData());
@@ -128,26 +129,32 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
@Override
public void onRealmRemoved(RealmModel realm) {
- em.createNamedQuery("deleteClientSessionsByRealm").setParameter("realmId", realm.getId()).executeUpdate();
- em.createNamedQuery("deleteUserSessionsByRealm").setParameter("realmId", realm.getId()).executeUpdate();
+ int num = em.createNamedQuery("deleteClientSessionsByRealm").setParameter("realmId", realm.getId()).executeUpdate();
+ num = em.createNamedQuery("deleteUserSessionsByRealm").setParameter("realmId", realm.getId()).executeUpdate();
}
@Override
public void onClientRemoved(RealmModel realm, ClientModel client) {
- em.createNamedQuery("deleteClientSessionsByClient").setParameter("clientId", client.getId()).executeUpdate();
- em.createNamedQuery("deleteDetachedUserSessions").executeUpdate();
+ int num = em.createNamedQuery("deleteClientSessionsByClient").setParameter("clientId", client.getId()).executeUpdate();
+ num = em.createNamedQuery("deleteDetachedUserSessions").executeUpdate();
}
@Override
public void onUserRemoved(RealmModel realm, UserModel user) {
- em.createNamedQuery("deleteClientSessionsByUser").setParameter("userId", user.getId()).executeUpdate();
- em.createNamedQuery("deleteUserSessionsByUser").setParameter("userId", user.getId()).executeUpdate();
+ int num = em.createNamedQuery("deleteClientSessionsByUser").setParameter("userId", user.getId()).executeUpdate();
+ num = em.createNamedQuery("deleteUserSessionsByUser").setParameter("userId", user.getId()).executeUpdate();
}
@Override
public void clearDetachedUserSessions() {
- em.createNamedQuery("deleteDetachedClientSessions").executeUpdate();
- em.createNamedQuery("deleteDetachedUserSessions").executeUpdate();
+ int num = em.createNamedQuery("deleteDetachedClientSessions").executeUpdate();
+ num = em.createNamedQuery("deleteDetachedUserSessions").executeUpdate();
+ }
+
+ @Override
+ public void updateAllTimestamps(int time) {
+ int num = em.createNamedQuery("updateClientSessionsTimestamps").setParameter("timestamp", time).executeUpdate();
+ num = em.createNamedQuery("updateUserSessionsTimestamps").setParameter("lastSessionRefresh", time).executeUpdate();
}
@Override
@@ -220,6 +227,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
model.setClientId(entity.getClientId());
model.setUserSessionId(userSession.getId());
model.setUserId(userSession.getUser().getId());
+ model.setTimestamp(entity.getTimestamp());
model.setData(entity.getData());
return new PersistentClientSessionAdapter(model, realm, client, userSession);
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java
index a11b87516a..faf3f80556 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java
@@ -17,13 +17,14 @@ import javax.persistence.Table;
* @author Marek Posolda
*/
@NamedQueries({
- @NamedQuery(name="deleteClientSessionsByRealm", query="delete from PersistentClientSessionEntity sess where sess.userSessionId in (select u from PersistentUserSessionEntity u where u.realmId=:realmId)"),
+ @NamedQuery(name="deleteClientSessionsByRealm", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.realmId=:realmId)"),
@NamedQuery(name="deleteClientSessionsByClient", query="delete from PersistentClientSessionEntity sess where sess.clientId=:clientId"),
- @NamedQuery(name="deleteClientSessionsByUser", query="delete from PersistentClientSessionEntity sess where sess.userSessionId in (select u from PersistentUserSessionEntity u where u.userId=:userId)"),
+ @NamedQuery(name="deleteClientSessionsByUser", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.userId=:userId)"),
@NamedQuery(name="deleteClientSessionsByUserSession", query="delete from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and offline=:offline"),
@NamedQuery(name="deleteDetachedClientSessions", query="delete from PersistentClientSessionEntity sess where sess.userSessionId NOT IN (select u.userSessionId from PersistentUserSessionEntity u)"),
@NamedQuery(name="findClientSessionsByUserSession", query="select sess from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and offline=:offline"),
@NamedQuery(name="findClientSessionsByUserSessions", query="select sess from PersistentClientSessionEntity sess where offline=:offline and sess.userSessionId IN (:userSessionIds) order by sess.userSessionId"),
+ @NamedQuery(name="updateClientSessionsTimestamps", query="update PersistentClientSessionEntity c set timestamp=:timestamp"),
})
@Table(name="OFFLINE_CLIENT_SESSION")
@Entity
@@ -40,6 +41,9 @@ public class PersistentClientSessionEntity {
@Column(name="CLIENT_ID", length = 36)
protected String clientId;
+ @Column(name="TIMESTAMP")
+ protected int timestamp;
+
@Id
@Column(name = "OFFLINE")
protected boolean offline;
@@ -71,6 +75,14 @@ public class PersistentClientSessionEntity {
this.clientId = clientId;
}
+ public int getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(int timestamp) {
+ this.timestamp = timestamp;
+ }
+
public boolean isOffline() {
return offline;
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java
index f739091fe9..95745a823a 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java
@@ -26,7 +26,8 @@ import org.keycloak.models.jpa.entities.UserEntity;
@NamedQuery(name="deleteUserSessionsByUser", query="delete from PersistentUserSessionEntity sess where sess.userId=:userId"),
@NamedQuery(name="deleteDetachedUserSessions", query="delete from PersistentUserSessionEntity sess where sess.userSessionId NOT IN (select c.userSessionId from PersistentClientSessionEntity c)"),
@NamedQuery(name="findUserSessionsCount", query="select count(sess) from PersistentUserSessionEntity sess where offline=:offline"),
- @NamedQuery(name="findUserSessions", query="select sess from PersistentUserSessionEntity sess where offline=:offline order by sess.userSessionId")
+ @NamedQuery(name="findUserSessions", query="select sess from PersistentUserSessionEntity sess where offline=:offline order by sess.userSessionId"),
+ @NamedQuery(name="updateUserSessionsTimestamps", query="update PersistentUserSessionEntity c set lastSessionRefresh=:lastSessionRefresh"),
})
@Table(name="OFFLINE_USER_SESSION")
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java
index 53917c2b11..f23e7fbe99 100644
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java
@@ -45,10 +45,6 @@ public class MongoUserSessionPersisterProvider implements UserSessionPersisterPr
return invocationContext.getMongoStore();
}
- private Class extends PersistentUserSessionEntity> getClazz(boolean offline) {
- return offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class;
- }
-
private MongoUserSessionEntity loadUserSession(String userSessionId, boolean offline) {
Class extends MongoUserSessionEntity> clazz = offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class;
return getMongoStore().loadEntity(clazz, userSessionId, invocationContext);
@@ -220,6 +216,41 @@ public class MongoUserSessionPersisterProvider implements UserSessionPersisterPr
return getMongoStore().countEntities(clazz, query, invocationContext);
}
+ @Override
+ public void updateAllTimestamps(int time) {
+ // 1) Update timestamp of clientSessions
+
+ DBObject timestampSubquery = new QueryBuilder()
+ .and("timestamp").notEquals(time).get();
+
+ DBObject query = new QueryBuilder()
+ .and("clientSessions").elemMatch(timestampSubquery).get();
+
+
+ DBObject update = new QueryBuilder()
+ .and("$set").is(new BasicDBObject("clientSessions.$.timestamp", time)).get();
+
+ // Not sure how to do in single query :/
+ int countModified = 1;
+ while (countModified > 0) {
+ countModified = getMongoStore().updateEntities(MongoOfflineUserSessionEntity.class, query, update, invocationContext);
+ }
+
+ countModified = 1;
+ while (countModified > 0) {
+ countModified = getMongoStore().updateEntities(MongoOnlineUserSessionEntity.class, query, update, invocationContext);
+ }
+
+ // 2) update lastSessionRefresh of userSessions
+ query = new QueryBuilder().get();
+
+ update = new QueryBuilder()
+ .and("$set").is(new BasicDBObject("lastSessionRefresh", time)).get();
+
+ getMongoStore().updateEntities(MongoOfflineUserSessionEntity.class, query, update, invocationContext);
+ getMongoStore().updateEntities(MongoOnlineUserSessionEntity.class, query, update, invocationContext);
+ }
+
@Override
public List loadUserSessions(int firstResult, int maxResults, boolean offline) {
DBObject query = new QueryBuilder()
@@ -232,13 +263,13 @@ public class MongoUserSessionPersisterProvider implements UserSessionPersisterPr
List results = new LinkedList<>();
for (MongoUserSessionEntity entity : entities) {
- PersistentUserSessionAdapter userSession = toAdapter(entity, offline);
+ PersistentUserSessionAdapter userSession = toAdapter(entity);
results.add(userSession);
}
return results;
}
- private PersistentUserSessionAdapter toAdapter(PersistentUserSessionEntity entity, boolean offline) {
+ private PersistentUserSessionAdapter toAdapter(PersistentUserSessionEntity entity) {
RealmModel realm = session.realms().getRealm(entity.getRealmId());
UserModel user = session.users().getUserById(entity.getUserId(), realm);
@@ -250,14 +281,14 @@ public class MongoUserSessionPersisterProvider implements UserSessionPersisterPr
List clientSessions = new LinkedList<>();
PersistentUserSessionAdapter userSessionAdapter = new PersistentUserSessionAdapter(model, realm, user, clientSessions);
for (PersistentClientSessionEntity clientSessEntity : entity.getClientSessions()) {
- PersistentClientSessionAdapter clientSessAdapter = toAdapter(realm, userSessionAdapter, offline, clientSessEntity);
+ PersistentClientSessionAdapter clientSessAdapter = toAdapter(realm, userSessionAdapter, clientSessEntity);
clientSessions.add(clientSessAdapter);
}
return userSessionAdapter;
}
- private PersistentClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, boolean offline, PersistentClientSessionEntity entity) {
+ private PersistentClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, PersistentClientSessionEntity entity) {
ClientModel client = realm.getClientById(entity.getClientId());
PersistentClientSessionModel model = new PersistentClientSessionModel();
@@ -265,6 +296,7 @@ public class MongoUserSessionPersisterProvider implements UserSessionPersisterPr
model.setClientId(entity.getClientId());
model.setUserSessionId(userSession.getId());
model.setUserId(userSession.getUser().getId());
+ model.setTimestamp(entity.getTimestamp());
model.setData(entity.getData());
return new PersistentClientSessionAdapter(model, realm, client, userSession);
}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 34cc4bc3c9..627edcfe96 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -329,25 +329,32 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
// Remove expired offline user sessions
- map = new MapReduceTask(offlineSessionCache)
- .mappedWith(UserSessionMapper.create(realm.getId()).expired(null, expiredOffline).emitKey())
+ Map map2 = new MapReduceTask(offlineSessionCache)
+ .mappedWith(UserSessionMapper.create(realm.getId()).expired(null, expiredOffline))
.reducedWith(new FirstResultReducer())
.execute();
- for (String id : map.keySet()) {
- tx.remove(offlineSessionCache, id);
- // propagate to persister
- persister.removeUserSession(id, true);
+ for (Map.Entry entry : map2.entrySet()) {
+ String userSessionId = entry.getKey();
+ tx.remove(offlineSessionCache, userSessionId);
+ // Propagate to persister
+ persister.removeUserSession(userSessionId, true);
+
+ UserSessionEntity entity = (UserSessionEntity) entry.getValue();
+ for (String clientSessionId : entity.getClientSessions()) {
+ tx.remove(offlineSessionCache, clientSessionId);
+ }
}
- // Remove offline client sessions of expired offline user sessions
+ // Remove expired offline client sessions
map = new MapReduceTask(offlineSessionCache)
- .mappedWith(new ClientSessionsOfUserSessionMapper(realm.getId(), new HashSet<>(map.keySet())).emitKey())
+ .mappedWith(ClientSessionMapper.create(realm.getId()).expiredRefresh(expiredOffline).emitKey())
.reducedWith(new FirstResultReducer())
.execute();
- for (String id : map.keySet()) {
- tx.remove(offlineSessionCache, id);
+ for (String clientSessionId : map.keySet()) {
+ tx.remove(offlineSessionCache, clientSessionId);
+ persister.removeClientSession(clientSessionId, true);
}
}
@@ -504,7 +511,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
tx.remove(cache, userSessionId);
- // TODO: We can retrieve it from userSessionEntity directly
+ // TODO: Isn't more effective to retrieve from userSessionEntity directly?
Map map = new MapReduceTask(cache)
.mappedWith(ClientSessionMapper.create(realm.getId()).userSession(userSessionId).emitKey())
.reducedWith(new FirstResultReducer())
@@ -554,27 +561,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public UserSessionModel createOfflineUserSession(UserSessionModel userSession) {
- UserSessionEntity entity = new UserSessionEntity();
- entity.setId(userSession.getId());
- entity.setRealm(userSession.getRealm().getId());
-
- entity.setAuthMethod(userSession.getAuthMethod());
- entity.setBrokerSessionId(userSession.getBrokerSessionId());
- entity.setBrokerUserId(userSession.getBrokerUserId());
- entity.setIpAddress(userSession.getIpAddress());
- entity.setLoginUsername(userSession.getLoginUsername());
- entity.setNotes(userSession.getNotes());
- entity.setRememberMe(userSession.isRememberMe());
- entity.setState(userSession.getState());
- entity.setUser(userSession.getUser().getId());
+ UserSessionAdapter offlineUserSession = importUserSession(userSession, true);
// started and lastSessionRefresh set to current time
int currentTime = Time.currentTime();
- entity.setStarted(currentTime);
- entity.setLastSessionRefresh(currentTime);
+ offlineUserSession.getEntity().setStarted(currentTime);
+ offlineUserSession.setLastSessionRefresh(currentTime);
- tx.put(offlineSessionCache, userSession.getId(), entity);
- return wrap(userSession.getRealm(), entity, true);
+ return offlineUserSession;
}
@Override
@@ -589,26 +583,12 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public ClientSessionModel createOfflineClientSession(ClientSessionModel clientSession) {
- ClientSessionEntity entity = new ClientSessionEntity();
- entity.setId(clientSession.getId());
- entity.setRealm(clientSession.getRealm().getId());
+ ClientSessionAdapter offlineClientSession = importClientSession(clientSession, true);
- entity.setAction(clientSession.getAction());
- entity.setAuthenticatorStatus(clientSession.getExecutionStatus());
- entity.setAuthMethod(clientSession.getAuthMethod());
- if (clientSession.getAuthenticatedUser() != null) {
- entity.setAuthUserId(clientSession.getAuthenticatedUser().getId());
- }
- entity.setClient(clientSession.getClient().getId());
- entity.setNotes(clientSession.getNotes());
- entity.setProtocolMappers(clientSession.getProtocolMappers());
- entity.setRedirectUri(clientSession.getRedirectUri());
- entity.setRoles(clientSession.getRoles());
- entity.setTimestamp(clientSession.getTimestamp());
- entity.setUserSessionNotes(clientSession.getUserSessionNotes());
+ // update timestamp to current time
+ offlineClientSession.setTimestamp(Time.currentTime());
- tx.put(offlineSessionCache, clientSession.getId(), entity);
- return wrap(clientSession.getRealm(), entity, true);
+ return offlineClientSession;
}
@Override
@@ -653,6 +633,55 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return getUserSessions(realm, client, first, max, true);
}
+ @Override
+ public UserSessionAdapter importUserSession(UserSessionModel userSession, boolean offline) {
+ UserSessionEntity entity = new UserSessionEntity();
+ entity.setId(userSession.getId());
+ entity.setRealm(userSession.getRealm().getId());
+
+ entity.setAuthMethod(userSession.getAuthMethod());
+ entity.setBrokerSessionId(userSession.getBrokerSessionId());
+ entity.setBrokerUserId(userSession.getBrokerUserId());
+ entity.setIpAddress(userSession.getIpAddress());
+ entity.setLoginUsername(userSession.getLoginUsername());
+ entity.setNotes(userSession.getNotes());
+ entity.setRememberMe(userSession.isRememberMe());
+ entity.setState(userSession.getState());
+ entity.setUser(userSession.getUser().getId());
+
+ entity.setStarted(userSession.getStarted());
+ entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
+
+ Cache cache = getCache(offline);
+ tx.put(cache, userSession.getId(), entity);
+ return wrap(userSession.getRealm(), entity, offline);
+ }
+
+ @Override
+ public ClientSessionAdapter importClientSession(ClientSessionModel clientSession, boolean offline) {
+ ClientSessionEntity entity = new ClientSessionEntity();
+ entity.setId(clientSession.getId());
+ entity.setRealm(clientSession.getRealm().getId());
+
+ entity.setAction(clientSession.getAction());
+ entity.setAuthenticatorStatus(clientSession.getExecutionStatus());
+ entity.setAuthMethod(clientSession.getAuthMethod());
+ if (clientSession.getAuthenticatedUser() != null) {
+ entity.setAuthUserId(clientSession.getAuthenticatedUser().getId());
+ }
+ entity.setClient(clientSession.getClient().getId());
+ entity.setNotes(clientSession.getNotes());
+ entity.setProtocolMappers(clientSession.getProtocolMappers());
+ entity.setRedirectUri(clientSession.getRedirectUri());
+ entity.setRoles(clientSession.getRoles());
+ entity.setTimestamp(clientSession.getTimestamp());
+ entity.setUserSessionNotes(clientSession.getUserSessionNotes());
+
+ Cache cache = getCache(offline);
+ tx.put(cache, clientSession.getId(), entity);
+ return wrap(clientSession.getRealm(), entity, offline);
+ }
+
class InfinispanKeycloakTransaction implements KeycloakTransaction {
private boolean active;
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
index c88e4901f8..1d7c279542 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
@@ -63,20 +63,12 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
if (compatMode) {
compatProviderFactory = new MemUserSessionProviderFactory();
}
-
- log.debug("Clearing detached sessions from persistent storage");
- UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
- if (persister == null) {
- throw new RuntimeException("userSessionPersister not configured. Please see the migration docs and upgrade your configuration");
- } else {
- persister.clearDetachedUserSessions();
- }
}
});
// Max count of worker errors. Initialization will end with exception when this number is reached
- int maxErrors = config.getInt("maxErrors", 50);
+ int maxErrors = config.getInt("maxErrors", 20);
// Count of sessions to be computed in each segment
int sessionsPerSegment = config.getInt("sessionsPerSegment", 100);
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java
index 6cbb1eb82c..23c1286c4d 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java
@@ -318,7 +318,7 @@ public class MemUserSessionProvider implements UserSessionProvider {
}
}
- // Remove expired offline sessions
+ // Remove expired offline user sessions
itr = offlineUserSessions.values().iterator();
while (itr.hasNext()) {
UserSessionEntity s = itr.next();
@@ -330,6 +330,18 @@ public class MemUserSessionProvider implements UserSessionProvider {
persister.removeUserSession(s.getId(), true);
}
}
+
+ // Remove expired offline client sessions
+ citr = offlineClientSessions.values().iterator();
+ while (citr.hasNext()) {
+ ClientSessionEntity s = citr.next();
+ if (s.getRealmId().equals(realm.getId()) && (s.getTimestamp() < Time.currentTime() - realm.getOfflineSessionIdleTimeout())) {
+ citr.remove();
+
+ // propagate to persister
+ persister.removeClientSession(s.getId(), true);
+ }
+ }
}
@Override
@@ -423,6 +435,18 @@ public class MemUserSessionProvider implements UserSessionProvider {
@Override
public UserSessionModel createOfflineUserSession(UserSessionModel userSession) {
+ UserSessionAdapter importedUserSession = importUserSession(userSession, true);
+
+ // started and lastSessionRefresh set to current time
+ int currentTime = Time.currentTime();
+ importedUserSession.getEntity().setStarted(currentTime);
+ importedUserSession.setLastSessionRefresh(currentTime);
+
+ return importedUserSession;
+ }
+
+ @Override
+ public UserSessionAdapter importUserSession(UserSessionModel userSession, boolean offline) {
UserSessionEntity entity = new UserSessionEntity();
entity.setId(userSession.getId());
entity.setRealm(userSession.getRealm().getId());
@@ -439,12 +463,11 @@ public class MemUserSessionProvider implements UserSessionProvider {
entity.setState(userSession.getState());
entity.setUser(userSession.getUser().getId());
- // started and lastSessionRefresh set to current time
- int currentTime = Time.currentTime();
- entity.setStarted(currentTime);
- entity.setLastSessionRefresh(currentTime);
+ entity.setStarted(userSession.getStarted());
+ entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
- offlineUserSessions.put(userSession.getId(), entity);
+ ConcurrentHashMap sessionsMap = offline ? offlineUserSessions : userSessions;
+ sessionsMap.put(userSession.getId(), entity);
return new UserSessionAdapter(session, this, userSession.getRealm(), entity);
}
@@ -469,6 +492,17 @@ public class MemUserSessionProvider implements UserSessionProvider {
@Override
public ClientSessionModel createOfflineClientSession(ClientSessionModel clientSession) {
+ ClientSessionAdapter offlineClientSession = importClientSession(clientSession, true);
+
+ // update timestamp to current time
+ offlineClientSession.setTimestamp(Time.currentTime());
+
+ return offlineClientSession;
+ }
+
+ @Override
+ public ClientSessionAdapter importClientSession(ClientSessionModel clientSession, boolean offline) {
+
ClientSessionEntity entity = new ClientSessionEntity();
entity.setId(clientSession.getId());
entity.setRealmId(clientSession.getRealm().getId());
@@ -492,7 +526,8 @@ public class MemUserSessionProvider implements UserSessionProvider {
entity.getUserSessionNotes().putAll(clientSession.getUserSessionNotes());
}
- offlineClientSessions.put(clientSession.getId(), entity);
+ ConcurrentHashMap clientSessionsMap = offline ? offlineClientSessions : clientSessions;
+ clientSessionsMap.put(clientSession.getId(), entity);
return new ClientSessionAdapter(session, this, clientSession.getRealm(), entity);
}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java
index 450bbe14b2..cb5a7e74f7 100644
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java
@@ -22,11 +22,23 @@ public class SimpleUserSessionInitializer {
}
public void loadPersistentSessions() {
+ // Rather use separate transactions for update and loading
+
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ sessionLoader.init(session);
+ }
+
+ });
+
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
int count = sessionLoader.getSessionsCount(session);
+
for (int i=0 ; i<=count ; i+=sessionsPerSegment) {
sessionLoader.loadSessions(session, i, sessionsPerSegment);
}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
index 0d038bd6a2..89f2d4f56a 100644
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
@@ -82,8 +82,8 @@ public class InfinispanUserSessionInitializer {
private boolean isFinished() {
- InitializerState stateEntity = (InitializerState) cache.get(stateKey);
- return stateEntity != null && stateEntity.isFinished();
+ InitializerState state = (InitializerState) cache.get(stateKey);
+ return state != null && state.isFinished();
}
@@ -92,6 +92,16 @@ public class InfinispanUserSessionInitializer {
if (state == null) {
final int[] count = new int[1];
+ // Rather use separate transactions for update and counting
+
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+ @Override
+ public void run(KeycloakSession session) {
+ sessionLoader.init(session);
+ }
+
+ });
+
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
@@ -133,7 +143,7 @@ public class InfinispanUserSessionInitializer {
}
- // Just coordinator is supposed to run this
+ // Just coordinator will run this
private void startLoading() {
InitializerState state = getOrCreateInitializerState();
@@ -196,7 +206,7 @@ public class InfinispanUserSessionInitializer {
saveStateToCache(state);
// TODO
- log.info("New initializer state pushed. The state is: " + state.printState(false));
+ log.info("New initializer state pushed. The state is: " + state.printState());
}
} finally {
distributedExecutorService.shutdown();
@@ -225,7 +235,7 @@ public class InfinispanUserSessionInitializer {
@ViewChanged
public void viewChanged(ViewChangedEvent event) {
boolean isCoordinator = isCoordinator();
- // TODO:
+ // TODO: debug
log.info("View Changed: is coordinator: " + isCoordinator);
if (isCoordinator) {
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java
index ccc6fd6a69..6066077779 100644
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java
@@ -26,8 +26,8 @@ public class InitializerState extends SessionEntity {
segmentsCount = segmentsCount + 1;
}
- // TODO: trace
- log.info(String.format("sessionsCount: %d, sessionsPerSegment: %d, segmentsCount: %d", sessionsCount, sessionsPerSegment, segmentsCount));
+ // TODO: debug
+ log.infof("sessionsCount: %d, sessionsPerSegment: %d, segmentsCount: %d", sessionsCount, sessionsPerSegment, segmentsCount);
for (int i=0 ; i finishedList = new ArrayList<>();
- List nonFinishedList = new ArrayList<>();
int size = segments.size();
for (int i=0 ; i sessions = persister.loadUserSessions(first, max, true);
- // TODO: Each worker may have different time. Improve if needed...
- int currentTime = Time.currentTime();
-
for (UserSessionModel persistentSession : sessions) {
- // Update and persist lastSessionRefresh time TODO: Do bulk DB update instead?
- persistentSession.setLastSessionRefresh(currentTime);
- persister.updateUserSession(persistentSession, true);
-
// Save to memory/infinispan
- UserSessionModel offlineUserSession = session.sessions().createOfflineUserSession(persistentSession);
+ UserSessionModel offlineUserSession = session.sessions().importUserSession(persistentSession, true);
for (ClientSessionModel persistentClientSession : persistentSession.getClientSessions()) {
- ClientSessionModel offlineClientSession = session.sessions().createOfflineClientSession(persistentClientSession);
+ ClientSessionModel offlineClientSession = session.sessions().importClientSession(persistentClientSession, true);
offlineClientSession.setUserSession(offlineUserSession);
}
}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java
index 5014147284..1fe977aead 100644
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java
@@ -9,6 +9,8 @@ import org.keycloak.models.KeycloakSession;
*/
public interface SessionLoader extends Serializable {
+ void init(KeycloakSession session);
+
int getSessionsCount(KeycloakSession session);
boolean loadSessions(KeycloakSession session, int first, int max);
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 f4de6656cb..a2acde95c6 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -172,14 +172,16 @@ public class TokenManager {
int currentTime = Time.currentTime();
- if (realm.isRevokeRefreshToken() && !refreshToken.getType().equals(TokenUtil.TOKEN_TYPE_OFFLINE)) {
- if (refreshToken.getIssuedAt() < validation.clientSession.getTimestamp()) {
+ if (realm.isRevokeRefreshToken()) {
+ int serverStartupTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
+
+ if (refreshToken.getIssuedAt() < validation.clientSession.getTimestamp() && (serverStartupTime != validation.clientSession.getTimestamp())) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
}
- validation.clientSession.setTimestamp(currentTime);
}
+ validation.clientSession.setTimestamp(currentTime);
validation.userSession.setLastSessionRefresh(currentTime);
AccessTokenResponse res = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession)
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
index c5be21be10..08699e07c8 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
@@ -28,6 +28,7 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
private Map, Map> factoriesMap = new HashMap, Map>();
protected CopyOnWriteArrayList listeners = new CopyOnWriteArrayList();
+ // TODO: Likely should be changed to int and use Time.currentTime() to be compatible with all our "time" reps
protected long serverStartupTimestamp;
@Override
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
index 46e2a096a3..0c90af9371 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
@@ -7,6 +7,7 @@ import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
@@ -433,6 +434,15 @@ public class ClientResource {
List userSessions = session.sessions().getOfflineUserSessions(client.getRealm(), client, firstResult, maxResults);
for (UserSessionModel userSession : userSessions) {
UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(userSession);
+
+ // Update lastSessionRefresh with the timestamp from clientSession
+ for (ClientSessionModel clientSession : userSession.getClientSessions()) {
+ if (client.getId().equals(clientSession.getClient().getId())) {
+ rep.setLastAccess(Time.toMillis(clientSession.getTimestamp()));
+ break;
+ }
+ }
+
sessions.add(rep);
}
return sessions;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index 4c8796d431..bd7924b955 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -79,6 +79,7 @@ import org.keycloak.models.UsernameLoginFailureModel;
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.resources.AccountService;
+import org.keycloak.util.Time;
/**
* Base resource for managing users
@@ -373,6 +374,15 @@ public class UsersResource {
List reps = new ArrayList();
for (UserSessionModel session : sessions) {
UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session);
+
+ // Update lastSessionRefresh with the timestamp from clientSession
+ for (ClientSessionModel clientSession : session.getClientSessions()) {
+ if (clientId.equals(clientSession.getClient().getId())) {
+ rep.setLastAccess(Time.toMillis(clientSession.getTimestamp()));
+ break;
+ }
+ }
+
reps.add(rep);
}
return reps;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java
index 9e0358ff09..6508cfb88d 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java
@@ -65,6 +65,9 @@ public class UserSessionInitializerTest {
resetSession();
// Create and persist offline sessions
+ int started = Time.currentTime();
+ int serverStartTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
+
for (UserSessionModel origSession : origSessions) {
UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId());
for (ClientSessionModel clientSession : userSession.getClientSessions()) {
@@ -88,32 +91,23 @@ public class UserSessionInitializerTest {
Assert.assertEquals(0, session.sessions().getOfflineSessionsCount(realm, testApp));
Assert.assertEquals(0, session.sessions().getOfflineSessionsCount(realm, thirdparty));
- int started = Time.currentTime();
+ // Load sessions from persister into infinispan/memory
+ UserSessionProviderFactory userSessionFactory = (UserSessionProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserSessionProvider.class);
+ userSessionFactory.loadPersistentSessions(session.getKeycloakSessionFactory(), 1, 2);
- try {
- // Set some offset to ensure lastSessionRefresh will be updated
- Time.setOffset(10);
+ resetSession();
- // Load sessions from persister into infinispan/memory
- UserSessionProviderFactory userSessionFactory = (UserSessionProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserSessionProvider.class);
- userSessionFactory.loadPersistentSessions(session.getKeycloakSessionFactory(), 10, 2);
+ // Assert sessions are in
+ testApp = realm.getClientByClientId("test-app");
+ Assert.assertEquals(3, session.sessions().getOfflineSessionsCount(realm, testApp));
+ Assert.assertEquals(1, session.sessions().getOfflineSessionsCount(realm, thirdparty));
- resetSession();
+ List loadedSessions = session.sessions().getOfflineUserSessions(realm, testApp, 0, 10);
+ UserSessionProviderTest.assertSessions(loadedSessions, origSessions);
- // Assert sessions are in
- testApp = realm.getClientByClientId("test-app");
- Assert.assertEquals(3, session.sessions().getOfflineSessionsCount(realm, testApp));
- Assert.assertEquals(1, session.sessions().getOfflineSessionsCount(realm, thirdparty));
-
- List loadedSessions = session.sessions().getOfflineUserSessions(realm, testApp, 0, 10);
- UserSessionProviderTest.assertSessions(loadedSessions, origSessions);
-
- UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[0].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.1", started, started+10, "test-app", "third-party");
- UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[1].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started+10, "test-app");
- UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[2].getId(), session.users().getUserByUsername("user2", realm), "127.0.0.3", started, started+10, "test-app");
- } finally {
- Time.setOffset(0);
- }
+ UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[0].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.1", started, serverStartTime, "test-app", "third-party");
+ UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[1].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.2", started, serverStartTime, "test-app");
+ UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[2].getId(), session.users().getUserByUsername("user2", realm), "127.0.0.3", started, serverStartTime, "test-app");
}
private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java
index 53480aa05f..4edf9516b5 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java
@@ -93,6 +93,52 @@ public class UserSessionPersisterProviderTest {
assertSessionLoaded(loadedSessions, origSessions[2].getId(), session.users().getUserByUsername("user2", realm), "127.0.0.3", started, started, "test-app");
}
+ @Test
+ public void testUpdateTimestamps() {
+ // Create some sessions in infinispan
+ int started = Time.currentTime();
+ UserSessionModel[] origSessions = createSessions();
+
+ resetSession();
+
+ // Persist 3 created userSessions and clientSessions as offline
+ ClientModel testApp = realm.getClientByClientId("test-app");
+ List userSessions = session.sessions().getUserSessions(realm, testApp);
+ for (UserSessionModel userSession : userSessions) {
+ persistUserSession(userSession, true);
+ }
+
+ // Persist 1 online session
+ UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[0].getId());
+ persistUserSession(userSession, false);
+
+ resetSession();
+
+ // update timestamps
+ int newTime = started + 50;
+ persister.updateAllTimestamps(newTime);
+
+ // Assert online session
+ List loadedSessions = loadPersistedSessionsPaginated(false, 1, 1, 1);
+ Assert.assertEquals(2, assertTimestampsUpdated(loadedSessions, newTime));
+
+ // Assert offline sessions
+ loadedSessions = loadPersistedSessionsPaginated(true, 2, 2, 3);
+ Assert.assertEquals(4, assertTimestampsUpdated(loadedSessions, newTime));
+ }
+
+ private int assertTimestampsUpdated(List loadedSessions, int expectedTime) {
+ int clientSessionsCount = 0;
+ for (UserSessionModel loadedSession : loadedSessions) {
+ Assert.assertEquals(expectedTime, loadedSession.getLastSessionRefresh());
+ for (ClientSessionModel clientSession : loadedSession.getClientSessions()) {
+ Assert.assertEquals(expectedTime, clientSession.getTimestamp());
+ clientSessionsCount++;
+ }
+ }
+ return clientSessionsCount;
+ }
+
@Test
public void testUpdateAndRemove() {
// Create some sessions in infinispan
@@ -245,11 +291,6 @@ public class UserSessionPersisterProviderTest {
realmMgr.removeRealm(realmMgr.getRealm("foo"));
}
-// @Test
-// public void testExpiredUserSessions() {
-//
-// }
-
private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) {
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java
index 57c99f8ad5..dc15b44574 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java
@@ -327,30 +327,42 @@ public class UserSessionProviderOfflineTest {
Assert.assertNotNull(session.sessions().getOfflineClientSession(realm, clientSession.getId()));
}
+ UserSessionModel session1 = session.sessions().getOfflineUserSession(realm, origSessions[1].getId());
+ Assert.assertEquals(1, session1.getClientSessions().size());
+ ClientSessionModel cls1 = session1.getClientSessions().get(0);
+
// sessions are in persister too
Assert.assertEquals(3, persister.getUserSessionsCount(true));
// Set lastSessionRefresh to session[0] to 0
session0.setLastSessionRefresh(0);
+ // Set timestamp to cls1 to 0
+ cls1.setTimestamp(0);
+
resetSession();
session.sessions().removeExpiredUserSessions(realm);
resetSession();
- // assert sessions not found now
+ // assert session0 not found now
Assert.assertNull(session.sessions().getOfflineUserSession(realm, origSessions[0].getId()));
for (String clientSession : clientSessions) {
Assert.assertNull(session.sessions().getOfflineClientSession(realm, origSessions[0].getId()));
offlineSessions.remove(clientSession);
}
- // Assert other offline sessions still found
+ // Assert cls1 not found too
for (Map.Entry entry : offlineSessions.entrySet()) {
- Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey(), entry.getValue()) != null);
+ String userSessionId = entry.getValue();
+ if (userSessionId.equals(session1.getId())) {
+ Assert.assertFalse(sessionManager.findOfflineClientSession(realm, entry.getKey(), userSessionId) != null);
+ } else {
+ Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey(), userSessionId) != null);
+ }
}
- Assert.assertEquals(2, persister.getUserSessionsCount(true));
+ Assert.assertEquals(1, persister.getUserSessionsCount(true));
// Expire everything and assert nothing found
Time.setOffset(3000000);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
index b7596a073d..4a86fec596 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
@@ -332,6 +332,71 @@ public class OfflineTokenTest {
Assert.assertEquals(0, offlineToken.getExpiration());
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
+
+ // Assert same token can be refreshed again
+ testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
+ }
+
+ @Test
+ public void offlineTokenDirectGrantFlowWithRefreshTokensRevoked() throws Exception {
+ keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setRevokeRefreshToken(true);
+ }
+
+ });
+
+ oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+ oauth.clientId("offline-client");
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("secret1", "test-user@localhost", "password");
+
+ AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
+ String offlineTokenString = tokenResponse.getRefreshToken();
+ RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
+
+ events.expectLogin()
+ .client("offline-client")
+ .user(userId)
+ .session(token.getSessionState())
+ .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.TOKEN_ID, token.getId())
+ .detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
+ .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
+ .detail(Details.USERNAME, "test-user@localhost")
+ .removeDetail(Details.CODE_ID)
+ .removeDetail(Details.REDIRECT_URI)
+ .removeDetail(Details.CONSENT)
+ .assertEvent();
+
+ Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
+ Assert.assertEquals(0, offlineToken.getExpiration());
+
+ String offlineTokenString2 = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
+ RefreshToken offlineToken2 = oauth.verifyRefreshToken(offlineTokenString2);
+
+ // Assert second refresh with same refresh token will fail
+ OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "secret1");
+ Assert.assertEquals(400, response.getStatusCode());
+ events.expectRefresh(offlineToken.getId(), token.getSessionState())
+ .client("offline-client")
+ .error(Errors.INVALID_TOKEN)
+ .user(userId)
+ .clearDetails()
+ .assertEvent();
+
+ // Refresh with new refreshToken is successful now
+ testRefreshWithOfflineToken(token, offlineToken2, offlineTokenString2, token.getSessionState(), userId);
+
+ keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setRevokeRefreshToken(false);
+ }
+
+ });
}
@Test