keycloak-memoizeit
Changes
forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties 1(+1 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html 2(+1 -1)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-offline-sessions.html 2(+1 -1)
model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java 5(+5 -0)
model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java 24(+16 -8)
model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java 16(+14 -2)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java 48(+40 -8)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java 49(+42 -7)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java 12(+12 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java 121(+75 -46)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java 10(+1 -9)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java 20(+15 -5)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java 19(+3 -16)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java 29(+20 -9)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java 2(+2 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java 38(+16 -22)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java 51(+46 -5)
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