KEYCLOAK-1961 revokeRefreshToken support for offline tokens and other fixes

This commit is contained in:
mposolda 2015-10-15 20:46:08 +02:00
parent b4520baee5
commit 67435791ed
30 changed files with 451 additions and 167 deletions

View file

@ -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>

View file

@ -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

View file

@ -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)">

View file

@ -13,7 +13,7 @@
<tr>
<th>IP Address</th>
<th>Started</th>
<th>Last Access</th>
<th>Last Refresh</th>
</tr>
</thead>
<tbody>

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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();

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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")

View file

@ -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);
}

View file

@ -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;

View file

@ -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);

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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) {

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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)

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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) {

View file

@ -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);

View file

@ -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);

View file

@ -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