keycloak-memoizeit

KEYCLOAK-432 Added user sessions

5/7/2014 5:33:29 AM

Changes

Details

diff --git a/audit/api/src/main/java/org/keycloak/audit/Audit.java b/audit/api/src/main/java/org/keycloak/audit/Audit.java
index 4cbd4f7..1ccdca8 100644
--- a/audit/api/src/main/java/org/keycloak/audit/Audit.java
+++ b/audit/api/src/main/java/org/keycloak/audit/Audit.java
@@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.provider.ProviderFactoryLoader;
 
 import java.util.HashMap;
@@ -61,6 +62,16 @@ public class Audit {
         return this;
     }
 
+    public Audit session(UserSessionModel session) {
+        event.setSessionId(session.getId());
+        return this;
+    }
+
+    public Audit session(String sessionId) {
+        event.setSessionId(sessionId);
+        return this;
+    }
+
     public Audit ipAddress(String ipAddress) {
         event.setIpAddress(ipAddress);
         return this;
diff --git a/audit/api/src/main/java/org/keycloak/audit/Errors.java b/audit/api/src/main/java/org/keycloak/audit/Errors.java
index 1ab7d4a..4b0d41d 100755
--- a/audit/api/src/main/java/org/keycloak/audit/Errors.java
+++ b/audit/api/src/main/java/org/keycloak/audit/Errors.java
@@ -34,4 +34,8 @@ public interface Errors {
     String SOCIAL_PROVIDER_NOT_FOUND = "social_provider_not_found";
     String SOCIAL_ID_IN_USE = "social_id_in_use";
 
+    String USER_NOT_LOGGED_IN = "user_not_logged_in";
+    String USER_SESSION_NOT_FOUND = "user_session_not_found";
+
+
 }
diff --git a/audit/api/src/main/java/org/keycloak/audit/Event.java b/audit/api/src/main/java/org/keycloak/audit/Event.java
index 6ab7400..8141acb 100644
--- a/audit/api/src/main/java/org/keycloak/audit/Event.java
+++ b/audit/api/src/main/java/org/keycloak/audit/Event.java
@@ -18,6 +18,8 @@ public class Event {
 
     private String userId;
 
+    private String sessionId;
+
     private String ipAddress;
 
     private String error;
@@ -64,6 +66,14 @@ public class Event {
         this.userId = userId;
     }
 
+    public String getSessionId() {
+        return sessionId;
+    }
+
+    public void setSessionId(String sessionId) {
+        this.sessionId = sessionId;
+    }
+
     public String getIpAddress() {
         return ipAddress;
     }
@@ -95,6 +105,7 @@ public class Event {
         clone.realmId = realmId;
         clone.clientId = clientId;
         clone.userId = userId;
+        clone.sessionId = sessionId;
         clone.ipAddress = ipAddress;
         clone.error = error;
         clone.details = details != null ? new HashMap<String, String>(details) : null;
diff --git a/audit/jpa/src/main/java/org/keycloak/audit/jpa/EventEntity.java b/audit/jpa/src/main/java/org/keycloak/audit/jpa/EventEntity.java
index 1227b70..f191813 100644
--- a/audit/jpa/src/main/java/org/keycloak/audit/jpa/EventEntity.java
+++ b/audit/jpa/src/main/java/org/keycloak/audit/jpa/EventEntity.java
@@ -23,6 +23,8 @@ public class EventEntity {
 
     private String userId;
 
+    private String sessionId;
+
     private String ipAddress;
 
     private String error;
@@ -78,6 +80,14 @@ public class EventEntity {
         this.userId = userId;
     }
 
+    public String getSessionId() {
+        return sessionId;
+    }
+
+    public void setSessionId(String sessionId) {
+        this.sessionId = sessionId;
+    }
+
     public String getIpAddress() {
         return ipAddress;
     }
diff --git a/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java
index 5401902..0640086 100644
--- a/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java
+++ b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java
@@ -83,6 +83,7 @@ public class JpaAuditProvider implements AuditProvider {
         e.setRealmId(o.getRealmId());
         e.setClientId(o.getClientId());
         e.setUserId(o.getUserId());
+        e.setSessionId(o.getSessionId());
         e.setIpAddress(o.getIpAddress());
         e.setError(o.getError());
         try {
@@ -100,6 +101,7 @@ public class JpaAuditProvider implements AuditProvider {
         e.setRealmId(o.getRealmId());
         e.setClientId(o.getClientId());
         e.setUserId(o.getUserId());
+        e.setSessionId(o.getSessionId());
         e.setIpAddress(o.getIpAddress());
         e.setError(o.getError());
         try {
diff --git a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java
index 77fbb79..3114628 100644
--- a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java
+++ b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java
@@ -60,6 +60,7 @@ public class MongoAuditProvider implements AuditProvider {
         e.put("realmId", o.getRealmId());
         e.put("clientId", o.getClientId());
         e.put("userId", o.getUserId());
+        e.put("sessionId", o.getSessionId());
         e.put("ipAddress", o.getIpAddress());
         e.put("error", o.getError());
 
@@ -81,6 +82,7 @@ public class MongoAuditProvider implements AuditProvider {
         e.setRealmId(o.getString("realmId"));
         e.setClientId(o.getString("clientId"));
         e.setUserId(o.getString("userId"));
+        e.setSessionId(o.getString("sessionId"));
         e.setIpAddress(o.getString("ipAddress"));
         e.setError(o.getString("error"));
 
diff --git a/bundled-war-example/src/main/resources/META-INF/persistence.xml b/bundled-war-example/src/main/resources/META-INF/persistence.xml
index 3eeed1f..616f8e8 100755
--- a/bundled-war-example/src/main/resources/META-INF/persistence.xml
+++ b/bundled-war-example/src/main/resources/META-INF/persistence.xml
@@ -15,6 +15,7 @@
         <class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
         <class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
         <class>org.keycloak.models.jpa.entities.UserEntity</class>
+        <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
         <class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
         <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
         <class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
diff --git a/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java b/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java
index 6b73234..b89b10d 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java
@@ -7,14 +7,16 @@ package org.keycloak.representations.adapters.action;
 public class LogoutAction extends AdminAction {
     public static final String LOGOUT = "LOGOUT";
     protected String user;
+    private String session;
     protected int notBefore;
 
     public LogoutAction() {
     }
 
-    public LogoutAction(String id, int expiration, String resource, String user, int notBefore) {
+    public LogoutAction(String id, int expiration, String resource, String user, String session, int notBefore) {
         super(id, expiration, resource, LOGOUT);
         this.user = user;
+        this.session = session;
         this.notBefore = notBefore;
     }
 
@@ -26,6 +28,14 @@ public class LogoutAction extends AdminAction {
         this.user = user;
     }
 
+    public String getSession() {
+        return session;
+    }
+
+    public void setSession(String session) {
+        this.session = session;
+    }
+
     public int getNotBefore() {
         return notBefore;
     }
diff --git a/core/src/main/java/org/keycloak/representations/IDToken.java b/core/src/main/java/org/keycloak/representations/IDToken.java
index 08514a9..975da61 100755
--- a/core/src/main/java/org/keycloak/representations/IDToken.java
+++ b/core/src/main/java/org/keycloak/representations/IDToken.java
@@ -88,6 +88,9 @@ public class IDToken extends JsonWebToken {
     @JsonProperty("claims_locales")
     protected String claimsLocales;
 
+    @JsonProperty("session_state")
+    protected String sessionState;
+
     public String getNonce() {
         return nonce;
     }
@@ -303,4 +306,12 @@ public class IDToken extends JsonWebToken {
     public void setClaimsLocales(String claimsLocales) {
         this.claimsLocales = claimsLocales;
     }
+
+    public String getSessionState() {
+        return sessionState;
+    }
+
+    public void setSessionState(String sessionState) {
+        this.sessionState = sessionState;
+    }
 }
diff --git a/core/src/main/java/org/keycloak/representations/RefreshToken.java b/core/src/main/java/org/keycloak/representations/RefreshToken.java
index 7d90931..b536a1c 100755
--- a/core/src/main/java/org/keycloak/representations/RefreshToken.java
+++ b/core/src/main/java/org/keycloak/representations/RefreshToken.java
@@ -13,7 +13,7 @@ public class RefreshToken extends AccessToken {
     }
 
     /**
-     * Deep copies issuer, subject, issuedFor, realmAccess, and resourceAccess
+     * Deep copies issuer, subject, issuedFor, sessionState, realmAccess, and resourceAccess
      * from AccessToken.
      *
      * @param token
@@ -23,6 +23,7 @@ public class RefreshToken extends AccessToken {
         this.issuer = token.issuer;
         this.subject = token.subject;
         this.issuedFor = token.issuedFor;
+        this.sessionState = token.sessionState;
         if (token.realmAccess != null) {
             realmAccess = token.realmAccess.clone();
         }
diff --git a/model/api/src/main/java/org/keycloak/models/Config.java b/model/api/src/main/java/org/keycloak/models/Config.java
index e50ed9a..f0416d1 100644
--- a/model/api/src/main/java/org/keycloak/models/Config.java
+++ b/model/api/src/main/java/org/keycloak/models/Config.java
@@ -13,9 +13,12 @@ public class Config {
 
     public static final String MODEL_PROVIDER_KEY = "keycloak.model";
 
+    public static final String USER_EXPIRATION_SCHEDULE_KEY = "keycloak.scheduled.clearExpiredUserSessions";
+    public static final String USER_EXPIRATION_SCHEDULE_DEFAULT = String.valueOf(TimeUnit.MINUTES.toMillis(15));
+
     public static final String AUDIT_PROVIDER_KEY = "keycloak.audit";
     public static final String AUDIT_PROVIDER_DEFAULT = "jpa";
-    public static final String AUDIT_EXPIRATION_SCHEDULE_KEY = "keycloak.audit.expirationSchedule";
+    public static final String AUDIT_EXPIRATION_SCHEDULE_KEY = "keycloak.scheduled.clearExpiredAuditEvents";
     public static final String AUDIT_EXPIRATION_SCHEDULE_DEFAULT = String.valueOf(TimeUnit.MINUTES.toMillis(15));
 
     public static final String PICKETLINK_PROVIDER_KEY = "keycloak.picketlink";
@@ -67,6 +70,14 @@ public class Config {
         System.setProperty(AUDIT_EXPIRATION_SCHEDULE_KEY, schedule);
     }
 
+    public static String getUserExpirationSchedule() {
+        return System.getProperty(USER_EXPIRATION_SCHEDULE_KEY, USER_EXPIRATION_SCHEDULE_DEFAULT);
+    }
+
+    public static void setUserExpirationSchedule(String schedule) {
+        System.setProperty(USER_EXPIRATION_SCHEDULE_KEY, schedule);
+    }
+
     public static String getModelProvider() {
         return System.getProperty(MODEL_PROVIDER_KEY);
     }
@@ -168,4 +179,5 @@ public class Config {
     public static void setExportImportZipPassword(String exportImportZipPassword) {
         System.setProperty(EXPORT_IMPORT_ZIP_PASSWORD, exportImportZipPassword);
     }
+
 }
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index 1c73b2f..fee50d3 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -249,4 +249,14 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
 
     void setAdminApp(ApplicationModel app);
 
+    UserSessionModel createUserSession(UserModel user, String ipAddress);
+
+    UserSessionModel getUserSession(String id);
+
+    void removeUserSession(UserSessionModel session);
+
+    void removeUserSessions(UserModel user);
+
+    void removeExpiredUserSessions();
+
 }
diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java
new file mode 100644
index 0000000..33c33fc
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java
@@ -0,0 +1,28 @@
+package org.keycloak.models;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface UserSessionModel {
+
+    String getId();
+
+    void setId(String id);
+
+    UserModel getUser();
+
+    void setUser(UserModel user);
+
+    String getIpAddress();
+
+    void setIpAddress(String ipAddress);
+
+    int getStarted();
+
+    void setStarted(int started);
+
+    int getExpires();
+
+    void setExpires(int expires);
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java
new file mode 100644
index 0000000..5d4fde7
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java
@@ -0,0 +1,77 @@
+package org.keycloak.models.jpa.entities;
+
+import org.hibernate.annotations.GenericGenerator;
+import org.keycloak.models.UserModel;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@Entity
+@NamedQueries({
+        @NamedQuery(name = "removeUserSessionByUser", query = "delete from UserSessionEntity s where s.user = :user"),
+        @NamedQuery(name = "removeUserSessionExpired", query = "delete from UserSessionEntity s where s.expires < :currentTime")
+})
+public class UserSessionEntity {
+
+    @Id
+    @GenericGenerator(name="uuid_generator", strategy="org.keycloak.models.jpa.utils.JpaIdGenerator")
+    @GeneratedValue(generator = "uuid_generator")
+    private String id;
+
+    @ManyToOne
+    private UserEntity user;
+
+    String ipAddress;
+
+    int started;
+
+    int expires;
+
+    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 String getIpAddress() {
+        return ipAddress;
+    }
+
+    public void setIpAddress(String ipAddress) {
+        this.ipAddress = ipAddress;
+    }
+
+    public int getStarted() {
+        return started;
+    }
+
+    public void setStarted(int started) {
+        this.started = started;
+    }
+
+    public int getExpires() {
+        return expires;
+    }
+
+    public void setExpires(int expires) {
+        this.expires = expires;
+    }
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java
index d54cf3c..5614053 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java
@@ -88,7 +88,7 @@ public class JpaKeycloakSession implements KeycloakSession {
             adapter.removeOAuthClient(oauth.getId());
         }
 
-        for (UserEntity u : em.createQuery("from UserEntity", UserEntity.class).getResultList()) {
+        for (UserEntity u : em.createQuery("from UserEntity u where u.realm = :realm", UserEntity.class).setParameter("realm", realm).getResultList()) {
             adapter.removeUser(u.getLoginName());
         }
 
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 4b6758d..a243d85 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -5,6 +5,7 @@ import org.keycloak.models.AuthenticationProviderModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.RoleContainerModel;
 import org.keycloak.models.UserCredentialValueModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.UsernameLoginFailureModel;
 import org.keycloak.models.jpa.entities.ApplicationEntity;
 import org.keycloak.models.jpa.entities.ApplicationRoleEntity;
@@ -19,6 +20,7 @@ import org.keycloak.models.jpa.entities.RoleEntity;
 import org.keycloak.models.jpa.entities.ScopeMappingEntity;
 import org.keycloak.models.jpa.entities.SocialLinkEntity;
 import org.keycloak.models.jpa.entities.UserEntity;
+import org.keycloak.models.jpa.entities.UserSessionEntity;
 import org.keycloak.models.jpa.entities.UserRoleMappingEntity;
 import org.keycloak.models.jpa.entities.UsernameLoginFailureEntity;
 import org.keycloak.models.utils.KeycloakModelUtils;
@@ -33,6 +35,7 @@ import org.keycloak.models.SocialLinkModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.TimeBasedOTP;
+import org.keycloak.util.Time;
 
 import javax.persistence.EntityManager;
 import javax.persistence.TypedQuery;
@@ -49,6 +52,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+
 import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.*;
 
 /**
@@ -452,7 +456,7 @@ public class RealmAdapter implements RealmModel {
         query.setParameter("email", email);
         query.setParameter("realm", realm);
         List<UserEntity> results = query.getResultList();
-        return results.isEmpty()? null : new UserAdapter(results.get(0));
+        return results.isEmpty() ? null : new UserAdapter(results.get(0));
     }
 
     @Override
@@ -504,6 +508,8 @@ public class RealmAdapter implements RealmModel {
     }
 
     private void removeUser(UserEntity user) {
+        removeUserSessions(user);
+
         em.createQuery("delete from " + UserRoleMappingEntity.class.getSimpleName() + " where user = :user").setParameter("user", user).executeUpdate();
         em.createQuery("delete from " + SocialLinkEntity.class.getSimpleName() + " where user = :user").setParameter("user", user).executeUpdate();
         if (user.getAuthenticationLink() != null) {
@@ -719,7 +725,7 @@ public class RealmAdapter implements RealmModel {
             em.remove(entity);
             em.flush();
             return true;
-        }  else {
+        } else {
             return false;
         }
     }
@@ -848,7 +854,7 @@ public class RealmAdapter implements RealmModel {
     public boolean removeOAuthClient(String id) {
         OAuthClientModel oauth = getOAuthClientById(id);
         if (oauth == null) return false;
-        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.remove(client);
         return true;
@@ -1001,7 +1007,7 @@ public class RealmAdapter implements RealmModel {
         }
         if (!role.getContainer().equals(this)) return false;
 
-        RoleEntity roleEntity = ((RoleAdapter)role).getRole();
+        RoleEntity roleEntity = ((RoleAdapter) role).getRole();
         realm.getRoles().remove(role);
         realm.getDefaultRoles().remove(role);
 
@@ -1030,10 +1036,10 @@ public class RealmAdapter implements RealmModel {
         RoleEntity entity = em.find(RoleEntity.class, id);
         if (entity == null) return null;
         if (entity instanceof RealmRoleEntity) {
-            RealmRoleEntity roleEntity = (RealmRoleEntity)entity;
+            RealmRoleEntity roleEntity = (RealmRoleEntity) entity;
             if (!roleEntity.getRealm().getId().equals(getId())) return null;
         } else {
-            ApplicationRoleEntity roleEntity = (ApplicationRoleEntity)entity;
+            ApplicationRoleEntity roleEntity = (ApplicationRoleEntity) entity;
             if (!roleEntity.getApplication().getRealm().getId().equals(getId())) return null;
         }
         return new RoleAdapter(this, em, entity);
@@ -1069,7 +1075,6 @@ public class RealmAdapter implements RealmModel {
     }
 
 
-
     protected TypedQuery<UserRoleMappingEntity> getUserRoleMappingEntityTypedQuery(UserAdapter user, RoleAdapter role) {
         TypedQuery<UserRoleMappingEntity> query = em.createNamedQuery("userHasRole", UserRoleMappingEntity.class);
         query.setParameter("user", user.getUser());
@@ -1082,7 +1087,7 @@ public class RealmAdapter implements RealmModel {
         if (hasRole(user, role)) return;
         UserRoleMappingEntity entity = new UserRoleMappingEntity();
         entity.setUser(((UserAdapter) user).getUser());
-        entity.setRole(((RoleAdapter)role).getRole());
+        entity.setRole(((RoleAdapter) role).getRole());
         em.persist(entity);
         em.flush();
     }
@@ -1095,7 +1100,7 @@ public class RealmAdapter implements RealmModel {
         for (RoleModel role : roleMappings) {
             RoleContainerModel container = role.getContainer();
             if (container instanceof RealmModel) {
-               realmRoles.add(role);
+                realmRoles.add(role);
             }
         }
         return realmRoles;
@@ -1105,7 +1110,7 @@ public class RealmAdapter implements RealmModel {
     @Override
     public Set<RoleModel> getRoleMappings(UserModel user) {
         TypedQuery<UserRoleMappingEntity> query = em.createNamedQuery("userRoleMappings", UserRoleMappingEntity.class);
-        query.setParameter("user", ((UserAdapter)user).getUser());
+        query.setParameter("user", ((UserAdapter) user).getUser());
         List<UserRoleMappingEntity> entities = query.getResultList();
         Set<RoleModel> roles = new HashSet<RoleModel>();
         for (UserRoleMappingEntity entity : entities) {
@@ -1135,7 +1140,7 @@ public class RealmAdapter implements RealmModel {
         for (RoleModel role : roleMappings) {
             RoleContainerModel container = role.getContainer();
             if (container instanceof RealmModel) {
-                if (((RealmModel)container).getId().equals(getId())) {
+                if (((RealmModel) container).getId().equals(getId())) {
                     appRoles.add(role);
                 }
             }
@@ -1148,7 +1153,7 @@ public class RealmAdapter implements RealmModel {
     @Override
     public Set<RoleModel> getScopeMappings(ClientModel client) {
         TypedQuery<ScopeMappingEntity> query = em.createNamedQuery("clientScopeMappings", ScopeMappingEntity.class);
-        query.setParameter("client", ((ClientAdapter)client).getEntity());
+        query.setParameter("client", ((ClientAdapter) client).getEntity());
         List<ScopeMappingEntity> entities = query.getResultList();
         Set<RoleModel> roles = new HashSet<RoleModel>();
         for (ScopeMappingEntity entity : entities) {
@@ -1162,7 +1167,7 @@ public class RealmAdapter implements RealmModel {
         if (hasScope(client, role)) return;
         ScopeMappingEntity entity = new ScopeMappingEntity();
         entity.setClient(((ClientAdapter) client).getEntity());
-        entity.setRole(((RoleAdapter)role).getRole());
+        entity.setRole(((RoleAdapter) role).getRole());
         em.persist(entity);
     }
 
@@ -1179,13 +1184,13 @@ public class RealmAdapter implements RealmModel {
     protected TypedQuery<ScopeMappingEntity> getRealmScopeMappingQuery(ClientAdapter client, RoleAdapter role) {
         TypedQuery<ScopeMappingEntity> query = em.createNamedQuery("hasScope", ScopeMappingEntity.class);
         query.setParameter("client", client.getEntity());
-        query.setParameter("role", ((RoleAdapter)role).getRole());
+        query.setParameter("role", ((RoleAdapter) role).getRole());
         return query;
     }
 
     @Override
     public boolean validatePassword(UserModel user, String password) {
-        for (CredentialEntity cred : ((UserAdapter)user).getUser().getCredentials()) {
+        for (CredentialEntity cred : ((UserAdapter) user).getUser().getCredentials()) {
             if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
                 return new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue());
             }
@@ -1196,7 +1201,7 @@ public class RealmAdapter implements RealmModel {
     @Override
     public boolean validateTOTP(UserModel user, String password, String token) {
         if (!validatePassword(user, password)) return false;
-        for (CredentialEntity cred : ((UserAdapter)user).getUser().getCredentials()) {
+        for (CredentialEntity cred : ((UserAdapter) user).getUser().getCredentials()) {
             if (cred.getType().equals(UserCredentialModel.TOTP)) {
                 return new TimeBasedOTP().validate(token, cred.getValue().getBytes());
             }
@@ -1297,7 +1302,7 @@ public class RealmAdapter implements RealmModel {
     public boolean equals(Object o) {
         if (o == null) return false;
         if (!(o instanceof RealmAdapter)) return false;
-        RealmAdapter r = (RealmAdapter)o;
+        RealmAdapter r = (RealmAdapter) o;
         return r.getId().equals(getId());
     }
 
@@ -1367,4 +1372,45 @@ public class RealmAdapter implements RealmModel {
         em.flush();
     }
 
+    @Override
+    public UserSessionModel createUserSession(UserModel user, String ipAddress) {
+        UserSessionEntity entity = new UserSessionEntity();
+        entity.setUser(((UserAdapter) user).getUser());
+        entity.setIpAddress(ipAddress);
+
+        int currentTime = Time.currentTime();
+        int expires = currentTime + realm.getCentralLoginLifespan();
+
+        entity.setStarted(currentTime);
+        entity.setExpires(expires);
+
+        em.persist(entity);
+        return new UserSessionAdapter(entity);
+    }
+
+    @Override
+    public UserSessionModel getUserSession(String id) {
+        UserSessionEntity entity = em.find(UserSessionEntity.class, id);
+        return entity != null ? new UserSessionAdapter(entity) : null;
+    }
+
+    @Override
+    public void removeUserSession(UserSessionModel session) {
+        em.remove(((UserSessionAdapter) session).getEntity());
+    }
+
+    @Override
+    public void removeUserSessions(UserModel user) {
+        removeUserSessions(((UserAdapter) user).getUser());
+    }
+
+    private void removeUserSessions(UserEntity user) {
+        em.createNamedQuery("removeUserSessionByUser").setParameter("user", user).executeUpdate();
+    }
+
+    @Override
+    public void removeExpiredUserSessions() {
+        em.createNamedQuery("removeUserSessionExpired").setParameter("currentTime", Time.currentTime()).executeUpdate();
+    }
+
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserSessionAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserSessionAdapter.java
new file mode 100644
index 0000000..1f4ba28
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserSessionAdapter.java
@@ -0,0 +1,72 @@
+package org.keycloak.models.jpa;
+
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.jpa.entities.UserSessionEntity;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class UserSessionAdapter implements UserSessionModel {
+
+    private UserSessionEntity entity;
+
+    public UserSessionAdapter(UserSessionEntity entity) {
+        this.entity = entity;
+    }
+
+    public UserSessionEntity getEntity() {
+        return entity;
+    }
+
+    @Override
+    public String getId() {
+        return entity.getId();
+    }
+
+    @Override
+    public void setId(String id) {
+        entity.setId(id);
+    }
+
+    @Override
+    public UserModel getUser() {
+        return new UserAdapter(entity.getUser());
+    }
+
+    @Override
+    public void setUser(UserModel user) {
+        entity.setUser(((UserAdapter) user).getUser());
+    }
+
+    @Override
+    public String getIpAddress() {
+        return entity.getIpAddress();
+    }
+
+    @Override
+    public void setIpAddress(String ipAddress) {
+        entity.setIpAddress(ipAddress);
+    }
+
+    @Override
+    public int getStarted() {
+        return entity.getStarted();
+    }
+
+    @Override
+    public void setStarted(int started) {
+        entity.setStarted(started);
+    }
+
+    @Override
+    public int getExpires() {
+        return entity.getExpires();
+    }
+
+    @Override
+    public void setExpires(int expires) {
+        entity.setExpires(expires);
+    }
+
+}
diff --git a/model/jpa/src/test/resources/META-INF/persistence.xml b/model/jpa/src/test/resources/META-INF/persistence.xml
index 558aa89..900cb5d 100755
--- a/model/jpa/src/test/resources/META-INF/persistence.xml
+++ b/model/jpa/src/test/resources/META-INF/persistence.xml
@@ -16,6 +16,7 @@
         <class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
         <class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
         <class>org.keycloak.models.jpa.entities.UserEntity</class>
+        <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
         <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
         <class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
         <class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java
index d431803..6feb373 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java
@@ -16,6 +16,7 @@ import org.keycloak.models.mongo.keycloak.entities.MongoOAuthClientEntity;
 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.MongoUsernameLoginFailureEntity;
 
 /**
@@ -37,7 +38,8 @@ public class MongoKeycloakSessionFactory implements KeycloakSessionFactory {
             AuthenticationLinkEntity.class,
             MongoApplicationEntity.class,
             MongoOAuthClientEntity.class,
-            MongoUsernameLoginFailureEntity.class
+            MongoUsernameLoginFailureEntity.class,
+            MongoUserSessionEntity.class
     };
 
     private final MongoClientProvider mongoClientProvider;
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index 52308de..82f95c0 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -1,5 +1,6 @@
 package org.keycloak.models.mongo.keycloak.adapters;
 
+import com.mongodb.BasicDBObject;
 import com.mongodb.DBObject;
 import com.mongodb.QueryBuilder;
 import org.jboss.logging.Logger;
@@ -16,6 +17,7 @@ import org.keycloak.models.SocialLinkModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserCredentialValueModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.UsernameLoginFailureModel;
 import org.keycloak.models.entities.AuthenticationLinkEntity;
 import org.keycloak.models.entities.AuthenticationProviderEntity;
@@ -28,11 +30,13 @@ import org.keycloak.models.mongo.keycloak.entities.MongoOAuthClientEntity;
 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.MongoUsernameLoginFailureEntity;
 import org.keycloak.models.mongo.utils.MongoModelUtils;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
 import org.keycloak.models.utils.TimeBasedOTP;
+import org.keycloak.util.Time;
 
 import java.security.PrivateKey;
 import java.security.PublicKey;
@@ -1333,4 +1337,51 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
     public MongoRealmEntity getMongoEntity() {
         return realm;
     }
+
+    @Override
+    public UserSessionModel createUserSession(UserModel user, String ipAddress) {
+        MongoUserSessionEntity entity = new MongoUserSessionEntity();
+        entity.setUser(user.getId());
+        entity.setIpAddress(ipAddress);
+
+        int currentTime = Time.currentTime();
+        int expires = currentTime + realm.getCentralLoginLifespan();
+
+        entity.setStarted(currentTime);
+        entity.setExpires(expires);
+
+        getMongoStore().insertEntity(entity, invocationContext);
+        return new UserSessionAdapter(entity, this, invocationContext);
+    }
+
+    @Override
+    public UserSessionModel getUserSession(String id) {
+        MongoUserSessionEntity entity = getMongoStore().loadEntity(MongoUserSessionEntity.class, id, invocationContext);
+        if (entity == null) {
+            return null;
+        } else {
+            return new UserSessionAdapter(entity, this, invocationContext);
+        }
+    }
+
+    @Override
+    public void removeUserSession(UserSessionModel session) {
+        getMongoStore().removeEntity(((UserSessionAdapter) session).getEntity(), invocationContext);
+    }
+
+    @Override
+    public void removeUserSessions(UserModel user) {
+        DBObject query = new BasicDBObject("user", user.getId());
+        getMongoStore().removeEntities(MongoUserSessionEntity.class, query, invocationContext);
+    }
+
+    @Override
+    public void removeExpiredUserSessions() {
+        DBObject query = new QueryBuilder()
+                .and("expires").lessThan(Time.currentTime())
+                .get();
+
+        getMongoStore().removeEntities(MongoUserSessionEntity.class, query, invocationContext);
+    }
+
 }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java
new file mode 100644
index 0000000..ab02e1c
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java
@@ -0,0 +1,77 @@
+package org.keycloak.models.mongo.keycloak.adapters;
+
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.mongo.keycloak.entities.MongoUserSessionEntity;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class UserSessionAdapter implements UserSessionModel {
+
+    private MongoUserSessionEntity entity;
+    private RealmAdapter realm;
+    private MongoStoreInvocationContext invContext;
+
+    public UserSessionAdapter(MongoUserSessionEntity entity, RealmAdapter realm, MongoStoreInvocationContext invContext) {
+        this.entity = entity;
+        this.realm = realm;
+        this.invContext = invContext;
+    }
+
+    public MongoUserSessionEntity getEntity() {
+        return entity;
+    }
+
+    @Override
+    public String getId() {
+        return entity.getId();
+    }
+
+    @Override
+    public void setId(String id) {
+        entity.setId(id);
+    }
+
+    @Override
+    public UserModel getUser() {
+        return realm.getUserById(entity.getUser());
+    }
+
+    @Override
+    public void setUser(UserModel user) {
+        entity.setUser(user.getId());
+    }
+
+    @Override
+    public String getIpAddress() {
+        return entity.getIpAddress();
+    }
+
+    @Override
+    public void setIpAddress(String ipAddress) {
+        entity.setIpAddress(ipAddress);
+    }
+
+    @Override
+    public int getStarted() {
+        return entity.getStarted();
+    }
+
+    @Override
+    public void setStarted(int started) {
+        entity.setStarted(started);
+    }
+
+    @Override
+    public int getExpires() {
+        return entity.getExpires();
+    }
+
+    @Override
+    public void setExpires(int expires) {
+        entity.setExpires(expires);
+    }
+
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserEntity.java
index e9dd851..084a153 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserEntity.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserEntity.java
@@ -1,5 +1,7 @@
 package org.keycloak.models.mongo.keycloak.entities;
 
+import com.mongodb.DBObject;
+import com.mongodb.QueryBuilder;
 import org.keycloak.models.entities.UserEntity;
 import org.keycloak.models.mongo.api.MongoCollection;
 import org.keycloak.models.mongo.api.MongoIdentifiableEntity;
@@ -27,6 +29,10 @@ public class MongoUserEntity extends UserEntity implements MongoIdentifiableEnti
 
     @Override
     public void afterRemove(MongoStoreInvocationContext invocationContext) {
-        //To change body of implemented methods use File | Settings | File Templates.
+        DBObject query = new QueryBuilder()
+                .and("userId").is(getId())
+                .get();
+
+        invocationContext.getMongoStore().removeEntities(MongoUserSessionEntity.class, query, invocationContext);
     }
 }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java
new file mode 100644
index 0000000..de2a6d0
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java
@@ -0,0 +1,58 @@
+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:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@MongoCollection(collectionName = "sessions")
+public class MongoUserSessionEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity {
+
+    private String user;
+
+    private String ipAddress;
+
+    private int started;
+
+    private int expires;
+
+    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 int getStarted() {
+        return started;
+    }
+
+    public void setStarted(int started) {
+        this.started = started;
+    }
+
+    public int getExpires() {
+        return expires;
+    }
+
+    public void setExpires(int expires) {
+        this.expires = expires;
+    }
+
+    @Override
+    public void afterRemove(MongoStoreInvocationContext invocationContext) {
+    }
+
+}
diff --git a/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java b/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java
index 4270d29..b15de5d 100755
--- a/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java
+++ b/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java
@@ -14,6 +14,7 @@ import org.keycloak.models.RoleModel;
 import org.keycloak.models.SocialLinkModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.services.managers.OAuthClientManager;
 import org.keycloak.services.managers.RealmManager;
@@ -24,6 +25,9 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
@@ -46,7 +50,7 @@ public class AdapterTest extends AbstractModelTest {
         realmModel.addDefaultRole("foo");
 
         realmModel = realmManager.getRealm(realmModel.getId());
-        Assert.assertNotNull(realmModel);
+        assertNotNull(realmModel);
         Assert.assertEquals(realmModel.getAccessCodeLifespan(), 100);
         Assert.assertEquals(600, realmModel.getAccessCodeLifespanUserAction());
         Assert.assertEquals(realmModel.getAccessTokenLifespan(), 1000);
@@ -73,7 +77,7 @@ public class AdapterTest extends AbstractModelTest {
         realmModel.addDefaultRole("foo");
 
         realmModel = realmManager.getRealm(realmModel.getId());
-        Assert.assertNotNull(realmModel);
+        assertNotNull(realmModel);
         Assert.assertEquals(realmModel.getAccessCodeLifespan(), 100);
         Assert.assertEquals(600, realmModel.getAccessCodeLifespanUserAction());
         Assert.assertEquals(realmModel.getAccessTokenLifespan(), 1000);
@@ -169,7 +173,7 @@ public class AdapterTest extends AbstractModelTest {
         realmModel = identitySession.getRealm("JUGGLER");
         Assert.assertTrue(realmModel.removeUser("bburke"));
         Assert.assertFalse(realmModel.removeUser("bburke"));
-        Assert.assertNull(realmModel.getUser("bburke"));
+        assertNull(realmModel.getUser("bburke"));
     }
 
     @Test
@@ -191,7 +195,7 @@ public class AdapterTest extends AbstractModelTest {
 
         Assert.assertTrue(realmModel.removeApplication(app.getId()));
         Assert.assertFalse(realmModel.removeApplication(app.getId()));
-        Assert.assertNull(realmModel.getApplicationById(app.getId()));
+        assertNull(realmModel.getApplicationById(app.getId()));
     }
 
 
@@ -226,7 +230,7 @@ public class AdapterTest extends AbstractModelTest {
 
         Assert.assertTrue(realmManager.removeRealm(realmModel));
         Assert.assertFalse(realmManager.removeRealm(realmModel));
-        Assert.assertNull(realmManager.getRealm(realmModel.getId()));
+        assertNull(realmManager.getRealm(realmModel.getId()));
     }
 
 
@@ -253,11 +257,11 @@ public class AdapterTest extends AbstractModelTest {
 
         Assert.assertTrue(realmModel.removeRoleById(realmRole.getId()));
         Assert.assertFalse(realmModel.removeRoleById(realmRole.getId()));
-        Assert.assertNull(realmModel.getRole(realmRole.getName()));
+        assertNull(realmModel.getRole(realmRole.getName()));
 
         Assert.assertTrue(realmModel.removeRoleById(appRole.getId()));
         Assert.assertFalse(realmModel.removeRoleById(appRole.getId()));
-        Assert.assertNull(app.getRole(appRole.getName()));
+        assertNull(app.getRole(appRole.getName()));
     }
 
     @Test
@@ -435,7 +439,7 @@ public class AdapterTest extends AbstractModelTest {
         realmModel.grantRole(user, realmUserRole);
         Assert.assertTrue(realmModel.hasRole(user, realmUserRole));
         RoleModel found = realmModel.getRoleById(realmUserRole.getId());
-        Assert.assertNotNull(found);
+        assertNotNull(found);
         assertRolesEquals(found, realmUserRole);
 
         // Test app roles
@@ -445,10 +449,10 @@ public class AdapterTest extends AbstractModelTest {
         Set<RoleModel> appRoles = application.getRoles();
         Assert.assertEquals(2, appRoles.size());
         RoleModel appBarRole = application.getRole("bar");
-        Assert.assertNotNull(appBarRole);
+        assertNotNull(appBarRole);
 
         found = realmModel.getRoleById(appBarRole.getId());
-        Assert.assertNotNull(found);
+        assertNotNull(found);
         assertRolesEquals(found, appBarRole);
 
         realmModel.grantRole(user, appBarRole);
@@ -719,4 +723,43 @@ public class AdapterTest extends AbstractModelTest {
         resetSession();
     }
 
+    @Test
+    public void userSessions() throws InterruptedException {
+        realmManager.createRealm("userSessions");
+        realmManager.getRealmByName("userSessions").setCentralLoginLifespan(5);
+
+        UserModel user = realmManager.getRealmByName("userSessions").addUser("userSessions1");
+
+        UserSessionModel userSession = realmManager.getRealmByName("userSessions").createUserSession(user, "127.0.0.1");
+        commit();
+
+        assertNotNull(realmManager.getRealmByName("userSessions").getUserSession(userSession.getId()));
+        commit();
+
+        realmManager.getRealmByName("userSessions").removeUserSession(realmManager.getRealmByName("userSessions").getUserSession(userSession.getId()));
+        commit();
+
+        assertNull(realmManager.getRealmByName("userSessions").getUserSession(userSession.getId()));
+
+        userSession = realmManager.getRealmByName("userSessions").createUserSession(user, "127.0.0.1");
+        commit();
+
+        realmManager.getRealmByName("userSessions").removeUserSessions(user);
+        commit();
+
+        assertNull(realmManager.getRealmByName("userSessions").getUserSession(userSession.getId()));
+
+        realmManager.getRealmByName("userSessions").setCentralLoginLifespan(1);
+
+        userSession = realmManager.getRealmByName("userSessions").createUserSession(user, "127.0.0.1");
+        commit();
+
+        Thread.sleep(2000);
+
+        realmManager.getRealmByName("userSessions").removeExpiredUserSessions();
+        commit();
+
+        assertNull(realmManager.getRealmByName("userSessions").getUserSession(userSession.getId()));
+    }
+
 }
diff --git a/project-integrations/aerogear-ups/auth-server/src/main/resources/META-INF/persistence.xml b/project-integrations/aerogear-ups/auth-server/src/main/resources/META-INF/persistence.xml
index 3eeed1f..616f8e8 100755
--- a/project-integrations/aerogear-ups/auth-server/src/main/resources/META-INF/persistence.xml
+++ b/project-integrations/aerogear-ups/auth-server/src/main/resources/META-INF/persistence.xml
@@ -15,6 +15,7 @@
         <class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
         <class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
         <class>org.keycloak.models.jpa.entities.UserEntity</class>
+        <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
         <class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
         <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
         <class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
diff --git a/server/src/main/resources/META-INF/persistence.xml b/server/src/main/resources/META-INF/persistence.xml
index 3eeed1f..616f8e8 100755
--- a/server/src/main/resources/META-INF/persistence.xml
+++ b/server/src/main/resources/META-INF/persistence.xml
@@ -15,6 +15,7 @@
         <class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
         <class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
         <class>org.keycloak.models.jpa.entities.UserEntity</class>
+        <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
         <class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
         <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
         <class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
diff --git a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java b/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java
index abbc793..93f9f6c 100755
--- a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java
+++ b/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java
@@ -6,6 +6,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserModel.RequiredAction;
+import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.util.Time;
 
@@ -23,6 +24,7 @@ public class AccessCodeEntry {
     protected String id = UUID.randomUUID().toString() + System.currentTimeMillis();
     protected String code;
     protected String state;
+    protected String sessionState;
     protected String redirectUri;
     protected boolean rememberMe;
     protected String authMethod;
@@ -117,6 +119,14 @@ public class AccessCodeEntry {
         this.state = state;
     }
 
+    public String getSessionState() {
+        return sessionState;
+    }
+
+    public void setSessionState(String sessionState) {
+        this.sessionState = sessionState;
+    }
+
     public String getRedirectUri() {
         return redirectUri;
     }
diff --git a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
index 0a3c305..41586d6 100755
--- a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
@@ -31,9 +31,12 @@ public class AppAuthManager extends AuthenticationManager {
     }
 
     public UserModel authenticateRequest(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
-        UserModel user = authenticateIdentityCookie(realm, uriInfo, headers);
-        if (user != null) return user;
-        return authenticateBearerToken(realm, uriInfo, headers);
+        AuthResult authResult = authenticateIdentityCookie(realm, uriInfo, headers);
+        if (authResult != null) {
+            return authResult.getUser();
+        } else {
+            return authenticateBearerToken(realm, uriInfo, headers);
+        }
     }
 
     public String extractAuthorizationHeaderToken(HttpHeaders headers) {
@@ -51,7 +54,8 @@ public class AppAuthManager extends AuthenticationManager {
     public UserModel authenticateBearerToken(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
         String tokenString = extractAuthorizationHeaderToken(headers);
         if (tokenString == null) return null;
-        return verifyIdentityToken(realm, uriInfo, true, tokenString);
+        AuthResult authResult = verifyIdentityToken(realm, uriInfo, true, tokenString);
+        return authResult != null ? authResult.getUser() : null;
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 2fb333a..c8df10c 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -11,6 +11,7 @@ import org.keycloak.models.ClientModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RequiredCredentialModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.provider.ProviderSession;
 import org.keycloak.representations.AccessToken;
@@ -55,28 +56,31 @@ public class AuthenticationManager {
         this.protector = protector;
     }
 
-    public AccessToken createIdentityToken(RealmModel realm, UserModel user) {
+    public AccessToken createIdentityToken(RealmModel realm, UserModel user, UserSessionModel session) {
         logger.info("createIdentityToken");
         AccessToken token = new AccessToken();
         token.id(KeycloakModelUtils.generateId());
         token.issuedNow();
         token.subject(user.getId());
         token.audience(realm.getName());
+        if (session != null) {
+            token.setSessionState(session.getId());
+        }
         if (realm.getCentralLoginLifespan() > 0) {
             token.expiration(Time.currentTime() + realm.getCentralLoginLifespan());
         }
         return token;
     }
 
-    public NewCookie createLoginCookie(RealmModel realm, UserModel user, UriInfo uriInfo, boolean rememberMe) {
+    public NewCookie createLoginCookie(RealmModel realm, UserModel user, UserSessionModel session, UriInfo uriInfo, boolean rememberMe) {
         logger.info("createLoginCookie");
         String cookieName = KEYCLOAK_IDENTITY_COOKIE;
         String cookiePath = getIdentityCookiePath(realm, uriInfo);
-        return createLoginCookie(realm, user, null, cookieName, cookiePath, rememberMe);
+        return createLoginCookie(realm, user, session, null, cookieName, cookiePath, rememberMe);
     }
 
-    protected NewCookie createLoginCookie(RealmModel realm, UserModel user, ClientModel client, String cookieName, String cookiePath, boolean rememberMe) {
-        AccessToken identityToken = createIdentityToken(realm, user);
+    protected NewCookie createLoginCookie(RealmModel realm, UserModel user, UserSessionModel session, ClientModel client, String cookieName, String cookiePath, boolean rememberMe) {
+        AccessToken identityToken = createIdentityToken(realm, user, session);
         if (client != null) {
             identityToken.issuedFor(client.getClientId());
         }
@@ -136,17 +140,17 @@ public class AuthenticationManager {
         response.addNewCookie(expireIt);
     }
 
-    public UserModel authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
+    public AuthResult authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
         return authenticateIdentityCookie(realm, uriInfo, headers, true);
     }
 
-    public UserModel authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, boolean checkActive) {
+    public AuthResult authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, boolean checkActive) {
         logger.info("authenticateIdentityCookie");
         String cookieName = KEYCLOAK_IDENTITY_COOKIE;
         return authenticateIdentityCookie(realm, uriInfo, headers, cookieName, checkActive);
     }
 
-    protected UserModel authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, String cookieName, boolean checkActive) {
+    private AuthResult authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, String cookieName, boolean checkActive) {
         logger.info("authenticateIdentityCookie");
         Cookie cookie = headers.getCookies().get(cookieName);
         if (cookie == null) {
@@ -155,14 +159,14 @@ public class AuthenticationManager {
         }
 
         String tokenString = cookie.getValue();
-        UserModel user = verifyIdentityToken(realm, uriInfo, checkActive, tokenString);
-        if (user == null) {
+        AuthResult authResult = verifyIdentityToken(realm, uriInfo, checkActive, tokenString);
+        if (authResult == null) {
             expireIdentityCookie(realm, uriInfo);
         }
-        return user;
+        return authResult;
     }
 
-    protected UserModel verifyIdentityToken(RealmModel realm, UriInfo uriInfo, boolean checkActive, String tokenString) {
+    protected AuthResult verifyIdentityToken(RealmModel realm, UriInfo uriInfo, boolean checkActive, String tokenString) {
         try {
             AccessToken token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), realm.getName(), checkActive);
             logger.info("identity token verified");
@@ -188,10 +192,16 @@ public class AuthenticationManager {
             if (token.getIssuedAt() < user.getNotBefore()) {
                 logger.info("Stale cookie");
                 return null;
+            }
 
+            UserSessionModel session = realm.getUserSession(token.getSessionState());
+            if (session == null || session.getExpires() < Time.currentTime()) {
+                logger.info("User session not active");
+                expireIdentityCookie(realm, uriInfo);
+                return null;
             }
 
-            return user;
+            return new AuthResult(user, session);
         } catch (VerificationException e) {
             logger.info("Failed to verify identity token", e);
         }
@@ -328,4 +338,22 @@ public class AuthenticationManager {
         SUCCESS, ACCOUNT_TEMPORARILY_DISABLED, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED
     }
 
+    public class AuthResult {
+        private final UserModel user;
+        private final UserSessionModel session;
+
+        public AuthResult(UserModel user, UserSessionModel session) {
+            this.user = user;
+            this.session = session;
+        }
+
+        public UserSessionModel getSession() {
+            return session;
+        }
+
+        public UserModel getUser() {
+            return user;
+        }
+    }
+
 }
diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
index 48600fd..ce06542 100755
--- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
@@ -1,10 +1,10 @@
 package org.keycloak.services.managers;
 
 import org.apache.http.client.HttpClient;
+import org.jboss.logging.Logger;
 import org.jboss.resteasy.client.ClientRequest;
 import org.jboss.resteasy.client.ClientResponse;
 import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor;
-import org.jboss.logging.Logger;
 import org.keycloak.TokenIdGenerator;
 import org.keycloak.adapters.AdapterConstants;
 import org.keycloak.models.ApplicationModel;
@@ -146,7 +146,7 @@ public class ResourceAdminManager {
 
     }
 
-    public void logoutUser(URI requestUri, RealmModel realm, UserModel user) {
+    public void logoutUser(URI requestUri, RealmModel realm, String user, String session) {
         ApacheHttpClient4Executor executor = createExecutor();
 
         try {
@@ -154,7 +154,7 @@ public class ResourceAdminManager {
             List<ApplicationModel> resources = realm.getApplications();
             logger.debugv("logging out {0} resources ", resources.size());
             for (ApplicationModel resource : resources) {
-                logoutApplication(requestUri, realm, resource, user.getId(), executor, 0);
+                logoutApplication(requestUri, realm, resource, user, session, executor, 0);
             }
         } finally {
             executor.getHttpClient().getConnectionManager().shutdown();
@@ -168,19 +168,19 @@ public class ResourceAdminManager {
             List<ApplicationModel> resources = realm.getApplications();
             logger.debugv("logging out {0} resources ", resources.size());
             for (ApplicationModel resource : resources) {
-                logoutApplication(requestUri, realm, resource, null, executor, realm.getNotBefore());
+                logoutApplication(requestUri, realm, resource, null, null, executor, realm.getNotBefore());
             }
         } finally {
             executor.getHttpClient().getConnectionManager().shutdown();
         }
     }
 
-    public void logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, String user) {
+    public void logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, String user, String session) {
         ApacheHttpClient4Executor executor = createExecutor();
 
         try {
             resource.setNotBefore(Time.currentTime());
-            logoutApplication(requestUri, realm, resource, user, executor, resource.getNotBefore());
+            logoutApplication(requestUri, realm, resource, user, session, executor, resource.getNotBefore());
         } finally {
             executor.getHttpClient().getConnectionManager().shutdown();
         }
@@ -188,10 +188,10 @@ public class ResourceAdminManager {
     }
 
 
-    protected boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, String user, ApacheHttpClient4Executor client, int notBefore) {
+    protected boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, String user, String session, ApacheHttpClient4Executor client, int notBefore) {
         String managementUrl = getManagementUrl(requestUri, resource);
         if (managementUrl != null) {
-            LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), user, notBefore);
+            LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), user, session, notBefore);
             String token = new TokenManager().encodeToken(realm, adminAction);
             logger.infov("logout user: {0} resource: {1} url: {2}", user, resource.getName(), managementUrl);
             ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_LOGOUT).build().toString());
@@ -209,7 +209,7 @@ public class ResourceAdminManager {
                 response.releaseConnection();
             }
         } else {
-            logger.info("Can't logout" + resource.getName() + " no mgmt url.");
+            logger.info("Can't logout " + resource.getName() + " no mgmt url.");
             return false;
         }
     }
diff --git a/services/src/main/java/org/keycloak/services/managers/TokenManager.java b/services/src/main/java/org/keycloak/services/managers/TokenManager.java
index 0816c0e..4995e81 100755
--- a/services/src/main/java/org/keycloak/services/managers/TokenManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/TokenManager.java
@@ -14,6 +14,7 @@ import org.keycloak.models.ClientModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.AccessTokenResponse;
@@ -70,18 +71,23 @@ public class TokenManager {
 
 
 
-    public AccessCodeEntry createAccessCode(String scopeParam, String state, String redirect, RealmModel realm, ClientModel client, UserModel user) {
-        AccessCodeEntry code = createAccessCodeEntry(scopeParam, state, redirect, realm, client, user);
+    public AccessCodeEntry createAccessCode(String scopeParam, String state, String redirect, RealmModel realm, ClientModel client, UserModel user, UserSessionModel session) {
+        AccessCodeEntry code = createAccessCodeEntry(scopeParam, state, redirect, realm, client, user, session);
         accessCodeMap.put(code.getId(), code);
         return code;
     }
 
-    private AccessCodeEntry createAccessCodeEntry(String scopeParam, String state, String redirect, RealmModel realm, ClientModel client, UserModel user) {
+    private AccessCodeEntry createAccessCodeEntry(String scopeParam, String state, String redirect, RealmModel realm, ClientModel client, UserModel user, UserSessionModel session) {
         AccessCodeEntry code = new AccessCodeEntry();
+        if (session != null) {
+            code.setSessionState(session.getId());
+        }
+
         List<RoleModel> realmRolesRequested = code.getRealmRolesRequested();
         MultivaluedMap<String, RoleModel> resourceRolesRequested = code.getResourceRolesRequested();
 
-        AccessToken token = createClientAccessToken(scopeParam, realm, client, user, realmRolesRequested, resourceRolesRequested);
+        AccessToken token = createClientAccessToken(scopeParam, realm, client, user, session, realmRolesRequested, resourceRolesRequested);
+        token.setSessionState(code.getSessionState());
 
         code.setToken(token);
         code.setRealm(realm);
@@ -119,7 +125,7 @@ public class TokenManager {
             throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token");
         }
 
-        audit.user(refreshToken.getSubject()).detail(Details.REFRESH_TOKEN_ID, refreshToken.getId());
+        audit.user(refreshToken.getSubject()).session(refreshToken.getSessionState()).detail(Details.REFRESH_TOKEN_ID, refreshToken.getId());
 
         UserModel user = realm.getUserById(refreshToken.getSubject());
         if (user == null) {
@@ -128,12 +134,15 @@ public class TokenManager {
 
         if (!user.isEnabled()) {
             throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled");
+        }
 
+        UserSessionModel session = realm.getUserSession(refreshToken.getSessionState());
+        if (session == null || session.getExpires() < Time.currentTime()) {
+            throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active");
         }
 
         if (!client.getClientId().equals(refreshToken.getIssuedFor())) {
             throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Unmatching clients", "Unmatching clients");
-
         }
 
         if (refreshToken.getIssuedAt() < client.getNotBefore() || refreshToken.getIssuedAt() < user.getNotBefore()) {
@@ -179,17 +188,17 @@ public class TokenManager {
             }
         }
 
-        AccessToken accessToken = initToken(realm, client, user);
+        AccessToken accessToken = initToken(realm, client, user, session);
         accessToken.setRealmAccess(refreshToken.getRealmAccess());
         accessToken.setResourceAccess(refreshToken.getResourceAccess());
         return accessToken;
     }
 
-    public AccessToken createClientAccessToken(String scopeParam, RealmModel realm, ClientModel client, UserModel user) {
-        return createClientAccessToken(scopeParam, realm, client, user, new LinkedList<RoleModel>(), new MultivaluedMapImpl<String, RoleModel>());
+    public AccessToken createClientAccessToken(String scopeParam, RealmModel realm, ClientModel client, UserModel user, UserSessionModel session) {
+        return createClientAccessToken(scopeParam, realm, client, user, session, new LinkedList<RoleModel>(), new MultivaluedMapImpl<String, RoleModel>());
     }
 
-    public AccessToken createClientAccessToken(String scopeParam, RealmModel realm, ClientModel client, UserModel user, List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested) {
+    public AccessToken createClientAccessToken(String scopeParam, RealmModel realm, ClientModel client, UserModel user, UserSessionModel session, List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested) {
         // todo scopeParam is ignored until we figure out a scheme that fits with openid connect
 
         Set<RoleModel> roleMappings = realm.getRoleMappings(user);
@@ -217,7 +226,7 @@ public class TokenManager {
             }
         }
 
-        AccessToken token = initToken(realm, client, user);
+        AccessToken token = initToken(realm, client, user, session);
 
         if (realmRolesRequested.size() > 0) {
             for (RoleModel role : realmRolesRequested) {
@@ -270,7 +279,7 @@ public class TokenManager {
 
 
 
-    protected AccessToken initToken(RealmModel realm, ClientModel client, UserModel user) {
+    protected AccessToken initToken(RealmModel realm, ClientModel client, UserModel user, UserSessionModel session) {
         AccessToken token = new AccessToken();
         token.id(KeycloakModelUtils.generateId());
         token.subject(user.getId());
@@ -278,6 +287,9 @@ public class TokenManager {
         token.issuedNow();
         token.issuedFor(client.getClientId());
         token.issuer(realm.getName());
+        if (session != null) {
+            token.setSessionState(session.getId());
+        }
         if (realm.getAccessTokenLifespan() > 0) {
             token.expiration(Time.currentTime() + realm.getAccessTokenLifespan());
         }
@@ -351,8 +363,8 @@ public class TokenManager {
             return this;
         }
 
-        public AccessTokenResponseBuilder generateAccessToken(String scopeParam, ClientModel client, UserModel user) {
-            accessToken = createClientAccessToken(scopeParam, realm, client, user);
+        public AccessTokenResponseBuilder generateAccessToken(String scopeParam, ClientModel client, UserModel user, UserSessionModel session) {
+            accessToken = createClientAccessToken(scopeParam, realm, client, user, session);
             return this;
         }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java
index fcf2e0d..d1c3706 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java
@@ -234,7 +234,7 @@ public class ApplicationResource {
     @POST
     public void logoutAll() {
         auth.requireManage();
-        new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, null);
+        new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, null, null);
     }
 
     @Path("logout-user/{username}")
@@ -245,7 +245,7 @@ public class ApplicationResource {
         if (user == null) {
             throw new NotFoundException("User not found");
         }
-        new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, user.getId());
+        new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, user.getId(), null);
     }
 
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index 10ce72d..56f5e3b 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -210,7 +210,7 @@ public class UsersResource {
         }
         // set notBefore so that user will be forced to log in.
         user.setNotBefore(Time.currentTime());
-        new ResourceAdminManager().logoutUser(uriInfo.getRequestUri(), realm, user);
+        new ResourceAdminManager().logoutUser(uriInfo.getRequestUri(), realm, user.getId(), null);
     }
 
 
@@ -540,7 +540,7 @@ public class UsersResource {
         Set<UserModel.RequiredAction> requiredActions = new HashSet<UserModel.RequiredAction>(user.getRequiredActions());
         requiredActions.add(UserModel.RequiredAction.UPDATE_PASSWORD);
 
-        AccessCodeEntry accessCode = tokenManager.createAccessCode(scope, state, redirect, realm, client, user);
+        AccessCodeEntry accessCode = tokenManager.createAccessCode(scope, state, redirect, realm, client, user, null);
         accessCode.setRequiredActions(requiredActions);
         accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction());
 
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
index 569ff93..a5c0abe 100755
--- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
@@ -34,6 +34,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RequiredCredentialModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserModel.RequiredAction;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.services.managers.AccessCodeEntry;
 import org.keycloak.services.managers.AuthenticationManager;
@@ -74,12 +75,12 @@ public class OAuthFlows {
         this.tokenManager = tokenManager;
     }
 
-    public Response redirectAccessCode(AccessCodeEntry accessCode, String state, String redirect) {
-        return redirectAccessCode(accessCode, state, redirect, false);
+    public Response redirectAccessCode(AccessCodeEntry accessCode, UserSessionModel session, String state, String redirect) {
+        return redirectAccessCode(accessCode, session, state, redirect, false);
     }
 
 
-    public Response redirectAccessCode(AccessCodeEntry accessCode, String state, String redirect, boolean rememberMe) {
+    public Response redirectAccessCode(AccessCodeEntry accessCode, UserSessionModel session, String state, String redirect, boolean rememberMe) {
         String code = accessCode.getCode();
 
         if (Constants.INSTALLED_APP_URN.equals(redirect)) {
@@ -92,7 +93,7 @@ public class OAuthFlows {
             Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
             Cookie remember = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME);
             rememberMe = rememberMe || remember != null;
-            location.cookie(authManager.createLoginCookie(realm, accessCode.getUser(), uriInfo, rememberMe));
+            location.cookie(authManager.createLoginCookie(realm, accessCode.getUser(), session, uriInfo, rememberMe));
             return location.build();
         }
     }
@@ -109,17 +110,12 @@ public class OAuthFlows {
         }
     }
 
-    public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, Audit audit) {
-        return processAccessCode(scopeParam, state, redirect, client, user, null, false, "form", audit);
-    }
-
-
-    public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, String username, boolean rememberMe, String authMethod, Audit audit) {
+    public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, UserSessionModel session, String username, boolean rememberMe, String authMethod, Audit audit) {
         isTotpConfigurationRequired(user);
         isEmailVerificationRequired(user);
 
         boolean isResource = client instanceof ApplicationModel;
-        AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user);
+        AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user, session);
         accessCode.setUsername(username);
         accessCode.setRememberMe(rememberMe);
         accessCode.setAuthMethod(authMethod);
@@ -155,7 +151,7 @@ public class OAuthFlows {
 
         if (redirect != null) {
             audit.success();
-            return redirectAccessCode(accessCode, state, redirect, rememberMe);
+            return redirectAccessCode(accessCode, session, state, redirect, rememberMe);
         } else {
             return null;
         }
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index a89ae12..c18304a 100755
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -29,6 +29,9 @@ import org.keycloak.services.managers.SocialRequestManager;
 import org.keycloak.services.managers.TokenManager;
 import org.keycloak.services.resources.admin.AdminRoot;
 import org.keycloak.models.utils.ModelProviderUtils;
+import org.keycloak.services.scheduled.ClearExpiredAuditEvents;
+import org.keycloak.services.scheduled.ClearExpiredUserSessions;
+import org.keycloak.services.scheduled.ScheduledTaskRunner;
 import org.keycloak.timer.TimerProvider;
 import org.keycloak.timer.TimerProviderFactory;
 import org.keycloak.util.JsonSerialization;
@@ -155,32 +158,8 @@ public class KeycloakApplication extends Application {
             return;
         }
         TimerProvider timer = timerFactory.create(null);
-
-        final ProviderFactory<AuditProvider> auditFactory = providerSessionFactory.getProviderFactory(AuditProvider.class);
-        if (auditFactory != null) {
-            timer.schedule(new Runnable() {
-                @Override
-                public void run() {
-                    KeycloakSession keycloakSession = keycloakSessionFactory.createSession();
-                    ProviderSession providerSession = providerSessionFactory.createSession();
-                    AuditProvider audit = providerSession.getProvider(AuditProvider.class);
-                    try {
-                        for (RealmModel realm : keycloakSession.getRealms()) {
-                            if (realm.isAuditEnabled() && realm.getAuditExpiration() > 0) {
-                                long olderThan = System.currentTimeMillis() - realm.getAuditExpiration() * 1000;
-                                log.info("Expiring audit events for " + realm.getName() + " older than " + new Date(olderThan));
-                                audit.clear(realm.getId(), olderThan);
-                            }
-                        }
-                    } finally {
-                        keycloakSession.close();
-                        audit.close();
-                    }
-                }
-            }, Config.getAuditExpirationSchedule());
-        } else {
-            log.info("Not scheduling audit expiration, no audit provider found");
-        }
+        timer.schedule(new ScheduledTaskRunner(keycloakSessionFactory, providerSessionFactory, new ClearExpiredAuditEvents()), Config.getAuditExpirationSchedule());
+        timer.schedule(new ScheduledTaskRunner(keycloakSessionFactory, providerSessionFactory, new ClearExpiredUserSessions()), Config.getUserExpirationSchedule());
     }
 
     public KeycloakSessionFactory getFactory() {
@@ -204,7 +183,6 @@ public class KeycloakApplication extends Application {
     public void importRealms(ServletContext context) {
         importRealmFile();
         importRealmResources(context);
-
     }
 
     public void importRealmResources(ServletContext context) {
diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
index 1343ec4..e760263 100755
--- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
@@ -36,9 +36,11 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserModel.RequiredAction;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.TimeBasedOTP;
 import org.keycloak.provider.ProviderSession;
 import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.ClientConnection;
 import org.keycloak.services.email.EmailException;
 import org.keycloak.services.email.EmailSender;
 import org.keycloak.services.managers.AccessCodeEntry;
@@ -62,7 +64,9 @@ import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 import javax.ws.rs.ext.Providers;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -83,6 +87,9 @@ public class RequiredActionsService {
     private UriInfo uriInfo;
 
     @Context
+    private ClientConnection clientConnection;
+
+    @Context
     protected Providers providers;
 
     @Context
@@ -317,7 +324,10 @@ public class RequiredActionsService {
             Set<RequiredAction> requiredActions = new HashSet<RequiredAction>(user.getRequiredActions());
             requiredActions.add(RequiredAction.UPDATE_PASSWORD);
 
-            AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user);
+            UserSessionModel session = realm.createUserSession(user, clientConnection.getRemoteAddr());
+            audit.session(session);
+
+            AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user, session);
             accessCode.setRequiredActions(requiredActions);
             accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction());
             accessCode.setAuthMethod("form");
@@ -395,18 +405,25 @@ public class RequiredActionsService {
             logger.debugv("redirectOauth: redirecting to: {0}", accessCode.getRedirectUri());
             accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespan());
 
-            audit.success();
-
             AuthenticationManager authManager = new AuthenticationManager(providerSession);
 
+            UserSessionModel session = realm.getUserSession(accessCode.getSessionState());
+            if (session == null || session.getExpires() < Time.currentTime()) {
+                return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectError(accessCode.getClient(), "access_denied", accessCode.getState(), accessCode.getRedirectUri());
+            }
+            audit.session(session);
+
+            audit.success();
+
             return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode,
-                    accessCode.getState(), accessCode.getRedirectUri());
+                    session, accessCode.getState(), accessCode.getRedirectUri());
         }
     }
 
     private void initAudit(AccessCodeEntry accessCode) {
         audit.event(Events.LOGIN).client(accessCode.getClient())
                 .user(accessCode.getUser())
+                .session(accessCode.getSessionState())
                 .detail(Details.CODE_ID, accessCode.getId())
                 .detail(Details.REDIRECT_URI, accessCode.getRedirectUri())
                 .detail(Details.RESPONSE_TYPE, "code")
diff --git a/services/src/main/java/org/keycloak/services/resources/SocialResource.java b/services/src/main/java/org/keycloak/services/resources/SocialResource.java
index 43f8435..06f0ae6 100755
--- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java
@@ -36,6 +36,7 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.SocialLinkModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.services.ClientConnection;
 import org.keycloak.provider.ProviderSession;
@@ -254,7 +255,10 @@ public class SocialResource {
             return oauth.forwardToSecurityFailure("Your account is not enabled.");
         }
 
-        return oauth.processAccessCode(scope, state, redirectUri, client, user, socialLink.getSocialUserId() + "@" + socialLink.getSocialProvider(), false, "social@" + provider.getId(), audit);
+        UserSessionModel session = realm.createUserSession(user, clientConnection.getRemoteAddr());
+        audit.session(session);
+
+        return oauth.processAccessCode(scope, state, redirectUri, client, user, session, socialLink.getSocialUserId() + "@" + socialLink.getSocialProvider(), false, "social@" + provider.getId(), audit);
     }
 
     @GET
diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java
index 95d8367..4054953 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -26,6 +26,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RequiredCredentialModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.provider.ProviderSession;
 import org.keycloak.representations.AccessToken;
@@ -210,6 +211,16 @@ public class TokenService {
 
         audit.event(Events.LOGIN).detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token");
 
+        String username = form.getFirst(AuthenticationManager.FORM_USERNAME);
+        if (username == null) {
+            audit.error(Errors.USERNAME_MISSING);
+            throw new UnauthorizedException("No username");
+        }
+        audit.detail(Details.USERNAME, username);
+
+        UserModel user = realm.getUser(username);
+        audit.user(user);
+
         ClientModel client = authorizeClient(authorizationHeader, form, audit);
 
         if (client.isPublicClient()) {
@@ -218,38 +229,49 @@ public class TokenService {
             throw new ForbiddenException("Public clients are not allowed to invoke grants/access");
         }
 
-        String username = form.getFirst(AuthenticationManager.FORM_USERNAME);
-        if (username == null) {
-            audit.error(Errors.USERNAME_MISSING);
-            throw new UnauthorizedException("No username");
-        }
-        audit.detail(Details.USERNAME, username);
         if (!realm.isEnabled()) {
             audit.error(Errors.REALM_DISABLED);
             throw new UnauthorizedException("Disabled realm");
         }
 
         AuthenticationStatus authenticationStatus = authManager.authenticateForm(clientConnection, realm, form);
+        Map<String, String> err;
 
         switch (authenticationStatus) {
             case SUCCESS:
                 break;
             case ACCOUNT_TEMPORARILY_DISABLED:
             case ACTIONS_REQUIRED:
+                err = new HashMap<String, String>();
+                err.put(OAuth2Constants.ERROR, "invalid_grant");
+                err.put(OAuth2Constants.ERROR_DESCRIPTION, "Account temporarily disabled");
                 audit.error(Errors.USER_TEMPORARILY_DISABLED);
-                return Response.status(503).type(MediaType.TEXT_PLAIN).entity("Account temporarily disabled").build();
+                return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
+                        .build();
             case ACCOUNT_DISABLED:
-                return Response.status(403).type(MediaType.TEXT_PLAIN).entity("Account disabled").build();
+                err = new HashMap<String, String>();
+                err.put(OAuth2Constants.ERROR, "invalid_grant");
+                err.put(OAuth2Constants.ERROR_DESCRIPTION, "Account disabled");
+                audit.error(Errors.USER_DISABLED);
+                return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
+                        .build();
             default:
+                err = new HashMap<String, String>();
+                err.put(OAuth2Constants.ERROR, "invalid_grant");
+                err.put(OAuth2Constants.ERROR_DESCRIPTION, "Invalid user credentials");
                 audit.error(Errors.INVALID_USER_CREDENTIALS);
-                throw new UnauthorizedException("Auth failed");
+                return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
+                        .build();
         }
 
-        UserModel user = realm.getUser(form.getFirst(AuthenticationManager.FORM_USERNAME));
         String scope = form.getFirst(OAuth2Constants.SCOPE);
 
+        UserSessionModel session = realm.createUserSession(user, clientConnection.getRemoteAddr());
+        audit.session(session);
+
         AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit)
-                .generateAccessToken(scope, client, user)
+                .generateAccessToken(scope, client, user, session)
+                .generateRefreshToken()
                 .generateIDToken()
                 .build();
 
@@ -362,8 +384,12 @@ public class TokenService {
             case SUCCESS:
             case ACTIONS_REQUIRED:
                 UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username);
-		        audit.user(user);
-                return oauth.processAccessCode(scopeParam, state, redirect, client, user, username, remember, "form", audit);
+                audit.user(user);
+
+                UserSessionModel session = realm.createUserSession(user, clientConnection.getRemoteAddr());
+		        audit.session(session);
+
+                return oauth.processAccessCode(scopeParam, state, redirect, client, user, session, username, remember, "form", audit);
             case ACCOUNT_TEMPORARILY_DISABLED:
                 audit.error(Errors.USER_TEMPORARILY_DISABLED);
                 return Flows.forms(realm, uriInfo).setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).setFormData(formData).createLogin();
@@ -561,6 +587,7 @@ public class TokenService {
         }
 
         audit.user(accessCode.getUser());
+        audit.session(accessCode.getSessionState());
 
         ClientModel client = authorizeClient(authorizationHeader, formData, audit);
 
@@ -589,6 +616,35 @@ public class TokenService {
                     .build();
         }
 
+        UserModel user = realm.getUserById(accessCode.getUser().getId());
+        if (user == null) {
+            Map<String, String> res = new HashMap<String, String>();
+            res.put(OAuth2Constants.ERROR, "invalid_grant");
+            res.put(OAuth2Constants.ERROR_DESCRIPTION, "User not found");
+            audit.error(Errors.INVALID_CODE);
+            return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
+                    .build();
+        }
+
+        if (!user.isEnabled()) {
+            Map<String, String> res = new HashMap<String, String>();
+            res.put(OAuth2Constants.ERROR, "invalid_grant");
+            res.put(OAuth2Constants.ERROR_DESCRIPTION, "User disabled");
+            audit.error(Errors.INVALID_CODE);
+            return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
+                    .build();
+        }
+
+        UserSessionModel session = realm.getUserSession(accessCode.getSessionState());
+        if (session == null || session.getExpires() < Time.currentTime()) {
+            Map<String, String> res = new HashMap<String, String>();
+            res.put(OAuth2Constants.ERROR, "invalid_grant");
+            res.put(OAuth2Constants.ERROR_DESCRIPTION, "Session not active");
+            audit.error(Errors.INVALID_CODE);
+            return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
+                    .build();
+        }
+
         logger.debug("accessRequest SUCCESS");
 
         AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit)
@@ -693,11 +749,14 @@ public class TokenService {
         }
 
         logger.info("Checking cookie...");
-        UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
-        if (user != null) {
+        AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
+        if (authResult != null) {
+            UserModel user = authResult.getUser();
+            UserSessionModel session = authResult.getSession();
+
             logger.debug(user.getLoginName() + " already logged in.");
-            audit.user(user).detail(Details.AUTH_METHOD, "sso");
-            return oauth.processAccessCode(scopeParam, state, redirect, client, user, null, false, "sso", audit);
+            audit.user(user).session(session).detail(Details.AUTH_METHOD, "sso");
+            return oauth.processAccessCode(scopeParam, state, redirect, client, user, session, null, false, "sso", audit);
         }
 
         if (prompt != null && prompt.equals("none")) {
@@ -760,25 +819,52 @@ public class TokenService {
     @Path("logout")
     @GET
     @NoCache
-    public Response logout(final @QueryParam("redirect_uri") String redirectUri) {
+    public Response logout(final @QueryParam("session_state") String sessionState, final @QueryParam("redirect_uri") String redirectUri) {
         // todo do we care if anybody can trigger this?
 
-        audit.event(Events.LOGOUT).detail(Details.REDIRECT_URI, redirectUri);
+        audit.event(Events.LOGOUT);
+        if (redirectUri != null) {
+            audit.detail(Details.REDIRECT_URI, redirectUri);
+        }
+        if (sessionState != null) {
+            audit.session(sessionState);
+        }
 
         // authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
-        UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers, false);
-        if (user != null) {
-            logger.infov("Logging out: {0}", user.getLoginName());
-            authManager.expireIdentityCookie(realm, uriInfo);
-            authManager.expireRememberMeCookie(realm, uriInfo);
-            resourceAdminManager.logoutUser(uriInfo.getRequestUri(), realm, user);
+        AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(realm, uriInfo, headers, false);
+        if (authResult != null) {
+            logout(authResult.getSession());
+        } else if (sessionState != null) {
+            UserSessionModel userSession = realm.getUserSession(sessionState);
+            if (userSession != null) {
+                logout(userSession);
+            } else {
+                audit.error(Errors.USER_SESSION_NOT_FOUND);
+            }
+        } else {
+            audit.error(Errors.USER_NOT_LOGGED_IN);
+        }
 
-            audit.user(user).success();
+        if (redirectUri != null) {
+            // todo manage legal redirects
+            return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build();
         } else {
-            logger.info("No user logged in for logout");
+            return Response.ok().build();
         }
-        // todo manage legal redirects
-        return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build();
+    }
+
+    private void logout(UserSessionModel session) {
+        UserModel user = session.getUser();
+
+        logger.infov("Logging out: {0} ({1})", user.getLoginName(), session.getId());
+
+        realm.removeUserSession(session);
+        authManager.expireIdentityCookie(realm, uriInfo);
+        authManager.expireRememberMeCookie(realm, uriInfo);
+
+        resourceAdminManager.logoutUser(uriInfo.getRequestUri(), realm, user.getId(), session.getId());
+
+        audit.user(user).session(session).success();
     }
 
     @Path("oauth/grant")
@@ -828,6 +914,13 @@ public class TokenService {
             audit.detail(Details.REMEMBER_ME, "true");
         }
 
+        UserSessionModel session = realm.getUserSession(accessCodeEntry.getSessionState());
+        if (session == null || session.getExpires() < Time.currentTime()) {
+            audit.error(Errors.INVALID_CODE);
+            return oauth.forwardToSecurityFailure("Session not active");
+        }
+        audit.session(session);
+
         if (formData.containsKey("cancel")) {
             audit.error(Errors.REJECTED_BY_USER);
             return redirectAccessDenied(redirect, state);
@@ -836,7 +929,7 @@ public class TokenService {
         audit.success();
 
         accessCodeEntry.setExpiration(Time.currentTime() + realm.getAccessCodeLifespan());
-        return oauth.redirectAccessCode(accessCodeEntry, state, redirect);
+        return oauth.redirectAccessCode(accessCodeEntry, session, state, redirect);
     }
 
     protected Response redirectAccessDenied(String redirect, String state) {
diff --git a/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredAuditEvents.java b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredAuditEvents.java
new file mode 100644
index 0000000..c12ea04
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredAuditEvents.java
@@ -0,0 +1,26 @@
+package org.keycloak.services.scheduled;
+
+import org.keycloak.audit.AuditProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ProviderSession;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClearExpiredAuditEvents implements ScheduledTask {
+
+    @Override
+    public void run(KeycloakSession keycloakSession, ProviderSession providerSession) {
+        AuditProvider audit = providerSession.getProvider(AuditProvider.class);
+        if (audit != null) {
+            for (RealmModel realm : keycloakSession.getRealms()) {
+                if (realm.isAuditEnabled() && realm.getAuditExpiration() > 0) {
+                    long olderThan = System.currentTimeMillis() - realm.getAuditExpiration() * 1000;
+                    audit.clear(realm.getId(), olderThan);
+                }
+            }
+        }
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java
new file mode 100644
index 0000000..cd770b6
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java
@@ -0,0 +1,19 @@
+package org.keycloak.services.scheduled;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ProviderSession;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClearExpiredUserSessions implements ScheduledTask {
+
+    @Override
+    public void run(KeycloakSession keycloakSession, ProviderSession providerSession) {
+        for (RealmModel realm : keycloakSession.getRealms()) {
+            realm.removeExpiredUserSessions();
+        }
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/scheduled/ScheduledTask.java b/services/src/main/java/org/keycloak/services/scheduled/ScheduledTask.java
new file mode 100644
index 0000000..e47f8b9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/scheduled/ScheduledTask.java
@@ -0,0 +1,13 @@
+package org.keycloak.services.scheduled;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.provider.ProviderSession;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface ScheduledTask {
+
+    public void run(KeycloakSession keycloakSession, ProviderSession providerSession);
+
+}
diff --git a/services/src/main/java/org/keycloak/services/scheduled/ScheduledTaskRunner.java b/services/src/main/java/org/keycloak/services/scheduled/ScheduledTaskRunner.java
new file mode 100644
index 0000000..1caa290
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/scheduled/ScheduledTaskRunner.java
@@ -0,0 +1,54 @@
+package org.keycloak.services.scheduled;
+
+import org.jboss.resteasy.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.provider.ProviderSession;
+import org.keycloak.provider.ProviderSessionFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ScheduledTaskRunner implements Runnable {
+
+    private static final Logger logger = Logger.getLogger(ScheduledTaskRunner.class);
+
+    private final KeycloakSessionFactory keycloakSessionFactory;
+    private final ProviderSessionFactory providerSessionFactory;
+    private final ScheduledTask task;
+
+    public ScheduledTaskRunner(KeycloakSessionFactory keycloakSessionFactory, ProviderSessionFactory providerSessionFactory, ScheduledTask task) {
+        this.keycloakSessionFactory = keycloakSessionFactory;
+        this.providerSessionFactory = providerSessionFactory;
+        this.task = task;
+    }
+
+    @Override
+    public void run() {
+        KeycloakSession keycloakSession = keycloakSessionFactory.createSession();
+        ProviderSession providerSession = providerSessionFactory.createSession();
+        try {
+            keycloakSession.getTransaction().begin();
+            task.run(keycloakSession, providerSession);
+            keycloakSession.getTransaction().commit();
+
+            logger.debug("Executed scheduled task " + task.getClass().getSimpleName());
+        } catch (Throwable t) {
+            logger.error("Failed to run scheduled task " + task.getClass().getSimpleName(), t);
+
+            keycloakSession.getTransaction().rollback();
+        } finally {
+            try {
+                keycloakSession.close();
+            } catch (Throwable t) {
+                logger.error("Failed to close KeycloakSession", t);
+            }
+            try {
+                providerSession.close();
+            } catch (Throwable t) {
+                logger.error("Failed to close ProviderSession", t);
+            }
+        }
+    }
+
+}
diff --git a/testsuite/integration/src/main/resources/META-INF/persistence.xml b/testsuite/integration/src/main/resources/META-INF/persistence.xml
index 1d2f21a..a5a7849 100755
--- a/testsuite/integration/src/main/resources/META-INF/persistence.xml
+++ b/testsuite/integration/src/main/resources/META-INF/persistence.xml
@@ -16,6 +16,7 @@
         <class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
         <class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
         <class>org.keycloak.models.jpa.entities.UserEntity</class>
+        <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
         <class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
         <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
         <class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 955474e..d3c5859 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -194,7 +194,7 @@ public class AccountTest {
         changePasswordPage.open();
         loginPage.login("test-user@localhost", "password");
 
-        events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent();
+        String sessionId = events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent().getSessionId();
 
         changePasswordPage.changePassword("", "new-password", "new-password");
 
@@ -212,14 +212,14 @@ public class AccountTest {
 
         changePasswordPage.logout();
 
-        events.expectLogout().detail(Details.REDIRECT_URI, ACCOUNT_URL).assertEvent();
+        events.expectLogout(sessionId).detail(Details.REDIRECT_URI, ACCOUNT_URL).assertEvent();
 
         loginPage.open();
         loginPage.login("test-user@localhost", "password");
 
         Assert.assertEquals("Invalid username or password.", loginPage.getError());
 
-        events.expectLogin().user((String) null).error("invalid_user_credentials").removeDetail(Details.CODE_ID).assertEvent();
+        events.expectLogin().user((String) null).session((String) null).error("invalid_user_credentials").removeDetail(Details.CODE_ID).assertEvent();
 
         loginPage.open();
         loginPage.login("test-user@localhost", "new-password");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
index db4c3ea..974ae88 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
@@ -121,6 +121,7 @@ public class RequiredActionEmailVerificationTest {
         String verificationUrl = m.group(1);
 
         Event sendEvent = events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent();
+        String sessionId = sendEvent.getSessionId();
 
         String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
 
@@ -128,11 +129,11 @@ public class RequiredActionEmailVerificationTest {
 
         driver.navigate().to(verificationUrl.trim());
 
-        events.expectRequiredAction("verify_email").detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
+        events.expectRequiredAction("verify_email").session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        events.expectLogin().detail(Details.CODE_ID, mailCodeId).assertEvent();
+        events.expectLogin().session(sessionId).detail(Details.CODE_ID, mailCodeId).assertEvent();
     }
 
     @Test
@@ -156,6 +157,7 @@ public class RequiredActionEmailVerificationTest {
         m.matches();
 
         Event sendEvent = events.expectRequiredAction("send_verify_email").user(userId).detail("username", "verifyEmail").detail("email", "email").assertEvent();
+        String sessionId = sendEvent.getSessionId();
 
         String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
 
@@ -165,9 +167,9 @@ public class RequiredActionEmailVerificationTest {
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        events.expectRequiredAction("verify_email").user(userId).detail("username", "verifyEmail").detail("email", "email").detail(Details.CODE_ID, mailCodeId).assertEvent();
+        events.expectRequiredAction("verify_email").user(userId).session(sessionId).detail("username", "verifyEmail").detail("email", "email").detail(Details.CODE_ID, mailCodeId).assertEvent();
 
-        events.expectLogin().user(userId).detail("username", "verifyEmail").detail(Details.CODE_ID, mailCodeId).assertEvent();
+        events.expectLogin().user(userId).session(sessionId).detail("username", "verifyEmail").detail(Details.CODE_ID, mailCodeId).assertEvent();
     }
 
     @Test
@@ -180,6 +182,7 @@ public class RequiredActionEmailVerificationTest {
         Assert.assertEquals(1, greenMail.getReceivedMessages().length);
 
         Event sendEvent = events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent();
+        String sessionId = sendEvent.getSessionId();
 
         String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
 
@@ -195,7 +198,7 @@ public class RequiredActionEmailVerificationTest {
         Matcher m = p.matcher(body);
         m.matches();
 
-        events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent(sendEvent);
+        events.expectRequiredAction("send_verify_email").session(sessionId).detail("email", "test-user@localhost").assertEvent(sendEvent);
 
         String verificationUrl = m.group(1);
 
@@ -203,9 +206,9 @@ public class RequiredActionEmailVerificationTest {
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        events.expectRequiredAction("verify_email").detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
+        events.expectRequiredAction("verify_email").session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
 
-        events.expectLogin().assertEvent();
+        events.expectLogin().session(sessionId).assertEvent();
     }
 
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java
index c2044ef..12a58b2 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java
@@ -89,36 +89,46 @@ public class RequiredActionMultipleActionsTest {
         loginPage.open();
         loginPage.login("test-user@localhost", "password");
 
+        String sessionId = null;
         if (changePasswordPage.isCurrent()) {
-            updatePassword();
+            sessionId = updatePassword(sessionId);
 
             updateProfilePage.assertCurrent();
-            updateProfile();
+            updateProfile(sessionId);
         } else if (updateProfilePage.isCurrent()) {
-            updateProfile();
+            sessionId = updateProfile(sessionId);
 
             changePasswordPage.assertCurrent();
-            updatePassword();
+            updatePassword(sessionId);
         } else {
             Assert.fail("Expected to update password and profile before login");
         }
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        events.expectLogin().assertEvent();
+        events.expectLogin().session(sessionId).assertEvent();
     }
 
-    public void updatePassword() {
+    public String updatePassword(String sessionId) {
         changePasswordPage.changePassword("new-password", "new-password");
 
-        events.expectRequiredAction("update_password").assertEvent();
+        AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction("update_password");
+        if (sessionId != null) {
+            expectedEvent.session(sessionId);
+        }
+        return expectedEvent.assertEvent().getSessionId();
     }
 
-    public void updateProfile() {
+    public String updateProfile(String sessionId) {
         updateProfilePage.update("New first", "New last", "new@email.com");
 
-        events.expectRequiredAction("update_profile").assertEvent();
-        events.expectRequiredAction("update_email").detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
+        AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction("update_profile");
+        if (sessionId != null) {
+            expectedEvent.session(sessionId);
+        }
+        sessionId = expectedEvent.assertEvent().getSessionId();
+        events.expectRequiredAction("update_email").session(sessionId).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
+        return sessionId;
     }
 
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
index 00fe91e..ad5f74b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
@@ -25,6 +25,7 @@ import org.junit.Assert;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
+import org.keycloak.audit.Event;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserModel.RequiredAction;
@@ -92,15 +93,15 @@ public class RequiredActionResetPasswordTest {
         changePasswordPage.assertCurrent();
         changePasswordPage.changePassword("new-password", "new-password");
 
-        events.expectRequiredAction("update_password").assertEvent();
+        String sessionId = events.expectRequiredAction("update_password").assertEvent().getSessionId();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        events.expectLogin().assertEvent();
+        Event loginEvent = events.expectLogin().session(sessionId).assertEvent();
 
         oauth.openLogout();
 
-        events.expectLogout().assertEvent();
+        events.expectLogout(loginEvent.getSessionId()).assertEvent();
 
         loginPage.open();
         loginPage.login("test-user@localhost", "new-password");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
index 43bec78..f69aef9 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
@@ -26,6 +26,7 @@ import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.audit.Details;
+import org.keycloak.audit.Event;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.utils.TimeBasedOTP;
 import org.keycloak.representations.idm.CredentialRepresentation;
@@ -108,11 +109,11 @@ public class RequiredActionTotpSetupTest {
 
         totpPage.configure(totp.generate(totpPage.getTotpSecret()));
 
-        events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp").assertEvent();
+        String sessionId = events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp").assertEvent().getSessionId();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp").assertEvent();
+        events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "setupTotp").assertEvent();
     }
 
     @Test
@@ -126,15 +127,15 @@ public class RequiredActionTotpSetupTest {
 
         totpPage.configure(totp.generate(totpSecret));
 
-        events.expectRequiredAction("update_totp").assertEvent();
+        String sessionId = events.expectRequiredAction("update_totp").assertEvent().getSessionId();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        events.expectLogin().assertEvent();
+        Event loginEvent = events.expectLogin().session(sessionId).assertEvent();
 
         oauth.openLogout();
 
-        events.expectLogout().assertEvent();
+        events.expectLogout(loginEvent.getSessionId()).assertEvent();
 
         loginPage.open();
         loginPage.login("test-user@localhost", "password");
@@ -165,11 +166,11 @@ public class RequiredActionTotpSetupTest {
 
         events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
 
-        events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
+        Event loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
 
         // Logout
         oauth.openLogout();
-        events.expectLogout().user(userId).assertEvent();
+        events.expectLogout(loginEvent.getSessionId()).user(userId).assertEvent();
 
         // Try to login after logout
         loginPage.open();
@@ -182,7 +183,7 @@ public class RequiredActionTotpSetupTest {
         // Login with one-time password
         loginTotpPage.login(totp.generate(totpCode));
 
-        events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
+        loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
 
         // Open account page
         accountTotpPage.open();
@@ -195,7 +196,7 @@ public class RequiredActionTotpSetupTest {
 
         // Logout
         oauth.openLogout();
-        events.expectLogout().user(userId).assertEvent();
+        events.expectLogout(loginEvent.getSessionId()).user(userId).assertEvent();
 
         // Try to login
         loginPage.open();
@@ -205,11 +206,11 @@ public class RequiredActionTotpSetupTest {
         totpPage.assertCurrent();
         totpPage.configure(totp.generate(totpPage.getTotpSecret()));
 
-        events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
+        String sessionId = events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent().getSessionId();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
+        events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "setupTotp2").assertEvent();
     }
 
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java
index 3c317b1..373b487 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java
@@ -87,12 +87,12 @@ public class RequiredActionUpdateProfileTest {
 
         updateProfilePage.update("New first", "New last", "new@email.com");
 
-        events.expectRequiredAction("update_profile").assertEvent();
-        events.expectRequiredAction("update_email").detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
+        String sessionId = events.expectRequiredAction("update_profile").assertEvent().getSessionId();
+        events.expectRequiredAction("update_email").session(sessionId).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        events.expectLogin().assertEvent();
+        events.expectLogin().session(sessionId).assertEvent();
     }
 
     @Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
index 0e93bd8..46c7bb2 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
@@ -30,6 +30,7 @@ import org.keycloak.models.ApplicationModel;
 import org.keycloak.models.Constants;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.adapters.action.SessionStats;
 import org.keycloak.representations.idm.RealmRepresentation;
@@ -83,7 +84,8 @@ public class AdapterTest {
             ApplicationModel adminConsole = adminRealm.getApplicationByName(Constants.ADMIN_CONSOLE_APPLICATION);
             TokenManager tm = new TokenManager();
             UserModel admin = adminRealm.getUser("admin");
-            AccessToken token = tm.createClientAccessToken(null, adminRealm, adminConsole, admin);
+            UserSessionModel session = adminRealm.createUserSession(admin, null);
+            AccessToken token = tm.createClientAccessToken(null, adminRealm, adminConsole, admin, session);
             adminToken = tm.encodeToken(adminRealm, token);
 
         }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/RelativeUriAdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/RelativeUriAdapterTest.java
index 75e8a23..96af4c1 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/RelativeUriAdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/RelativeUriAdapterTest.java
@@ -30,6 +30,7 @@ import org.keycloak.models.ApplicationModel;
 import org.keycloak.models.Constants;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.adapters.action.SessionStats;
 import org.keycloak.representations.idm.RealmRepresentation;
@@ -85,7 +86,8 @@ public class RelativeUriAdapterTest {
             ApplicationModel adminConsole = adminRealm.getApplicationByName(Constants.ADMIN_CONSOLE_APPLICATION);
             TokenManager tm = new TokenManager();
             UserModel admin = adminRealm.getUser("admin");
-            AccessToken token = tm.createClientAccessToken(null, adminRealm, adminConsole, admin);
+            UserSessionModel session = adminRealm.createUserSession(admin, null);
+            AccessToken token = tm.createClientAccessToken(null, adminRealm, adminConsole, admin, session);
             adminToken = tm.encodeToken(adminRealm, token);
 
         }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
index e94b4a9..a57aff1 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
@@ -15,6 +15,7 @@ import org.keycloak.audit.Event;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.provider.ProviderSession;
 import org.keycloak.representations.idm.UserRepresentation;
@@ -115,7 +116,7 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
     }
 
     public ExpectedEvent expectRequiredAction(String event) {
-        return expectLogin().event(event);
+        return expectLogin().event(event).session(isUUID());
     }
 
     public ExpectedEvent expectLogin() {
@@ -124,26 +125,30 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
                 .detail(Details.USERNAME, DEFAULT_USERNAME)
                 .detail(Details.RESPONSE_TYPE, "code")
                 .detail(Details.AUTH_METHOD, "form")
-                .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI);
+                .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI)
+                .session(isUUID());
     }
 
-    public ExpectedEvent expectCodeToToken(String codeId) {
+    public ExpectedEvent expectCodeToToken(String codeId, String sessionId) {
         return expect("code_to_token")
                 .detail(Details.CODE_ID, codeId)
                 .detail(Details.TOKEN_ID, isUUID())
-                .detail(Details.REFRESH_TOKEN_ID, isUUID());
+                .detail(Details.REFRESH_TOKEN_ID, isUUID())
+                .session(sessionId);
     }
 
-    public ExpectedEvent expectRefresh(String refreshTokenId) {
+    public ExpectedEvent expectRefresh(String refreshTokenId, String sessionId) {
         return expect("refresh_token")
                 .detail(Details.TOKEN_ID, isUUID())
                 .detail(Details.REFRESH_TOKEN_ID, refreshTokenId)
-                .detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID());
+                .detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID())
+                .session(sessionId);
     }
 
-    public ExpectedEvent expectLogout() {
+    public ExpectedEvent expectLogout(String sessionId) {
         return expect("logout").client((String) null)
-                .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI);
+                .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI)
+                .session(sessionId);
     }
 
     public ExpectedEvent expectRegister(String username, String email) {
@@ -162,7 +167,13 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
     }
 
     public ExpectedEvent expect(String event) {
-        return new ExpectedEvent().realm(DEFAULT_REALM).client(DEFAULT_CLIENT_ID).user(keycloak.getUser(DEFAULT_REALM, DEFAULT_USERNAME).getId()).ipAddress(DEFAULT_IP_ADDRESS).event(event);
+        return new ExpectedEvent()
+                .realm(DEFAULT_REALM)
+                .client(DEFAULT_CLIENT_ID)
+                .user(keycloak.getUser(DEFAULT_REALM, DEFAULT_USERNAME).getId())
+                .ipAddress(DEFAULT_IP_ADDRESS)
+                .session((String) null)
+                .event(event);
     }
 
     @Override
@@ -193,6 +204,7 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
     public static class ExpectedEvent {
         private Event expected = new Event();
         private Matcher<String> userId;
+        private Matcher<String> sessionId;
         private HashMap<String, Matcher<String>> details;
 
         public ExpectedEvent realm(RealmModel realm) {
@@ -216,7 +228,7 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
         }
 
         public ExpectedEvent user(UserModel user) {
-            return user(CoreMatchers.equalTo(user.getId()));
+            return user(user.getId());
         }
 
         public ExpectedEvent user(String userId) {
@@ -228,6 +240,19 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
             return this;
         }
 
+        public ExpectedEvent session(UserSessionModel session) {
+            return session(session.getId());
+        }
+
+        public ExpectedEvent session(String sessionId) {
+            return session(CoreMatchers.equalTo(sessionId));
+        }
+
+        public ExpectedEvent session(Matcher<String> sessionId) {
+            this.sessionId = sessionId;
+            return this;
+        }
+
         public ExpectedEvent ipAddress(String ipAddress) {
             expected.setIpAddress(ipAddress);
             return this;
@@ -277,8 +302,9 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
             Assert.assertEquals(expected.getError(), actual.getError());
             Assert.assertEquals(expected.getIpAddress(), actual.getIpAddress());
             Assert.assertThat(actual.getUserId(), userId);
+            Assert.assertThat(actual.getSessionId(), sessionId);
 
-            if (details == null) {
+            if (details == null || details.isEmpty()) {
                 Assert.assertNull(actual.getDetails());
             } else {
                 Assert.assertNotNull(actual.getDetails());
@@ -288,9 +314,7 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
                         Assert.fail(d.getKey() + " missing");
                     }
 
-                    if (!d.getValue().matches(actualValue)) {
-                        Assert.fail(d.getKey() + " doesn't match");
-                    }
+                    Assert.assertThat("Unexpected value for " + d.getKey(), actualValue, d.getValue());
                 }
 
                 for (String k : actual.getDetails().keySet()) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
index b4dc34c..aa51050 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
@@ -94,7 +94,7 @@ public class LoginTest {
 
         Assert.assertEquals("Invalid username or password.", loginPage.getError());
 
-        events.expectLogin().user((String) null).error("invalid_user_credentials").detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).assertEvent();
+        events.expectLogin().user((String) null).session((String) null).error("invalid_user_credentials").detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).assertEvent();
     }
 
     @Test
@@ -106,7 +106,7 @@ public class LoginTest {
 
         Assert.assertEquals("Invalid username or password.", loginPage.getError());
 
-        events.expectLogin().user((String) null).error("user_not_found").detail(Details.USERNAME, "invalid").removeDetail(Details.CODE_ID).assertEvent();
+        events.expectLogin().user((String) null).session((String) null).error("user_not_found").detail(Details.USERNAME, "invalid").removeDetail(Details.CODE_ID).assertEvent();
     }
 
     @Test
@@ -139,7 +139,7 @@ public class LoginTest {
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
         Assert.assertEquals("access_denied", oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
 
-        events.expectLogin().error("rejected_by_user").user((String) null).removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent();
+        events.expectLogin().error("rejected_by_user").user((String) null).session((String) null).removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent();
     }
 
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
index 783f07a..9502251 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
@@ -111,7 +111,7 @@ public class LoginTotpTest {
         loginPage.assertCurrent();
         Assert.assertEquals("Invalid username or password.", loginPage.getError());
 
-        events.expectLogin().error("invalid_user_credentials").removeDetail(Details.CODE_ID).user((String) null).assertEvent();
+        events.expectLogin().error("invalid_user_credentials").removeDetail(Details.CODE_ID).user((String) null).session((String) null).assertEvent();
     }
 
     @Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
new file mode 100644
index 0000000..4ec9193
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
@@ -0,0 +1,175 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.forms;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.audit.Details;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.Cookie;
+import org.openqa.selenium.WebDriver;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class LogoutTest {
+
+    @ClassRule
+    public static KeycloakRule keycloakRule = new KeycloakRule();
+
+    @Rule
+    public AssertEvents events = new AssertEvents(keycloakRule);
+
+    @Rule
+    public WebRule webRule = new WebRule(this);
+
+    @WebResource
+    protected OAuthClient oauth;
+
+    @WebResource
+    protected WebDriver driver;
+
+    @WebResource
+    protected AppPage appPage;
+
+    @WebResource
+    protected LoginPage loginPage;
+
+    @Test
+    public void logoutRedirect() {
+        loginPage.open();
+        loginPage.login("test-user@localhost", "password");
+        assertTrue(appPage.isCurrent());
+
+        String sessionId = events.expectLogin().assertEvent().getSessionId();
+
+        String redirectUri = AppPage.baseUrl + "?logout";
+
+        String logoutUrl = oauth.getLogoutUrl(redirectUri, null);
+        driver.navigate().to(logoutUrl);
+
+        events.expectLogout(sessionId).detail(Details.REDIRECT_URI, redirectUri).assertEvent();
+
+        assertEquals(redirectUri, driver.getCurrentUrl());
+
+        loginPage.open();
+        loginPage.login("test-user@localhost", "password");
+        assertTrue(appPage.isCurrent());
+
+        String sessionId2 = events.expectLogin().assertEvent().getSessionId();
+        assertNotEquals(sessionId, sessionId2);
+    }
+
+    @Test
+    public void logoutSession() {
+        loginPage.open();
+        loginPage.login("test-user@localhost", "password");
+        assertTrue(appPage.isCurrent());
+
+        String sessionId = events.expectLogin().assertEvent().getSessionId();
+
+        String logoutUrl = oauth.getLogoutUrl(null, sessionId);
+        driver.navigate().to(logoutUrl);
+
+        events.expectLogout(sessionId).removeDetail(Details.REDIRECT_URI).assertEvent();
+
+        assertEquals(logoutUrl, driver.getCurrentUrl());
+
+        loginPage.open();
+        loginPage.login("test-user@localhost", "password");
+        assertTrue(appPage.isCurrent());
+
+        String sessionId2 = events.expectLogin().assertEvent().getSessionId();
+        assertNotEquals(sessionId, sessionId2);
+    }
+
+    @Test
+    public void logoutMultipleSessions() throws IOException {
+        // Login session 1
+        loginPage.open();
+        loginPage.login("test-user@localhost", "password");
+        assertTrue(appPage.isCurrent());
+
+        String sessionId = events.expectLogin().assertEvent().getSessionId();
+
+        // Login session 2
+        WebDriver driver2 = WebRule.createWebDriver();
+
+        OAuthClient oauth2 = new OAuthClient(driver2);
+        oauth2.doLogin("test-user@localhost", "password");
+
+        String sessionId2 = events.expectLogin().assertEvent().getSessionId();
+        assertNotEquals(sessionId, sessionId2);
+
+        // Check session 1 logged-in
+        oauth.openLoginForm();
+        events.expectLogin().session(sessionId).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
+
+        // Check session 2 logged-in
+        oauth2.openLoginForm();
+        events.expectLogin().session(sessionId2).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
+
+        // Logout session 1 by redirect
+        driver.navigate().to(oauth.getLogoutUrl(AppPage.baseUrl, null));
+        events.expectLogout(sessionId).detail(Details.REDIRECT_URI, AppPage.baseUrl).assertEvent();
+
+        // Check session 2 logged-in
+        oauth2.openLoginForm();
+        events.expectLogin().session(sessionId2).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
+
+        // Check session 1 not logged-in
+        oauth.openLoginForm();
+        assertEquals(oauth.getLoginFormUrl(), driver.getCurrentUrl());
+
+        // Login session 3
+        oauth.doLogin("test-user@localhost", "password");
+        String sessionId3 = events.expectLogin().assertEvent().getSessionId();
+        assertNotEquals(sessionId, sessionId3);
+        assertNotEquals(sessionId2, sessionId3);
+
+        // Logout session 2 by session_state
+        oauth2.doLogout(null, sessionId2);
+        events.expectLogout(sessionId2).removeDetail(Details.REDIRECT_URI).assertEvent();
+
+        // Check session 3 logged-in
+        oauth.openLoginForm();
+        events.expectLogin().session(sessionId3).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
+
+        // Check session 2 not logged-in
+        oauth2.openLoginForm();
+        assertEquals(oauth2.getLoginFormUrl(), driver2.getCurrentUrl());
+    }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index 0863c1a..e2259f9 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -26,6 +26,7 @@ import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.audit.Details;
+import org.keycloak.audit.Event;
 import org.keycloak.models.PasswordPolicy;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserCredentialModel;
@@ -123,7 +124,7 @@ public class ResetPasswordTest {
 
         resetPasswordPage.assertCurrent();
 
-        events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent();
+        String sessionId = events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId();
 
         Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage());
 
@@ -140,15 +141,15 @@ public class ResetPasswordTest {
 
         updatePasswordPage.changePassword("resetPassword", "resetPassword");
 
-        events.expectRequiredAction("update_password").user(userId).detail(Details.USERNAME, username).assertEvent();
+        events.expectRequiredAction("update_password").user(userId).session(sessionId).detail(Details.USERNAME, username).assertEvent();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        events.expectLogin().user(userId).detail(Details.USERNAME, username).assertEvent();
+        Event loginEvent = events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, username).assertEvent();
 
         oauth.openLogout();
 
-        events.expectLogout().user(userId).assertEvent();
+        events.expectLogout(loginEvent.getSessionId()).user(userId).session(sessionId).assertEvent();
 
         loginPage.open();
 
@@ -176,7 +177,7 @@ public class ResetPasswordTest {
 
         Assert.assertEquals(0, greenMail.getReceivedMessages().length);
 
-        events.expectRequiredAction("send_reset_password").user((String) null).detail(Details.USERNAME, "invalid").removeDetail(Details.EMAIL).removeDetail(Details.CODE_ID).error("user_not_found").assertEvent();
+        events.expectRequiredAction("send_reset_password").user((String) null).session((String) null).detail(Details.USERNAME, "invalid").removeDetail(Details.EMAIL).removeDetail(Details.CODE_ID).error("user_not_found").assertEvent();
     }
 
     @Test
@@ -206,7 +207,7 @@ public class ResetPasswordTest {
         String body = (String) message.getContent();
         String changePasswordUrl = body.split("\n")[3];
 
-        events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
+        String sessionId = events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId();
 
         driver.navigate().to(changePasswordUrl.trim());
 
@@ -218,15 +219,15 @@ public class ResetPasswordTest {
 
         updatePasswordPage.changePassword("resetPasswordWithPasswordPolicy", "resetPasswordWithPasswordPolicy");
 
-        events.expectRequiredAction("update_password").user(userId).detail(Details.USERNAME, "login-test").assertEvent();
+        events.expectRequiredAction("update_password").user(userId).session(sessionId).detail(Details.USERNAME, "login-test").assertEvent();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
+        Event loginEvent = events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "login-test").assertEvent();
 
         oauth.openLogout();
 
-        events.expectLogout().user(userId).assertEvent();
+        events.expectLogout(loginEvent.getSessionId()).user(userId).session(sessionId).assertEvent();
 
         loginPage.open();
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
index 3dc81fc..16c9d36 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
@@ -41,6 +41,9 @@ import org.openqa.selenium.WebDriver;
 
 import javax.ws.rs.core.UriBuilder;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
@@ -76,22 +79,34 @@ public class SSOTest {
         loginPage.open();
         loginPage.login("test-user@localhost", "password");
         
-        Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
         Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
 
-        events.expectLogin().assertEvent();
+        String sessionId = events.expectLogin().assertEvent().getSessionId();
 
         appPage.open();
 
         oauth.openLoginForm();
 
-        Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
         profilePage.open();
 
         Assert.assertTrue(profilePage.isCurrent());
 
-        events.expectLogin().detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).client("test-app").assertEvent();
+        String sessionId2 = events.expectLogin().detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).client("test-app").assertEvent().getSessionId();
+
+        assertEquals(sessionId, sessionId2);
+
+        // Expire session
+        keycloakRule.removeUserSession(sessionId);
+
+        oauth.doLogin("test-user@localhost", "password");
+
+        String sessionId4 = events.expectLogin().assertEvent().getSessionId();
+        assertNotEquals(sessionId, sessionId4);
+
+        events.clear();
     }
 
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index 4829497..a8ae436 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -27,6 +27,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.audit.Details;
+import org.keycloak.audit.Errors;
 import org.keycloak.audit.Event;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.testsuite.AssertEvents;
@@ -41,6 +42,8 @@ import org.openqa.selenium.WebDriver;
 import static org.hamcrest.Matchers.allOf;
 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
 import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -69,7 +72,10 @@ public class AccessTokenTest {
     public void accessTokenRequest() throws Exception {
         oauth.doLogin("test-user@localhost", "password");
 
-        String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
+        Event loginEvent = events.expectLogin().assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
 
         String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
         AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
@@ -85,33 +91,61 @@ public class AccessTokenTest {
         Assert.assertEquals(keycloakRule.getUser("test", "test-user@localhost").getId(), token.getSubject());
         Assert.assertNotEquals("test-user@localhost", token.getSubject());
 
+        Assert.assertEquals(sessionId, token.getSessionState());
+
         Assert.assertEquals(1, token.getRealmAccess().getRoles().size());
         Assert.assertTrue(token.getRealmAccess().isUserInRole("user"));
 
         Assert.assertEquals(1, token.getResourceAccess(oauth.getClientId()).getRoles().size());
         Assert.assertTrue(token.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user"));
 
-        Event event = events.expectCodeToToken(codeId).assertEvent();
+        Event event = events.expectCodeToToken(codeId, sessionId).assertEvent();
         Assert.assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID));
         Assert.assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID));
+        Assert.assertEquals(sessionId, token.getSessionState());
 
         response = oauth.doAccessTokenRequest(code, "password");
         Assert.assertEquals(400, response.getStatusCode());
 
-        events.expectCodeToToken(codeId).error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).client((String) null).user((String) null).assertEvent();
+        events.expectCodeToToken(codeId, null).error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).client((String) null).user((String) null).assertEvent();
     }
 
     @Test
     public void accessTokenInvalidClientCredentials() throws Exception {
         oauth.doLogin("test-user@localhost", "password");
 
-        String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
+        Event loginEvent = events.expectLogin().assertEvent();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
 
         String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
         AccessTokenResponse response = oauth.doAccessTokenRequest(code, "invalid");
         Assert.assertEquals(400, response.getStatusCode());
 
-        events.expectCodeToToken(codeId).error("invalid_client_credentials").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).assertEvent();
+        events.expectCodeToToken(codeId, loginEvent.getSessionId()).error("invalid_client_credentials").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).assertEvent();
     }
 
+    @Test
+    public void accessTokenUserSessionExpired() {
+        oauth.doLogin("test-user@localhost", "password");
+
+        Event loginEvent = events.expectLogin().assertEvent();
+
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+        String sessionId = loginEvent.getSessionId();
+
+        keycloakRule.removeUserSession(sessionId);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+        assertEquals(400, tokenResponse.getStatusCode());
+        assertNull(tokenResponse.getAccessToken());
+        assertNull(tokenResponse.getRefreshToken());
+
+        events.expectCodeToToken(codeId, sessionId).removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).error(Errors.INVALID_CODE).assertEvent();
+
+        events.clear();
+    }
+
+
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
index 2072e6e..03b9086 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
@@ -27,6 +27,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.audit.Details;
+import org.keycloak.audit.Event;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.testsuite.AssertEvents;
 import org.keycloak.testsuite.OAuthClient;
@@ -40,6 +41,8 @@ import org.openqa.selenium.WebDriver;
 import java.io.IOException;
 import java.util.Map;
 
+import static org.junit.Assert.assertEquals;
+
 /**
  * @author <a href="mailto:vrockai@redhat.com">Viliam Rockai</a>
  */
@@ -82,22 +85,25 @@ public class OAuthGrantTest {
 
         Assert.assertTrue(oauth.getCurrentQuery().containsKey(OAuth2Constants.CODE));
 
-        String codeId = events.expectLogin().client("third-party").assertEvent().getDetails().get(Details.CODE_ID);
+        Event loginEvent = events.expectLogin().client("third-party").assertEvent();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+        String sessionId = loginEvent.getSessionId();
 
         OAuthClient.AccessTokenResponse accessToken = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password");
 
         AccessToken token = oauth.verifyToken(accessToken.getAccessToken());
+        assertEquals(sessionId, token.getSessionState());
 
         AccessToken.Access realmAccess = token.getRealmAccess();
-        Assert.assertEquals(1, realmAccess.getRoles().size());
+        assertEquals(1, realmAccess.getRoles().size());
         Assert.assertTrue(realmAccess.isUserInRole("user"));
 
         Map<String,AccessToken.Access> resourceAccess = token.getResourceAccess();
-        Assert.assertEquals(1, resourceAccess.size());
-        Assert.assertEquals(1, resourceAccess.get("test-app").getRoles().size());
+        assertEquals(1, resourceAccess.size());
+        assertEquals(1, resourceAccess.get("test-app").getRoles().size());
         Assert.assertTrue(resourceAccess.get("test-app").isUserInRole("customer-user"));
 
-        events.expectCodeToToken(codeId).client("third-party").assertEvent();
+        events.expectCodeToToken(codeId, loginEvent.getSessionId()).client("third-party").assertEvent();
     }
 
     @Test
@@ -112,7 +118,7 @@ public class OAuthGrantTest {
         grantPage.cancel();
 
         Assert.assertTrue(oauth.getCurrentQuery().containsKey(OAuth2Constants.ERROR));
-        Assert.assertEquals("access_denied", oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
+        assertEquals("access_denied", oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
 
         events.expectLogin().client("third-party").error("rejected_by_user").assertEvent();
     }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
index beb8e77..030bec7 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
@@ -27,6 +27,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.audit.Details;
+import org.keycloak.audit.Errors;
 import org.keycloak.audit.Event;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.RefreshToken;
@@ -43,6 +44,8 @@ import org.openqa.selenium.WebDriver;
 import static org.hamcrest.Matchers.allOf;
 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
 import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -71,7 +74,10 @@ public class RefreshTokenTest {
     public void refreshTokenRequest() throws Exception {
         oauth.doLogin("test-user@localhost", "password");
 
-        String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
+        Event loginEvent = events.expectLogin().assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
 
         String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
 
@@ -80,7 +86,7 @@ public class RefreshTokenTest {
         String refreshTokenString = tokenResponse.getRefreshToken();
         RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString);
 
-        Event tokenEvent = events.expectCodeToToken(codeId).assertEvent();
+        Event tokenEvent = events.expectCodeToToken(codeId, sessionId).assertEvent();
 
         Assert.assertNotNull(refreshTokenString);
 
@@ -89,6 +95,8 @@ public class RefreshTokenTest {
         Assert.assertThat(token.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
         Assert.assertThat(refreshToken.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(35950), lessThanOrEqualTo(36000)));
 
+        Assert.assertEquals(sessionId, refreshToken.getSessionState());
+
         Thread.sleep(2000);
 
         AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString, "password");
@@ -97,6 +105,9 @@ public class RefreshTokenTest {
 
         Assert.assertEquals(200, response.getStatusCode());
 
+        Assert.assertEquals(sessionId, refreshedToken.getSessionState());
+        Assert.assertEquals(sessionId, refreshedRefreshToken.getSessionState());
+
         Assert.assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
         Assert.assertThat(refreshedToken.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
 
@@ -117,9 +128,37 @@ public class RefreshTokenTest {
         Assert.assertEquals(1, refreshedToken.getResourceAccess(oauth.getClientId()).getRoles().size());
         Assert.assertTrue(refreshedToken.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user"));
 
-        Event refreshEvent = events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID)).assertEvent();
+        Event refreshEvent = events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), sessionId).assertEvent();
         Assert.assertNotEquals(tokenEvent.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID));
         Assert.assertNotEquals(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID));
     }
 
+    @Test
+    public void refreshTokenUserSessionExpired() {
+        oauth.doLogin("test-user@localhost", "password");
+
+        Event loginEvent = events.expectLogin().assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+
+        events.poll();
+
+        String refreshId = oauth.verifyRefreshToken(tokenResponse.getRefreshToken()).getId();
+
+        keycloakRule.removeUserSession(sessionId);
+
+        tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
+
+        assertEquals(400, tokenResponse.getStatusCode());
+        assertNull(tokenResponse.getAccessToken());
+        assertNull(tokenResponse.getRefreshToken());
+
+        events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
+
+        events.clear();
+    }
+
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
new file mode 100644
index 0000000..15a3f23
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
@@ -0,0 +1,181 @@
+package org.keycloak.testsuite.oauth;
+
+import net.iharder.Base64;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.audit.Details;
+import org.keycloak.audit.Errors;
+import org.keycloak.audit.Event;
+import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.WebDriver;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ResourceOwnerPasswordCredentialsGrantTest {
+
+    @ClassRule
+    public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            ApplicationModel app = appRealm.addApplication("resource-owner");
+            app.setSecret("secret");
+        }
+    });
+
+    @Rule
+    public AssertEvents events = new AssertEvents(keycloakRule);
+
+    @Rule
+    public WebRule webRule = new WebRule(this);
+
+    @WebResource
+    protected WebDriver driver;
+
+    @WebResource
+    protected OAuthClient oauth;
+
+    @Test
+    public void grantAccessToken() throws Exception {
+        oauth.clientId("resource-owner");
+
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "test-user@localhost", "password");
+
+        assertEquals(200, response.getStatusCode());
+
+        AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+        RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
+
+        events.expectLogin()
+                .client("resource-owner")
+                .session(accessToken.getSessionState())
+                .detail(Details.AUTH_METHOD, "oauth_credentials")
+                .detail(Details.RESPONSE_TYPE, "token")
+                .detail(Details.TOKEN_ID, accessToken.getId())
+                .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+                .removeDetail(Details.CODE_ID)
+                .removeDetail(Details.REDIRECT_URI)
+                .assertEvent();
+
+        assertEquals(accessToken.getSessionState(), refreshToken.getSessionState());
+
+        OAuthClient.AccessTokenResponse refreshedResponse = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret");
+
+        AccessToken refreshedAccessToken = oauth.verifyToken(refreshedResponse.getAccessToken());
+        RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(refreshedResponse.getRefreshToken());
+
+        assertEquals(accessToken.getSessionState(), refreshedAccessToken.getSessionState());
+        assertEquals(accessToken.getSessionState(), refreshedRefreshToken.getSessionState());
+
+        events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).client("resource-owner").assertEvent();
+    }
+
+    @Test
+    public void grantAccessTokenLogout() throws Exception {
+        oauth.clientId("resource-owner");
+
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "test-user@localhost", "password");
+
+        assertEquals(200, response.getStatusCode());
+
+        AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+        RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
+
+        events.expectLogin()
+                .client("resource-owner")
+                .session(accessToken.getSessionState())
+                .detail(Details.AUTH_METHOD, "oauth_credentials")
+                .detail(Details.RESPONSE_TYPE, "token")
+                .detail(Details.TOKEN_ID, accessToken.getId())
+                .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+                .removeDetail(Details.CODE_ID)
+                .removeDetail(Details.REDIRECT_URI)
+                .assertEvent();
+
+        HttpResponse logoutResponse = oauth.doLogout(null, accessToken.getSessionState());
+        assertEquals(200, logoutResponse.getStatusLine().getStatusCode());
+        events.expectLogout(accessToken.getSessionState()).removeDetail(Details.REDIRECT_URI).assertEvent();
+
+        logoutResponse = oauth.doLogout(null, accessToken.getSessionState());
+        assertEquals(200, logoutResponse.getStatusLine().getStatusCode());
+        events.expectLogout(accessToken.getSessionState()).user((String) null).removeDetail(Details.REDIRECT_URI).error(Errors.USER_SESSION_NOT_FOUND).assertEvent();
+
+        response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret");
+        assertEquals(400, response.getStatusCode());
+        assertEquals("invalid_grant", response.getError());
+
+        events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).client("resource-owner")
+                .removeDetail(Details.TOKEN_ID)
+                .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
+                .error(Errors.INVALID_TOKEN).assertEvent();
+    }
+
+    @Test
+    public void grantAccessTokenInvalidClientCredentials() throws Exception {
+        oauth.clientId("resource-owner");
+
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("invalid", "test-user@localhost", "password");
+
+        assertEquals(400, response.getStatusCode());
+
+        assertEquals("unauthorized_client", response.getError());
+
+        events.expectLogin()
+                .client("resource-owner")
+                .session((String) null)
+                .detail(Details.AUTH_METHOD, "oauth_credentials")
+                .detail(Details.RESPONSE_TYPE, "token")
+                .removeDetail(Details.CODE_ID)
+                .removeDetail(Details.REDIRECT_URI)
+                .error(Errors.INVALID_CLIENT_CREDENTIALS)
+                .assertEvent();
+    }
+
+    @Test
+    public void grantAccessTokenInvalidUserCredentials() throws Exception {
+        oauth.clientId("resource-owner");
+
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "test-user@localhost", "invalid");
+
+        assertEquals(400, response.getStatusCode());
+
+        assertEquals("invalid_grant", response.getError());
+
+        events.expectLogin()
+                .client("resource-owner")
+                .session((String) null)
+                .detail(Details.AUTH_METHOD, "oauth_credentials")
+                .detail(Details.RESPONSE_TYPE, "token")
+                .removeDetail(Details.CODE_ID)
+                .removeDetail(Details.REDIRECT_URI)
+                .error(Errors.INVALID_USER_CREDENTIALS)
+                .assertEvent();
+    }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
index 2b8ea8f..77bd97e 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
@@ -21,11 +21,13 @@
  */
 package org.keycloak.testsuite;
 
+import net.iharder.Base64;
 import org.apache.commons.io.IOUtils;
 import org.apache.http.HttpResponse;
 import org.apache.http.NameValuePair;
 import org.apache.http.client.HttpClient;
 import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.utils.URLEncodedUtils;
 import org.apache.http.impl.client.DefaultHttpClient;
@@ -36,9 +38,12 @@ import org.junit.Assert;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.RSATokenVerifier;
 import org.keycloak.VerificationException;
+import org.keycloak.audit.Details;
+import org.keycloak.audit.Event;
 import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.jose.jws.crypto.RSAProvider;
 import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.RefreshToken;
 import org.keycloak.services.resources.TokenService;
 import org.keycloak.util.BasicAuthHelper;
@@ -46,6 +51,7 @@ import org.openqa.selenium.By;
 import org.openqa.selenium.WebDriver;
 
 import javax.ws.rs.core.UriBuilder;
+import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -144,6 +150,35 @@ public class OAuthClient {
         }
     }
 
+    public AccessTokenResponse doGrantAccessTokenRequest(String clientSecret, String username,  String password) throws Exception {
+        HttpClient client = new DefaultHttpClient();
+        HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl());
+
+        String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
+        post.setHeader("Authorization", authorization);
+
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair("username", username));
+        parameters.add(new BasicNameValuePair("password", password));
+
+        UrlEncodedFormEntity formEntity;
+        try {
+            formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+        post.setEntity(formEntity);
+
+        return new AccessTokenResponse(client.execute(post));
+    }
+
+    public HttpResponse doLogout(String redirectUri, String sessionState) throws IOException {
+        HttpClient client = new DefaultHttpClient();
+        HttpGet get = new HttpGet(getLogoutUrl(redirectUri, sessionState));
+
+        return client.execute(get);
+    }
+
     public AccessTokenResponse doRefreshTokenRequest(String refreshToken, String password) {
         HttpClient client = new DefaultHttpClient();
         HttpPost post = new HttpPost(getRefreshTokenUrl());
@@ -163,7 +198,7 @@ public class OAuthClient {
             parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, clientId));
         }
 
-        UrlEncodedFormEntity formEntity = null;
+        UrlEncodedFormEntity formEntity;
         try {
             formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
         } catch (UnsupportedEncodingException e) {
@@ -267,6 +302,22 @@ public class OAuthClient {
         return b.build(realm).toString();
     }
 
+    public String getLogoutUrl(String redirectUri, String sessionState) {
+        UriBuilder b = TokenService.logoutUrl(UriBuilder.fromUri(baseUrl));
+        if (redirectUri != null) {
+            b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri);
+        }
+        if (sessionState != null) {
+            b.queryParam("session_state", sessionState);
+        }
+        return b.build(realm).toString();
+    }
+
+    public String getResourceOwnerPasswordCredentialGrantUrl() {
+        UriBuilder b = TokenService.grantAccessTokenUrl(UriBuilder.fromUri(baseUrl));
+        return b.build(realm).toString();
+    }
+
     public String getRefreshTokenUrl() {
         UriBuilder b = TokenService.refreshUrl(UriBuilder.fromUri(baseUrl));
         return b.build(realm).toString();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
index 4cd2b68..d04cc21 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
@@ -24,10 +24,13 @@ package org.keycloak.testsuite.rule;
 import org.keycloak.models.Config;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.provider.ProviderSession;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.testsuite.ApplicationServlet;
 
+import static org.junit.Assert.assertNotNull;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
@@ -78,6 +81,28 @@ public class KeycloakRule extends AbstractKeycloakRule {
         }
     }
 
+    public KeycloakSession startSession() {
+        KeycloakSession session = server.getKeycloakSessionFactory().createSession();
+        session.getTransaction().begin();
+        return session;
+    }
+
+    public void stopSession(KeycloakSession session, boolean commit) {
+        if (commit) {
+            session.getTransaction().commit();
+        }
+        session.close();
+    }
+
+    public void removeUserSession(String sessionId) {
+        KeycloakSession keycloakSession = startSession();
+        RealmModel realm = keycloakSession.getRealm("test");
+        UserSessionModel session = realm.getUserSession(sessionId);
+        assertNotNull(session);
+        realm.removeUserSession(session);
+        stopSession(keycloakSession, true);
+    }
+
     public abstract static class KeycloakSetup {
 
         protected ProviderSession providerSession;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
index ef37e21..35454dc 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
@@ -47,6 +47,13 @@ public class WebRule extends ExternalResource {
 
     @Override
     protected void before() throws Throwable {
+        driver = createWebDriver();
+        oauth = new OAuthClient(driver);
+        initWebResources(test);
+    }
+
+    public static WebDriver createWebDriver() {
+        WebDriver driver;
         String browser = "htmlunit";
         if (System.getProperty("browser") != null) {
             browser = System.getProperty("browser");
@@ -64,10 +71,7 @@ public class WebRule extends ExternalResource {
         } else {
             throw new RuntimeException("Unsupported browser " + browser);
         }
-
-        oauth = new OAuthClient(driver);
-
-        initWebResources(test);
+        return driver;
     }
 
     protected void initWebResources(Object o) {
@@ -122,7 +126,7 @@ public class WebRule extends ExternalResource {
         driver.close();
     }
 
-    public class HtmlUnitDriver extends org.openqa.selenium.htmlunit.HtmlUnitDriver {
+    public static class HtmlUnitDriver extends org.openqa.selenium.htmlunit.HtmlUnitDriver {
         
         @Override
         public WebClient getWebClient() {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java
index 6ed3294..f7693bf 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java
@@ -28,6 +28,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.audit.Details;
+import org.keycloak.audit.Event;
 import org.keycloak.models.RealmModel;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.idm.UserRepresentation;
@@ -116,16 +117,21 @@ public class SocialLoginTest {
                 .detail(Details.REGISTER_METHOD, "social@dummy")
                 .detail(Details.REDIRECT_URI, AssertEvents.DEFAULT_REDIRECT_URI)
                 .detail(Details.USERNAME, "1@dummy")
+                .session((String) null)
                 .assertEvent().getUserId();
 
-        String codeId = events.expectLogin().user(userId).detail(Details.USERNAME, "1@dummy").detail(Details.AUTH_METHOD, "social@dummy").assertEvent().getDetails().get(Details.CODE_ID);
+        Event loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "1@dummy").detail(Details.AUTH_METHOD, "social@dummy").assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
 
         AccessTokenResponse response = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password");
 
-        events.expectCodeToToken(codeId).user(userId).assertEvent();
+        events.expectCodeToToken(codeId, sessionId).user(userId).assertEvent();
 
         AccessToken token = oauth.verifyToken(response.getAccessToken());
         Assert.assertEquals(36, token.getSubject().length());
+        Assert.assertEquals(sessionId, token.getSessionState());
 
         UserRepresentation profile = keycloakRule.getUserById("test", token.getSubject());
         Assert.assertEquals(36, profile.getUsername().length());
@@ -136,7 +142,7 @@ public class SocialLoginTest {
 
         oauth.openLogout();
 
-        events.expectLogout().user(userId).assertEvent();
+        events.expectLogout(sessionId).user(userId).assertEvent();
 
         loginPage.open();
 
@@ -160,7 +166,7 @@ public class SocialLoginTest {
         Assert.assertTrue(loginPage.isCurrent());
         Assert.assertEquals("Access denied", loginPage.getWarning());
 
-        events.expectLogin().error("rejected_by_user").user((String) null).detail(Details.AUTH_METHOD, "social@dummy").removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent();
+        events.expectLogin().error("rejected_by_user").user((String) null).session((String) null).detail(Details.AUTH_METHOD, "social@dummy").removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent();
 
         loginPage.login("test-user@localhost", "password");
 
@@ -212,12 +218,13 @@ public class SocialLoginTest {
 
             Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-            String codeId = events.expectLogin().user(userId).removeDetail(Details.USERNAME).detail(Details.AUTH_METHOD, "social@dummy").detail(Details.USERNAME, "2@dummy").assertEvent().getDetails().get(Details.CODE_ID);
+            Event loginEvent = events.expectLogin().user(userId).removeDetail(Details.USERNAME).detail(Details.AUTH_METHOD, "social@dummy").detail(Details.USERNAME, "2@dummy").assertEvent();
+            String codeId = loginEvent.getDetails().get(Details.CODE_ID);
 
             AccessTokenResponse response = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password");
             AccessToken token = oauth.verifyToken(response.getAccessToken());
 
-            events.expectCodeToToken(codeId).user(userId).assertEvent();
+            events.expectCodeToToken(codeId, loginEvent.getSessionId()).user(userId).assertEvent();
 
             UserRepresentation profile = keycloakRule.getUserById("test", token.getSubject());
 
diff --git a/testsuite/performance/src/test/resources/META-INF/persistence.xml b/testsuite/performance/src/test/resources/META-INF/persistence.xml
index 29af038..e5ae89a 100755
--- a/testsuite/performance/src/test/resources/META-INF/persistence.xml
+++ b/testsuite/performance/src/test/resources/META-INF/persistence.xml
@@ -16,6 +16,7 @@
         <class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
         <class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
         <class>org.keycloak.models.jpa.entities.UserEntity</class>
+        <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
         <class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
         <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
         <class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>