Merge pull request #1740 from mposolda/master
Offline tokens fixes & improvements
This commit is contained in:
commit
92ed01a61b
56 changed files with 996 additions and 189 deletions
|
@ -2,6 +2,13 @@
|
|||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
<changeSet author="mposolda@redhat.com" id="1.6.0">
|
||||
|
||||
<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">
|
||||
<column name="SCOPE_PARAM_REQUIRED" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
|
@ -43,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>
|
|
@ -14,6 +14,7 @@ public class RealmRepresentation {
|
|||
protected Integer accessTokenLifespan;
|
||||
protected Integer ssoSessionIdleTimeout;
|
||||
protected Integer ssoSessionMaxLifespan;
|
||||
protected Integer offlineSessionIdleTimeout;
|
||||
protected Integer accessCodeLifespan;
|
||||
protected Integer accessCodeLifespanUserAction;
|
||||
protected Integer accessCodeLifespanLogin;
|
||||
|
@ -199,6 +200,14 @@ public class RealmRepresentation {
|
|||
this.ssoSessionMaxLifespan = ssoSessionMaxLifespan;
|
||||
}
|
||||
|
||||
public Integer getOfflineSessionIdleTimeout() {
|
||||
return offlineSessionIdleTimeout;
|
||||
}
|
||||
|
||||
public void setOfflineSessionIdleTimeout(Integer offlineSessionIdleTimeout) {
|
||||
this.offlineSessionIdleTimeout = offlineSessionIdleTimeout;
|
||||
}
|
||||
|
||||
public List<ScopeMappingRepresentation> getScopeMappings() {
|
||||
return scopeMappings;
|
||||
}
|
||||
|
|
|
@ -76,6 +76,8 @@ days=Days
|
|||
sso-session-max=SSO Session Max
|
||||
sso-session-idle.tooltip=Time a session is allowed to be idle before it expires. Tokens and browser sessions are invalidated when a session is expired.
|
||||
sso-session-max.tooltip=Max time before a session is expired. Tokens and browser sessions are invalidated when a session is expired.
|
||||
offline-session-idle=Offline Session Idle
|
||||
offline-session-idle.tooltip=Time an offline session is allowed to be idle before it expires. You need to use offline token to refresh at least once within this period, otherwise offline session will expire.
|
||||
access-token-lifespan=Access Token Lifespan
|
||||
access-token-lifespan.tooltip=Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout.
|
||||
client-login-timeout=Client login timeout
|
||||
|
@ -336,6 +338,8 @@ offline-tokens.tooltip=Total number of offline tokens for this client.
|
|||
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
|
||||
|
|
|
@ -498,6 +498,24 @@ module.config([ '$routeProvider', function($routeProvider) {
|
|||
},
|
||||
controller : 'UserConsentsCtrl'
|
||||
})
|
||||
.when('/realms/:realm/users/:user/offline-sessions/:client', {
|
||||
templateUrl : resourceUrl + '/partials/user-offline-sessions.html',
|
||||
resolve : {
|
||||
realm : function(RealmLoader) {
|
||||
return RealmLoader();
|
||||
},
|
||||
user : function(UserLoader) {
|
||||
return UserLoader();
|
||||
},
|
||||
client : function(ClientLoader) {
|
||||
return ClientLoader();
|
||||
},
|
||||
offlineSessions : function(UserOfflineSessionsLoader) {
|
||||
return UserOfflineSessionsLoader();
|
||||
}
|
||||
},
|
||||
controller : 'UserOfflineSessionsCtrl'
|
||||
})
|
||||
.when('/realms/:realm/users', {
|
||||
templateUrl : resourceUrl + '/partials/user-list.html',
|
||||
resolve : {
|
||||
|
|
|
@ -912,6 +912,12 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
|||
$scope.realm.ssoSessionMaxLifespan = TimeUnit.convert($scope.realm.ssoSessionMaxLifespan, from, to);
|
||||
});
|
||||
|
||||
$scope.realm.offlineSessionIdleTimeoutUnit = TimeUnit.autoUnit(realm.offlineSessionIdleTimeout);
|
||||
$scope.realm.offlineSessionIdleTimeout = TimeUnit.toUnit(realm.offlineSessionIdleTimeout, $scope.realm.offlineSessionIdleTimeoutUnit);
|
||||
$scope.$watch('realm.offlineSessionIdleTimeoutUnit', function(to, from) {
|
||||
$scope.realm.offlineSessionIdleTimeout = TimeUnit.convert($scope.realm.offlineSessionIdleTimeout, from, to);
|
||||
});
|
||||
|
||||
$scope.realm.accessCodeLifespanUnit = TimeUnit.autoUnit(realm.accessCodeLifespan);
|
||||
$scope.realm.accessCodeLifespan = TimeUnit.toUnit(realm.accessCodeLifespan, $scope.realm.accessCodeLifespanUnit);
|
||||
$scope.$watch('realm.accessCodeLifespanUnit', function(to, from) {
|
||||
|
@ -943,6 +949,7 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
|||
var realmCopy = angular.copy($scope.realm);
|
||||
delete realmCopy["accessTokenLifespanUnit"];
|
||||
delete realmCopy["ssoSessionMaxLifespanUnit"];
|
||||
delete realmCopy["offlineSessionIdleTimeoutUnit"];
|
||||
delete realmCopy["accessCodeLifespanUnit"];
|
||||
delete realmCopy["ssoSessionIdleTimeoutUnit"];
|
||||
delete realmCopy["accessCodeLifespanUserActionUnit"];
|
||||
|
@ -951,6 +958,7 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
|||
realmCopy.accessTokenLifespan = TimeUnit.toSeconds($scope.realm.accessTokenLifespan, $scope.realm.accessTokenLifespanUnit)
|
||||
realmCopy.ssoSessionIdleTimeout = TimeUnit.toSeconds($scope.realm.ssoSessionIdleTimeout, $scope.realm.ssoSessionIdleTimeoutUnit)
|
||||
realmCopy.ssoSessionMaxLifespan = TimeUnit.toSeconds($scope.realm.ssoSessionMaxLifespan, $scope.realm.ssoSessionMaxLifespanUnit)
|
||||
realmCopy.offlineSessionIdleTimeout = TimeUnit.toSeconds($scope.realm.offlineSessionIdleTimeout, $scope.realm.offlineSessionIdleTimeoutUnit)
|
||||
realmCopy.accessCodeLifespan = TimeUnit.toSeconds($scope.realm.accessCodeLifespan, $scope.realm.accessCodeLifespanUnit)
|
||||
realmCopy.accessCodeLifespanUserAction = TimeUnit.toSeconds($scope.realm.accessCodeLifespanUserAction, $scope.realm.accessCodeLifespanUserActionUnit)
|
||||
realmCopy.accessCodeLifespanLogin = TimeUnit.toSeconds($scope.realm.accessCodeLifespanLogin, $scope.realm.accessCodeLifespanLoginUnit)
|
||||
|
|
|
@ -216,6 +216,17 @@ module.controller('UserConsentsCtrl', function($scope, realm, user, userConsents
|
|||
}
|
||||
});
|
||||
|
||||
module.controller('UserOfflineSessionsCtrl', function($scope, $location, realm, user, client, offlineSessions) {
|
||||
$scope.realm = realm;
|
||||
$scope.user = user;
|
||||
$scope.client = client;
|
||||
$scope.offlineSessions = offlineSessions;
|
||||
|
||||
$scope.cancel = function() {
|
||||
$location.url("/realms/" + realm.realm + '/users/' + user.id + '/consents');
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
module.controller('UserListCtrl', function($scope, realm, User, UserImpersonation, BruteForce, Notifications, $route, Dialog) {
|
||||
$scope.realm = realm;
|
||||
|
|
|
@ -181,6 +181,16 @@ module.factory('UserSessionsLoader', function(Loader, UserSessions, $route, $q)
|
|||
});
|
||||
});
|
||||
|
||||
module.factory('UserOfflineSessionsLoader', function(Loader, UserOfflineSessions, $route, $q) {
|
||||
return Loader.query(UserOfflineSessions, function() {
|
||||
return {
|
||||
realm : $route.current.params.realm,
|
||||
user : $route.current.params.user,
|
||||
client : $route.current.params.client
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.factory('UserFederatedIdentityLoader', function(Loader, UserFederatedIdentities, $route, $q) {
|
||||
return Loader.query(UserFederatedIdentities, function() {
|
||||
return {
|
||||
|
|
|
@ -369,6 +369,13 @@ module.factory('UserSessions', function($resource) {
|
|||
user : '@user'
|
||||
});
|
||||
});
|
||||
module.factory('UserOfflineSessions', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/users/:user/offline-sessions/:client', {
|
||||
realm : '@realm',
|
||||
user : '@user',
|
||||
client : '@client'
|
||||
});
|
||||
});
|
||||
|
||||
module.factory('UserSessionLogout', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/sessions/:session', {
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<table class="table table-striped table-bordered" data-ng-show="count > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="kc-table-actions" colspan="3">
|
||||
<th class="kc-table-actions" colspan="4">
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-default" ng-click="loadUsers()" tooltip-placement="left" tooltip-trigger="mouseover mouseout" tooltip="{{:: 'show-offline-tokens.tooltip' | translate}}">{{:: 'show-offline-tokens' | translate}}</a>
|
||||
</div>
|
||||
|
@ -31,6 +31,7 @@
|
|||
<th>{{:: 'user' | translate}}</th>
|
||||
<th>{{:: 'from-ip' | translate}}</th>
|
||||
<th>{{:: 'token-issued' | translate}}</th>
|
||||
<th>{{:: 'last-refresh' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot data-ng-show="sessions && (sessions.length >= 5 || query.first != 0)">
|
||||
|
@ -49,6 +50,7 @@
|
|||
<td><a href="#/realms/{{realm.realm}}/users/{{session.userId}}">{{session.username}}</a></td>
|
||||
<td>{{session.ipAddress}}</td>
|
||||
<td>{{session.start | date:'medium'}}</td>
|
||||
<td>{{session.lastAccess | date:'medium'}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -48,6 +48,23 @@
|
|||
<kc-tooltip>{{:: 'sso-session-max.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="offlineSessionIdleTimeout">{{:: 'offline-session-idle' | translate}}</label>
|
||||
|
||||
<div class="col-md-6 time-selector">
|
||||
<input class="form-control" type="number" required min="1"
|
||||
max="31536000" data-ng-model="realm.offlineSessionIdleTimeout"
|
||||
id="offlineSessionIdleTimeout" name="offlineSessionIdleTimeout"/>
|
||||
<select class="form-control" name="offlineSessionIdleTimeoutUnit" data-ng-model="realm.offlineSessionIdleTimeoutUnit">
|
||||
<option data-ng-selected="!realm.offlineSessionIdleTimeoutUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
|
||||
<option value="Minutes">{{:: 'minutes' | translate}}</option>
|
||||
<option value="Hours">{{:: 'hours' | translate}}</option>
|
||||
<option value="Days">{{:: 'days' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'offline-session-idle.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="accessTokenLifespan">{{:: 'access-token-lifespan' | translate}}</label>
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
</td>
|
||||
<td>
|
||||
<span data-ng-repeat="additionalGrant in consent.additionalGrants">
|
||||
<span ng-if="!$first">, </span>{{additionalGrant}}
|
||||
<span ng-if="!$first">, </span><a href="#/realms/{{realm.realm}}/users/{{user.id}}/offline-sessions/{{additionalGrant.client}}">{{additionalGrant.key}}</a>
|
||||
</span>
|
||||
</td>
|
||||
<td class="kc-action-cell">
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}/users">Users</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/users/{{user.id}}">{{user.username}}</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/users/{{user.id}}/consents">consents</a></li>
|
||||
<li>{{client.clientId}}</li>
|
||||
</ol>
|
||||
|
||||
<kc-tabs-user></kc-tabs-user>
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Started</th>
|
||||
<th>Last Refresh</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr data-ng-repeat="session in offlineSessions">
|
||||
<td>{{session.ipAddress}}</td>
|
||||
<td>{{session.start | date:'medium'}}</td>
|
||||
<td>{{session.lastAccess | date:'medium'}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-10 col-md-offset-2">
|
||||
<button kc-cancel data-ng-click="cancel()">Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<kc-menu></kc-menu>
|
|
@ -47,6 +47,8 @@ public class MigrateTo1_6_0 {
|
|||
|
||||
List<RealmModel> realms = session.realms().getRealms();
|
||||
for (RealmModel realm : realms) {
|
||||
realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
|
||||
|
||||
if (realm.getRole(Constants.OFFLINE_ACCESS_ROLE) == null) {
|
||||
for (RoleModel realmRole : realm.getRoles()) {
|
||||
realmRole.setScopeParamRequired(false);
|
||||
|
|
|
@ -19,4 +19,7 @@ public interface Constants {
|
|||
String READ_TOKEN_ROLE = "read-token";
|
||||
String[] BROKER_SERVICE_ROLES = {READ_TOKEN_ROLE};
|
||||
String OFFLINE_ACCESS_ROLE = OAuth2Constants.OFFLINE_ACCESS;
|
||||
|
||||
// 30 days
|
||||
int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
|
||||
}
|
||||
|
|
|
@ -100,8 +100,8 @@ public interface RealmModel extends RoleContainerModel {
|
|||
int getSsoSessionMaxLifespan();
|
||||
void setSsoSessionMaxLifespan(int seconds);
|
||||
|
||||
// int getOfflineSessionIdleTimeout();
|
||||
// void setOfflineSessionIdleTimeout(int seconds);
|
||||
int getOfflineSessionIdleTimeout();
|
||||
void setOfflineSessionIdleTimeout(int seconds);
|
||||
|
||||
int getAccessTokenLifespan();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
|
|||
private boolean revokeRefreshToken;
|
||||
private int ssoSessionIdleTimeout;
|
||||
private int ssoSessionMaxLifespan;
|
||||
private int offlineSessionIdleTimeout;
|
||||
private int accessTokenLifespan;
|
||||
private int accessCodeLifespan;
|
||||
private int accessCodeLifespanUserAction;
|
||||
|
@ -254,6 +255,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
|
|||
this.ssoSessionMaxLifespan = ssoSessionMaxLifespan;
|
||||
}
|
||||
|
||||
public int getOfflineSessionIdleTimeout() {
|
||||
return offlineSessionIdleTimeout;
|
||||
}
|
||||
|
||||
public void setOfflineSessionIdleTimeout(int offlineSessionIdleTimeout) {
|
||||
this.offlineSessionIdleTimeout = offlineSessionIdleTimeout;
|
||||
}
|
||||
|
||||
public int getAccessTokenLifespan() {
|
||||
return accessTokenLifespan;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -148,6 +148,7 @@ public class ModelToRepresentation {
|
|||
rep.setAccessTokenLifespan(realm.getAccessTokenLifespan());
|
||||
rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
|
||||
rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan());
|
||||
rep.setOfflineSessionIdleTimeout(realm.getOfflineSessionIdleTimeout());
|
||||
rep.setAccessCodeLifespan(realm.getAccessCodeLifespan());
|
||||
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
|
||||
rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin());
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.keycloak.models.utils;
|
||||
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.util.Base64;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.enums.SslRequired;
|
||||
|
@ -106,6 +107,8 @@ public class RepresentationToModel {
|
|||
else newRealm.setSsoSessionIdleTimeout(1800);
|
||||
if (rep.getSsoSessionMaxLifespan() != null) newRealm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
|
||||
else newRealm.setSsoSessionMaxLifespan(36000);
|
||||
if (rep.getOfflineSessionIdleTimeout() != null) newRealm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout());
|
||||
else newRealm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
|
||||
|
||||
if (rep.getAccessCodeLifespan() != null) newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
|
||||
else newRealm.setAccessCodeLifespan(60);
|
||||
|
@ -535,6 +538,7 @@ public class RepresentationToModel {
|
|||
if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
|
||||
if (rep.getSsoSessionIdleTimeout() != null) realm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout());
|
||||
if (rep.getSsoSessionMaxLifespan() != null) realm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
|
||||
if (rep.getOfflineSessionIdleTimeout() != null) realm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout());
|
||||
if (rep.getRequiredCredentials() != null) {
|
||||
realm.updateRequiredCredentials(rep.getRequiredCredentials());
|
||||
}
|
||||
|
|
|
@ -355,6 +355,16 @@ public class RealmAdapter implements RealmModel {
|
|||
realm.setSsoSessionMaxLifespan(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOfflineSessionIdleTimeout() {
|
||||
return realm.getOfflineSessionIdleTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOfflineSessionIdleTimeout(int seconds) {
|
||||
realm.setOfflineSessionIdleTimeout(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAccessTokenLifespan() {
|
||||
return realm.getAccessTokenLifespan();
|
||||
|
|
|
@ -275,6 +275,19 @@ public class RealmAdapter implements RealmModel {
|
|||
updated.setSsoSessionMaxLifespan(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOfflineSessionIdleTimeout() {
|
||||
if (updated != null) return updated.getOfflineSessionIdleTimeout();
|
||||
return cached.getOfflineSessionIdleTimeout();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setOfflineSessionIdleTimeout(int seconds) {
|
||||
getDelegateForUpdate();
|
||||
updated.setOfflineSessionIdleTimeout(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAccessTokenLifespan() {
|
||||
if (updated != null) return updated.getAccessTokenLifespan();
|
||||
|
|
|
@ -58,6 +58,7 @@ public class CachedRealm implements Serializable {
|
|||
private boolean revokeRefreshToken;
|
||||
private int ssoSessionIdleTimeout;
|
||||
private int ssoSessionMaxLifespan;
|
||||
private int offlineSessionIdleTimeout;
|
||||
private int accessTokenLifespan;
|
||||
private int accessCodeLifespan;
|
||||
private int accessCodeLifespanUserAction;
|
||||
|
@ -140,6 +141,7 @@ public class CachedRealm implements Serializable {
|
|||
revokeRefreshToken = model.isRevokeRefreshToken();
|
||||
ssoSessionIdleTimeout = model.getSsoSessionIdleTimeout();
|
||||
ssoSessionMaxLifespan = model.getSsoSessionMaxLifespan();
|
||||
offlineSessionIdleTimeout = model.getOfflineSessionIdleTimeout();
|
||||
accessTokenLifespan = model.getAccessTokenLifespan();
|
||||
accessCodeLifespan = model.getAccessCodeLifespan();
|
||||
accessCodeLifespanUserAction = model.getAccessCodeLifespanUserAction();
|
||||
|
@ -327,6 +329,10 @@ public class CachedRealm implements Serializable {
|
|||
return ssoSessionMaxLifespan;
|
||||
}
|
||||
|
||||
public int getOfflineSessionIdleTimeout() {
|
||||
return offlineSessionIdleTimeout;
|
||||
}
|
||||
|
||||
public int getAccessTokenLifespan() {
|
||||
return accessTokenLifespan;
|
||||
}
|
||||
|
|
|
@ -377,6 +377,16 @@ public class RealmAdapter implements RealmModel {
|
|||
realm.setSsoSessionMaxLifespan(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOfflineSessionIdleTimeout() {
|
||||
return realm.getOfflineSessionIdleTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOfflineSessionIdleTimeout(int seconds) {
|
||||
realm.setOfflineSessionIdleTimeout(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAccessCodeLifespan() {
|
||||
return realm.getAccessCodeLifespan();
|
||||
|
|
|
@ -82,6 +82,8 @@ public class RealmEntity {
|
|||
private int ssoSessionIdleTimeout;
|
||||
@Column(name="SSO_MAX_LIFESPAN")
|
||||
private int ssoSessionMaxLifespan;
|
||||
@Column(name="OFFLINE_SESSION_IDLE_TIMEOUT")
|
||||
private int offlineSessionIdleTimeout;
|
||||
@Column(name="ACCESS_TOKEN_LIFESPAN")
|
||||
protected int accessTokenLifespan;
|
||||
@Column(name="ACCESS_CODE_LIFESPAN")
|
||||
|
@ -314,6 +316,14 @@ public class RealmEntity {
|
|||
this.ssoSessionMaxLifespan = ssoSessionMaxLifespan;
|
||||
}
|
||||
|
||||
public int getOfflineSessionIdleTimeout() {
|
||||
return offlineSessionIdleTimeout;
|
||||
}
|
||||
|
||||
public void setOfflineSessionIdleTimeout(int offlineSessionIdleTimeout) {
|
||||
this.offlineSessionIdleTimeout = offlineSessionIdleTimeout;
|
||||
}
|
||||
|
||||
public int getAccessTokenLifespan() {
|
||||
return accessTokenLifespan;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -344,6 +344,17 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
updateRealm();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOfflineSessionIdleTimeout() {
|
||||
return realm.getOfflineSessionIdleTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOfflineSessionIdleTimeout(int seconds) {
|
||||
realm.setOfflineSessionIdleTimeout(seconds);
|
||||
updateRealm();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAccessTokenLifespan() {
|
||||
return realm.getAccessTokenLifespan();
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.UserSessionProvider;
|
||||
import org.keycloak.models.UsernameLoginFailureModel;
|
||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
|
||||
|
@ -302,8 +303,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
|
||||
@Override
|
||||
public void removeExpiredUserSessions(RealmModel realm) {
|
||||
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
|
||||
|
||||
int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan();
|
||||
int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout();
|
||||
int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout();
|
||||
int expiredDettachedClientSession = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm);
|
||||
|
||||
Map<String, String> map = new MapReduceTask(sessionCache)
|
||||
|
@ -323,6 +327,36 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
for (String id : map.keySet()) {
|
||||
tx.remove(sessionCache, id);
|
||||
}
|
||||
|
||||
// Remove expired offline user sessions
|
||||
Map<String, SessionEntity> map2 = new MapReduceTask(offlineSessionCache)
|
||||
.mappedWith(UserSessionMapper.create(realm.getId()).expired(null, expiredOffline))
|
||||
.reducedWith(new FirstResultReducer())
|
||||
.execute();
|
||||
|
||||
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 expired offline client sessions
|
||||
map = new MapReduceTask(offlineSessionCache)
|
||||
.mappedWith(ClientSessionMapper.create(realm.getId()).expiredRefresh(expiredOffline).emitKey())
|
||||
.reducedWith(new FirstResultReducer())
|
||||
.execute();
|
||||
|
||||
for (String clientSessionId : map.keySet()) {
|
||||
tx.remove(offlineSessionCache, clientSessionId);
|
||||
persister.removeClientSession(clientSessionId, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -477,6 +511,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
|
||||
tx.remove(cache, userSessionId);
|
||||
|
||||
// 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())
|
||||
|
@ -526,24 +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());
|
||||
UserSessionAdapter offlineUserSession = importUserSession(userSession, true);
|
||||
|
||||
entity.setAuthMethod(userSession.getAuthMethod());
|
||||
entity.setBrokerSessionId(userSession.getBrokerSessionId());
|
||||
entity.setBrokerUserId(userSession.getBrokerUserId());
|
||||
entity.setIpAddress(userSession.getIpAddress());
|
||||
entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
|
||||
entity.setLoginUsername(userSession.getLoginUsername());
|
||||
entity.setNotes(userSession.getNotes());
|
||||
entity.setRememberMe(userSession.isRememberMe());
|
||||
entity.setStarted(userSession.getStarted());
|
||||
entity.setState(userSession.getState());
|
||||
entity.setUser(userSession.getUser().getId());
|
||||
// started and lastSessionRefresh set to current time
|
||||
int currentTime = Time.currentTime();
|
||||
offlineUserSession.getEntity().setStarted(currentTime);
|
||||
offlineUserSession.setLastSessionRefresh(currentTime);
|
||||
|
||||
tx.put(offlineSessionCache, userSession.getId(), entity);
|
||||
return wrap(userSession.getRealm(), entity, true);
|
||||
return offlineUserSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -558,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
|
||||
|
@ -622,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);
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.UserSessionProvider;
|
||||
import org.keycloak.models.UsernameLoginFailureModel;
|
||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
import org.keycloak.models.sessions.infinispan.compat.entities.ClientSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureEntity;
|
||||
|
@ -297,6 +298,8 @@ public class MemUserSessionProvider implements UserSessionProvider {
|
|||
|
||||
@Override
|
||||
public void removeExpiredUserSessions(RealmModel realm) {
|
||||
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
|
||||
|
||||
Iterator<UserSessionEntity> itr = userSessions.values().iterator();
|
||||
while (itr.hasNext()) {
|
||||
UserSessionEntity s = itr.next();
|
||||
|
@ -314,6 +317,31 @@ public class MemUserSessionProvider implements UserSessionProvider {
|
|||
citr.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove expired offline user sessions
|
||||
itr = offlineUserSessions.values().iterator();
|
||||
while (itr.hasNext()) {
|
||||
UserSessionEntity s = itr.next();
|
||||
if (s.getRealm().equals(realm.getId()) && (s.getLastSessionRefresh() < Time.currentTime() - realm.getOfflineSessionIdleTimeout())) {
|
||||
itr.remove();
|
||||
remove(s, true);
|
||||
|
||||
// propagate to persister
|
||||
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
|
||||
|
@ -407,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());
|
||||
|
@ -415,17 +455,19 @@ public class MemUserSessionProvider implements UserSessionProvider {
|
|||
entity.setBrokerSessionId(userSession.getBrokerSessionId());
|
||||
entity.setBrokerUserId(userSession.getBrokerUserId());
|
||||
entity.setIpAddress(userSession.getIpAddress());
|
||||
entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
|
||||
entity.setLoginUsername(userSession.getLoginUsername());
|
||||
if (userSession.getNotes() != null) {
|
||||
entity.getNotes().putAll(userSession.getNotes());
|
||||
}
|
||||
entity.setRememberMe(userSession.isRememberMe());
|
||||
entity.setStarted(userSession.getStarted());
|
||||
entity.setState(userSession.getState());
|
||||
entity.setUser(userSession.getUser().getId());
|
||||
|
||||
offlineUserSessions.put(userSession.getId(), entity);
|
||||
entity.setStarted(userSession.getStarted());
|
||||
entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
|
||||
|
||||
ConcurrentHashMap<String, UserSessionEntity> sessionsMap = offline ? offlineUserSessions : userSessions;
|
||||
sessionsMap.put(userSession.getId(), entity);
|
||||
return new UserSessionAdapter(session, this, userSession.getRealm(), entity);
|
||||
}
|
||||
|
||||
|
@ -450,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());
|
||||
|
@ -473,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) {
|
||||
|
|
|
@ -15,6 +15,7 @@ public class InitializerState extends SessionEntity {
|
|||
|
||||
private int sessionsCount;
|
||||
private List<Boolean> segments = new ArrayList<>();
|
||||
private int lowestUnfinishedSegment = 0;
|
||||
|
||||
|
||||
public void init(int sessionsCount, int sessionsPerSegment) {
|
||||
|
@ -25,24 +26,27 @@ 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);
|
||||
}
|
||||
|
||||
updateLowestUnfinishedSegment();
|
||||
}
|
||||
|
||||
// Return true just if computation is entirely finished (all segments are true)
|
||||
public boolean isFinished() {
|
||||
return getNextUnfinishedSegmentFromIndex(0) == -1;
|
||||
return lowestUnfinishedSegment == -1;
|
||||
}
|
||||
|
||||
// Return next un-finished segments. It can return "segmentCount" segments or less
|
||||
public List<Integer> getUnfinishedSegments(int segmentCount) {
|
||||
List<Integer> result = new ArrayList<>();
|
||||
boolean remaining = true;
|
||||
int next=0;
|
||||
int next = lowestUnfinishedSegment;
|
||||
boolean remaining = lowestUnfinishedSegment != -1;
|
||||
|
||||
while (remaining && result.size() < segmentCount) {
|
||||
next = getNextUnfinishedSegmentFromIndex(next);
|
||||
if (next == -1) {
|
||||
|
@ -58,6 +62,11 @@ public class InitializerState extends SessionEntity {
|
|||
|
||||
public void markSegmentFinished(int index) {
|
||||
segments.set(index, true);
|
||||
updateLowestUnfinishedSegment();
|
||||
}
|
||||
|
||||
private void updateLowestUnfinishedSegment() {
|
||||
this.lowestUnfinishedSegment = getNextUnfinishedSegmentFromIndex(lowestUnfinishedSegment);
|
||||
}
|
||||
|
||||
private int getNextUnfinishedSegmentFromIndex(int index) {
|
||||
|
@ -72,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,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
|
||||
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);
|
||||
|
|
|
@ -13,18 +13,29 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
|||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ClientSessionsOfUserSessionMapper implements Mapper<String, SessionEntity, String, ClientSessionEntity>, Serializable {
|
||||
public class ClientSessionsOfUserSessionMapper implements Mapper<String, SessionEntity, String, Object>, Serializable {
|
||||
|
||||
private String realm;
|
||||
private Collection<String> userSessions;
|
||||
|
||||
private EmitValue emit = EmitValue.ENTITY;
|
||||
|
||||
private enum EmitValue {
|
||||
KEY, ENTITY
|
||||
}
|
||||
|
||||
public ClientSessionsOfUserSessionMapper(String realm, Collection<String> userSessions) {
|
||||
this.realm = realm;
|
||||
this.userSessions = userSessions;
|
||||
}
|
||||
|
||||
public ClientSessionsOfUserSessionMapper emitKey() {
|
||||
emit = EmitValue.KEY;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void map(String key, SessionEntity e, Collector<String, ClientSessionEntity> collector) {
|
||||
public void map(String key, SessionEntity e, Collector<String, Object> collector) {
|
||||
if (!realm.equals(e.getRealm())) {
|
||||
return;
|
||||
}
|
||||
|
@ -35,9 +46,14 @@ public class ClientSessionsOfUserSessionMapper implements Mapper<String, Session
|
|||
|
||||
ClientSessionEntity entity = (ClientSessionEntity) e;
|
||||
|
||||
for (String userSessionId : userSessions) {
|
||||
if (userSessionId.equals(((ClientSessionEntity) e).getUserSession())) {
|
||||
collector.emit(entity.getId(), entity);
|
||||
if (userSessions.contains(entity.getUserSession())) {
|
||||
switch (emit) {
|
||||
case KEY:
|
||||
collector.emit(entity.getId(), entity.getId());
|
||||
break;
|
||||
case ENTITY:
|
||||
collector.emit(entity.getId(), entity);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,9 +26,9 @@ public class UserSessionMapper implements Mapper<String, SessionEntity, String,
|
|||
|
||||
private String user;
|
||||
|
||||
private Long expired;
|
||||
private Integer expired;
|
||||
|
||||
private Long expiredRefresh;
|
||||
private Integer expiredRefresh;
|
||||
|
||||
private String brokerSessionId;
|
||||
private String brokerUserId;
|
||||
|
@ -47,7 +47,7 @@ public class UserSessionMapper implements Mapper<String, SessionEntity, String,
|
|||
return this;
|
||||
}
|
||||
|
||||
public UserSessionMapper expired(long expired, long expiredRefresh) {
|
||||
public UserSessionMapper expired(Integer expired, Integer expiredRefresh) {
|
||||
this.expired = expired;
|
||||
this.expiredRefresh = expiredRefresh;
|
||||
return this;
|
||||
|
@ -86,6 +86,10 @@ public class UserSessionMapper implements Mapper<String, SessionEntity, String,
|
|||
return;
|
||||
}
|
||||
|
||||
if (expired == null && expiredRefresh != null && entity.getLastSessionRefresh() > expiredRefresh) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (emit) {
|
||||
case KEY:
|
||||
collector.emit(key, key);
|
||||
|
|
|
@ -44,6 +44,8 @@ import java.util.Arrays;
|
|||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -98,9 +100,17 @@ public class TokenManager {
|
|||
ClientSessionModel clientSession = null;
|
||||
if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) {
|
||||
|
||||
clientSession = new UserSessionManager(session).findOfflineClientSession(realm, oldToken.getClientSession(), oldToken.getSessionState());
|
||||
UserSessionManager sessionManager = new UserSessionManager(session);
|
||||
clientSession = sessionManager.findOfflineClientSession(realm, oldToken.getClientSession(), oldToken.getSessionState());
|
||||
if (clientSession != null) {
|
||||
userSession = clientSession.getUserSession();
|
||||
|
||||
// Revoke timeouted offline userSession
|
||||
if (userSession.getLastSessionRefresh() < Time.currentTime() - realm.getOfflineSessionIdleTimeout()) {
|
||||
sessionManager.revokeOfflineUserSession(userSession);
|
||||
userSession = null;
|
||||
clientSession = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Find userSession regularly for online tokens
|
||||
|
@ -162,26 +172,24 @@ 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);
|
||||
|
||||
AccessTokenResponseBuilder responseBuilder = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession)
|
||||
AccessTokenResponse res = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession)
|
||||
.accessToken(validation.newToken)
|
||||
.generateIDToken();
|
||||
.generateIDToken()
|
||||
.generateRefreshToken()
|
||||
.build();
|
||||
|
||||
// Don't generate refresh token again if refresh was triggered with offline token
|
||||
if (!refreshToken.getType().equals(TokenUtil.TOKEN_TYPE_OFFLINE)) {
|
||||
responseBuilder.generateRefreshToken();
|
||||
}
|
||||
|
||||
AccessTokenResponse res = responseBuilder.build();
|
||||
return new RefreshResult(res, TokenUtil.TOKEN_TYPE_OFFLINE.equals(refreshToken.getType()));
|
||||
}
|
||||
|
||||
|
@ -321,6 +329,21 @@ public class TokenManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add all roles specified in scope parameter directly into requestedRoles, even if they are available just through composite role
|
||||
List<RoleModel> scopeRoles = new LinkedList<>();
|
||||
for (String scopeParamPart : scopeParamRoles) {
|
||||
RoleModel scopeParamRole = getRoleFromScopeParam(client.getRealm(), scopeParamPart);
|
||||
if (scopeParamRole != null) {
|
||||
for (RoleModel role : roles) {
|
||||
if (role.hasRole(scopeParamRole)) {
|
||||
scopeRoles.add(scopeParamRole);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
roles.addAll(scopeRoles);
|
||||
requestedRoles = roles;
|
||||
}
|
||||
|
||||
|
@ -337,6 +360,17 @@ public class TokenManager {
|
|||
}
|
||||
}
|
||||
|
||||
// For now, just use "roleName" for realm roles and "clientId/roleName" for client roles
|
||||
private static RoleModel getRoleFromScopeParam(RealmModel realm, String scopeParamRole) {
|
||||
String[] parts = scopeParamRole.split("/");
|
||||
if (parts.length == 1) {
|
||||
return realm.getRole(parts[0]);
|
||||
} else {
|
||||
ClientModel roleClient = realm.getClientByClientId(parts[0]);
|
||||
return roleClient!=null ? roleClient.getRole(parts[1]) : null;
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyAccess(AccessToken token, AccessToken newToken) throws OAuthErrorException {
|
||||
if (token.getRealmAccess() != null) {
|
||||
if (newToken.getRealmAccess() == null) throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "User no long has permission for realm roles");
|
||||
|
@ -507,7 +541,7 @@ public class TokenManager {
|
|||
|
||||
refreshToken = new RefreshToken(accessToken);
|
||||
refreshToken.type(TokenUtil.TOKEN_TYPE_OFFLINE);
|
||||
sessionManager.persistOfflineSession(clientSession, userSession);
|
||||
sessionManager.createOrUpdateOfflineSession(clientSession, userSession);
|
||||
} else {
|
||||
refreshToken = new RefreshToken(accessToken);
|
||||
refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -52,6 +52,7 @@ public class ApplianceBootstrap {
|
|||
realm.setSsoSessionIdleTimeout(1800);
|
||||
realm.setAccessTokenLifespan(60);
|
||||
realm.setSsoSessionMaxLifespan(36000);
|
||||
realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
|
||||
realm.setAccessCodeLifespan(60);
|
||||
realm.setAccessCodeLifespanUserAction(300);
|
||||
realm.setAccessCodeLifespanLogin(1800);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.keycloak.services.managers;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -15,6 +16,7 @@ import org.keycloak.models.RoleModel;
|
|||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
import org.keycloak.util.Time;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -32,17 +34,23 @@ public class UserSessionManager {
|
|||
this.persister = session.getProvider(UserSessionPersisterProvider.class);
|
||||
}
|
||||
|
||||
public void persistOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) {
|
||||
public void createOrUpdateOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) {
|
||||
UserModel user = userSession.getUser();
|
||||
|
||||
// Verify if we already have UserSession with this ID. If yes, don't create another one
|
||||
// Create and persist offline userSession if we don't have one
|
||||
UserSessionModel offlineUserSession = kcSession.sessions().getOfflineUserSession(clientSession.getRealm(), userSession.getId());
|
||||
if (offlineUserSession == null) {
|
||||
offlineUserSession = createOfflineUserSession(user, userSession);
|
||||
} else {
|
||||
// update lastSessionRefresh but don't need to persist
|
||||
offlineUserSession.setLastSessionRefresh(Time.currentTime());
|
||||
}
|
||||
|
||||
// Create clientSession and save to DB.
|
||||
createOfflineClientSession(user, clientSession, offlineUserSession);
|
||||
// Create and persist clientSession
|
||||
ClientSessionModel offlineClientSession = kcSession.sessions().getOfflineClientSession(clientSession.getRealm(), clientSession.getId());
|
||||
if (offlineClientSession == null) {
|
||||
createOfflineClientSession(user, clientSession, offlineUserSession);
|
||||
}
|
||||
}
|
||||
|
||||
// userSessionId is provided from offline token. It's used just to verify if it match the ID from clientSession representation
|
||||
|
@ -69,6 +77,15 @@ public class UserSessionManager {
|
|||
return clients;
|
||||
}
|
||||
|
||||
public List<UserSessionModel> findOfflineSessions(RealmModel realm, ClientModel client, UserModel user) {
|
||||
List<ClientSessionModel> clientSessions = kcSession.sessions().getOfflineClientSessions(realm, user);
|
||||
List<UserSessionModel> userSessions = new LinkedList<>();
|
||||
for (ClientSessionModel clientSession : clientSessions) {
|
||||
userSessions.add(clientSession.getUserSession());
|
||||
}
|
||||
return userSessions;
|
||||
}
|
||||
|
||||
public boolean revokeOfflineToken(UserModel user, ClientModel client) {
|
||||
RealmModel realm = client.getRealm();
|
||||
|
||||
|
@ -91,6 +108,14 @@ public class UserSessionManager {
|
|||
return anyRemoved;
|
||||
}
|
||||
|
||||
public void revokeOfflineUserSession(UserSessionModel userSession) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Removing offline user session '%s' for user '%s' ", userSession.getId(), userSession.getLoginUsername());
|
||||
}
|
||||
kcSession.sessions().removeOfflineUserSession(userSession.getRealm(), userSession.getId());
|
||||
persister.removeUserSession(userSession.getId(), true);
|
||||
}
|
||||
|
||||
public boolean isOfflineTokenAllowed(ClientSessionModel clientSession) {
|
||||
RoleModel offlineAccessRole = clientSession.getRealm().getRole(Constants.OFFLINE_ACCESS_ROLE);
|
||||
if (offlineAccessRole == null) {
|
||||
|
@ -107,7 +132,7 @@ public class UserSessionManager {
|
|||
}
|
||||
|
||||
UserSessionModel offlineUserSession = kcSession.sessions().createOfflineUserSession(userSession);
|
||||
persister.createUserSession(userSession, true);
|
||||
persister.createUserSession(offlineUserSession, true);
|
||||
return offlineUserSession;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -349,6 +350,44 @@ public class UsersResource {
|
|||
return reps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get offline sessions associated with the user and client
|
||||
*
|
||||
* @param id User id
|
||||
* @return
|
||||
*/
|
||||
@Path("{id}/offline-sessions/{clientId}")
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public List<UserSessionRepresentation> getSessions(final @PathParam("id") String id, final @PathParam("clientId") String clientId) {
|
||||
auth.requireView();
|
||||
UserModel user = session.users().getUserById(id, realm);
|
||||
if (user == null) {
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
ClientModel client = realm.getClientById(clientId);
|
||||
if (client == null) {
|
||||
throw new NotFoundException("Client not found");
|
||||
}
|
||||
List<UserSessionModel> sessions = new UserSessionManager(session).findOfflineSessions(realm, client, user);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get social logins associated with the user
|
||||
*
|
||||
|
@ -469,7 +508,14 @@ public class UsersResource {
|
|||
currentRep.put("grantedRealmRoles", (rep==null ? Collections.emptyList() : rep.getGrantedRealmRoles()));
|
||||
currentRep.put("grantedClientRoles", (rep==null ? Collections.emptyMap() : rep.getGrantedClientRoles()));
|
||||
|
||||
List<String> additionalGrants = hasOfflineToken ? Arrays.asList("Offline Token") : Collections.<String>emptyList();
|
||||
List<Map<String, String>> additionalGrants = new LinkedList<>();
|
||||
if (hasOfflineToken) {
|
||||
Map<String, String> offlineTokens = new HashMap<>();
|
||||
offlineTokens.put("client", client.getId());
|
||||
// TODO: translate
|
||||
offlineTokens.put("key", "Offline Token");
|
||||
additionalGrants.add(offlineTokens);
|
||||
}
|
||||
currentRep.put("additionalGrants", additionalGrants);
|
||||
|
||||
result.add(currentRep);
|
||||
|
|
|
@ -76,6 +76,7 @@ public class ImportTest extends AbstractModelTest {
|
|||
// Moved to static method, so it's possible to test this from other places too (for example export-import tests)
|
||||
public static void assertDataImportedInRealm(KeycloakSession session, RealmModel realm) {
|
||||
Assert.assertTrue(realm.isVerifyEmail());
|
||||
Assert.assertEquals(3600000, realm.getOfflineSessionIdleTimeout());
|
||||
|
||||
List<RequiredCredentialModel> creds = realm.getRequiredCredentials();
|
||||
Assert.assertEquals(1, creds.size());
|
||||
|
@ -361,6 +362,7 @@ public class ImportTest extends AbstractModelTest {
|
|||
RealmModel realm =manager.importRealm(rep);
|
||||
|
||||
Assert.assertEquals(600, realm.getAccessCodeLifespanUserAction());
|
||||
Assert.assertEquals(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT, realm.getOfflineSessionIdleTimeout());
|
||||
verifyRequiredCredentials(realm.getRequiredCredentials(), "password");
|
||||
}
|
||||
|
||||
|
|
|
@ -65,10 +65,13 @@ 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()) {
|
||||
sessionManager.persistOfflineSession(clientSession, userSession);
|
||||
sessionManager.createOrUpdateOfflineSession(clientSession, userSession);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.keycloak.testsuite.model;
|
|||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
@ -17,6 +18,7 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.services.managers.ClientManager;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
|
@ -36,6 +38,7 @@ public class UserSessionProviderOfflineTest {
|
|||
private KeycloakSession session;
|
||||
private RealmModel realm;
|
||||
private UserSessionManager sessionManager;
|
||||
private UserSessionPersisterProvider persister;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
|
@ -44,6 +47,7 @@ public class UserSessionProviderOfflineTest {
|
|||
session.users().addUser(realm, "user1").setEmail("user1@localhost");
|
||||
session.users().addUser(realm, "user2").setEmail("user2@localhost");
|
||||
sessionManager = new UserSessionManager(session);
|
||||
persister = session.getProvider(UserSessionPersisterProvider.class);
|
||||
}
|
||||
|
||||
@After
|
||||
|
@ -157,7 +161,7 @@ public class UserSessionProviderOfflineTest {
|
|||
fooRealm = session.realms().getRealm("foo");
|
||||
userSession = session.sessions().getUserSession(fooRealm, userSession.getId());
|
||||
clientSession = session.sessions().getClientSession(fooRealm, clientSession.getId());
|
||||
sessionManager.persistOfflineSession(userSession.getClientSessions().get(0), userSession);
|
||||
sessionManager.createOrUpdateOfflineSession(userSession.getClientSessions().get(0), userSession);
|
||||
|
||||
resetSession();
|
||||
|
||||
|
@ -291,13 +295,97 @@ public class UserSessionProviderOfflineTest {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpired() {
|
||||
// Create some online sessions in infinispan
|
||||
int started = Time.currentTime();
|
||||
UserSessionModel[] origSessions = createSessions();
|
||||
|
||||
resetSession();
|
||||
|
||||
Map<String, String> offlineSessions = new HashMap<>();
|
||||
|
||||
// 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) {
|
||||
offlineSessions.putAll(createOfflineSessionIncludeClientSessions(userSession));
|
||||
}
|
||||
|
||||
resetSession();
|
||||
|
||||
// Assert all previously saved offline sessions found
|
||||
for (Map.Entry<String, String> entry : offlineSessions.entrySet()) {
|
||||
Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey(), entry.getValue()) != null);
|
||||
}
|
||||
|
||||
UserSessionModel session0 = session.sessions().getOfflineUserSession(realm, origSessions[0].getId());
|
||||
Assert.assertNotNull(session0);
|
||||
List<String> clientSessions = new LinkedList<>();
|
||||
for (ClientSessionModel clientSession : session0.getClientSessions()) {
|
||||
clientSessions.add(clientSession.getId());
|
||||
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 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 cls1 not found too
|
||||
for (Map.Entry<String, String> entry : offlineSessions.entrySet()) {
|
||||
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(1, persister.getUserSessionsCount(true));
|
||||
|
||||
// Expire everything and assert nothing found
|
||||
Time.setOffset(3000000);
|
||||
try {
|
||||
session.sessions().removeExpiredUserSessions(realm);
|
||||
|
||||
resetSession();
|
||||
|
||||
for (Map.Entry<String, String> entry : offlineSessions.entrySet()) {
|
||||
Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey(), entry.getValue()) == null);
|
||||
}
|
||||
Assert.assertEquals(0, persister.getUserSessionsCount(true));
|
||||
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> createOfflineSessionIncludeClientSessions(UserSessionModel userSession) {
|
||||
Map<String, String> offlineSessions = new HashMap<>();
|
||||
|
||||
UserSessionModel offlineUserSession = session.sessions().createOfflineUserSession(userSession);
|
||||
for (ClientSessionModel clientSession : userSession.getClientSessions()) {
|
||||
ClientSessionModel offlineClientSession = session.sessions().createOfflineClientSession(clientSession);
|
||||
offlineClientSession.setUserSession(offlineUserSession);
|
||||
sessionManager.createOrUpdateOfflineSession(clientSession, userSession);
|
||||
offlineSessions.put(clientSession.getId(), userSession.getId());
|
||||
}
|
||||
return offlineSessions;
|
||||
|
@ -310,6 +398,7 @@ public class UserSessionProviderOfflineTest {
|
|||
session = kc.startSession();
|
||||
realm = session.realms().getRealm("test");
|
||||
sessionManager = new UserSessionManager(session);
|
||||
persister = session.getProvider(UserSessionPersisterProvider.class);
|
||||
}
|
||||
|
||||
private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.keycloak.models.Constants;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.services.managers.ClientManager;
|
||||
|
@ -227,10 +228,27 @@ public class OfflineTokenTest {
|
|||
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
|
||||
Assert.assertEquals(0, offlineToken.getExpiration());
|
||||
|
||||
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, sessionId, userId);
|
||||
String newRefreshTokenString = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, sessionId, userId);
|
||||
|
||||
// Change offset to very big value to ensure offline session expires
|
||||
Time.setOffset(3000000);
|
||||
|
||||
OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(newRefreshTokenString, "secret1");
|
||||
Assert.assertEquals(400, response.getStatusCode());
|
||||
assertEquals("invalid_grant", response.getError());
|
||||
|
||||
events.expectRefresh(offlineToken.getId(), sessionId)
|
||||
.client("offline-client")
|
||||
.error(Errors.INVALID_TOKEN)
|
||||
.user(userId)
|
||||
.clearDetails()
|
||||
.assertEvent();
|
||||
|
||||
|
||||
Time.setOffset(0);
|
||||
}
|
||||
|
||||
private void testRefreshWithOfflineToken(AccessToken oldToken, RefreshToken offlineToken, String offlineTokenString,
|
||||
private String testRefreshWithOfflineToken(AccessToken oldToken, RefreshToken offlineToken, String offlineTokenString,
|
||||
final String sessionId, String userId) {
|
||||
// Change offset to big value to ensure userSession expired
|
||||
Time.setOffset(99999);
|
||||
|
@ -261,13 +279,13 @@ public class OfflineTokenTest {
|
|||
Assert.assertEquals(200, response.getStatusCode());
|
||||
Assert.assertEquals(sessionId, refreshedToken.getSessionState());
|
||||
|
||||
// Assert no refreshToken in the response
|
||||
Assert.assertNull(response.getRefreshToken());
|
||||
// Assert new refreshToken in the response
|
||||
String newRefreshToken = response.getRefreshToken();
|
||||
Assert.assertNotNull(newRefreshToken);
|
||||
Assert.assertNotEquals(oldToken.getId(), refreshedToken.getId());
|
||||
|
||||
Assert.assertEquals(userId, refreshedToken.getSubject());
|
||||
|
||||
Assert.assertEquals(2, refreshedToken.getRealmAccess().getRoles().size());
|
||||
Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole("user"));
|
||||
Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole(Constants.OFFLINE_ACCESS_ROLE));
|
||||
|
||||
|
@ -283,6 +301,7 @@ public class OfflineTokenTest {
|
|||
Assert.assertNotEquals(oldToken.getId(), refreshEvent.getDetails().get(Details.TOKEN_ID));
|
||||
|
||||
Time.setOffset(0);
|
||||
return newRefreshToken;
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -313,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
|
||||
|
@ -365,6 +449,54 @@ public class OfflineTokenTest {
|
|||
testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void offlineTokenAllowedWithCompositeRole() throws Exception {
|
||||
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
|
||||
|
||||
@Override
|
||||
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||
ClientModel offlineClient = appRealm.getClientByClientId("offline-client");
|
||||
UserModel testUser = session.users().getUserByUsername("test-user@localhost", appRealm);
|
||||
RoleModel offlineAccess = appRealm.getRole(Constants.OFFLINE_ACCESS_ROLE);
|
||||
|
||||
// Test access
|
||||
Assert.assertFalse(TokenManager.getAccess(null, true, offlineClient, testUser).contains(offlineAccess));
|
||||
Assert.assertTrue(TokenManager.getAccess(OAuth2Constants.OFFLINE_ACCESS, true, offlineClient, testUser).contains(offlineAccess));
|
||||
|
||||
// Grant offline_access role indirectly through composite role
|
||||
RoleModel composite = appRealm.addRole("composite");
|
||||
composite.addCompositeRole(offlineAccess);
|
||||
|
||||
testUser.deleteRoleMapping(offlineAccess);
|
||||
testUser.grantRole(composite);
|
||||
|
||||
// Test access
|
||||
Assert.assertFalse(TokenManager.getAccess(null, true, offlineClient, testUser).contains(offlineAccess));
|
||||
Assert.assertTrue(TokenManager.getAccess(OAuth2Constants.OFFLINE_ACCESS, true, offlineClient, testUser).contains(offlineAccess));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Integration test
|
||||
offlineTokenDirectGrantFlow();
|
||||
|
||||
// Revert changes
|
||||
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
|
||||
|
||||
@Override
|
||||
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||
RoleModel composite = appRealm.getRole("composite");
|
||||
RoleModel offlineAccess = appRealm.getRole(Constants.OFFLINE_ACCESS_ROLE);
|
||||
UserModel testUser = session.users().getUserByUsername("test-user@localhost", appRealm);
|
||||
|
||||
testUser.deleteRoleMapping(composite);
|
||||
appRealm.removeRole(composite);
|
||||
testUser.grantRole(offlineAccess);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testServlet() {
|
||||
OfflineTokenServlet.tokenInfo = null;
|
||||
|
@ -382,11 +514,11 @@ public class OfflineTokenTest {
|
|||
String accessTokenId = OfflineTokenServlet.tokenInfo.accessToken.getId();
|
||||
String refreshTokenId = OfflineTokenServlet.tokenInfo.refreshToken.getId();
|
||||
|
||||
// Assert access token will be refreshed, but offline token will be still the same
|
||||
// Assert access token and offline token are refreshed
|
||||
Time.setOffset(9999);
|
||||
driver.navigate().to(offlineClientAppUri);
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri));
|
||||
Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getId(), refreshTokenId);
|
||||
Assert.assertNotEquals(OfflineTokenServlet.tokenInfo.refreshToken.getId(), refreshTokenId);
|
||||
Assert.assertNotEquals(OfflineTokenServlet.tokenInfo.accessToken.getId(), accessTokenId);
|
||||
|
||||
// Ensure that logout works for webapp (even if offline token will be still valid in Keycloak DB)
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"accessTokenLifespan": 6000,
|
||||
"accessCodeLifespan": 30,
|
||||
"accessCodeLifespanUserAction": 600,
|
||||
"offlineSessionIdleTimeout": 3600000,
|
||||
"requiredCredentials": [ "password" ],
|
||||
"defaultRoles": [ "foo", "bar" ],
|
||||
"verifyEmail" : "true",
|
||||
|
|
Loading…
Reference in a new issue