KEYCLOAK-1961 revokeRefreshToken support for offline tokens and other fixes
This commit is contained in:
parent
b4520baee5
commit
67435791ed
30 changed files with 451 additions and 167 deletions
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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)">
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Started</th>
|
||||
<th>Last Access</th>
|
||||
<th>Last Refresh</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -92,6 +92,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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -45,10 +45,6 @@ public class MongoUserSessionPersisterProvider implements UserSessionPersisterPr
|
|||
return invocationContext.getMongoStore();
|
||||
}
|
||||
|
||||
private Class<? extends PersistentUserSessionEntity> getClazz(boolean offline) {
|
||||
return offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class;
|
||||
}
|
||||
|
||||
private MongoUserSessionEntity loadUserSession(String userSessionId, boolean offline) {
|
||||
Class<? extends MongoUserSessionEntity> clazz = offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class;
|
||||
return getMongoStore().loadEntity(clazz, userSessionId, invocationContext);
|
||||
|
@ -220,6 +216,41 @@ public class MongoUserSessionPersisterProvider implements UserSessionPersisterPr
|
|||
return getMongoStore().countEntities(clazz, query, invocationContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAllTimestamps(int time) {
|
||||
// 1) Update timestamp of clientSessions
|
||||
|
||||
DBObject timestampSubquery = new QueryBuilder()
|
||||
.and("timestamp").notEquals(time).get();
|
||||
|
||||
DBObject query = new QueryBuilder()
|
||||
.and("clientSessions").elemMatch(timestampSubquery).get();
|
||||
|
||||
|
||||
DBObject update = new QueryBuilder()
|
||||
.and("$set").is(new BasicDBObject("clientSessions.$.timestamp", time)).get();
|
||||
|
||||
// Not sure how to do in single query :/
|
||||
int countModified = 1;
|
||||
while (countModified > 0) {
|
||||
countModified = getMongoStore().updateEntities(MongoOfflineUserSessionEntity.class, query, update, invocationContext);
|
||||
}
|
||||
|
||||
countModified = 1;
|
||||
while (countModified > 0) {
|
||||
countModified = getMongoStore().updateEntities(MongoOnlineUserSessionEntity.class, query, update, invocationContext);
|
||||
}
|
||||
|
||||
// 2) update lastSessionRefresh of userSessions
|
||||
query = new QueryBuilder().get();
|
||||
|
||||
update = new QueryBuilder()
|
||||
.and("$set").is(new BasicDBObject("lastSessionRefresh", time)).get();
|
||||
|
||||
getMongoStore().updateEntities(MongoOfflineUserSessionEntity.class, query, update, invocationContext);
|
||||
getMongoStore().updateEntities(MongoOnlineUserSessionEntity.class, query, update, invocationContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserSessionModel> loadUserSessions(int firstResult, int maxResults, boolean offline) {
|
||||
DBObject query = new QueryBuilder()
|
||||
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<UserSessionModel> 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<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) {
|
||||
|
|
|
@ -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<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
|
||||
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue