client user-session association

This commit is contained in:
Bill Burke 2014-05-17 14:24:32 -04:00
parent ae4d83107a
commit ab8de6ba25
50 changed files with 993 additions and 264 deletions

View file

@ -0,0 +1,67 @@
package org.keycloak.representations.idm;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UserSessionRepresentation {
private String id;
private String user;
private String ipAddress;
private long start;
private Set<String> applications = new HashSet<String>();
private Map<String, String> clients = new HashMap<String, String>();
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public long getStart() {
return start;
}
public void setStart(long start) {
this.start = start;
}
public Set<String> getApplications() {
return applications;
}
public void setApplications(Set<String> applications) {
this.applications = applications;
}
public Map<String, String> getClients() {
return clients;
}
public void setClients(Map<String, String> clients) {
this.clients = clients;
}
}

View file

@ -3,5 +3,6 @@
"resource" : "database-service", "resource" : "database-service",
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"bearer-only" : true, "bearer-only" : true,
"ssl-not-required": true,
"enable-cors": true "enable-cors": true
} }

View file

@ -3,6 +3,5 @@
"resource" : "database-service", "resource" : "database-service",
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"bearer-only" : true, "bearer-only" : true,
"enable-cors" : true "ssl-not-required": true
} }

View file

@ -290,8 +290,8 @@ module.config([ '$routeProvider', function($routeProvider) {
user : function(UserLoader) { user : function(UserLoader) {
return UserLoader(); return UserLoader();
}, },
stats : function(UserSessionStatsLoader) { sessions : function(UserSessionsLoader) {
return UserSessionStatsLoader(); return UserSessionsLoader();
} }
}, },
controller : 'UserSessionsCtrl' controller : 'UserSessionsCtrl'
@ -436,8 +436,8 @@ module.config([ '$routeProvider', function($routeProvider) {
application : function(ApplicationLoader) { application : function(ApplicationLoader) {
return ApplicationLoader(); return ApplicationLoader();
}, },
stats : function(ApplicationSessionStatsLoader) { sessionCount : function(ApplicationSessionCountLoader) {
return ApplicationSessionStatsLoader(); return ApplicationSessionCountLoader();
} }
}, },
controller : 'ApplicationSessionsCtrl' controller : 'ApplicationSessionsCtrl'
@ -705,8 +705,8 @@ module.config([ '$routeProvider', function($routeProvider) {
realm : function(RealmLoader) { realm : function(RealmLoader) {
return RealmLoader(); return RealmLoader();
}, },
stats : function(RealmSessionStatsLoader) { stats : function(RealmApplicationSessionStatsLoader) {
return RealmSessionStatsLoader(); return RealmApplicationSessionStatsLoader();
} }
}, },
controller : 'RealmSessionStatsCtrl' controller : 'RealmSessionStatsCtrl'

View file

@ -43,15 +43,12 @@ module.controller('ApplicationCredentialsCtrl', function($scope, $location, real
}); });
}); });
module.controller('ApplicationSessionsCtrl', function($scope, realm, stats, application, module.controller('ApplicationSessionsCtrl', function($scope, realm, sessionCount, application,
ApplicationLogoutUser, ApplicationUserSessions,
ApplicationLogoutAll,
ApplicationSessionStats,
ApplicationSessionStatsWithUsers,
$location, Dialog, Notifications) { $location, Dialog, Notifications) {
$scope.realm = realm; $scope.realm = realm;
$scope.stats = stats; $scope.count = sessionCount.count;
$scope.users = {}; $scope.sessions = [];
$scope.application = application; $scope.application = application;
$scope.toDate = function(val) { $scope.toDate = function(val) {
@ -59,27 +56,11 @@ module.controller('ApplicationSessionsCtrl', function($scope, realm, stats, appl
}; };
$scope.loadUsers = function() { $scope.loadUsers = function() {
ApplicationSessionStatsWithUsers.get({ realm : realm.realm, application: $scope.application.name }, function(updated) { ApplicationUserSessions.query({ realm : realm.realm, application: $scope.application.name }, function(updated) {
$scope.stats = updated; $scope.count = updated.length;
$scope.users = updated.users; $scope.sessions = updated;
}) })
}; };
$scope.logoutAll = function() {
ApplicationLogoutAll.save({realm : realm.realm, application: $scope.application.name}, function () {
Notifications.success('Logged out all users');
$scope.loadUsers();
});
};
$scope.logoutUser = function(user) {
console.log('Trying to logout user: ' + user);
ApplicationLogoutUser.save({realm : realm.realm, application: $scope.application.name, user: user}, function () {
Notifications.success('Logged out user' + user);
$scope.loadUsers();
});
};
}); });
module.controller('ApplicationClaimsCtrl', function($scope, realm, application, claims, module.controller('ApplicationClaimsCtrl', function($scope, realm, application, claims,

View file

@ -99,32 +99,29 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, ap
}); });
module.controller('UserSessionsCtrl', function($scope, realm, user, stats, UserLogout, ApplicationLogoutUser, UserSessionStats, Notifications) { module.controller('UserSessionsCtrl', function($scope, realm, user, sessions, UserSessions, UserLogout, UserSessionLogout, Notifications) {
$scope.realm = realm; $scope.realm = realm;
$scope.user = user; $scope.user = user;
$scope.stats = stats; $scope.sessions = sessions;
$scope.logoutAll = function() { $scope.logoutAll = function() {
UserLogout.save({realm : realm.realm, user: user.username}, function () { UserLogout.save({realm : realm.realm, user: user.username}, function () {
Notifications.success('Logged out user in all applications'); Notifications.success('Logged out user in all applications');
UserSessionStats.get({realm: realm.realm, user: user.username}, function(updated) { UserSessions.get({realm: realm.realm, user: user.username}, function(updated) {
$scope.stats = updated; $scope.sessions = updated;
}) })
}); });
}; };
$scope.logoutApplication = function(app) { $scope.logoutSession = function(sessionId) {
console.log('log user out of app: ' + app); console.log('here in logoutSession');
ApplicationLogoutUser.save({realm : realm.realm, application: app, user: user.username}, function () { UserSessionLogout.delete({realm : realm.realm, session: sessionId}, function() {
Notifications.success('Logged out user from application'); Notifications.success('Logged out session');
UserSessionStats.get({realm: realm.realm, user: user.username}, function(updated) { UserSessions.get({realm: realm.realm, user: user.username}, function(updated) {
$scope.stats = updated; $scope.sessions = updated;
}) })
}); });
}; }
}); });
module.controller('UserSocialCtrl', function($scope, realm, user, socialLinks) { module.controller('UserSocialCtrl', function($scope, realm, user, socialLinks) {

View file

@ -71,6 +71,14 @@ module.factory('RealmSessionStatsLoader', function(Loader, RealmSessionStats, $r
}); });
}); });
module.factory('RealmApplicationSessionStatsLoader', function(Loader, RealmApplicationSessionStats, $route, $q) {
return Loader.get(RealmApplicationSessionStats, function() {
return {
realm : $route.current.params.realm
}
});
});
module.factory('UserLoader', function(Loader, User, $route, $q) { module.factory('UserLoader', function(Loader, User, $route, $q) {
return Loader.get(User, function() { return Loader.get(User, function() {
return { return {
@ -89,6 +97,15 @@ module.factory('UserSessionStatsLoader', function(Loader, UserSessionStats, $rou
}); });
}); });
module.factory('UserSessionsLoader', function(Loader, UserSessions, $route, $q) {
return Loader.query(UserSessions, function() {
return {
realm : $route.current.params.realm,
user : $route.current.params.user
}
});
});
module.factory('UserSocialLinksLoader', function(Loader, UserSocialLinks, $route, $q) { module.factory('UserSocialLinksLoader', function(Loader, UserSocialLinks, $route, $q) {
return Loader.query(UserSocialLinks, function() { return Loader.query(UserSocialLinks, function() {
return { return {
@ -134,6 +151,15 @@ module.factory('ApplicationSessionStatsLoader', function(Loader, ApplicationSess
}); });
}); });
module.factory('ApplicationSessionCountLoader', function(Loader, ApplicationSessionCount, $route, $q) {
return Loader.get(ApplicationSessionCount, function() {
return {
realm : $route.current.params.realm,
application : $route.current.params.application
}
});
});
module.factory('ApplicationClaimsLoader', function(Loader, ApplicationClaims, $route, $q) { module.factory('ApplicationClaimsLoader', function(Loader, ApplicationClaims, $route, $q) {
return Loader.get(ApplicationClaims, function() { return Loader.get(ApplicationClaims, function() {
return { return {

View file

@ -183,6 +183,20 @@ module.factory('UserSessionStats', function($resource) {
user : '@user' user : '@user'
}); });
}); });
module.factory('UserSessions', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/users/:user/sessions', {
realm : '@realm',
user : '@user'
});
});
module.factory('UserSessionLogout', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/sessions/:session', {
realm : '@realm',
session : '@session'
});
});
module.factory('UserLogout', function($resource) { module.factory('UserLogout', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/users/:user/logout', { return $resource(authUrl + '/admin/realms/:realm/users/:user/logout', {
realm : '@realm', realm : '@realm',
@ -347,6 +361,12 @@ module.factory('RealmSessionStats', function($resource) {
}); });
}); });
module.factory('RealmApplicationSessionStats', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/application-session-stats', {
realm : '@realm'
});
});
module.factory('RoleApplicationComposites', function($resource) { module.factory('RoleApplicationComposites', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/roles-by-id/:role/composites/applications/:application', { return $resource(authUrl + '/admin/realms/:realm/roles-by-id/:role/composites/applications/:application', {
@ -589,6 +609,20 @@ module.factory('ApplicationSessionStatsWithUsers', function($resource) {
}); });
}); });
module.factory('ApplicationSessionCount', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/applications/:application/session-count', {
realm : '@realm',
application : "@application"
});
});
module.factory('ApplicationUserSessions', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/applications/:application/user-sessions', {
realm : '@realm',
application : "@application"
});
});
module.factory('ApplicationLogoutAll', function($resource) { module.factory('ApplicationLogoutAll', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/applications/:application/logout-all', { return $resource(authUrl + '/admin/realms/:realm/applications/:application/logout-all', {
realm : '@realm', realm : '@realm',

View file

@ -21,38 +21,31 @@
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label" for="activeSessions">Active Sessions</label> <label class="col-sm-2 control-label" for="activeSessions">Active Sessions</label>
<div class="col-sm-4"> <div class="col-sm-4">
<input class="form-control" type="text" id="activeSessions" name="activeSessions" data-ng-model="stats.activeSessions" ng-disabled="true"> <input class="form-control" type="text" id="activeSessions" name="activeSessions" data-ng-model="count" ng-disabled="true">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="activeUsers">Active Users</label>
<div class="col-sm-4">
<input class="form-control" type="text" id="activeUsers" name="activeUsers" data-ng-model="stats.activeUsers" ng-disabled="true">
</div> </div>
</div> </div>
</fieldset> </fieldset>
</form> </form>
<table class="table table-striped table-bordered" data-ng-show="stats.activeSessions > 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="3">
<div class="pull-right"> <div class="pull-right">
<a class="btn btn-primary" ng-click="logoutAll()">Invalidate All Sessions</a>
<a class="btn btn-primary" ng-click="loadUsers()">Show Users</a> <a class="btn btn-primary" ng-click="loadUsers()">Show Users</a>
</div> </div>
</th> </th>
</tr> </tr>
<tr> <tr>
<th>User</th> <th>User</th>
<th>Login Time</th> <th>From IP</th>
<th></th> <th>Session Start</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr data-ng-repeat="(user, data) in users"> <tr data-ng-repeat="session in sessions">
<td><a href="#/realms/{{realm.realm}}/users/{{user}}">{{user}}</a></td> <td><a href="#/realms/{{realm.realm}}/users/{{session.user}}">{{session.user}}</a></td>
<td>{{data.whenLoggedIn | date:'medium'}}</td> <td>{{session.ipAddress}}</td>
<td><a ng-click="logoutUser(user)">invalidate session</a> </td> <td>{{session.start | date:'medium'}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View file

@ -24,14 +24,12 @@
<tr> <tr>
<th>Application</th> <th>Application</th>
<th>Active Sessions</th> <th>Active Sessions</th>
<th>Active Users</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr data-ng-repeat="(application, data) in stats"> <tr data-ng-repeat="(application, data) in stats">
<td><a href="#/realms/{{realm.realm}}/applications/{{application}}/sessions">{{application}}</a></td> <td><a href="#/realms/{{realm.realm}}/applications/{{application}}/sessions">{{application}}</a></td>
<td>{{data.activeSessions}}</td> <td>{{data}}</td>
<td>{{data.activeUsers}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View file

@ -18,23 +18,37 @@
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th class="kc-table-actions" colspan="3"> <th class="kc-table-actions" colspan="5">
<div class="pull-right"> <div class="pull-right">
<a class="btn btn-primary" ng-click="logoutAll()">Logout</a> <a class="btn btn-primary" ng-click="logoutAll()">Logout All Sessions</a>
</div> </div>
</th> </th>
</tr> </tr>
<tr> <tr>
<th>Application</th> <th>IP Address</th>
<th>Login Time</th> <th>Login Time</th>
<th></th> <th>Applications</th>
<th>OAuth Clients</th>
<th>Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr data-ng-repeat="(application, data) in stats"> <tr data-ng-repeat="session in sessions">
<td><a href="#/realms/{{realm.realm}}/applications/{{application}}/sessions">{{application}}</a></td> <td>{{session.ipAddress}}</td>
<td>{{data.whenLoggedIn | date:'medium'}}</td> <td>{{session.start | date:'medium'}}</td>
<td><a ng-click="logoutApplication(application)">invalidate session</a> </td> <td><ul style="list-style: none; ">
<li data-ng-repeat="app in session.applications">
<a href="#/realms/{{realm.realm}}/applications/{{app}}/sessions">{{app}}</a>
</li>
</ul>
</td>
<td><ul style="list-style: none; ">
<li data-ng-repeat="(clientId, clientName) in session.clients">
<a href="#/realms/{{realm.realm}}/oauth-clients/{{clientId}}">{{clientName}}</a>
</li>
</ul>
</td>
<td><a ng-click="logoutSession(session.id)">logout</a> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View file

@ -120,7 +120,9 @@ public class PreAuthActionsHandler {
String user = action.getUser(); String user = action.getUser();
if (user != null) { if (user != null) {
log.info("logout of session for: " + user); log.info("logout of session for: " + user);
userSessionManagement.logout(user); userSessionManagement.logoutUser(user);
} else if (action.getSession() != null) {
userSessionManagement.logoutKeycloakSession(action.getSession());
} else { } else {
log.info("logout of all sessions"); log.info("logout of all sessions");
if (action.getNotBefore() > deployment.getNotBefore()) { if (action.getNotBefore() > deployment.getNotBefore()) {

View file

@ -15,5 +15,7 @@ public interface UserSessionManagement {
void logoutAll(); void logoutAll();
void logout(String user); void logoutUser(String user);
void logoutKeycloakSession(String id);
} }

View file

@ -62,7 +62,7 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
session.setNote(KeycloakSecurityContext.class.getName(), securityContext); session.setNote(KeycloakSecurityContext.class.getName(), securityContext);
String username = securityContext.getToken().getSubject(); String username = securityContext.getToken().getSubject();
log.debug("userSessionManage.login: " + username); log.debug("userSessionManage.login: " + username);
userSessionManagement.login(session, username); userSessionManagement.login(session, username, securityContext.getToken().getSessionState());
} }
@Override @Override

View file

@ -8,6 +8,7 @@ import org.jboss.logging.Logger;
import org.keycloak.adapters.UserSessionManagement; import org.keycloak.adapters.UserSessionManagement;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -23,31 +24,21 @@ import java.util.concurrent.ConcurrentHashMap;
public class CatalinaUserSessionManagement implements SessionListener, UserSessionManagement { public class CatalinaUserSessionManagement implements SessionListener, UserSessionManagement {
private static final Logger log = Logger.getLogger(CatalinaUserSessionManagement.class); private static final Logger log = Logger.getLogger(CatalinaUserSessionManagement.class);
protected ConcurrentHashMap<String, UserSessions> userSessionMap = new ConcurrentHashMap<String, UserSessions>(); protected ConcurrentHashMap<String, UserSessions> userSessionMap = new ConcurrentHashMap<String, UserSessions>();
protected ConcurrentHashMap<String, UserSessions> keycloakSessionMap = new ConcurrentHashMap<String, UserSessions>();
public static class UserSessions { public static class UserSessions {
protected Map<String, Session> sessions = new ConcurrentHashMap<String, Session>(); protected String user;
protected long loggedIn = System.currentTimeMillis(); protected long loggedIn = System.currentTimeMillis();
protected Map<String, String> keycloakSessionToHttpSession = new HashMap<String, String>();
protected Map<String, String> httpSessionToKeycloakSession = new HashMap<String, String>();
public Map<String, Session> getSessions() { protected Map<String, Session> sessions = new HashMap<String, Session>();
return sessions;
}
public long getLoggedIn() { public long getLoggedIn() {
return loggedIn; return loggedIn;
} }
} }
@Override public synchronized int getActiveSessions() {
public int getActiveSessions() { return keycloakSessionMap.size();
int active = 0;
synchronized (userSessionMap) {
for (UserSessions sessions : userSessionMap.values()) {
active += sessions.getSessions().size();
}
}
return active;
} }
/** /**
@ -56,60 +47,85 @@ public class CatalinaUserSessionManagement implements SessionListener, UserSessi
* @return null if user not logged in * @return null if user not logged in
*/ */
@Override @Override
public Long getUserLoginTime(String username) { public synchronized Long getUserLoginTime(String username) {
UserSessions sessions = userSessionMap.get(username); UserSessions sessions = userSessionMap.get(username);
if (sessions == null) return null; if (sessions == null) return null;
return sessions.getLoggedIn(); return sessions.getLoggedIn();
} }
@Override @Override
public Set<String> getActiveUsers() { public synchronized Set<String> getActiveUsers() {
HashSet<String> set = new HashSet<String>(); HashSet<String> set = new HashSet<String>();
set.addAll(userSessionMap.keySet()); set.addAll(userSessionMap.keySet());
return set; return set;
} }
protected void login(Session session, String username) { public synchronized void login(Session session, String username, String keycloakSessionId) {
synchronized (userSessionMap) { String sessionId = session.getId();
UserSessions userSessions = userSessionMap.get(username);
if (userSessions == null) { UserSessions sessions = userSessionMap.get(username);
userSessions = new UserSessions(); if (sessions == null) {
userSessionMap.put(username, userSessions); sessions = new UserSessions();
} sessions.user = username;
userSessions.getSessions().put(session.getId(), session); userSessionMap.put(username, sessions);
} }
keycloakSessionMap.put(keycloakSessionId, sessions);
sessions.httpSessionToKeycloakSession.put(sessionId, keycloakSessionId);
sessions.keycloakSessionToHttpSession.put(keycloakSessionId, sessionId);
session.addSessionListener(this); session.addSessionListener(this);
} }
@Override @Override
public void logoutAll() { public void logoutAll() {
List<String> users = new ArrayList<String>(); for (String user : userSessionMap.keySet()) logoutUser(user);
users.addAll(userSessionMap.keySet());
for (String user : users) logout(user);
} }
@Override @Override
public void logout(String user) { public void logoutUser(String user) {
log.debug("logoutUser: " + user); log.debug("logoutUser: " + user);
UserSessions sessions = null; UserSessions sessions = null;
synchronized (userSessionMap) { sessions = userSessionMap.remove(user);
sessions = userSessionMap.remove(user);
}
if (sessions == null) { if (sessions == null) {
log.debug("no session for user: " + user); log.debug("no session for user: " + user);
return; return;
} }
log.debug("found session for user"); log.debug("found session for user");
for (Session session : sessions.getSessions().values()) { for (Map.Entry<String, String> entry : sessions.httpSessionToKeycloakSession.entrySet()) {
log.debug("invalidating session for user: " + user);
String sessionId = entry.getKey();
String keycloakSessionId = entry.getValue();
Session session = sessions.sessions.get(sessionId);
session.setPrincipal(null); session.setPrincipal(null);
session.setAuthType(null); session.setAuthType(null);
session.getSession().invalidate(); session.getSession().invalidate();
keycloakSessionMap.remove(keycloakSessionId);
} }
} }
public synchronized void logoutKeycloakSession(String keycloakSessionId) {
log.debug("logoutKeycloakSession: " + keycloakSessionId);
UserSessions sessions = keycloakSessionMap.remove(keycloakSessionId);
if (sessions == null) {
log.debug("no session for keycloak session id: " + keycloakSessionId);
return;
}
String sessionId = sessions.keycloakSessionToHttpSession.remove(keycloakSessionId);
if (sessionId == null) {
log.debug("no session for keycloak session id: " + keycloakSessionId);
}
sessions.httpSessionToKeycloakSession.remove(sessionId);
Session session = sessions.sessions.remove(sessionId);
session.setPrincipal(null);
session.setAuthType(null);
session.getSession().invalidate();
if (sessions.keycloakSessionToHttpSession.size() == 0) {
userSessionMap.remove(sessions.user);
}
}
public void sessionEvent(SessionEvent event) { public void sessionEvent(SessionEvent event) {
// We only care about session destroyed events // We only care about session destroyed events
if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType()) if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType())
@ -124,14 +140,22 @@ public class CatalinaUserSessionManagement implements SessionListener, UserSessi
session.setAuthType(null); session.setAuthType(null);
String username = principal.getUserPrincipal().getName(); String username = principal.getUserPrincipal().getName();
synchronized (userSessionMap) { UserSessions userSessions = userSessionMap.get(username);
UserSessions sessions = userSessionMap.get(username); if (userSessions == null) {
if (sessions != null) { return;
sessions.getSessions().remove(session.getId()); }
if (sessions.getSessions().isEmpty()) { String sessionid = session.getId();
userSessionMap.remove(username); synchronized (this) {
} String keycloakSessionId = userSessions.httpSessionToKeycloakSession.remove(sessionid);
if (keycloakSessionId != null) {
userSessions.keycloakSessionToHttpSession.remove(keycloakSessionId);
keycloakSessionMap.remove(keycloakSessionId);
} }
userSessions.sessions.remove(sessionid);
if (userSessions.httpSessionToKeycloakSession.size() == 0) {
userSessionMap.remove(username);
}
} }
} }
} }

View file

@ -62,7 +62,7 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
session.setNote(KeycloakSecurityContext.class.getName(), securityContext); session.setNote(KeycloakSecurityContext.class.getName(), securityContext);
String username = securityContext.getToken().getSubject(); String username = securityContext.getToken().getSubject();
log.finer("userSessionManage.login: " + username); log.finer("userSessionManage.login: " + username);
userSessionManagement.login(session, username); userSessionManagement.login(session, username, securityContext.getToken().getSessionState());
} }
@Override @Override

View file

@ -1,6 +1,7 @@
package org.keycloak.adapters.tomcat7; package org.keycloak.adapters.tomcat7;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -23,31 +24,21 @@ import org.keycloak.adapters.UserSessionManagement;
public class CatalinaUserSessionManagement implements SessionListener, UserSessionManagement { public class CatalinaUserSessionManagement implements SessionListener, UserSessionManagement {
private static final Logger log = Logger.getLogger(""+CatalinaUserSessionManagement.class); private static final Logger log = Logger.getLogger(""+CatalinaUserSessionManagement.class);
protected ConcurrentHashMap<String, UserSessions> userSessionMap = new ConcurrentHashMap<String, UserSessions>(); protected ConcurrentHashMap<String, UserSessions> userSessionMap = new ConcurrentHashMap<String, UserSessions>();
protected ConcurrentHashMap<String, UserSessions> keycloakSessionMap = new ConcurrentHashMap<String, UserSessions>();
public static class UserSessions { public static class UserSessions {
protected Map<String, Session> sessions = new ConcurrentHashMap<String, Session>(); protected String user;
protected long loggedIn = System.currentTimeMillis(); protected long loggedIn = System.currentTimeMillis();
protected Map<String, String> keycloakSessionToHttpSession = new HashMap<String, String>();
protected Map<String, String> httpSessionToKeycloakSession = new HashMap<String, String>();
public Map<String, Session> getSessions() { protected Map<String, Session> sessions = new HashMap<String, Session>();
return sessions;
}
public long getLoggedIn() { public long getLoggedIn() {
return loggedIn; return loggedIn;
} }
} }
@Override public synchronized int getActiveSessions() {
public int getActiveSessions() { return keycloakSessionMap.size();
int active = 0;
synchronized (userSessionMap) {
for (UserSessions sessions : userSessionMap.values()) {
active += sessions.getSessions().size();
}
}
return active;
} }
/** /**
@ -56,60 +47,78 @@ public class CatalinaUserSessionManagement implements SessionListener, UserSessi
* @return null if user not logged in * @return null if user not logged in
*/ */
@Override @Override
public Long getUserLoginTime(String username) { public synchronized Long getUserLoginTime(String username) {
UserSessions sessions = userSessionMap.get(username); UserSessions sessions = userSessionMap.get(username);
if (sessions == null) return null; if (sessions == null) return null;
return sessions.getLoggedIn(); return sessions.getLoggedIn();
} }
@Override @Override
public Set<String> getActiveUsers() { public synchronized Set<String> getActiveUsers() {
HashSet<String> set = new HashSet<String>(); HashSet<String> set = new HashSet<String>();
set.addAll(userSessionMap.keySet()); set.addAll(userSessionMap.keySet());
return set; return set;
} }
protected void login(Session session, String username) {
synchronized (userSessionMap) { public synchronized void login(Session session, String username, String keycloakSessionId) {
UserSessions userSessions = userSessionMap.get(username); String sessionId = session.getId();
if (userSessions == null) {
userSessions = new UserSessions(); UserSessions sessions = userSessionMap.get(username);
userSessionMap.put(username, userSessions); if (sessions == null) {
} sessions = new UserSessions();
userSessions.getSessions().put(session.getId(), session); sessions.user = username;
userSessionMap.put(username, sessions);
} }
keycloakSessionMap.put(keycloakSessionId, sessions);
sessions.httpSessionToKeycloakSession.put(sessionId, keycloakSessionId);
sessions.keycloakSessionToHttpSession.put(keycloakSessionId, sessionId);
session.addSessionListener(this); session.addSessionListener(this);
} }
@Override @Override
public void logoutAll() { public void logoutAll() {
List<String> users = new ArrayList<String>(); for (String user : userSessionMap.keySet()) logoutUser(user);
users.addAll(userSessionMap.keySet());
for (String user : users) logout(user);
} }
@Override @Override
public void logout(String user) { public void logoutUser(String user) {
log.finer("logoutUser: " + user);
UserSessions sessions = null; UserSessions sessions = null;
synchronized (userSessionMap) { sessions = userSessionMap.remove(user);
sessions = userSessionMap.remove(user);
}
if (sessions == null) { if (sessions == null) {
log.finer("no session for user: " + user);
return; return;
} }
for (Map.Entry<String, String> entry : sessions.httpSessionToKeycloakSession.entrySet()) {
log.finer("found session for user"); String sessionId = entry.getKey();
for (Session session : sessions.getSessions().values()) { String keycloakSessionId = entry.getValue();
Session session = sessions.sessions.get(sessionId);
session.setPrincipal(null); session.setPrincipal(null);
session.setAuthType(null); session.setAuthType(null);
session.getSession().invalidate(); session.getSession().invalidate();
keycloakSessionMap.remove(keycloakSessionId);
} }
} }
public synchronized void logoutKeycloakSession(String keycloakSessionId) {
UserSessions sessions = keycloakSessionMap.remove(keycloakSessionId);
if (sessions == null) {
return;
}
String sessionId = sessions.keycloakSessionToHttpSession.remove(keycloakSessionId);
if (sessionId == null) {
}
sessions.httpSessionToKeycloakSession.remove(sessionId);
Session session = sessions.sessions.remove(sessionId);
session.setPrincipal(null);
session.setAuthType(null);
session.getSession().invalidate();
if (sessions.keycloakSessionToHttpSession.size() == 0) {
userSessionMap.remove(sessions.user);
}
}
public void sessionEvent(SessionEvent event) { public void sessionEvent(SessionEvent event) {
// We only care about session destroyed events // We only care about session destroyed events
if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType()) if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType())
@ -124,14 +133,22 @@ public class CatalinaUserSessionManagement implements SessionListener, UserSessi
session.setAuthType(null); session.setAuthType(null);
String username = principal.getUserPrincipal().getName(); String username = principal.getUserPrincipal().getName();
synchronized (userSessionMap) { UserSessions userSessions = userSessionMap.get(username);
UserSessions sessions = userSessionMap.get(username); if (userSessions == null) {
if (sessions != null) { return;
sessions.getSessions().remove(session.getId()); }
if (sessions.getSessions().isEmpty()) { String sessionid = session.getId();
userSessionMap.remove(username); synchronized (this) {
} String keycloakSessionId = userSessions.httpSessionToKeycloakSession.remove(sessionid);
if (keycloakSessionId != null) {
userSessions.keycloakSessionToHttpSession.remove(keycloakSessionId);
keycloakSessionMap.remove(keycloakSessionId);
} }
userSessions.sessions.remove(sessionid);
if (userSessions.httpSessionToKeycloakSession.size() == 0) {
userSessionMap.remove(username);
}
} }
} }
} }

View file

@ -66,7 +66,7 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator {
HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest(); HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest();
HttpSession session = req.getSession(true); HttpSession session = req.getSession(true);
session.setAttribute(KeycloakUndertowAccount.class.getName(), account); session.setAttribute(KeycloakUndertowAccount.class.getName(), account);
userSessionManagement.login(servletRequestContext.getDeployment().getSessionManager(), session, account.getPrincipal().getName()); userSessionManagement.login(servletRequestContext.getDeployment().getSessionManager(), session, account.getPrincipal().getName(), account.getKeycloakSecurityContext().getToken().getSessionState());
} }
} }

View file

@ -1,6 +1,5 @@
package org.keycloak.adapters.undertow; package org.keycloak.adapters.undertow;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.session.SessionManager; import io.undertow.server.session.SessionManager;
import org.keycloak.adapters.UserSessionManagement; import org.keycloak.adapters.UserSessionManagement;
@ -41,7 +40,12 @@ public class SessionManagementBridge implements UserSessionManagement {
} }
@Override @Override
public void logout(String user) { public void logoutUser(String user) {
userSessionManagement.logout(sessionManager, user); userSessionManagement.logoutUser(sessionManager, user);
}
@Override
public void logoutKeycloakSession(String id) {
userSessionManagement.logoutKeycloakSession(sessionManager, id);
} }
} }

View file

@ -6,19 +6,14 @@ import io.undertow.server.session.Session;
import io.undertow.server.session.SessionListener; import io.undertow.server.session.SessionListener;
import io.undertow.server.session.SessionManager; import io.undertow.server.session.SessionManager;
import io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler; import io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler;
import io.undertow.util.StatusCodes;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.representations.adapters.action.LogoutAction;
import org.keycloak.util.JsonSerialization;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -32,30 +27,22 @@ public class UndertowUserSessionManagement implements SessionListener {
private static final Logger log = Logger.getLogger(UndertowUserSessionManagement.class); private static final Logger log = Logger.getLogger(UndertowUserSessionManagement.class);
private static final String AUTH_SESSION_NAME = CachedAuthenticatedSessionHandler.class.getName() + ".AuthenticatedSession"; private static final String AUTH_SESSION_NAME = CachedAuthenticatedSessionHandler.class.getName() + ".AuthenticatedSession";
protected ConcurrentHashMap<String, UserSessions> userSessionMap = new ConcurrentHashMap<String, UserSessions>(); protected ConcurrentHashMap<String, UserSessions> userSessionMap = new ConcurrentHashMap<String, UserSessions>();
protected ConcurrentHashMap<String, UserSessions> keycloakSessionMap = new ConcurrentHashMap<String, UserSessions>();
protected volatile boolean registered;
public static class UserSessions { public static class UserSessions {
protected Set<String> sessionIds = new HashSet<String>(); protected String user;
protected long loggedIn = System.currentTimeMillis(); protected long loggedIn = System.currentTimeMillis();
protected Map<String, String> keycloakSessionToHttpSession = new HashMap<String, String>();
public Set<String> getSessionIds() { protected Map<String, String> httpSessionToKeycloakSession = new HashMap<String, String>();
return sessionIds;
}
public long getLoggedIn() { public long getLoggedIn() {
return loggedIn; return loggedIn;
} }
} }
public int getActiveSessions() { public synchronized int getActiveSessions() {
int active = 0; return keycloakSessionMap.size();
synchronized (userSessionMap) {
for (UserSessions sessions : userSessionMap.values()) {
active += sessions.getSessionIds().size();
}
}
return active;
} }
/** /**
@ -63,74 +50,88 @@ public class UndertowUserSessionManagement implements SessionListener {
* @param username * @param username
* @return null if user not logged in * @return null if user not logged in
*/ */
public Long getUserLoginTime(String username) { public synchronized Long getUserLoginTime(String username) {
UserSessions sessions = userSessionMap.get(username); UserSessions sessions = userSessionMap.get(username);
if (sessions == null) return null; if (sessions == null) return null;
return sessions.getLoggedIn(); return sessions.getLoggedIn();
} }
public Set<String> getActiveUsers() { public synchronized Set<String> getActiveUsers() {
HashSet<String> set = new HashSet<String>(); HashSet<String> set = new HashSet<String>();
set.addAll(userSessionMap.keySet()); set.addAll(userSessionMap.keySet());
return set; return set;
} }
public void login(SessionManager manager, HttpSession session, String username) { public synchronized void login(SessionManager manager, HttpSession session, String username, String keycloakSessionId) {
String sessionId = session.getId(); String sessionId = session.getId();
addAuthenticatedSession(username, sessionId);
manager.registerSessionListener(this);
}
protected void addAuthenticatedSession(String username, String sessionId) { UserSessions sessions = userSessionMap.get(username);
synchronized (userSessionMap) {
UserSessions sessions = userSessionMap.get(username);
if (sessions == null) {
sessions = new UserSessions();
userSessionMap.put(username, sessions);
}
sessions.getSessionIds().add(sessionId);
}
}
protected void removeAuthenticatedSession(String sessionId, String username) {
synchronized (userSessionMap) {
UserSessions sessions = userSessionMap.get(username);
if (sessions == null) return;
sessions.getSessionIds().remove(sessionId);
if (sessions.getSessionIds().isEmpty()) {
userSessionMap.remove(username);
}
}
}
public void logoutAll(SessionManager manager) {
List<String> users = new ArrayList<String>();
users.addAll(userSessionMap.keySet());
for (String user : users) logout(manager, user);
}
public void logout(SessionManager manager, String user) {
log.info("logoutUser: " + user);
UserSessions sessions = null;
synchronized (userSessionMap) {
sessions = userSessionMap.remove(user);
}
if (sessions == null) { if (sessions == null) {
log.info("no session for user: " + user); sessions = new UserSessions();
sessions.user = username;
userSessionMap.put(username, sessions);
}
sessions.httpSessionToKeycloakSession.put(sessionId, keycloakSessionId);
sessions.keycloakSessionToHttpSession.put(keycloakSessionId, sessionId);
keycloakSessionMap.put(keycloakSessionId, sessions);
if (!registered) {
manager.registerSessionListener(this);
registered = true;
}
}
public synchronized void logoutAll(SessionManager manager) {
for (String user : userSessionMap.keySet()) logoutUser(manager, user);
}
public synchronized void logoutUser(SessionManager manager, String user) {
log.debug("logoutUser: " + user);
UserSessions sessions = null;
sessions = userSessionMap.remove(user);
if (sessions == null) {
log.debug("no session for user: " + user);
return; return;
} }
log.info("found session for user"); log.debug("found session for user");
for (String id : sessions.getSessionIds()) { for (Map.Entry<String, String> entry : sessions.httpSessionToKeycloakSession.entrySet()) {
log.debug("invalidating session for user: " + user); log.debug("invalidating session for user: " + user);
Session session = manager.getSession(id); String sessionId = entry.getKey();
String keycloakSessionId = entry.getValue();
Session session = manager.getSession(sessionId);
try { try {
session.invalidate(null); session.invalidate(null);
} catch (Exception e) { } catch (Exception e) {
log.warn("Session already invalidated."); log.warn("Session already invalidated.");
} }
keycloakSessionMap.remove(keycloakSessionId);
} }
} }
public synchronized void logoutKeycloakSession(SessionManager manager, String keycloakSessionId) {
log.debug("logoutKeycloakSession: " + keycloakSessionId);
UserSessions sessions = keycloakSessionMap.remove(keycloakSessionId);
if (sessions == null) {
log.debug("no session for keycloak session id: " + keycloakSessionId);
return;
}
String sessionId = sessions.keycloakSessionToHttpSession.remove(keycloakSessionId);
if (sessionId == null) {
log.debug("no session for keycloak session id: " + keycloakSessionId);
}
sessions.httpSessionToKeycloakSession.remove(sessionId);
Session session = manager.getSession(sessionId);
try {
session.invalidate(null);
} catch (Exception e) {
log.warn("Session already invalidated.");
}
if (sessions.keycloakSessionToHttpSession.size() == 0) {
userSessionMap.remove(sessions.user);
}
}
@Override @Override
public void sessionCreated(Session session, HttpServerExchange exchange) { public void sessionCreated(Session session, HttpServerExchange exchange) {
} }
@ -141,7 +142,21 @@ public class UndertowUserSessionManagement implements SessionListener {
String username = getUsernameFromSession(session); String username = getUsernameFromSession(session);
if (username == null) return; if (username == null) return;
String sessionId = session.getId(); String sessionId = session.getId();
removeAuthenticatedSession(sessionId, username); UserSessions userSessions = userSessionMap.get(username);
if (userSessions == null) {
return;
}
synchronized (this) {
String keycloakSessionId = userSessions.httpSessionToKeycloakSession.remove(sessionId);
if (keycloakSessionId != null) {
userSessions.keycloakSessionToHttpSession.remove(keycloakSessionId);
keycloakSessionMap.remove(keycloakSessionId);
}
if (userSessions.httpSessionToKeycloakSession.size() == 0) {
userSessionMap.remove(username);
}
}
} }
protected String getUsernameFromSession(Session session) { protected String getUsernameFromSession(Session session) {
@ -156,8 +171,21 @@ public class UndertowUserSessionManagement implements SessionListener {
public void sessionIdChanged(Session session, String oldSessionId) { public void sessionIdChanged(Session session, String oldSessionId) {
String username = getUsernameFromSession(session); String username = getUsernameFromSession(session);
if (username == null) return; if (username == null) return;
removeAuthenticatedSession(oldSessionId, username); String sessionId = session.getId();
addAuthenticatedSession(session.getId(), username);
UserSessions userSessions = userSessionMap.get(username);
if (userSessions == null) {
return;
}
synchronized (this) {
String keycloakSessionId = userSessions.httpSessionToKeycloakSession.remove(oldSessionId);
if (keycloakSessionId != null) {
userSessions.keycloakSessionToHttpSession.remove(keycloakSessionId);
userSessions.keycloakSessionToHttpSession.put(keycloakSessionId, sessionId);
userSessions.httpSessionToKeycloakSession.put(sessionId, keycloakSessionId);
}
}
} }
@Override @Override

View file

@ -1,5 +1,6 @@
package org.keycloak.models; package org.keycloak.models;
import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
@ -64,4 +65,7 @@ public interface ClientModel {
void setNotBefore(int notBefore); void setNotBefore(int notBefore);
Set<UserSessionModel> getUserSessions();
int getActiveUserSessions();
} }

View file

@ -263,4 +263,7 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
void removeExpiredUserSessions(); void removeExpiredUserSessions();
ClientModel findClientById(String id);
void removeUserSessions();
} }

