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