keycloak-memoizeit

Changes

Details

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 07a187a..3d5b99f 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 @@
 
         <addColumn tableName="REALM">
             <column name="OFFLINE_SESSION_IDLE_TIMEOUT" type="INT"/>
+            <column name="REVOKE_REFRESH_TOKEN" type="BOOLEAN" defaultValueBoolean="false">
+                <constraints nullable="false"/>
+            </column>
         </addColumn>
 
         <addColumn tableName="KEYCLOAK_ROLE">
@@ -47,16 +50,11 @@
             <column name="OFFLINE" type="BOOLEAN" defaultValueBoolean="false">
                 <constraints nullable="false"/>
             </column>
+            <column name="TIMESTAMP" type="INT"/>
             <column name="DATA" type="CLOB"/>
         </createTable>
 
         <addPrimaryKey columnNames="USER_SESSION_ID, OFFLINE" constraintName="CONSTRAINT_OFFLINE_US_SES_PK" tableName="OFFLINE_USER_SESSION"/>
         <addPrimaryKey columnNames="CLIENT_SESSION_ID, OFFLINE" constraintName="CONSTRAINT_OFFLINE_CL_SES_PK" tableName="OFFLINE_CLIENT_SESSION"/>
-
-        <addColumn tableName="REALM">
-            <column name="REVOKE_REFRESH_TOKEN" type="BOOLEAN" defaultValueBoolean="false">
-                <constraints nullable="false"/>
-            </column>
-        </addColumn>
     </changeSet>
 </databaseChangeLog>
\ 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 36da3e8..f13d6e8 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 82f5562..3d2aaf7 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 @@
             <th>{{:: 'user' | translate}}</th>
             <th>{{:: 'from-ip' | translate}}</th>
             <th>{{:: 'token-issued' | translate}}</th>
-            <th>{{:: 'last-access' | translate}}</th>
+            <th>{{:: 'last-refresh' | translate}}</th>
         </tr>
         </thead>
         <tfoot data-ng-show="sessions && (sessions.length >= 5 || query.first != 0)">
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 b06f326..bc7ad50 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 @@
         <tr>
             <th>IP Address</th>
             <th>Started</th>
-            <th>Last Access</th>
+            <th>Last Refresh</th>
         </tr>
         </thead>
         <tbody>
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 a03edd3..1c1802e 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 6daf7c7..809fbd2 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
@@ -93,6 +93,11 @@ public class DisabledUserSessionPersisterProvider implements UserSessionPersiste
     }
 
     @Override