View file

@ -1,5 +1,8 @@
package org.keycloak.models; package org.keycloak.models;
import java.util.List;
import java.util.Set;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
@ -25,4 +28,9 @@ public interface UserSessionModel {
void setLastSessionRefresh(int seconds); void setLastSessionRefresh(int seconds);
void associateClient(ClientModel client);
List<ClientModel> getClientAssociations();
void removeAssociatedClient(ClientModel client);
} }

View file

@ -27,7 +27,7 @@ public class ApplicationAdapter extends ClientAdapter implements ApplicationMode
protected ApplicationEntity applicationEntity; protected ApplicationEntity applicationEntity;
public ApplicationAdapter(RealmModel realm, EntityManager em, ApplicationEntity applicationEntity) { public ApplicationAdapter(RealmModel realm, EntityManager em, ApplicationEntity applicationEntity) {
super(realm, applicationEntity); super(realm, applicationEntity, em);
this.realm = realm; this.realm = realm;
this.em = em; this.em = em;
this.applicationEntity = applicationEntity; this.applicationEntity = applicationEntity;

View file

@ -2,10 +2,16 @@ package org.keycloak.models.jpa;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.jpa.entities.ClientEntity; import org.keycloak.models.jpa.entities.ClientEntity;
import org.keycloak.models.jpa.entities.OAuthClientEntity; import org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
@ -15,10 +21,12 @@ import java.util.Set;
public class ClientAdapter implements ClientModel { public class ClientAdapter implements ClientModel {
protected ClientEntity entity; protected ClientEntity entity;
protected RealmModel realm; protected RealmModel realm;
protected EntityManager em;
public ClientAdapter(RealmModel realm, ClientEntity entity) { public ClientAdapter(RealmModel realm, ClientEntity entity, EntityManager em) {
this.realm = realm; this.realm = realm;
this.entity = entity; this.entity = entity;
this.em = em;
} }
public ClientEntity getEntity() { public ClientEntity getEntity() {
@ -141,6 +149,31 @@ public class ClientAdapter implements ClientModel {
entity.setNotBefore(notBefore); entity.setNotBefore(notBefore);
} }
@Override
public int getActiveUserSessions() {
Query query = em.createNamedQuery("getActiveClientSessions");
query.setParameter("clientId", getId());
Object count = query.getSingleResult();
return ((Number)count).intValue();
}
@Override
public Set<UserSessionModel> getUserSessions() {
Set<UserSessionModel> list = new HashSet<UserSessionModel>();
TypedQuery<ClientUserSessionAssociationEntity> query = em.createNamedQuery("getClientUserSessionByClient", ClientUserSessionAssociationEntity.class);
String id = getId();
query.setParameter("clientId", id);
List<ClientUserSessionAssociationEntity> results = query.getResultList();
for (ClientUserSessionAssociationEntity entity : results) {
list.add(new UserSessionAdapter(em, realm, entity.getSession()));
}
return list;
}
public void deleteUserSessionAssociation() {
em.createNamedQuery("removeClientUserSessionByClient").setParameter("clientId", getId()).executeUpdate();
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -80,6 +80,7 @@ public class JpaKeycloakSession implements KeycloakSession {
} }
RealmAdapter adapter = new RealmAdapter(em, realm); RealmAdapter adapter = new RealmAdapter(em, realm);
adapter.removeUserSessions();
for (ApplicationEntity a : new LinkedList<ApplicationEntity>(realm.getApplications())) { for (ApplicationEntity a : new LinkedList<ApplicationEntity>(realm.getApplications())) {
adapter.removeApplication(a.getId()); adapter.removeApplication(a.getId());
} }

View file

@ -5,6 +5,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.entities.OAuthClientEntity; import org.keycloak.models.jpa.entities.OAuthClientEntity;
import javax.persistence.EntityManager;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -14,8 +15,8 @@ import java.util.Set;
*/ */
public class OAuthClientAdapter extends ClientAdapter implements OAuthClientModel { public class OAuthClientAdapter extends ClientAdapter implements OAuthClientModel {
public OAuthClientAdapter(RealmModel realm, OAuthClientEntity entity) { public OAuthClientAdapter(RealmModel realm, OAuthClientEntity entity, EntityManager em) {
super(realm, entity); super(realm, entity, em);
} }
@Override @Override

View file

@ -72,6 +72,10 @@ public class RealmAdapter implements RealmModel {
this.realm = realm; this.realm = realm;
} }
public RealmEntity getEntity() {
return realm;
}
@Override @Override
public String getId() { public String getId() {
return realm.getId(); return realm.getId();
@ -582,6 +586,13 @@ public class RealmAdapter implements RealmModel {
return getOAuthClient(clientId); return getOAuthClient(clientId);
} }
@Override
public ClientModel findClientById(String id) {
ClientModel model = getApplicationById(id);
if (model != null) return model;
return getOAuthClientById(id);
}
@Override @Override
public Map<String, ApplicationModel> getApplicationNameMap() { public Map<String, ApplicationModel> getApplicationNameMap() {
Map<String, ApplicationModel> map = new HashMap<String, ApplicationModel>(); Map<String, ApplicationModel> map = new HashMap<String, ApplicationModel>();
@ -627,6 +638,7 @@ public class RealmAdapter implements RealmModel {
ApplicationModel application = getApplicationById(id); ApplicationModel application = getApplicationById(id);
if (application == null) return false; if (application == null) return false;
((ApplicationAdapter)application).deleteUserSessionAssociation();
for (RoleModel role : application.getRoles()) { for (RoleModel role : application.getRoles()) {
application.removeRole(role); application.removeRole(role);
} }
@ -846,13 +858,14 @@ public class RealmAdapter implements RealmModel {
data.setRealm(realm); data.setRealm(realm);
em.persist(data); em.persist(data);
em.flush(); em.flush();
return new OAuthClientAdapter(this, data); return new OAuthClientAdapter(this, data, em);
} }
@Override @Override
public boolean removeOAuthClient(String id) { public boolean removeOAuthClient(String id) {
OAuthClientModel oauth = getOAuthClientById(id); OAuthClientModel oauth = getOAuthClientById(id);
if (oauth == null) return false; if (oauth == null) return false;
((OAuthClientAdapter)oauth).deleteUserSessionAssociation();
OAuthClientEntity client = (OAuthClientEntity) ((OAuthClientAdapter) oauth).getEntity(); OAuthClientEntity client = (OAuthClientEntity) ((OAuthClientAdapter) oauth).getEntity();
em.createQuery("delete from " + ScopeMappingEntity.class.getSimpleName() + " where client = :client").setParameter("client", client).executeUpdate(); em.createQuery("delete from " + ScopeMappingEntity.class.getSimpleName() + " where client = :client").setParameter("client", client).executeUpdate();
em.remove(client); em.remove(client);
@ -867,7 +880,7 @@ public class RealmAdapter implements RealmModel {
query.setParameter("realm", realm); query.setParameter("realm", realm);
List<OAuthClientEntity> entities = query.getResultList(); List<OAuthClientEntity> entities = query.getResultList();
if (entities.size() == 0) return null; if (entities.size() == 0) return null;
return new OAuthClientAdapter(this, entities.get(0)); return new OAuthClientAdapter(this, entities.get(0), em);
} }
@Override @Override
@ -876,7 +889,7 @@ public class RealmAdapter implements RealmModel {
// Check if client belongs to this realm // Check if client belongs to this realm
if (client == null || !this.realm.getId().equals(client.getRealm().getId())) return null; if (client == null || !this.realm.getId().equals(client.getRealm().getId())) return null;
return new OAuthClientAdapter(this, client); return new OAuthClientAdapter(this, client, em);
} }
@ -886,7 +899,7 @@ public class RealmAdapter implements RealmModel {
query.setParameter("realm", realm); query.setParameter("realm", realm);
List<OAuthClientEntity> entities = query.getResultList(); List<OAuthClientEntity> entities = query.getResultList();
List<OAuthClientModel> list = new ArrayList<OAuthClientModel>(); List<OAuthClientModel> list = new ArrayList<OAuthClientModel>();
for (OAuthClientEntity entity : entities) list.add(new OAuthClientAdapter(this, entity)); for (OAuthClientEntity entity : entities) list.add(new OAuthClientAdapter(this, entity, em));
return list; return list;
} }
@ -1385,6 +1398,7 @@ public class RealmAdapter implements RealmModel {
@Override @Override
public UserSessionModel createUserSession(UserModel user, String ipAddress) { public UserSessionModel createUserSession(UserModel user, String ipAddress) {
UserSessionEntity entity = new UserSessionEntity(); UserSessionEntity entity = new UserSessionEntity();
entity.setRealm(realm);
entity.setUser(((UserAdapter) user).getUser()); entity.setUser(((UserAdapter) user).getUser());
entity.setIpAddress(ipAddress); entity.setIpAddress(ipAddress);
@ -1394,20 +1408,20 @@ public class RealmAdapter implements RealmModel {
entity.setLastSessionRefresh(currentTime); entity.setLastSessionRefresh(currentTime);
em.persist(entity); em.persist(entity);
return new UserSessionAdapter(entity); return new UserSessionAdapter(em, this, entity);
} }
@Override @Override
public UserSessionModel getUserSession(String id) { public UserSessionModel getUserSession(String id) {
UserSessionEntity entity = em.find(UserSessionEntity.class, id); UserSessionEntity entity = em.find(UserSessionEntity.class, id);
return entity != null ? new UserSessionAdapter(entity) : null; return entity != null ? new UserSessionAdapter(em, this, entity) : null;
} }
@Override @Override
public List<UserSessionModel> getUserSessions(UserModel user) { public List<UserSessionModel> getUserSessions(UserModel user) {
List<UserSessionModel> sessions = new LinkedList<UserSessionModel>(); List<UserSessionModel> sessions = new LinkedList<UserSessionModel>();
for (UserSessionEntity e : em.createNamedQuery("getUserSessionByUser", UserSessionEntity.class).setParameter("user", ((UserAdapter) user).getUser()).getResultList()) { for (UserSessionEntity e : em.createNamedQuery("getUserSessionByUser", UserSessionEntity.class).setParameter("user", ((UserAdapter) user).getUser()).getResultList()) {
sessions.add(new UserSessionAdapter(e)); sessions.add(new UserSessionAdapter(em, this, e));
} }
return sessions; return sessions;
} }
@ -1417,12 +1431,20 @@ public class RealmAdapter implements RealmModel {
em.remove(((UserSessionAdapter) session).getEntity()); em.remove(((UserSessionAdapter) session).getEntity());
} }
@Override
public void removeUserSessions() {
em.createNamedQuery("removeClientUserSessionByRealm").setParameter("realm", realm).executeUpdate();
em.createNamedQuery("removeRealmUserSessions").setParameter("realm", realm).executeUpdate();
}
@Override @Override
public void removeUserSessions(UserModel user) { public void removeUserSessions(UserModel user) {
removeUserSessions(((UserAdapter) user).getUser()); removeUserSessions(((UserAdapter) user).getUser());
} }
private void removeUserSessions(UserEntity user) { private void removeUserSessions(UserEntity user) {
em.createNamedQuery("removeClientUserSessionByUser").setParameter("user", user).executeUpdate();
em.createNamedQuery("removeUserSessionByUser").setParameter("user", user).executeUpdate(); em.createNamedQuery("removeUserSessionByUser").setParameter("user", user).executeUpdate();
} }

View file

@ -1,18 +1,30 @@
package org.keycloak.models.jpa; package org.keycloak.models.jpa;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity;
import org.keycloak.models.jpa.entities.UserSessionEntity; import org.keycloak.models.jpa.entities.UserSessionEntity;
import javax.persistence.EntityManager;
import java.util.ArrayList;
import java.util.List;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class UserSessionAdapter implements UserSessionModel { public class UserSessionAdapter implements UserSessionModel {
private RealmModel realm;
private UserSessionEntity entity; private UserSessionEntity entity;
private EntityManager em;
public UserSessionAdapter(UserSessionEntity entity) { public UserSessionAdapter(EntityManager em, RealmModel realm, UserSessionEntity entity) {
this.entity = entity; this.entity = entity;
this.em = em;
this.realm = realm;
} }
public UserSessionEntity getEntity() { public UserSessionEntity getEntity() {
@ -68,4 +80,51 @@ public class UserSessionAdapter implements UserSessionModel {
public void setLastSessionRefresh(int seconds) { public void setLastSessionRefresh(int seconds) {
entity.setLastSessionRefresh(seconds); entity.setLastSessionRefresh(seconds);
} }
@Override
public void associateClient(ClientModel client) {
for (ClientUserSessionAssociationEntity ass : entity.getClients()) {
if (ass.getClientId().equals(client.getId())) return;
}
ClientUserSessionAssociationEntity association = new ClientUserSessionAssociationEntity();
association.setClientId(client.getId());
association.setSession(entity);
association.setUser(entity.getUser());
association.setRealm(((RealmAdapter)realm).getEntity());
em.persist(association);
entity.getClients().add(association);
}
@Override
public void removeAssociatedClient(ClientModel client) {
em.createNamedQuery("removeClientUserSessionByClient").setParameter("clientId", client.getId()).executeUpdate();
}
@Override
public List<ClientModel> getClientAssociations() {
List<ClientModel> clients = new ArrayList<ClientModel>();
for (ClientUserSessionAssociationEntity association : entity.getClients()) {
ClientModel client = realm.findClientById(association.getClientId());
if (client == null) {
throw new ModelException("couldnt find client");
}
clients.add(client);
}
return clients;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserSessionAdapter that = (UserSessionAdapter) o;
return that.getId().equals(this.getId());
}
@Override
public int hashCode() {
return getId().hashCode();
}
} }

View file

@ -0,0 +1,83 @@
package org.keycloak.models.jpa.entities;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@Entity
@NamedQueries({
@NamedQuery(name = "getAllClientUserSessions", query = "select s from ClientUserSessionAssociationEntity s"),
@NamedQuery(name = "getClientUserSessionBySession", query = "select s from ClientUserSessionAssociationEntity s where s.session = :session"),
@NamedQuery(name = "getClientUserSessionByClient", query = "select s from ClientUserSessionAssociationEntity s where s.clientId = :clientId"),
@NamedQuery(name = "getActiveClientSessions", query = "select COUNT(s) from ClientUserSessionAssociationEntity s where s.clientId = :clientId"),
@NamedQuery(name = "removeClientUserSessionByClient", query = "delete from ClientUserSessionAssociationEntity s where s.clientId = :clientId"),
@NamedQuery(name = "removeClientUserSessionByUser", query = "delete from ClientUserSessionAssociationEntity s where s.user = :user"),
@NamedQuery(name = "removeClientUserSessionByRealm", query = "delete from ClientUserSessionAssociationEntity s where s.realm = :realm")
})
public class ClientUserSessionAssociationEntity {
@Id
@GenericGenerator(name="uuid_generator", strategy="org.keycloak.models.jpa.utils.JpaIdGenerator")
@GeneratedValue(generator = "uuid_generator")
private String id;
@ManyToOne(fetch= FetchType.LAZY)
private UserSessionEntity session;
@ManyToOne(fetch= FetchType.LAZY)
private UserEntity user;
@ManyToOne(fetch= FetchType.LAZY)
private RealmEntity realm;
private String clientId;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public UserEntity getUser() {
return user;
}
public void setUser(UserEntity user) {
this.user = user;
}
public UserSessionEntity getSession() {
return session;
}
public void setSession(UserSessionEntity session) {
this.session = session;
}
public RealmEntity getRealm() {
return realm;
}
public void setRealm(RealmEntity realm) {
this.realm = realm;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
}

View file

@ -3,12 +3,17 @@ package org.keycloak.models.jpa.entities;
import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import javax.persistence.CascadeType;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue; import javax.persistence.GeneratedValue;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries; import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery; import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import java.util.ArrayList;
import java.util.Collection;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -16,6 +21,7 @@ import javax.persistence.NamedQuery;
@Entity @Entity
@NamedQueries({ @NamedQueries({
@NamedQuery(name = "getUserSessionByUser", query = "select s from UserSessionEntity s where s.user = :user"), @NamedQuery(name = "getUserSessionByUser", query = "select s from UserSessionEntity s where s.user = :user"),
@NamedQuery(name = "removeRealmUserSessions", query = "delete from UserSessionEntity s where s.realm = :realm"),
@NamedQuery(name = "removeUserSessionByUser", query = "delete from UserSessionEntity s where s.user = :user"), @NamedQuery(name = "removeUserSessionByUser", query = "delete from UserSessionEntity s where s.user = :user"),
@NamedQuery(name = "removeUserSessionExpired", query = "delete from UserSessionEntity s where s.started < :maxTime or s.lastSessionRefresh < :idleTime") @NamedQuery(name = "removeUserSessionExpired", query = "delete from UserSessionEntity s where s.started < :maxTime or s.lastSessionRefresh < :idleTime")
}) })
@ -29,12 +35,19 @@ public class UserSessionEntity {
@ManyToOne @ManyToOne
private UserEntity user; private UserEntity user;
@ManyToOne(fetch = FetchType.LAZY)
private RealmEntity realm;
String ipAddress; String ipAddress;
int started; int started;
int lastSessionRefresh; int lastSessionRefresh;
@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy="session")
Collection<ClientUserSessionAssociationEntity> clients = new ArrayList<ClientUserSessionAssociationEntity>();
public String getId() { public String getId() {
return id; return id;
} }
@ -74,4 +87,20 @@ public class UserSessionEntity {
public void setLastSessionRefresh(int lastSessionRefresh) { public void setLastSessionRefresh(int lastSessionRefresh) {
this.lastSessionRefresh = lastSessionRefresh; this.lastSessionRefresh = lastSessionRefresh;
} }
public Collection<ClientUserSessionAssociationEntity> getClients() {
return clients;
}
public void setClients(Collection<ClientUserSessionAssociationEntity> clients) {
this.clients = clients;
}
public RealmEntity getRealm() {
return realm;
}
public void setRealm(RealmEntity realm) {
this.realm = realm;
}
} }

View file

@ -17,6 +17,7 @@
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class> <class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class> <class>org.keycloak.models.jpa.entities.UserEntity</class>
<class>org.keycloak.models.jpa.entities.UserSessionEntity</class> <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
<class>org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class> <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class> <class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class> <class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>

View file

@ -5,11 +5,16 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.entities.ClientEntity; import org.keycloak.models.entities.ClientEntity;
import org.keycloak.models.mongo.api.MongoIdentifiableEntity; import org.keycloak.models.mongo.api.MongoIdentifiableEntity;
import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.mongo.keycloak.entities.MongoClientUserSessionAssociationEntity;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -164,4 +169,25 @@ public class ClientAdapter<T extends MongoIdentifiableEntity> extends AbstractMo
updateMongoEntity(); updateMongoEntity();
} }
@Override
public Set<UserSessionModel> getUserSessions() {
DBObject query = new QueryBuilder()
.and("clientId").is(getId())
.get();
List<MongoClientUserSessionAssociationEntity> associations = getMongoStore().loadEntities(MongoClientUserSessionAssociationEntity.class, query, invocationContext);
Set<UserSessionModel> result = new HashSet<UserSessionModel>();
for (MongoClientUserSessionAssociationEntity association : associations) {
UserSessionModel session = realm.getUserSession(association.getSessionId());
result.add(session);
}
return result;
}
@Override
public int getActiveUserSessions() {
// todo, something more efficient like COUNT in JPAQL?
return getUserSessions().size();
}
} }

View file

@ -661,6 +661,14 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
return getOAuthClient(clientId); return getOAuthClient(clientId);
} }
@Override
public ClientModel findClientById(String id) {
ClientModel model = getApplicationById(id);
if (model != null) return model;
return getOAuthClientById(id);
}
@Override @Override
public ApplicationModel getApplicationById(String id) { public ApplicationModel getApplicationById(String id) {
@ -1352,6 +1360,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override @Override
public UserSessionModel createUserSession(UserModel user, String ipAddress) { public UserSessionModel createUserSession(UserModel user, String ipAddress) {
MongoUserSessionEntity entity = new MongoUserSessionEntity(); MongoUserSessionEntity entity = new MongoUserSessionEntity();
entity.setRealmId(getId());
entity.setUser(user.getId()); entity.setUser(user.getId());
entity.setIpAddress(ipAddress); entity.setIpAddress(ipAddress);
@ -1386,7 +1395,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override @Override
public void removeUserSession(UserSessionModel session) { public void removeUserSession(UserSessionModel session) {
getMongoStore().removeEntity(((UserSessionAdapter) session).getEntity(), invocationContext); getMongoStore().removeEntity(((UserSessionAdapter) session).getMongoEntity(), invocationContext);
} }
@Override @Override
@ -1395,6 +1404,12 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
getMongoStore().removeEntities(MongoUserSessionEntity.class, query, invocationContext); getMongoStore().removeEntities(MongoUserSessionEntity.class, query, invocationContext);
} }
@Override
public void removeUserSessions() {
DBObject query = new BasicDBObject("realmId", getId());
getMongoStore().removeEntities(MongoUserSessionEntity.class, query, invocationContext);
}
@Override @Override
public void removeExpiredUserSessions() { public void removeExpiredUserSessions() {
int currentTime = Time.currentTime(); int currentTime = Time.currentTime();

View file

@ -1,26 +1,40 @@
package org.keycloak.models.mongo.keycloak.adapters; package org.keycloak.models.mongo.keycloak.adapters;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.keycloak.models.ClientModel;
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.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.mongo.keycloak.entities.MongoClientUserSessionAssociationEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserSessionEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserSessionEntity;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class UserSessionAdapter implements UserSessionModel { public class UserSessionAdapter extends AbstractMongoAdapter<MongoUserSessionEntity> implements UserSessionModel {
private MongoUserSessionEntity entity; private MongoUserSessionEntity entity;
private RealmAdapter realm; private RealmAdapter realm;
private MongoStoreInvocationContext invContext;
public UserSessionAdapter(MongoUserSessionEntity entity, RealmAdapter realm, MongoStoreInvocationContext invContext) { public UserSessionAdapter(MongoUserSessionEntity entity, RealmAdapter realm, MongoStoreInvocationContext invContext)
{
super(invContext);
this.entity = entity; this.entity = entity;
this.realm = realm; this.realm = realm;
this.invContext = invContext;
} }
public MongoUserSessionEntity getEntity() { @Override
protected MongoUserSessionEntity getMongoEntity() {
return entity; return entity;
} }
@ -74,4 +88,56 @@ public class UserSessionAdapter implements UserSessionModel {
entity.setLastSessionRefresh(seconds); entity.setLastSessionRefresh(seconds);
} }
@Override
public void associateClient(ClientModel client) {
List<ClientModel> clients = getClientAssociations();
for (ClientModel ass : clients) {
if (ass.getId().equals(client.getId())) return;
}
MongoClientUserSessionAssociationEntity association = new MongoClientUserSessionAssociationEntity();
association.setClientId(client.getId());
association.setSessionId(getId());
getMongoStore().insertEntity(association, invocationContext);
}
@Override
public List<ClientModel> getClientAssociations() {
DBObject query = new QueryBuilder()
.and("sessionId").is(getId())
.get();
List<MongoClientUserSessionAssociationEntity> associations = getMongoStore().loadEntities(MongoClientUserSessionAssociationEntity.class, query, invocationContext);
List<ClientModel> result = new ArrayList<ClientModel>();
for (MongoClientUserSessionAssociationEntity association : associations) {
ClientModel client = realm.findClientById(association.getClientId());
result.add(client);
}
return result;
}
@Override
public void removeAssociatedClient(ClientModel client) {
DBObject query = new QueryBuilder()
.and("sessionId").is(getId())
.and("clientId").is(client.getId())
.get();
getMongoStore().removeEntities(MongoClientUserSessionAssociationEntity.class, query, invocationContext);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
UserSessionAdapter that = (UserSessionAdapter) o;
return getId().equals(that.getId());
}
@Override
public int hashCode() {
return getId().hashCode();
}
} }

View file

@ -22,5 +22,11 @@ public class MongoApplicationEntity extends ApplicationEntity implements MongoId
.and("applicationId").is(getId()) .and("applicationId").is(getId())
.get(); .get();
context.getMongoStore().removeEntities(MongoRoleEntity.class, query, context); context.getMongoStore().removeEntities(MongoRoleEntity.class, query, context);
query = new QueryBuilder()
.and("clientId").is(getId())
.get();
context.getMongoStore().removeEntities(MongoClientUserSessionAssociationEntity.class, query, context);
} }
} }

View file

@ -0,0 +1,37 @@
package org.keycloak.models.mongo.keycloak.entities;
import org.keycloak.models.entities.AbstractIdentifiableEntity;
import org.keycloak.models.mongo.api.MongoCollection;
import org.keycloak.models.mongo.api.MongoIdentifiableEntity;
import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@MongoCollection(collectionName = "session-client-associations")
public class MongoClientUserSessionAssociationEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity {
private String clientId;
private String sessionId;
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
@Override
public void afterRemove(MongoStoreInvocationContext invocationContext) {
}
}

View file

@ -1,5 +1,7 @@
package org.keycloak.models.mongo.keycloak.entities; package org.keycloak.models.mongo.keycloak.entities;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.keycloak.models.entities.OAuthClientEntity; import org.keycloak.models.entities.OAuthClientEntity;
import org.keycloak.models.mongo.api.MongoCollection; import org.keycloak.models.mongo.api.MongoCollection;
import org.keycloak.models.mongo.api.MongoIdentifiableEntity; import org.keycloak.models.mongo.api.MongoIdentifiableEntity;
@ -15,5 +17,9 @@ public class MongoOAuthClientEntity extends OAuthClientEntity implements MongoId
@Override @Override
public void afterRemove(MongoStoreInvocationContext invocationContext) { public void afterRemove(MongoStoreInvocationContext invocationContext) {
DBObject query = new QueryBuilder()
.and("clientId").is(getId())
.get();
invocationContext.getMongoStore().removeEntities(MongoClientUserSessionAssociationEntity.class, query, invocationContext);
} }
} }

View file

@ -32,5 +32,8 @@ public class MongoRealmEntity extends RealmEntity implements MongoIdentifiableEn
// Remove all clients of this realm // Remove all clients of this realm
context.getMongoStore().removeEntities(MongoOAuthClientEntity.class, query, context); context.getMongoStore().removeEntities(MongoOAuthClientEntity.class, query, context);
// Remove all sessions of this realm
context.getMongoStore().removeEntities(MongoUserSessionEntity.class, query, context);
} }
} }

