KEYCLOAK-904 Offline session idle timeout + admin console
This commit is contained in:
parent
229b964867
commit
802a39b1ce
39 changed files with 494 additions and 47 deletions
|
@ -2,6 +2,10 @@
|
||||||
<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">
|
<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">
|
<changeSet author="mposolda@redhat.com" id="1.6.0">
|
||||||
|
|
||||||
|
<addColumn tableName="REALM">
|
||||||
|
<column name="OFFLINE_SESSION_IDLE_TIMEOUT" type="INT"/>
|
||||||
|
</addColumn>
|
||||||
|
|
||||||
<addColumn tableName="KEYCLOAK_ROLE">
|
<addColumn tableName="KEYCLOAK_ROLE">
|
||||||
<column name="SCOPE_PARAM_REQUIRED" type="BOOLEAN" defaultValueBoolean="false">
|
<column name="SCOPE_PARAM_REQUIRED" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
<constraints nullable="false"/>
|
<constraints nullable="false"/>
|
||||||
|
|
|
@ -14,6 +14,7 @@ public class RealmRepresentation {
|
||||||
protected Integer accessTokenLifespan;
|
protected Integer accessTokenLifespan;
|
||||||
protected Integer ssoSessionIdleTimeout;
|
protected Integer ssoSessionIdleTimeout;
|
||||||
protected Integer ssoSessionMaxLifespan;
|
protected Integer ssoSessionMaxLifespan;
|
||||||
|
protected Integer offlineSessionIdleTimeout;
|
||||||
protected Integer accessCodeLifespan;
|
protected Integer accessCodeLifespan;
|
||||||
protected Integer accessCodeLifespanUserAction;
|
protected Integer accessCodeLifespanUserAction;
|
||||||
protected Integer accessCodeLifespanLogin;
|
protected Integer accessCodeLifespanLogin;
|
||||||
|
@ -199,6 +200,14 @@ public class RealmRepresentation {
|
||||||
this.ssoSessionMaxLifespan = ssoSessionMaxLifespan;
|
this.ssoSessionMaxLifespan = ssoSessionMaxLifespan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getOfflineSessionIdleTimeout() {
|
||||||
|
return offlineSessionIdleTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOfflineSessionIdleTimeout(Integer offlineSessionIdleTimeout) {
|
||||||
|
this.offlineSessionIdleTimeout = offlineSessionIdleTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
public List<ScopeMappingRepresentation> getScopeMappings() {
|
public List<ScopeMappingRepresentation> getScopeMappings() {
|
||||||
return scopeMappings;
|
return scopeMappings;
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,8 @@ days=Days
|
||||||
sso-session-max=SSO Session Max
|
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-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.
|
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=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.
|
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
|
client-login-timeout=Client login timeout
|
||||||
|
@ -336,6 +338,7 @@ offline-tokens.tooltip=Total number of offline tokens for this client.
|
||||||
show-offline-tokens=Show Offline Tokens
|
show-offline-tokens=Show Offline Tokens
|
||||||
show-offline-tokens.tooltip=Warning, this is a potentially expensive operation depending on number of offline tokens.
|
show-offline-tokens.tooltip=Warning, this is a potentially expensive operation depending on number of offline tokens.
|
||||||
token-issued=Token Issued
|
token-issued=Token Issued
|
||||||
|
last-access=Last Access
|
||||||
key-export=Key Export
|
key-export=Key Export
|
||||||
key-import=Key Import
|
key-import=Key Import
|
||||||
export-saml-key=Export SAML Key
|
export-saml-key=Export SAML Key
|
||||||
|
|
|
@ -498,6 +498,24 @@ module.config([ '$routeProvider', function($routeProvider) {
|
||||||
},
|
},
|
||||||
controller : 'UserConsentsCtrl'
|
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', {
|
.when('/realms/:realm/users', {
|
||||||
templateUrl : resourceUrl + '/partials/user-list.html',
|
templateUrl : resourceUrl + '/partials/user-list.html',
|
||||||
resolve : {
|
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.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.accessCodeLifespanUnit = TimeUnit.autoUnit(realm.accessCodeLifespan);
|
||||||
$scope.realm.accessCodeLifespan = TimeUnit.toUnit(realm.accessCodeLifespan, $scope.realm.accessCodeLifespanUnit);
|
$scope.realm.accessCodeLifespan = TimeUnit.toUnit(realm.accessCodeLifespan, $scope.realm.accessCodeLifespanUnit);
|
||||||
$scope.$watch('realm.accessCodeLifespanUnit', function(to, from) {
|
$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);
|
var realmCopy = angular.copy($scope.realm);
|
||||||
delete realmCopy["accessTokenLifespanUnit"];
|
delete realmCopy["accessTokenLifespanUnit"];
|
||||||
delete realmCopy["ssoSessionMaxLifespanUnit"];
|
delete realmCopy["ssoSessionMaxLifespanUnit"];
|
||||||
|
delete realmCopy["offlineSessionIdleTimeoutUnit"];
|
||||||
delete realmCopy["accessCodeLifespanUnit"];
|
delete realmCopy["accessCodeLifespanUnit"];
|
||||||
delete realmCopy["ssoSessionIdleTimeoutUnit"];
|
delete realmCopy["ssoSessionIdleTimeoutUnit"];
|
||||||
delete realmCopy["accessCodeLifespanUserActionUnit"];
|
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.accessTokenLifespan = TimeUnit.toSeconds($scope.realm.accessTokenLifespan, $scope.realm.accessTokenLifespanUnit)
|
||||||
realmCopy.ssoSessionIdleTimeout = TimeUnit.toSeconds($scope.realm.ssoSessionIdleTimeout, $scope.realm.ssoSessionIdleTimeoutUnit)
|
realmCopy.ssoSessionIdleTimeout = TimeUnit.toSeconds($scope.realm.ssoSessionIdleTimeout, $scope.realm.ssoSessionIdleTimeoutUnit)
|
||||||
realmCopy.ssoSessionMaxLifespan = TimeUnit.toSeconds($scope.realm.ssoSessionMaxLifespan, $scope.realm.ssoSessionMaxLifespanUnit)
|
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.accessCodeLifespan = TimeUnit.toSeconds($scope.realm.accessCodeLifespan, $scope.realm.accessCodeLifespanUnit)
|
||||||
realmCopy.accessCodeLifespanUserAction = TimeUnit.toSeconds($scope.realm.accessCodeLifespanUserAction, $scope.realm.accessCodeLifespanUserActionUnit)
|
realmCopy.accessCodeLifespanUserAction = TimeUnit.toSeconds($scope.realm.accessCodeLifespanUserAction, $scope.realm.accessCodeLifespanUserActionUnit)
|
||||||
realmCopy.accessCodeLifespanLogin = TimeUnit.toSeconds($scope.realm.accessCodeLifespanLogin, $scope.realm.accessCodeLifespanLoginUnit)
|
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) {
|
module.controller('UserListCtrl', function($scope, realm, User, UserImpersonation, BruteForce, Notifications, $route, Dialog) {
|
||||||
$scope.realm = realm;
|
$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) {
|
module.factory('UserFederatedIdentityLoader', function(Loader, UserFederatedIdentities, $route, $q) {
|
||||||
return Loader.query(UserFederatedIdentities, function() {
|
return Loader.query(UserFederatedIdentities, function() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -369,6 +369,13 @@ module.factory('UserSessions', function($resource) {
|
||||||
user : '@user'
|
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) {
|
module.factory('UserSessionLogout', function($resource) {
|
||||||
return $resource(authUrl + '/admin/realms/:realm/sessions/:session', {
|
return $resource(authUrl + '/admin/realms/:realm/sessions/:session', {
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<table class="table table-striped table-bordered" data-ng-show="count > 0">
|
<table class="table table-striped table-bordered" data-ng-show="count > 0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="kc-table-actions" colspan="3">
|
<th class="kc-table-actions" colspan="4">
|
||||||
<div class="pull-right">
|
<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>
|
<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>
|
</div>
|
||||||
|
@ -31,6 +31,7 @@
|
||||||
<th>{{:: 'user' | translate}}</th>
|
<th>{{:: 'user' | translate}}</th>
|
||||||
<th>{{:: 'from-ip' | translate}}</th>
|
<th>{{:: 'from-ip' | translate}}</th>
|
||||||
<th>{{:: 'token-issued' | translate}}</th>
|
<th>{{:: 'token-issued' | translate}}</th>
|
||||||
|
<th>{{:: 'last-access' | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tfoot data-ng-show="sessions && (sessions.length >= 5 || query.first != 0)">
|
<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><a href="#/realms/{{realm.realm}}/users/{{session.userId}}">{{session.username}}</a></td>
|
||||||
<td>{{session.ipAddress}}</td>
|
<td>{{session.ipAddress}}</td>
|
||||||
<td>{{session.start | date:'medium'}}</td>
|
<td>{{session.start | date:'medium'}}</td>
|
||||||
|
<td>{{session.lastAccess | date:'medium'}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -48,6 +48,23 @@
|
||||||
<kc-tooltip>{{:: 'sso-session-max.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'sso-session-max.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label class="col-md-2 control-label" for="accessTokenLifespan">{{:: 'access-token-lifespan' | translate}}</label>
|
<label class="col-md-2 control-label" for="accessTokenLifespan">{{:: 'access-token-lifespan' | translate}}</label>
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span data-ng-repeat="additionalGrant in consent.additionalGrants">
|
<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>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="kc-action-cell">
|
<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 Access</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();
|
List<RealmModel> realms = session.realms().getRealms();
|
||||||
for (RealmModel realm : realms) {
|
for (RealmModel realm : realms) {
|
||||||
|
realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
|
||||||
|
|
||||||
if (realm.getRole(Constants.OFFLINE_ACCESS_ROLE) == null) {
|
if (realm.getRole(Constants.OFFLINE_ACCESS_ROLE) == null) {
|
||||||
for (RoleModel realmRole : realm.getRoles()) {
|
for (RoleModel realmRole : realm.getRoles()) {
|
||||||
realmRole.setScopeParamRequired(false);
|
realmRole.setScopeParamRequired(false);
|
||||||
|
|
|
@ -19,4 +19,7 @@ public interface Constants {
|
||||||
String READ_TOKEN_ROLE = "read-token";
|
String READ_TOKEN_ROLE = "read-token";
|
||||||
String[] BROKER_SERVICE_ROLES = {READ_TOKEN_ROLE};
|
String[] BROKER_SERVICE_ROLES = {READ_TOKEN_ROLE};
|
||||||
String OFFLINE_ACCESS_ROLE = OAuth2Constants.OFFLINE_ACCESS;
|
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();
|
int getSsoSessionMaxLifespan();
|
||||||
void setSsoSessionMaxLifespan(int seconds);
|
void setSsoSessionMaxLifespan(int seconds);
|
||||||
|
|
||||||
// int getOfflineSessionIdleTimeout();
|
int getOfflineSessionIdleTimeout();
|
||||||
// void setOfflineSessionIdleTimeout(int seconds);
|
void setOfflineSessionIdleTimeout(int seconds);
|
||||||
|
|
||||||
int getAccessTokenLifespan();
|
int getAccessTokenLifespan();
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
|
||||||
private boolean revokeRefreshToken;
|
private boolean revokeRefreshToken;
|
||||||
private int ssoSessionIdleTimeout;
|
private int ssoSessionIdleTimeout;
|
||||||
private int ssoSessionMaxLifespan;
|
private int ssoSessionMaxLifespan;
|
||||||
|
private int offlineSessionIdleTimeout;
|
||||||
private int accessTokenLifespan;
|
private int accessTokenLifespan;
|
||||||
private int accessCodeLifespan;
|
private int accessCodeLifespan;
|
||||||
private int accessCodeLifespanUserAction;
|
private int accessCodeLifespanUserAction;
|
||||||
|
@ -254,6 +255,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
|
||||||
this.ssoSessionMaxLifespan = ssoSessionMaxLifespan;
|
this.ssoSessionMaxLifespan = ssoSessionMaxLifespan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getOfflineSessionIdleTimeout() {
|
||||||
|
return offlineSessionIdleTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOfflineSessionIdleTimeout(int offlineSessionIdleTimeout) {
|
||||||
|
this.offlineSessionIdleTimeout = offlineSessionIdleTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
public int getAccessTokenLifespan() {
|
public int getAccessTokenLifespan() {
|
||||||
return accessTokenLifespan;
|
return accessTokenLifespan;
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,6 +148,7 @@ public class ModelToRepresentation {
|
||||||
rep.setAccessTokenLifespan(realm.getAccessTokenLifespan());
|
rep.setAccessTokenLifespan(realm.getAccessTokenLifespan());
|
||||||
rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
|
rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
|
||||||
rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan());
|
rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan());
|
||||||
|
rep.setOfflineSessionIdleTimeout(realm.getOfflineSessionIdleTimeout());
|
||||||
rep.setAccessCodeLifespan(realm.getAccessCodeLifespan());
|
rep.setAccessCodeLifespan(realm.getAccessCodeLifespan());
|
||||||
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
|
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
|
||||||
rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin());
|
rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin());
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.keycloak.models.utils;
|
package org.keycloak.models.utils;
|
||||||
|
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.util.Base64;
|
import org.keycloak.util.Base64;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.enums.SslRequired;
|
import org.keycloak.enums.SslRequired;
|
||||||
|
@ -106,6 +107,8 @@ public class RepresentationToModel {
|
||||||
else newRealm.setSsoSessionIdleTimeout(1800);
|
else newRealm.setSsoSessionIdleTimeout(1800);
|
||||||
if (rep.getSsoSessionMaxLifespan() != null) newRealm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
|
if (rep.getSsoSessionMaxLifespan() != null) newRealm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
|
||||||
else newRealm.setSsoSessionMaxLifespan(36000);
|
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());
|
if (rep.getAccessCodeLifespan() != null) newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
|
||||||
else newRealm.setAccessCodeLifespan(60);
|
else newRealm.setAccessCodeLifespan(60);
|
||||||
|
@ -535,6 +538,7 @@ public class RepresentationToModel {
|
||||||
if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
|
if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
|
||||||
if (rep.getSsoSessionIdleTimeout() != null) realm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout());
|
if (rep.getSsoSessionIdleTimeout() != null) realm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout());
|
||||||
if (rep.getSsoSessionMaxLifespan() != null) realm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
|
if (rep.getSsoSessionMaxLifespan() != null) realm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
|
||||||
|
if (rep.getOfflineSessionIdleTimeout() != null) realm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout());
|
||||||
if (rep.getRequiredCredentials() != null) {
|
if (rep.getRequiredCredentials() != null) {
|
||||||
realm.updateRequiredCredentials(rep.getRequiredCredentials());
|
realm.updateRequiredCredentials(rep.getRequiredCredentials());
|
||||||
}
|
}
|
||||||
|
|
|
@ -355,6 +355,16 @@ public class RealmAdapter implements RealmModel {
|
||||||
realm.setSsoSessionMaxLifespan(seconds);
|
realm.setSsoSessionMaxLifespan(seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOfflineSessionIdleTimeout() {
|
||||||
|
return realm.getOfflineSessionIdleTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOfflineSessionIdleTimeout(int seconds) {
|
||||||
|
realm.setOfflineSessionIdleTimeout(seconds);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAccessTokenLifespan() {
|
public int getAccessTokenLifespan() {
|
||||||
return realm.getAccessTokenLifespan();
|
return realm.getAccessTokenLifespan();
|
||||||
|
|
|
@ -275,6 +275,19 @@ public class RealmAdapter implements RealmModel {
|
||||||
updated.setSsoSessionMaxLifespan(seconds);
|
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
|
@Override
|
||||||
public int getAccessTokenLifespan() {
|
public int getAccessTokenLifespan() {
|
||||||
if (updated != null) return updated.getAccessTokenLifespan();
|
if (updated != null) return updated.getAccessTokenLifespan();
|
||||||
|
|
|
@ -58,6 +58,7 @@ public class CachedRealm implements Serializable {
|
||||||
private boolean revokeRefreshToken;
|
private boolean revokeRefreshToken;
|
||||||
private int ssoSessionIdleTimeout;
|
private int ssoSessionIdleTimeout;
|
||||||
private int ssoSessionMaxLifespan;
|
private int ssoSessionMaxLifespan;
|
||||||
|
private int offlineSessionIdleTimeout;
|
||||||
private int accessTokenLifespan;
|
private int accessTokenLifespan;
|
||||||
private int accessCodeLifespan;
|
private int accessCodeLifespan;
|
||||||
private int accessCodeLifespanUserAction;
|
private int accessCodeLifespanUserAction;
|
||||||
|
@ -140,6 +141,7 @@ public class CachedRealm implements Serializable {
|
||||||
revokeRefreshToken = model.isRevokeRefreshToken();
|
revokeRefreshToken = model.isRevokeRefreshToken();
|
||||||
ssoSessionIdleTimeout = model.getSsoSessionIdleTimeout();
|
ssoSessionIdleTimeout = model.getSsoSessionIdleTimeout();
|
||||||
ssoSessionMaxLifespan = model.getSsoSessionMaxLifespan();
|
ssoSessionMaxLifespan = model.getSsoSessionMaxLifespan();
|
||||||
|
offlineSessionIdleTimeout = model.getOfflineSessionIdleTimeout();
|
||||||
accessTokenLifespan = model.getAccessTokenLifespan();
|
accessTokenLifespan = model.getAccessTokenLifespan();
|
||||||
accessCodeLifespan = model.getAccessCodeLifespan();
|
accessCodeLifespan = model.getAccessCodeLifespan();
|
||||||
accessCodeLifespanUserAction = model.getAccessCodeLifespanUserAction();
|
accessCodeLifespanUserAction = model.getAccessCodeLifespanUserAction();
|
||||||
|
@ -327,6 +329,10 @@ public class CachedRealm implements Serializable {
|
||||||
return ssoSessionMaxLifespan;
|
return ssoSessionMaxLifespan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getOfflineSessionIdleTimeout() {
|
||||||
|
return offlineSessionIdleTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
public int getAccessTokenLifespan() {
|
public int getAccessTokenLifespan() {
|
||||||
return accessTokenLifespan;
|
return accessTokenLifespan;
|
||||||
}
|
}
|
||||||
|
|
|
@ -377,6 +377,16 @@ public class RealmAdapter implements RealmModel {
|
||||||
realm.setSsoSessionMaxLifespan(seconds);
|
realm.setSsoSessionMaxLifespan(seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOfflineSessionIdleTimeout() {
|
||||||
|
return realm.getOfflineSessionIdleTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOfflineSessionIdleTimeout(int seconds) {
|
||||||
|
realm.setOfflineSessionIdleTimeout(seconds);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAccessCodeLifespan() {
|
public int getAccessCodeLifespan() {
|
||||||
return realm.getAccessCodeLifespan();
|
return realm.getAccessCodeLifespan();
|
||||||
|
|
|
@ -82,6 +82,8 @@ public class RealmEntity {
|
||||||
private int ssoSessionIdleTimeout;
|
private int ssoSessionIdleTimeout;
|
||||||
@Column(name="SSO_MAX_LIFESPAN")
|
@Column(name="SSO_MAX_LIFESPAN")
|
||||||
private int ssoSessionMaxLifespan;
|
private int ssoSessionMaxLifespan;
|
||||||
|
@Column(name="OFFLINE_SESSION_IDLE_TIMEOUT")
|
||||||
|
private int offlineSessionIdleTimeout;
|
||||||
@Column(name="ACCESS_TOKEN_LIFESPAN")
|
@Column(name="ACCESS_TOKEN_LIFESPAN")
|
||||||
protected int accessTokenLifespan;
|
protected int accessTokenLifespan;
|
||||||
@Column(name="ACCESS_CODE_LIFESPAN")
|
@Column(name="ACCESS_CODE_LIFESPAN")
|
||||||
|
@ -314,6 +316,14 @@ public class RealmEntity {
|
||||||
this.ssoSessionMaxLifespan = ssoSessionMaxLifespan;
|
this.ssoSessionMaxLifespan = ssoSessionMaxLifespan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getOfflineSessionIdleTimeout() {
|
||||||
|
return offlineSessionIdleTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOfflineSessionIdleTimeout(int offlineSessionIdleTimeout) {
|
||||||
|
this.offlineSessionIdleTimeout = offlineSessionIdleTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
public int getAccessTokenLifespan() {
|
public int getAccessTokenLifespan() {
|
||||||
return accessTokenLifespan;
|
return accessTokenLifespan;
|
||||||
}
|
}
|
||||||
|
|
|
@ -344,6 +344,17 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
||||||
updateRealm();
|
updateRealm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOfflineSessionIdleTimeout() {
|
||||||
|
return realm.getOfflineSessionIdleTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOfflineSessionIdleTimeout(int seconds) {
|
||||||
|
realm.setOfflineSessionIdleTimeout(seconds);
|
||||||
|
updateRealm();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAccessTokenLifespan() {
|
public int getAccessTokenLifespan() {
|
||||||
return realm.getAccessTokenLifespan();
|
return realm.getAccessTokenLifespan();
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.UserSessionProvider;
|
import org.keycloak.models.UserSessionProvider;
|
||||||
import org.keycloak.models.UsernameLoginFailureModel;
|
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.ClientSessionEntity;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
|
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
|
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
|
||||||
|
@ -302,8 +303,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeExpiredUserSessions(RealmModel realm) {
|
public void removeExpiredUserSessions(RealmModel realm) {
|
||||||
|
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
|
||||||
|
|
||||||
int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan();
|
int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan();
|
||||||
int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout();
|
int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout();
|
||||||
|
int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout();
|
||||||
int expiredDettachedClientSession = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm);
|
int expiredDettachedClientSession = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm);
|
||||||
|
|
||||||
Map<String, String> map = new MapReduceTask(sessionCache)
|
Map<String, String> map = new MapReduceTask(sessionCache)
|
||||||
|
@ -323,6 +327,29 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
for (String id : map.keySet()) {
|
for (String id : map.keySet()) {
|
||||||
tx.remove(sessionCache, id);
|
tx.remove(sessionCache, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove expired offline user sessions
|
||||||
|
map = new MapReduceTask(offlineSessionCache)
|
||||||
|
.mappedWith(UserSessionMapper.create(realm.getId()).expired(null, expiredOffline).emitKey())
|
||||||
|
.reducedWith(new FirstResultReducer())
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
for (String id : map.keySet()) {
|
||||||
|
tx.remove(offlineSessionCache, id);
|
||||||
|
// propagate to persister
|
||||||
|
persister.removeUserSession(id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove offline client sessions of expired offline user sessions
|
||||||
|
map = new MapReduceTask(offlineSessionCache)
|
||||||
|
.mappedWith(new ClientSessionsOfUserSessionMapper(realm.getId(), new HashSet<>(map.keySet())).emitKey())
|
||||||
|
.reducedWith(new FirstResultReducer())
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
for (String id : map.keySet()) {
|
||||||
|
tx.remove(offlineSessionCache, id);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -477,6 +504,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
|
|
||||||
tx.remove(cache, userSessionId);
|
tx.remove(cache, userSessionId);
|
||||||
|
|
||||||
|
// TODO: We can retrieve it from userSessionEntity directly
|
||||||
Map<String, String> map = new MapReduceTask(cache)
|
Map<String, String> map = new MapReduceTask(cache)
|
||||||
.mappedWith(ClientSessionMapper.create(realm.getId()).userSession(userSessionId).emitKey())
|
.mappedWith(ClientSessionMapper.create(realm.getId()).userSession(userSessionId).emitKey())
|
||||||
.reducedWith(new FirstResultReducer())
|
.reducedWith(new FirstResultReducer())
|
||||||
|
@ -534,14 +562,17 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
entity.setBrokerSessionId(userSession.getBrokerSessionId());
|
entity.setBrokerSessionId(userSession.getBrokerSessionId());
|
||||||
entity.setBrokerUserId(userSession.getBrokerUserId());
|
entity.setBrokerUserId(userSession.getBrokerUserId());
|
||||||
entity.setIpAddress(userSession.getIpAddress());
|
entity.setIpAddress(userSession.getIpAddress());
|
||||||
entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
|
|
||||||
entity.setLoginUsername(userSession.getLoginUsername());
|
entity.setLoginUsername(userSession.getLoginUsername());
|
||||||
entity.setNotes(userSession.getNotes());
|
entity.setNotes(userSession.getNotes());
|
||||||
entity.setRememberMe(userSession.isRememberMe());
|
entity.setRememberMe(userSession.isRememberMe());
|
||||||
entity.setStarted(userSession.getStarted());
|
|
||||||
entity.setState(userSession.getState());
|
entity.setState(userSession.getState());
|
||||||
entity.setUser(userSession.getUser().getId());
|
entity.setUser(userSession.getUser().getId());
|
||||||
|
|
||||||
|
// started and lastSessionRefresh set to current time
|
||||||
|
int currentTime = Time.currentTime();
|
||||||
|
entity.setStarted(currentTime);
|
||||||
|
entity.setLastSessionRefresh(currentTime);
|
||||||
|
|
||||||
tx.put(offlineSessionCache, userSession.getId(), entity);
|
tx.put(offlineSessionCache, userSession.getId(), entity);
|
||||||
return wrap(userSession.getRealm(), entity, true);
|
return wrap(userSession.getRealm(), entity, true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.UserSessionProvider;
|
import org.keycloak.models.UserSessionProvider;
|
||||||
import org.keycloak.models.UsernameLoginFailureModel;
|
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.ClientSessionEntity;
|
||||||
import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity;
|
import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity;
|
||||||
import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureEntity;
|
import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureEntity;
|
||||||
|
@ -297,6 +298,8 @@ public class MemUserSessionProvider implements UserSessionProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeExpiredUserSessions(RealmModel realm) {
|
public void removeExpiredUserSessions(RealmModel realm) {
|
||||||
|
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
|
||||||
|
|
||||||
Iterator<UserSessionEntity> itr = userSessions.values().iterator();
|
Iterator<UserSessionEntity> itr = userSessions.values().iterator();
|
||||||
while (itr.hasNext()) {
|
while (itr.hasNext()) {
|
||||||
UserSessionEntity s = itr.next();
|
UserSessionEntity s = itr.next();
|
||||||
|
@ -314,6 +317,19 @@ public class MemUserSessionProvider implements UserSessionProvider {
|
||||||
citr.remove();
|
citr.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove expired offline 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -415,16 +431,19 @@ public class MemUserSessionProvider implements UserSessionProvider {
|
||||||
entity.setBrokerSessionId(userSession.getBrokerSessionId());
|
entity.setBrokerSessionId(userSession.getBrokerSessionId());
|
||||||
entity.setBrokerUserId(userSession.getBrokerUserId());
|
entity.setBrokerUserId(userSession.getBrokerUserId());
|
||||||
entity.setIpAddress(userSession.getIpAddress());
|
entity.setIpAddress(userSession.getIpAddress());
|
||||||
entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
|
|
||||||
entity.setLoginUsername(userSession.getLoginUsername());
|
entity.setLoginUsername(userSession.getLoginUsername());
|
||||||
if (userSession.getNotes() != null) {
|
if (userSession.getNotes() != null) {
|
||||||
entity.getNotes().putAll(userSession.getNotes());
|
entity.getNotes().putAll(userSession.getNotes());
|
||||||
}
|
}
|
||||||
entity.setRememberMe(userSession.isRememberMe());
|
entity.setRememberMe(userSession.isRememberMe());
|
||||||
entity.setStarted(userSession.getStarted());
|
|
||||||
entity.setState(userSession.getState());
|
entity.setState(userSession.getState());
|
||||||
entity.setUser(userSession.getUser().getId());
|
entity.setUser(userSession.getUser().getId());
|
||||||
|
|
||||||
|
// started and lastSessionRefresh set to current time
|
||||||
|
int currentTime = Time.currentTime();
|
||||||
|
entity.setStarted(currentTime);
|
||||||
|
entity.setLastSessionRefresh(currentTime);
|
||||||
|
|
||||||
offlineUserSessions.put(userSession.getId(), entity);
|
offlineUserSessions.put(userSession.getId(), entity);
|
||||||
return new UserSessionAdapter(session, this, userSession.getRealm(), entity);
|
return new UserSessionAdapter(session, this, userSession.getRealm(), entity);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ public class InitializerState extends SessionEntity {
|
||||||
|
|
||||||
private int sessionsCount;
|
private int sessionsCount;
|
||||||
private List<Boolean> segments = new ArrayList<>();
|
private List<Boolean> segments = new ArrayList<>();
|
||||||
|
private int lowestUnfinishedSegment = 0;
|
||||||
|
|
||||||
|
|
||||||
public void init(int sessionsCount, int sessionsPerSegment) {
|
public void init(int sessionsCount, int sessionsPerSegment) {
|
||||||
|
@ -31,18 +32,21 @@ public class InitializerState extends SessionEntity {
|
||||||
for (int i=0 ; i<segmentsCount ; i++) {
|
for (int i=0 ; i<segmentsCount ; i++) {
|
||||||
segments.add(false);
|
segments.add(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateLowestUnfinishedSegment();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return true just if computation is entirely finished (all segments are true)
|
// Return true just if computation is entirely finished (all segments are true)
|
||||||
public boolean isFinished() {
|
public boolean isFinished() {
|
||||||
return getNextUnfinishedSegmentFromIndex(0) == -1;
|
return lowestUnfinishedSegment == -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return next un-finished segments. It can return "segmentCount" segments or less
|
// Return next un-finished segments. It can return "segmentCount" segments or less
|
||||||
public List<Integer> getUnfinishedSegments(int segmentCount) {
|
public List<Integer> getUnfinishedSegments(int segmentCount) {
|
||||||
List<Integer> result = new ArrayList<>();
|
List<Integer> result = new ArrayList<>();
|
||||||
boolean remaining = true;
|
int next = lowestUnfinishedSegment;
|
||||||
int next=0;
|
boolean remaining = lowestUnfinishedSegment != -1;
|
||||||
|
|
||||||
while (remaining && result.size() < segmentCount) {
|
while (remaining && result.size() < segmentCount) {
|
||||||
next = getNextUnfinishedSegmentFromIndex(next);
|
next = getNextUnfinishedSegmentFromIndex(next);
|
||||||
if (next == -1) {
|
if (next == -1) {
|
||||||
|
@ -58,6 +62,11 @@ public class InitializerState extends SessionEntity {
|
||||||
|
|
||||||
public void markSegmentFinished(int index) {
|
public void markSegmentFinished(int index) {
|
||||||
segments.set(index, true);
|
segments.set(index, true);
|
||||||
|
updateLowestUnfinishedSegment();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLowestUnfinishedSegment() {
|
||||||
|
this.lowestUnfinishedSegment = getNextUnfinishedSegmentFromIndex(lowestUnfinishedSegment);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getNextUnfinishedSegmentFromIndex(int index) {
|
private int getNextUnfinishedSegmentFromIndex(int index) {
|
||||||
|
|
|
@ -29,7 +29,7 @@ public class OfflineUserSessionLoader implements SessionLoader {
|
||||||
|
|
||||||
for (UserSessionModel persistentSession : sessions) {
|
for (UserSessionModel persistentSession : sessions) {
|
||||||
|
|
||||||
// Update and persist lastSessionRefresh time
|
// Update and persist lastSessionRefresh time TODO: Do bulk DB update instead?
|
||||||
persistentSession.setLastSessionRefresh(currentTime);
|
persistentSession.setLastSessionRefresh(currentTime);
|
||||||
persister.updateUserSession(persistentSession, true);
|
persister.updateUserSession(persistentSession, true);
|
||||||
|
|
||||||
|
|
|
@ -13,18 +13,29 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @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 String realm;
|
||||||
private Collection<String> userSessions;
|
private Collection<String> userSessions;
|
||||||
|
|
||||||
|
private EmitValue emit = EmitValue.ENTITY;
|
||||||
|
|
||||||
|
private enum EmitValue {
|
||||||
|
KEY, ENTITY
|
||||||
|
}
|
||||||
|
|
||||||
public ClientSessionsOfUserSessionMapper(String realm, Collection<String> userSessions) {
|
public ClientSessionsOfUserSessionMapper(String realm, Collection<String> userSessions) {
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
this.userSessions = userSessions;
|
this.userSessions = userSessions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ClientSessionsOfUserSessionMapper emitKey() {
|
||||||
|
emit = EmitValue.KEY;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@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())) {
|
if (!realm.equals(e.getRealm())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -35,9 +46,14 @@ public class ClientSessionsOfUserSessionMapper implements Mapper<String, Session
|
||||||
|
|
||||||
ClientSessionEntity entity = (ClientSessionEntity) e;
|
ClientSessionEntity entity = (ClientSessionEntity) e;
|
||||||
|
|
||||||
for (String userSessionId : userSessions) {
|
if (userSessions.contains(entity.getUserSession())) {
|
||||||
if (userSessionId.equals(((ClientSessionEntity) e).getUserSession())) {
|
switch (emit) {
|
||||||
collector.emit(entity.getId(), entity);
|
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 String user;
|
||||||
|
|
||||||
private Long expired;
|
private Integer expired;
|
||||||
|
|
||||||
private Long expiredRefresh;
|
private Integer expiredRefresh;
|
||||||
|
|
||||||
private String brokerSessionId;
|
private String brokerSessionId;
|
||||||
private String brokerUserId;
|
private String brokerUserId;
|
||||||
|
@ -47,7 +47,7 @@ public class UserSessionMapper implements Mapper<String, SessionEntity, String,
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserSessionMapper expired(long expired, long expiredRefresh) {
|
public UserSessionMapper expired(Integer expired, Integer expiredRefresh) {
|
||||||
this.expired = expired;
|
this.expired = expired;
|
||||||
this.expiredRefresh = expiredRefresh;
|
this.expiredRefresh = expiredRefresh;
|
||||||
return this;
|
return this;
|
||||||
|
@ -86,6 +86,10 @@ public class UserSessionMapper implements Mapper<String, SessionEntity, String,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (expired == null && expiredRefresh != null && entity.getLastSessionRefresh() > expiredRefresh) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (emit) {
|
switch (emit) {
|
||||||
case KEY:
|
case KEY:
|
||||||
collector.emit(key, key);
|
collector.emit(key, key);
|
||||||
|
|
|
@ -98,9 +98,17 @@ public class TokenManager {
|
||||||
ClientSessionModel clientSession = null;
|
ClientSessionModel clientSession = null;
|
||||||
if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) {
|
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) {
|
if (clientSession != null) {
|
||||||
userSession = clientSession.getUserSession();
|
userSession = clientSession.getUserSession();
|
||||||
|
|
||||||
|
// Revoke timeouted offline userSession
|
||||||
|
if (userSession.getLastSessionRefresh() < Time.currentTime() - realm.getOfflineSessionIdleTimeout()) {
|
||||||
|
sessionManager.revokeOfflineUserSession(userSession);
|
||||||
|
userSession = null;
|
||||||
|
clientSession = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Find userSession regularly for online tokens
|
// Find userSession regularly for online tokens
|
||||||
|
@ -172,16 +180,12 @@ public class TokenManager {
|
||||||
|
|
||||||
validation.userSession.setLastSessionRefresh(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)
|
.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()));
|
return new RefreshResult(res, TokenUtil.TOKEN_TYPE_OFFLINE.equals(refreshToken.getType()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -507,7 +511,7 @@ public class TokenManager {
|
||||||
|
|
||||||
refreshToken = new RefreshToken(accessToken);
|
refreshToken = new RefreshToken(accessToken);
|
||||||
refreshToken.type(TokenUtil.TOKEN_TYPE_OFFLINE);
|
refreshToken.type(TokenUtil.TOKEN_TYPE_OFFLINE);
|
||||||
sessionManager.persistOfflineSession(clientSession, userSession);
|
sessionManager.createOrUpdateOfflineSession(clientSession, userSession);
|
||||||
} else {
|
} else {
|
||||||
refreshToken = new RefreshToken(accessToken);
|
refreshToken = new RefreshToken(accessToken);
|
||||||
refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
|
refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
|
||||||
|
|
|
@ -52,6 +52,7 @@ public class ApplianceBootstrap {
|
||||||
realm.setSsoSessionIdleTimeout(1800);
|
realm.setSsoSessionIdleTimeout(1800);
|
||||||
realm.setAccessTokenLifespan(60);
|
realm.setAccessTokenLifespan(60);
|
||||||
realm.setSsoSessionMaxLifespan(36000);
|
realm.setSsoSessionMaxLifespan(36000);
|
||||||
|
realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
|
||||||
realm.setAccessCodeLifespan(60);
|
realm.setAccessCodeLifespan(60);
|
||||||
realm.setAccessCodeLifespanUserAction(300);
|
realm.setAccessCodeLifespanUserAction(300);
|
||||||
realm.setAccessCodeLifespanLogin(1800);
|
realm.setAccessCodeLifespanLogin(1800);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.keycloak.services.managers;
|
package org.keycloak.services.managers;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ import org.keycloak.models.RoleModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||||
|
import org.keycloak.util.Time;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -32,17 +34,23 @@ public class UserSessionManager {
|
||||||
this.persister = session.getProvider(UserSessionPersisterProvider.class);
|
this.persister = session.getProvider(UserSessionPersisterProvider.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void persistOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) {
|
public void createOrUpdateOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) {
|
||||||
UserModel user = userSession.getUser();
|
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());
|
UserSessionModel offlineUserSession = kcSession.sessions().getOfflineUserSession(clientSession.getRealm(), userSession.getId());
|
||||||
if (offlineUserSession == null) {
|
if (offlineUserSession == null) {
|
||||||
offlineUserSession = createOfflineUserSession(user, userSession);
|
offlineUserSession = createOfflineUserSession(user, userSession);
|
||||||
|
} else {
|
||||||
|
// update lastSessionRefresh but don't need to persist
|
||||||
|
offlineUserSession.setLastSessionRefresh(Time.currentTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create clientSession and save to DB.
|
// Create and persist clientSession
|
||||||
createOfflineClientSession(user, clientSession, offlineUserSession);
|
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
|
// 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;
|
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) {
|
public boolean revokeOfflineToken(UserModel user, ClientModel client) {
|
||||||
RealmModel realm = client.getRealm();
|
RealmModel realm = client.getRealm();
|
||||||
|
|
||||||
|
@ -91,6 +108,14 @@ public class UserSessionManager {
|
||||||
return anyRemoved;
|
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) {
|
public boolean isOfflineTokenAllowed(ClientSessionModel clientSession) {
|
||||||
RoleModel offlineAccessRole = clientSession.getRealm().getRole(Constants.OFFLINE_ACCESS_ROLE);
|
RoleModel offlineAccessRole = clientSession.getRealm().getRole(Constants.OFFLINE_ACCESS_ROLE);
|
||||||
if (offlineAccessRole == null) {
|
if (offlineAccessRole == null) {
|
||||||
|
@ -107,7 +132,7 @@ public class UserSessionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
UserSessionModel offlineUserSession = kcSession.sessions().createOfflineUserSession(userSession);
|
UserSessionModel offlineUserSession = kcSession.sessions().createOfflineUserSession(userSession);
|
||||||
persister.createUserSession(userSession, true);
|
persister.createUserSession(offlineUserSession, true);
|
||||||
return offlineUserSession;
|
return offlineUserSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -349,6 +349,35 @@ public class UsersResource {
|
||||||
return reps;
|
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);
|
||||||
|
reps.add(rep);
|
||||||
|
}
|
||||||
|
return reps;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get social logins associated with the user
|
* Get social logins associated with the user
|
||||||
*
|
*
|
||||||
|
@ -469,7 +498,14 @@ public class UsersResource {
|
||||||
currentRep.put("grantedRealmRoles", (rep==null ? Collections.emptyList() : rep.getGrantedRealmRoles()));
|
currentRep.put("grantedRealmRoles", (rep==null ? Collections.emptyList() : rep.getGrantedRealmRoles()));
|
||||||
currentRep.put("grantedClientRoles", (rep==null ? Collections.emptyMap() : rep.getGrantedClientRoles()));
|
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);
|
currentRep.put("additionalGrants", additionalGrants);
|
||||||
|
|
||||||
result.add(currentRep);
|
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)
|
// 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) {
|
public static void assertDataImportedInRealm(KeycloakSession session, RealmModel realm) {
|
||||||
Assert.assertTrue(realm.isVerifyEmail());
|
Assert.assertTrue(realm.isVerifyEmail());
|
||||||
|
Assert.assertEquals(3600000, realm.getOfflineSessionIdleTimeout());
|
||||||
|
|
||||||
List<RequiredCredentialModel> creds = realm.getRequiredCredentials();
|
List<RequiredCredentialModel> creds = realm.getRequiredCredentials();
|
||||||
Assert.assertEquals(1, creds.size());
|
Assert.assertEquals(1, creds.size());
|
||||||
|
@ -361,6 +362,7 @@ public class ImportTest extends AbstractModelTest {
|
||||||
RealmModel realm =manager.importRealm(rep);
|
RealmModel realm =manager.importRealm(rep);
|
||||||
|
|
||||||
Assert.assertEquals(600, realm.getAccessCodeLifespanUserAction());
|
Assert.assertEquals(600, realm.getAccessCodeLifespanUserAction());
|
||||||
|
Assert.assertEquals(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT, realm.getOfflineSessionIdleTimeout());
|
||||||
verifyRequiredCredentials(realm.getRequiredCredentials(), "password");
|
verifyRequiredCredentials(realm.getRequiredCredentials(), "password");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ public class UserSessionInitializerTest {
|
||||||
for (UserSessionModel origSession : origSessions) {
|
for (UserSessionModel origSession : origSessions) {
|
||||||
UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId());
|
UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId());
|
||||||
for (ClientSessionModel clientSession : userSession.getClientSessions()) {
|
for (ClientSessionModel clientSession : userSession.getClientSessions()) {
|
||||||
sessionManager.persistOfflineSession(clientSession, userSession);
|
sessionManager.createOrUpdateOfflineSession(clientSession, userSession);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.keycloak.testsuite.model;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -17,6 +18,7 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.services.managers.ClientManager;
|
import org.keycloak.services.managers.ClientManager;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
|
@ -36,6 +38,7 @@ public class UserSessionProviderOfflineTest {
|
||||||
private KeycloakSession session;
|
private KeycloakSession session;
|
||||||
private RealmModel realm;
|
private RealmModel realm;
|
||||||
private UserSessionManager sessionManager;
|
private UserSessionManager sessionManager;
|
||||||
|
private UserSessionPersisterProvider persister;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void before() {
|
public void before() {
|
||||||
|
@ -44,6 +47,7 @@ public class UserSessionProviderOfflineTest {
|
||||||
session.users().addUser(realm, "user1").setEmail("user1@localhost");
|
session.users().addUser(realm, "user1").setEmail("user1@localhost");
|
||||||
session.users().addUser(realm, "user2").setEmail("user2@localhost");
|
session.users().addUser(realm, "user2").setEmail("user2@localhost");
|
||||||
sessionManager = new UserSessionManager(session);
|
sessionManager = new UserSessionManager(session);
|
||||||
|
persister = session.getProvider(UserSessionPersisterProvider.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
@ -157,7 +161,7 @@ public class UserSessionProviderOfflineTest {
|
||||||
fooRealm = session.realms().getRealm("foo");
|
fooRealm = session.realms().getRealm("foo");
|
||||||
userSession = session.sessions().getUserSession(fooRealm, userSession.getId());
|
userSession = session.sessions().getUserSession(fooRealm, userSession.getId());
|
||||||
clientSession = session.sessions().getClientSession(fooRealm, clientSession.getId());
|
clientSession = session.sessions().getClientSession(fooRealm, clientSession.getId());
|
||||||
sessionManager.persistOfflineSession(userSession.getClientSessions().get(0), userSession);
|
sessionManager.createOrUpdateOfflineSession(userSession.getClientSessions().get(0), userSession);
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
|
@ -291,13 +295,85 @@ 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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// sessions are in persister too
|
||||||
|
Assert.assertEquals(3, persister.getUserSessionsCount(true));
|
||||||
|
|
||||||
|
// Set lastSessionRefresh to session[0] to 0
|
||||||
|
session0.setLastSessionRefresh(0);
|
||||||
|
|
||||||
|
resetSession();
|
||||||
|
|
||||||
|
session.sessions().removeExpiredUserSessions(realm);
|
||||||
|
|
||||||
|
resetSession();
|
||||||
|
|
||||||
|
// assert sessions not found now
|
||||||
|
Assert.assertNull(session.sessions().getOfflineUserSession(realm, origSessions[0].getId()));
|
||||||
|
for (String clientSession : clientSessions) {
|
||||||
|
Assert.assertNull(session.sessions().getOfflineClientSession(realm, origSessions[0].getId()));
|
||||||
|
offlineSessions.remove(clientSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert other offline sessions still found
|
||||||
|
for (Map.Entry<String, String> entry : offlineSessions.entrySet()) {
|
||||||
|
Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey(), entry.getValue()) != null);
|
||||||
|
}
|
||||||
|
Assert.assertEquals(2, 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) {
|
private Map<String, String> createOfflineSessionIncludeClientSessions(UserSessionModel userSession) {
|
||||||
Map<String, String> offlineSessions = new HashMap<>();
|
Map<String, String> offlineSessions = new HashMap<>();
|
||||||
|
|
||||||
UserSessionModel offlineUserSession = session.sessions().createOfflineUserSession(userSession);
|
|
||||||
for (ClientSessionModel clientSession : userSession.getClientSessions()) {
|
for (ClientSessionModel clientSession : userSession.getClientSessions()) {
|
||||||
ClientSessionModel offlineClientSession = session.sessions().createOfflineClientSession(clientSession);
|
sessionManager.createOrUpdateOfflineSession(clientSession, userSession);
|
||||||
offlineClientSession.setUserSession(offlineUserSession);
|
|
||||||
offlineSessions.put(clientSession.getId(), userSession.getId());
|
offlineSessions.put(clientSession.getId(), userSession.getId());
|
||||||
}
|
}
|
||||||
return offlineSessions;
|
return offlineSessions;
|
||||||
|
@ -310,6 +386,7 @@ public class UserSessionProviderOfflineTest {
|
||||||
session = kc.startSession();
|
session = kc.startSession();
|
||||||
realm = session.realms().getRealm("test");
|
realm = session.realms().getRealm("test");
|
||||||
sessionManager = new UserSessionManager(session);
|
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) {
|
private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
|
||||||
|
|
|
@ -227,10 +227,27 @@ public class OfflineTokenTest {
|
||||||
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
|
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
|
||||||
Assert.assertEquals(0, offlineToken.getExpiration());
|
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) {
|
final String sessionId, String userId) {
|
||||||
// Change offset to big value to ensure userSession expired
|
// Change offset to big value to ensure userSession expired
|
||||||
Time.setOffset(99999);
|
Time.setOffset(99999);
|
||||||
|
@ -261,8 +278,9 @@ public class OfflineTokenTest {
|
||||||
Assert.assertEquals(200, response.getStatusCode());
|
Assert.assertEquals(200, response.getStatusCode());
|
||||||
Assert.assertEquals(sessionId, refreshedToken.getSessionState());
|
Assert.assertEquals(sessionId, refreshedToken.getSessionState());
|
||||||
|
|
||||||
// Assert no refreshToken in the response
|
// Assert new refreshToken in the response
|
||||||
Assert.assertNull(response.getRefreshToken());
|
String newRefreshToken = response.getRefreshToken();
|
||||||
|
Assert.assertNotNull(newRefreshToken);
|
||||||
Assert.assertNotEquals(oldToken.getId(), refreshedToken.getId());
|
Assert.assertNotEquals(oldToken.getId(), refreshedToken.getId());
|
||||||
|
|
||||||
Assert.assertEquals(userId, refreshedToken.getSubject());
|
Assert.assertEquals(userId, refreshedToken.getSubject());
|
||||||
|
@ -283,6 +301,7 @@ public class OfflineTokenTest {
|
||||||
Assert.assertNotEquals(oldToken.getId(), refreshEvent.getDetails().get(Details.TOKEN_ID));
|
Assert.assertNotEquals(oldToken.getId(), refreshEvent.getDetails().get(Details.TOKEN_ID));
|
||||||
|
|
||||||
Time.setOffset(0);
|
Time.setOffset(0);
|
||||||
|
return newRefreshToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -382,11 +401,11 @@ public class OfflineTokenTest {
|
||||||
String accessTokenId = OfflineTokenServlet.tokenInfo.accessToken.getId();
|
String accessTokenId = OfflineTokenServlet.tokenInfo.accessToken.getId();
|
||||||
String refreshTokenId = OfflineTokenServlet.tokenInfo.refreshToken.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);
|
Time.setOffset(9999);
|
||||||
driver.navigate().to(offlineClientAppUri);
|
driver.navigate().to(offlineClientAppUri);
|
||||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(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);
|
Assert.assertNotEquals(OfflineTokenServlet.tokenInfo.accessToken.getId(), accessTokenId);
|
||||||
|
|
||||||
// Ensure that logout works for webapp (even if offline token will be still valid in Keycloak DB)
|
// Ensure that logout works for webapp (even if offline token will be still valid in Keycloak DB)
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"accessTokenLifespan": 6000,
|
"accessTokenLifespan": 6000,
|
||||||
"accessCodeLifespan": 30,
|
"accessCodeLifespan": 30,
|
||||||
"accessCodeLifespanUserAction": 600,
|
"accessCodeLifespanUserAction": 600,
|
||||||
|
"offlineSessionIdleTimeout": 3600000,
|
||||||
"requiredCredentials": [ "password" ],
|
"requiredCredentials": [ "password" ],
|
||||||
"defaultRoles": [ "foo", "bar" ],
|
"defaultRoles": [ "foo", "bar" ],
|
||||||
"verifyEmail" : "true",
|
"verifyEmail" : "true",
|
||||||
|
|
Loading…
Reference in a new issue