+    public void updateAllTimestamps(int time) {
+
+    }
+
+    @Override
     public List<UserSessionModel> 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 1fced88..8465269 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<String, ClientSessionModel.ExecutionStatus> 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 96e900f..b1a388b 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 4b3355e..5863fdb 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<UserSessionModel> loadUserSessions(int firstResult, int maxResults, boolean offline);
 
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 836cc75..1a59f4f 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<UserSessionModel> 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/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 ffc0455..bf19d96 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 a11b875..faf3f80 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 @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 f739091..95745a8 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 53917c2..f23e7fb 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);
@@ -221,6 +217,41 @@ public class MongoUserSessionPersisterProvider implements UserSessionPersisterPr
     }
 
     @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<UserSessionModel> loadUserSessions(int firstResult, int maxResults, boolean offline) {
         DBObject query = new QueryBuilder()
                 .get();
@@ -232,13 +263,13 @@ public class MongoUserSessionPersisterProvider implements UserSessionPersisterPr
 
         List<UserSessionModel> 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<ClientSessionModel> 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/compat/MemUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java
index 6cbb1eb..23c1286 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<String, UserSessionEntity> 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<String, ClientSessionEntity> 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 450bbe1..cb5a7e7 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/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 34cc4bc..627edcf 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<String, SessionEntity> 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<String, SessionEntity> 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<String, String> 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<String, SessionEntity> 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<String, SessionEntity> 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 c88e490..1d7c279 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/initializer/InfinispanUserSessionInitializer.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
index 0d038bd..89f2d4f 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 ccc6fd6..6066077 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<segmentsCount ; i++) {
             segments.add(false);
@@ -81,25 +81,17 @@ public class InitializerState extends SessionEntity {
         return -1;
     }
 
-    public String printState(boolean includeSegments) {
+    public String printState() {
         int finished = 0;
         int nonFinished = 0;
-        List<Integer> finishedList = new ArrayList<>();
-        List<Integer> nonFinishedList = new ArrayList<>();
 
         int size = segments.size();
         for (int i=0 ; i<size ; i++) {
             Boolean done = segments.get(i);
             if (done) {
                 finished++;
-                if (includeSegments) {
-                    finishedList.add(i);
-                }
             } else {
                 nonFinished++;
-                if (includeSegments) {
-                    nonFinishedList.add(i);
-                }
             }
         }
 
@@ -107,11 +99,6 @@ public class InitializerState extends SessionEntity {
                 .append(", finished segments count: " + finished)
                 .append(", non-finished segments count: " + nonFinished);
 
-        if (includeSegments) {
-            strBuilder.append(", finished segments: " + finishedList)
-                    .append(", non-finished segments: " + nonFinishedList);
-        }
-
         return strBuilder.toString();
     }
 }
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java
index 20ec696..db218e2 100644
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java
@@ -2,6 +2,7 @@ package org.keycloak.models.sessions.infinispan.initializer;
 
 import java.util.List;
 
+import org.jboss.logging.Logger;
 import org.keycloak.models.ClientSessionModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.UserSessionModel;
@@ -13,6 +14,20 @@ import org.keycloak.util.Time;
  */
 public class OfflineUserSessionLoader implements SessionLoader {
 
+    private static final Logger log = Logger.getLogger(OfflineUserSessionLoader.class);
+
+    @Override
+    public void init(KeycloakSession session) {
+        UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
+        int startTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
+
+        // TODO: debug
+        log.infof("Clearing detached sessions from persistent storage and updating timestamps to %d", startTime);
+
+        persister.clearDetachedUserSessions();
+        persister.updateAllTimestamps(startTime);
+    }
+
     @Override
     public int getSessionsCount(KeycloakSession session) {
         UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
@@ -21,23 +36,19 @@ public class OfflineUserSessionLoader implements SessionLoader {
 
     @Override
     public boolean loadSessions(KeycloakSession session, int first, int max) {
+        // TODO: trace
+        log.infof("Loading sessions - first: %d, max: %d", first, max);
+
         UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
         List<UserSessionModel> 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 5014147..1fe977a 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 f4de665..a2acde9 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 c5be21b..08699e0 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<Class<? extends Provider>, Map<String, ProviderFactory>> factoriesMap = new HashMap<Class<? extends Provider>, Map<String, ProviderFactory>>();
     protected CopyOnWriteArrayList<ProviderEventListener> listeners = new CopyOnWriteArrayList<ProviderEventListener>();
 
+    // 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 46e2a09..0c90af9 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<UserSessionModel> 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 4c8796d..bd7924b 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<UserSessionRepresentation> reps = new ArrayList<UserSessionRepresentation>();
         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 9e0358f..6508cfb 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();
-
-        try {
-            // Set some offset to ensure lastSessionRefresh will be updated
-            Time.setOffset(10);
+        // Load sessions from persister into infinispan/memory
+        UserSessionProviderFactory userSessionFactory = (UserSessionProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserSessionProvider.class);
+        userSessionFactory.loadPersistentSessions(session.getKeycloakSessionFactory(), 1, 2);
 
-            // Load sessions from persister into infinispan/memory
-            UserSessionProviderFactory userSessionFactory = (UserSessionProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserSessionProvider.class);
-            userSessionFactory.loadPersistentSessions(session.getKeycloakSessionFactory(), 10, 2);
-
-            resetSession();
+        resetSession();
 
-            // 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));
+        // 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<UserSessionModel> loadedSessions = session.sessions().getOfflineUserSessions(realm, testApp, 0, 10);
-            UserSessionProviderTest.assertSessions(loadedSessions, origSessions);
+        List<UserSessionModel> 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<String> roles, Set<String> 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 53480aa..4edf951 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
@@ -94,6 +94,52 @@ public class UserSessionPersisterProviderTest {
     }
 
     @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<UserSessionModel> 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<UserSessionModel> 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<UserSessionModel> 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
         int started = Time.currentTime();
@@ -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<String> roles, Set<String> 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 57c99f8..dc15b44 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<String, String> 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 b7596a0..4a86fec 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