View file

@ -1,5 +1,7 @@
package org.keycloak.models.mongo.keycloak.entities; package org.keycloak.models.mongo.keycloak.entities;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.keycloak.models.entities.AbstractIdentifiableEntity; import org.keycloak.models.entities.AbstractIdentifiableEntity;
import org.keycloak.models.mongo.api.MongoCollection; import org.keycloak.models.mongo.api.MongoCollection;
import org.keycloak.models.mongo.api.MongoIdentifiableEntity; import org.keycloak.models.mongo.api.MongoIdentifiableEntity;
@ -11,6 +13,8 @@ import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext;
@MongoCollection(collectionName = "sessions") @MongoCollection(collectionName = "sessions")
public class MongoUserSessionEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity { public class MongoUserSessionEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity {
private String realmId;
private String user; private String user;
private String ipAddress; private String ipAddress;
@ -19,6 +23,14 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement
private int lastSessionRefresh; private int lastSessionRefresh;
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.realmId = realmId;
}
public String getUser() { public String getUser() {
return user; return user;
} }
@ -52,7 +64,12 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement
} }
@Override @Override
public void afterRemove(MongoStoreInvocationContext invocationContext) { public void afterRemove(MongoStoreInvocationContext context) {
// Remove all roles, which belongs to this application
DBObject query = new QueryBuilder()
.and("sessionId").is(getId())
.get();
context.getMongoStore().removeEntities(MongoClientUserSessionAssociationEntity.class, query, context);
} }
} }

View file

@ -16,6 +16,7 @@
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class> <class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class> <class>org.keycloak.models.jpa.entities.UserEntity</class>
<class>org.keycloak.models.jpa.entities.UserSessionEntity</class> <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
<class>org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity</class>
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class> <class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class> <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class> <class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>

View file

@ -16,6 +16,7 @@
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class> <class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class> <class>org.keycloak.models.jpa.entities.UserEntity</class>
<class>org.keycloak.models.jpa.entities.UserSessionEntity</class> <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
<class>org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity</class>
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class> <class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class> <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class> <class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>

View file

@ -5,12 +5,14 @@ import org.keycloak.models.AuthenticationProviderModel;
import org.keycloak.models.ClaimMask; import org.keycloak.models.ClaimMask;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.OAuthClientModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.SocialLinkModel; import org.keycloak.models.SocialLinkModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.idm.AuthenticationProviderRepresentation; import org.keycloak.representations.idm.AuthenticationProviderRepresentation;
import org.keycloak.representations.idm.ClaimRepresentation; import org.keycloak.representations.idm.ClaimRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
@ -19,6 +21,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.SocialLinkRepresentation; import org.keycloak.representations.idm.SocialLinkRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -179,4 +182,20 @@ public class ModelToRepresentation {
rep.setSocialUserId(socialLink.getSocialUserId()); rep.setSocialUserId(socialLink.getSocialUserId());
return rep; return rep;
} }
public static UserSessionRepresentation toRepresentation(UserSessionModel session) {
UserSessionRepresentation rep = new UserSessionRepresentation();
rep.setId(session.getId());
rep.setStart(((long)session.getStarted()) * 1000);
rep.setUser(session.getUser().getLoginName());
rep.setIpAddress(session.getIpAddress());
for (ClientModel client : session.getClientAssociations()) {
if (client instanceof ApplicationModel) {
rep.getApplications().add(client.getClientId());
} else if (client instanceof OAuthClientModel) {
rep.getClients().put(client.getId(), client.getClientId());
}
}
return rep;
}
} }

View file

@ -160,6 +160,22 @@ public class ResourceAdminManager {
executor.getHttpClient().getConnectionManager().shutdown(); executor.getHttpClient().getConnectionManager().shutdown();
} }
} }
public void logoutSession(URI requestUri, RealmModel realm, String session) {
ApacheHttpClient4Executor executor = createExecutor();
try {
// don't set user notBefore as we don't want a database hit on a user driven logout
List<ApplicationModel> resources = realm.getApplications();
logger.debugv("logging out {0} resources ", resources.size());
for (ApplicationModel resource : resources) {
logoutApplication(requestUri, realm, resource, null, session, executor, 0);
}
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
}
}
public void logoutAll(URI requestUri, RealmModel realm) { public void logoutAll(URI requestUri, RealmModel realm) {
ApacheHttpClient4Executor executor = createExecutor(); ApacheHttpClient4Executor executor = createExecutor();

View file

@ -266,6 +266,7 @@ public class TokenService {
String scope = form.getFirst(OAuth2Constants.SCOPE); String scope = form.getFirst(OAuth2Constants.SCOPE);
UserSessionModel session = realm.createUserSession(user, clientConnection.getRemoteAddr()); UserSessionModel session = realm.createUserSession(user, clientConnection.getRemoteAddr());
session.associateClient(client);
audit.session(session); audit.session(session);
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit) AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit)
@ -648,6 +649,8 @@ public class TokenService {
logger.debug("accessRequest SUCCESS"); logger.debug("accessRequest SUCCESS");
session.associateClient(client);
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit) AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit)
.accessToken(accessCode.getToken()) .accessToken(accessCode.getToken())
.generateIDToken() .generateIDToken()

View file

@ -9,10 +9,12 @@ import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.adapters.action.SessionStats; import org.keycloak.representations.adapters.action.SessionStats;
import org.keycloak.representations.adapters.action.UserStats; import org.keycloak.representations.adapters.action.UserStats;
import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.ApplicationRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.services.managers.ApplicationManager; import org.keycloak.services.managers.ApplicationManager;
import org.keycloak.services.managers.ModelToRepresentation; import org.keycloak.services.managers.ModelToRepresentation;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
@ -36,7 +38,10 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@ -228,7 +233,32 @@ public class ApplicationResource {
logger.info("activeSessions: " + stats.getActiveSessions()); logger.info("activeSessions: " + stats.getActiveSessions());
} }
return stats; return stats;
} }
@Path("session-count")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Integer> getApplicationSessionCount() {
auth.requireView();
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("count", application.getActiveUserSessions());
return map;
}
@Path("user-sessions")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<UserSessionRepresentation> getUserSessions() {
auth.requireView();
List<UserSessionRepresentation> sessions = new ArrayList<UserSessionRepresentation>();
for (UserSessionModel session : application.getUserSessions()) {
UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session);
sessions.add(rep);
}
return sessions;
}
@Path("logout-all") @Path("logout-all")
@POST @POST

View file

@ -11,6 +11,7 @@ import org.keycloak.models.ApplicationModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.ProviderSession; import org.keycloak.provider.ProviderSession;
import org.keycloak.representations.adapters.action.SessionStats; import org.keycloak.representations.adapters.action.SessionStats;
import org.keycloak.representations.idm.RealmAuditRepresentation; import org.keycloak.representations.idm.RealmAuditRepresentation;
@ -27,6 +28,7 @@ import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.PUT; import javax.ws.rs.PUT;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
@ -156,9 +158,34 @@ public class RealmAdminResource {
@POST @POST
public void logoutAll() { public void logoutAll() {
auth.requireManage(); auth.requireManage();
realm.removeUserSessions();
new ResourceAdminManager().logoutAll(uriInfo.getRequestUri(), realm); new ResourceAdminManager().logoutAll(uriInfo.getRequestUri(), realm);
} }
@Path("sessions/{session}")
@DELETE
public void deleteSession(@PathParam("session") String sessionId) {
UserSessionModel session = realm.getUserSession(sessionId);
if (session == null) throw new NotFoundException("Sesssion not found");
realm.removeUserSession(session);
new ResourceAdminManager().logoutSession(uriInfo.getRequestUri(), realm, session.getId());
}
@Path("application-session-stats")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Integer> getApplicationSessionStats() {
auth.requireView();
Map<String, Integer> stats = new HashMap<String, Integer>();
for (ApplicationModel applicationModel : realm.getApplications()) {
int size = applicationModel.getActiveUserSessions();
if (size == 0) continue;
stats.put(applicationModel.getName(), size);
}
return stats;
}
@Path("session-stats") @Path("session-stats")
@GET @GET
@NoCache @NoCache

View file

@ -14,6 +14,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.SocialLinkModel; import org.keycloak.models.SocialLinkModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.adapters.action.UserStats; import org.keycloak.representations.adapters.action.UserStats;
import org.keycloak.representations.idm.ApplicationMappingsRepresentation; import org.keycloak.representations.idm.ApplicationMappingsRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
@ -21,6 +22,7 @@ import org.keycloak.representations.idm.MappingsRepresentation;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.SocialLinkRepresentation; import org.keycloak.representations.idm.SocialLinkRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.services.email.EmailException; import org.keycloak.services.email.EmailException;
import org.keycloak.services.email.EmailSender; import org.keycloak.services.email.EmailSender;
import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.AccessCodeEntry;
@ -181,6 +183,26 @@ public class UsersResource {
return stats; return stats;
} }
@Path("{username}/sessions")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<UserSessionRepresentation> getSessions(final @PathParam("username") String username) {
logger.info("sessions");
auth.requireView();
UserModel user = realm.getUser(username);
if (user == null) {
throw new NotFoundException("User not found");
}
List<UserSessionModel> sessions = realm.getUserSessions(user);
List<UserSessionRepresentation> reps = new ArrayList<UserSessionRepresentation>();
for (UserSessionModel session : sessions) {
UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session);
reps.add(rep);
}
return reps;
}
@Path("{username}/social-links") @Path("{username}/social-links")
@GET @GET
@NoCache @NoCache
@ -208,6 +230,7 @@ public class UsersResource {
if (user == null) { if (user == null) {
throw new NotFoundException("User not found"); throw new NotFoundException("User not found");
} }
realm.removeUserSessions(user);
// set notBefore so that user will be forced to log in. // set notBefore so that user will be forced to log in.
user.setNotBefore(Time.currentTime()); user.setNotBefore(Time.currentTime());
new ResourceAdminManager().logoutUser(uriInfo.getRequestUri(), realm, user.getId(), null); new ResourceAdminManager().logoutUser(uriInfo.getRequestUri(), realm, user.getId(), null);

View file

@ -16,6 +16,7 @@
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class> <class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class> <class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class> <class>org.keycloak.models.jpa.entities.UserEntity</class>
<class>org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity</class>
<class>org.keycloak.models.jpa.entities.UserSessionEntity</class> <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class> <class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class> <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>

View file

@ -17,6 +17,7 @@
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class> <class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class> <class>org.keycloak.models.jpa.entities.UserEntity</class>
<class>org.keycloak.models.jpa.entities.UserSessionEntity</class> <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
<class>org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity</class>
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class> <class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class> <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class> <class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>