keycloak-memoizeit

Changes

Details

diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
index 4dcea95..44458fd 100755
--- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
@@ -55,6 +55,7 @@ public class UserRepresentation {
     protected List<String> realmRoles;
     protected Map<String, List<String>> clientRoles;
     protected List<UserConsentRepresentation> clientConsents;
+    protected Integer notBefore;
 
     @Deprecated
     protected Map<String, List<String>> applicationRoles;
@@ -216,6 +217,14 @@ public class UserRepresentation {
         this.clientConsents = clientConsents;
     }
 
+    public Integer getNotBefore() {
+        return notBefore;
+    }
+
+    public void setNotBefore(Integer notBefore) {
+        this.notBefore = notBefore;
+    }
+
     @Deprecated
     public Map<String, List<String>> getApplicationRoles() {
         return applicationRoles;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java
index 1bf6e42..68dfc37 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java
@@ -45,10 +45,11 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm 
     private Set<String> requiredActions = new HashSet<>();
     private Set<String> roleMappings = new HashSet<>();
     private Set<String> groups = new HashSet<>();
+    private int notBefore;
 
 
 
-    public CachedUser(Long revision, RealmModel realm, UserModel user) {
+    public CachedUser(Long revision, RealmModel realm, UserModel user, int notBefore) {
         super(revision, user.getId());
         this.realm = realm.getId();
         this.username = user.getUsername();
@@ -71,6 +72,7 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm 
                 groups.add(group.getId());
             }
         }
+        this.notBefore = notBefore;
     }
 
     public String getRealm() {
@@ -129,4 +131,7 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm 
         return groups;
     }
 
+    public int getNotBefore() {
+        return notBefore;
+    }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
index 0d971f7..390c25c 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
@@ -334,6 +334,8 @@ public class UserCacheSession implements UserCache {
     }
 
     protected UserModel cacheUser(RealmModel realm, UserModel delegate, Long revision) {
+        int notBefore = getDelegate().getNotBeforeOfUser(realm, delegate);
+
         StorageId storageId = new StorageId(delegate.getId());
         CachedUser cached = null;
         if (!storageId.isLocal()) {
@@ -343,7 +345,7 @@ public class UserCacheSession implements UserCache {
             if (policy != null && policy == UserStorageProviderModel.CachePolicy.NO_CACHE) {
                 return delegate;
             }
-            cached = new CachedUser(revision, realm, delegate);
+            cached = new CachedUser(revision, realm, delegate, notBefore);
             if (policy == null || policy == UserStorageProviderModel.CachePolicy.DEFAULT) {
                 cache.addRevisioned(cached, startupRevision);
             } else {
@@ -366,7 +368,7 @@ public class UserCacheSession implements UserCache {
                 }
             }
         } else {
-            cached = new CachedUser(revision, realm, delegate);
+            cached = new CachedUser(revision, realm, delegate, notBefore);
             cache.addRevisioned(cached, startupRevision);
         }
         UserAdapter adapter = new UserAdapter(cached, this, session, realm);
@@ -765,6 +767,32 @@ public class UserCacheSession implements UserCache {
         return consentModel;
     }
 
+    @Override
+    public void setNotBeforeForUser(RealmModel realm, UserModel user, int notBefore) {
+        if (!isRegisteredForInvalidation(realm, user.getId())) {
+            UserModel foundUser = getUserById(user.getId(), realm);
+            if (foundUser instanceof UserAdapter) {
+                ((UserAdapter) foundUser).invalidate();
+            }
+        }
+
+        getDelegate().setNotBeforeForUser(realm, user, notBefore);
+
+    }
+
+    @Override
+    public int getNotBeforeOfUser(RealmModel realm, UserModel user) {
+        if (isRegisteredForInvalidation(realm, user.getId())) {
+            return getDelegate().getNotBeforeOfUser(realm, user);
+        }
+
+        UserModel foundUser = getUserById(user.getId(), realm);
+        if (foundUser instanceof UserAdapter) {
+            return ((UserAdapter) foundUser).cached.getNotBefore();
+        } else {
+            return getDelegate().getNotBeforeOfUser(realm, user);
+        }
+    }
 
     @Override
     public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions) {
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
index 50b7760..771487f 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
@@ -103,6 +103,9 @@ public class UserEntity {
     @Column(name="SERVICE_ACCOUNT_CLIENT_LINK")
     protected String serviceAccountClientLink;
 
+    @Column(name="NOT_BEFORE")
+    protected int notBefore;
+
     public String getId() {
         return id;
     }
@@ -224,6 +227,14 @@ public class UserEntity {
         this.serviceAccountClientLink = serviceAccountClientLink;
     }
 
+    public int getNotBefore() {
+        return notBefore;
+    }
+
+    public void setNotBefore(int notBefore) {
+        this.notBefore = notBefore;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index b9352c0..b543d6b 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -39,7 +39,6 @@ import org.keycloak.models.UserProvider;
 import org.keycloak.models.jpa.entities.CredentialAttributeEntity;
 import org.keycloak.models.jpa.entities.CredentialEntity;
 import org.keycloak.models.jpa.entities.FederatedIdentityEntity;
-import org.keycloak.models.jpa.entities.UserAttributeEntity;
 import org.keycloak.models.jpa.entities.UserConsentEntity;
 import org.keycloak.models.jpa.entities.UserConsentProtocolMapperEntity;
 import org.keycloak.models.jpa.entities.UserConsentRoleEntity;
@@ -364,6 +363,18 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
 
 
     @Override
+    public void setNotBeforeForUser(RealmModel realm, UserModel user, int notBefore) {
+        UserEntity entity = em.getReference(UserEntity.class, user.getId());
+        entity.setNotBefore(notBefore);
+    }
+
+    @Override
+    public int getNotBeforeOfUser(RealmModel realm, UserModel user) {
+        UserEntity entity = em.getReference(UserEntity.class, user.getId());
+        return entity.getNotBefore();
+    }
+
+    @Override
     public void grantToAllUsers(RealmModel realm, RoleModel role) {
         int num = em.createNamedQuery("grantRoleToAllUsers")
                 .setParameter("realmId", realm.getId())
diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java
index cded4e9..f6de431 100644
--- a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java
@@ -416,6 +416,20 @@ public class JpaUserFederatedStorageProvider implements
     }
 
 
+    @Override
+    public void setNotBeforeForUser(RealmModel realm, String userId, int notBefore) {
+        // Track it as attribute for now
+        String notBeforeStr = String.valueOf(notBefore);
+        setSingleAttribute(realm, userId, "fedNotBefore", notBeforeStr);
+    }
+
+    @Override
+    public int getNotBeforeOfUser(RealmModel realm, String userId) {
+        MultivaluedHashMap<String, String> attrs = getAttributes(realm, userId);
+        String notBeforeStr = attrs.getFirst("fedNotBefore");
+
+        return notBeforeStr==null ? 0 : Integer.parseInt(notBeforeStr);
+    }
 
     @Override
     public Set<GroupModel> getGroups(RealmModel realm, String userId) {
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-3.3.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.3.0.xml
new file mode 100644
index 0000000..c6d201e
--- /dev/null
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.3.0.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+  ~ Copyright 2017 Red Hat, Inc. and/or its affiliates
+  ~ and other contributors as indicated by the @author tags.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
+
+    <changeSet author="keycloak" id="3.3.0">
+        <addColumn tableName="USER_ENTITY">
+            <column name="NOT_BEFORE" type="INT" defaultValueNumeric="0"/>
+        </addColumn>
+    </changeSet>
+
+</databaseChangeLog>
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
index ae7d98b..96b9a18 100755
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
@@ -48,4 +48,5 @@
     <include file="META-INF/jpa-changelog-2.5.1.xml"/>
     <include file="META-INF/jpa-changelog-3.0.0.xml"/>
     <include file="META-INF/jpa-changelog-3.2.0.xml"/>
+    <include file="META-INF/jpa-changelog-3.3.0.xml"/>
 </databaseChangeLog>
diff --git a/server-spi/src/main/java/org/keycloak/models/UserProvider.java b/server-spi/src/main/java/org/keycloak/models/UserProvider.java
index 46bb4c3..c337f3a 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserProvider.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserProvider.java
@@ -51,6 +51,8 @@ public interface UserProvider extends Provider,
     void updateConsent(RealmModel realm, String userId, UserConsentModel consent);
     boolean revokeConsentForClient(RealmModel realm, String userId, String clientInternalId);
 
+    void setNotBeforeForUser(RealmModel realm, UserModel user, int notBefore);
+    int getNotBeforeOfUser(RealmModel realm, UserModel user);
 
     UserModel getServiceAccount(ClientModel client);
     List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts);
diff --git a/server-spi/src/main/java/org/keycloak/storage/federated/UserFederatedStorageProvider.java b/server-spi/src/main/java/org/keycloak/storage/federated/UserFederatedStorageProvider.java
index 1d12d36..da42c4c 100755
--- a/server-spi/src/main/java/org/keycloak/storage/federated/UserFederatedStorageProvider.java
+++ b/server-spi/src/main/java/org/keycloak/storage/federated/UserFederatedStorageProvider.java
@@ -36,6 +36,7 @@ public interface UserFederatedStorageProvider extends Provider,
         UserAttributeFederatedStorage,
         UserBrokerLinkFederatedStorage,
         UserConsentFederatedStorage,
+        UserNotBeforeFederatedStorage,
         UserGroupMembershipFederatedStorage,
         UserRequiredActionsFederatedStorage,
         UserRoleMappingsFederatedStorage,
diff --git a/server-spi/src/main/java/org/keycloak/storage/federated/UserNotBeforeFederatedStorage.java b/server-spi/src/main/java/org/keycloak/storage/federated/UserNotBeforeFederatedStorage.java
new file mode 100644
index 0000000..4b18026
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/storage/federated/UserNotBeforeFederatedStorage.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.storage.federated;
+
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface UserNotBeforeFederatedStorage {
+
+    void setNotBeforeForUser(RealmModel realm, String userId, int notBefore);
+    int getNotBeforeOfUser(RealmModel realm, String userId);
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 6b7016f..ef95c0a 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -185,6 +185,8 @@ public class ModelToRepresentation {
         rep.setDisableableCredentialTypes(session.userCredentialManager().getDisableableCredentialTypes(realm, user));
         rep.setFederationLink(user.getFederationLink());
 
+        rep.setNotBefore(session.users().getNotBeforeOfUser(realm, user));
+
         List<String> reqActions = new ArrayList<String>();
         Set<String> requiredActions = user.getRequiredActions();
         for (String ra : requiredActions){
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index fe27fae..3fdddde 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -1454,6 +1454,11 @@ public class RepresentationToModel {
                 session.users().addConsent(newRealm, user.getId(), consentModel);
             }
         }
+
+        if (userRep.getNotBefore() != null) {
+            session.users().setNotBeforeForUser(newRealm, user, userRep.getNotBefore());
+        }
+
         if (userRep.getServiceAccountClientId() != null) {
             String clientId = userRep.getServiceAccountClientId();
             ClientModel client = newRealm.getClientByClientId(clientId);
@@ -2378,6 +2383,9 @@ public class RepresentationToModel {
                 federatedStorage.addConsent(newRealm, userRep.getId(), consentModel);
             }
         }
+        if (userRep.getNotBefore() != null) {
+            federatedStorage.setNotBeforeForUser(newRealm, userRep.getId(), userRep.getNotBefore());
+        }
 
 
     }
diff --git a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
index facce6c..fa1e238 100755
--- a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
+++ b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
@@ -530,6 +530,10 @@ public class ExportUtils {
             userRep.setClientConsents(consentReps);
         }
 
+        // Not Before
+        int notBefore = session.users().getNotBeforeOfUser(realm, user);
+        userRep.setNotBefore(notBefore);
+
         // Service account
         if (user.getServiceAccountClientLink() != null) {
             String clientInternalId = user.getServiceAccountClientLink();
@@ -717,6 +721,10 @@ public class ExportUtils {
             userRep.setClientConsents(consentReps);
         }
 
+        // Not Before
+        int notBefore = session.userFederatedStorage().getNotBeforeOfUser(realm, userRep.getId());
+        userRep.setNotBefore(notBefore);
+
         if (options.isGroupsAndRolesIncluded()) {
             List<String> groups = new LinkedList<>();
             for (GroupModel group : session.userFederatedStorage().getGroups(realm, id)) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index 6a26c69..769947a 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -180,6 +180,9 @@ public class TokenManager {
         if (oldToken.getIssuedAt() < realm.getNotBefore()) {
             throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
         }
+        if (oldToken.getIssuedAt() < session.users().getNotBeforeOfUser(realm, user)) {
+            throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
+        }
 
 
         // recreate token.
@@ -207,9 +210,12 @@ public class TokenManager {
         if (!user.isEnabled()) {
             return false;
         }
+        if (token.getIssuedAt() < session.users().getNotBeforeOfUser(realm, user)) {
+            return false;
+        }
 
         ClientModel client = realm.getClientByClientId(token.getIssuedFor());
-        if (client == null || !client.isEnabled()) {
+        if (client == null || !client.isEnabled() || token.getIssuedAt() < client.getNotBefore()) {
             return false;
         }
 
@@ -816,9 +822,13 @@ public class TokenManager {
                     res.setRefreshExpiresIn(refreshToken.getExpiration() - Time.currentTime());
                 }
             }
+
             int notBefore = realm.getNotBefore();
             if (client.getNotBefore() > notBefore) notBefore = client.getNotBefore();
+            int userNotBefore = session.users().getNotBeforeOfUser(realm, userSession.getUser());
+            if (userNotBefore > notBefore) notBefore = userNotBefore;
             res.setNotBeforePolicy(notBefore);
+
             return res;
         }
     }
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 bc28fc4..0235120 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -817,6 +817,12 @@ public class AuthenticationManager {
                 return null;
             }
 
+            int userNotBefore = session.users().getNotBeforeOfUser(realm, user);
+            if (token.getIssuedAt() < userNotBefore) {
+                logger.debug("User notBefore newer than token");
+                return null;
+            }
+
             UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
             if (!isSessionValid(realm, userSession)) {
                 // Check if accessToken was for the offline session.
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 3d71c2a..dbb0e78 100755
--- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
@@ -107,6 +107,8 @@ public class ResourceAdminManager {
     }
 
     public void logoutUser(URI requestUri, RealmModel realm, UserModel user, KeycloakSession keycloakSession) {
+        keycloakSession.users().setNotBeforeForUser(realm, user, Time.currentTime());
+
         List<UserSessionModel> userSessions = keycloakSession.sessions().getUserSessions(realm, user);
         logoutUserSessions(requestUri, realm, userSessions);
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index b814abd..9b26b18 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -18,6 +18,7 @@ package org.keycloak.services.resources;
 
 import org.jboss.logging.Logger;
 import org.keycloak.common.util.Base64Url;
+import org.keycloak.common.util.Time;
 import org.keycloak.common.util.UriUtils;
 import org.keycloak.credential.CredentialModel;
 import org.keycloak.events.Details;
@@ -505,6 +506,11 @@ public class AccountService extends AbstractSecuredLocalService {
         csrfCheck(stateChecker);
 
         UserModel user = auth.getUser();
+
+        // Rather decrease time a bit. To avoid situation when user is immediatelly redirected to login screen, then automatically authenticated (eg. with Kerberos) and then seeing issues due the stale token
+        // as time on the token will be same like notBefore
+        session.users().setNotBeforeForUser(realm, user, Time.currentTime() - 1);
+
         List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
         for (UserSessionModel userSession : userSessions) {
             AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
index fbd318a..21943cc 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
@@ -508,6 +508,8 @@ public class UserResource {
     public void logout() {
         auth.users().requireManage(user);
 
+        session.users().setNotBeforeForUser(realm, user, Time.currentTime());
+
         List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
         for (UserSessionModel userSession : userSessions) {
             AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java
index 2baeb8c..8c5b633 100755
--- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java
+++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java
@@ -231,6 +231,25 @@ public class UserStorageManager implements UserProvider, OnUserCache, OnCreateCo
         }
     }
 
+    @Override
+    public void setNotBeforeForUser(RealmModel realm, UserModel user, int notBefore) {
+        if (StorageId.isLocalStorage(user)) {
+            localStorage().setNotBeforeForUser(realm, user, notBefore);
+        } else {
+            getFederatedStorage().setNotBeforeForUser(realm, user.getId(), notBefore);
+        }
+    }
+
+    @Override
+    public int getNotBeforeOfUser(RealmModel realm, UserModel user) {
+        if (StorageId.isLocalStorage(user)) {
+            return localStorage().getNotBeforeOfUser(realm, user);
+
+        } else {
+            return getFederatedStorage().getNotBeforeOfUser(realm, user.getId());
+        }
+    }
+
     /**
      * Allows a UserStorageProvider to proxy and/or synchronize an imported user.
      *
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
index e006820..98215f7 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
@@ -126,6 +126,14 @@ public class AdapterTestStrategy extends ExternalResource {
     protected void after() {
         super.after();
         webRule.after();
+
+        // Revert notBefore
+        KeycloakSession session = keycloakRule.startSession();
+        RealmModel realm = session.realms().getRealmByName("demo");
+        UserModel user = session.users().getUserByUsername("bburke@redhat.com", realm);
+        session.users().setNotBeforeForUser(realm, user, 0);
+        session.getTransactionManager().commit();
+        session.close();
     }
 
     public void testSavedPostRequest() throws Exception {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/FederatedStorageExportImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/FederatedStorageExportImportTest.java
index 8a1bf5d..a1e4411 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/FederatedStorageExportImportTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/FederatedStorageExportImportTest.java
@@ -137,7 +137,7 @@ public class FederatedStorageExportImportTest {
 
         Assert.assertEquals(1, session.userFederatedStorage().getStoredUsersCount(realm));
         MultivaluedHashMap<String, String> attributes = session.userFederatedStorage().getAttributes(realm, userId);
-        Assert.assertEquals(2, attributes.size());
+        Assert.assertEquals(3, attributes.size());
         Assert.assertEquals("value1", attributes.getFirst("single1"));
         Assert.assertTrue(attributes.getList("list1").contains("1"));
         Assert.assertTrue(attributes.getList("list1").contains("2"));
@@ -174,6 +174,7 @@ public class FederatedStorageExportImportTest {
         session.userFederatedStorage().createCredential(realm, userId, credential);
         session.userFederatedStorage().grantRole(realm, userId, role);
         session.userFederatedStorage().joinGroup(realm, userId, group);
+        session.userFederatedStorage().setNotBeforeForUser(realm, userId, 50);
         keycloakRule.stopSession(session, true);
 
 
@@ -203,13 +204,14 @@ public class FederatedStorageExportImportTest {
 
         Assert.assertEquals(1, session.userFederatedStorage().getStoredUsersCount(realm));
         MultivaluedHashMap<String, String> attributes = session.userFederatedStorage().getAttributes(realm, userId);
-        Assert.assertEquals(2, attributes.size());
+        Assert.assertEquals(3, attributes.size());
         Assert.assertEquals("value1", attributes.getFirst("single1"));
         Assert.assertTrue(attributes.getList("list1").contains("1"));
         Assert.assertTrue(attributes.getList("list1").contains("2"));
         Assert.assertTrue(session.userFederatedStorage().getRequiredActions(realm, userId).contains("UPDATE_PASSWORD"));
         Assert.assertTrue(session.userFederatedStorage().getRoleMappings(realm, userId).contains(role));
         Assert.assertTrue(session.userFederatedStorage().getGroups(realm, userId).contains(group));
+        Assert.assertEquals(50, session.userFederatedStorage().getNotBeforeOfUser(realm, userId));
         List<CredentialModel> creds = session.userFederatedStorage().getStoredCredentials(realm, userId);
         Assert.assertEquals(1, creds.size());
         Assert.assertTrue(getHashProvider(session, realm.getPasswordPolicy()).verify("password", creds.get(0)));
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
index 191a39a..4e0dca5 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
@@ -241,7 +241,7 @@ public class UserModelTest extends AbstractModelTest {
     @Test
     public void testUpdateUserSingleAttribute() {
         Map<String, List<String>> expected = ImmutableMap.of(
-                "key1", Arrays.asList("value3"), 
+                "key1", Arrays.asList("value3"),
                 "key2", Arrays.asList("value2"));
         
         RealmModel realm = realmManager.createRealm("original");
@@ -398,6 +398,31 @@ public class UserModelTest extends AbstractModelTest {
         Assert.assertFalse(realm2User1.hasRole(role1));
     }
 
+    @Test
+    public void testUserNotBefore() throws Exception {
+        RealmModel realm = realmManager.createRealm("original");
+
+        UserModel user1 = session.users().addUser(realm, "user1");
+        session.users().setNotBeforeForUser(realm, user1, 10);
+
+        commit();
+
+        realm = realmManager.getRealmByName("original");
+        user1 = session.users().getUserByUsername("user1", realm);
+        int notBefore = session.users().getNotBeforeOfUser(realm, user1);
+        Assert.assertEquals(10, notBefore);
+
+        // Try to update
+        session.users().setNotBeforeForUser(realm, user1, 20);
+
+        commit();
+
+        realm = realmManager.getRealmByName("original");
+        user1 = session.users().getUserByUsername("user1", realm);
+        notBefore = session.users().getNotBeforeOfUser(realm, user1);
+        Assert.assertEquals(20, notBefore);
+    }
+
     public static void assertEquals(UserModel expected, UserModel actual) {
         Assert.assertEquals(expected.getUsername(), actual.getUsername());
         Assert.assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java
index 19e45d1..a7f9559 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java
@@ -18,6 +18,9 @@
 package org.keycloak.testsuite.crossdc;
 
 
+import java.util.ArrayList;
+import java.util.List;
+
 import javax.ws.rs.NotFoundException;
 
 import org.hamcrest.Matchers;
@@ -101,7 +104,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
             @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
             @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
-        createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics);
+        createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
 
 //        log.infof("Sleeping!");
 //        Thread.sleep(10000000);
@@ -118,7 +121,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
 
     // Return last used accessTokenResponse
-    private OAuthClient.AccessTokenResponse createInitialSessions(String cacheName, boolean offline, InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics) throws Exception {
+    private List<OAuthClient.AccessTokenResponse> createInitialSessions(String cacheName, boolean offline, InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics, boolean includeRemoteStats) throws Exception {
 
         // Enable second DC
         enableDcOnLoadBalancer(DC.SECOND);
@@ -137,9 +140,9 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
             oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
         }
 
-        OAuthClient.AccessTokenResponse lastAccessTokenResponse = null;
+        List<OAuthClient.AccessTokenResponse> responses = new ArrayList<>();
         for (int i=0 ; i<SESSIONS_COUNT ; i++) {
-            lastAccessTokenResponse = oauth.doGrantAccessTokenRequest("password", "login-test", "password");
+            responses.add(oauth.doGrantAccessTokenRequest("password", "login-test", "password"));
         }
 
         // Assert 20 sessions exists on node1 and node2 and on remote caches
@@ -152,11 +155,14 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
             Assert.assertEquals(sessions11, sessions01 + SESSIONS_COUNT);
             Assert.assertEquals(sessions12, sessions02 + SESSIONS_COUNT);
-            Assert.assertEquals(remoteSessions11, remoteSessions01 + SESSIONS_COUNT);
-            Assert.assertEquals(remoteSessions12, remoteSessions02 + SESSIONS_COUNT);
+
+            if (includeRemoteStats) {
+                Assert.assertEquals(remoteSessions11, remoteSessions01 + SESSIONS_COUNT);
+                Assert.assertEquals(remoteSessions12, remoteSessions02 + SESSIONS_COUNT);
+            }
         }, 50, 50);
 
-        return lastAccessTokenResponse;
+        return responses;
     }
 
 
@@ -191,7 +197,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
             @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
 
-        createInitialSessions(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics);
+        createInitialSessions(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics, true);
 
         channelStatisticsCrossDc.reset();
 
@@ -210,7 +216,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
             @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
 
-        createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics);
+        createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
 
         channelStatisticsCrossDc.reset();
 
@@ -229,7 +235,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
             @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
 
-        OAuthClient.AccessTokenResponse lastAccessTokenResponse = createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics);
+        OAuthClient.AccessTokenResponse lastAccessTokenResponse = createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true).get(SESSIONS_COUNT - 1);
 
         // Assert I am able to refresh
         OAuthClient.AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
@@ -273,7 +279,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
             @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
             @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
-        createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics);
+        createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
 
 //        log.infof("Sleeping!");
 //        Thread.sleep(10000000);
@@ -295,7 +301,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
             @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
             @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
-        createInitialSessions(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics);
+        createInitialSessions(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics, true);
 
 //        log.infof("Sleeping!");
 //        Thread.sleep(10000000);
@@ -318,7 +324,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
             @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
 
-        createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics);
+        createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
 
         channelStatisticsCrossDc.reset();
 
@@ -340,6 +346,39 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
     }
 
 
+    @Test
+    public void testLogoutUserWithFailover(
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+            @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
+
+        // Start node2 on first DC
+        startBackendNode(DC.FIRST, 1);
+
+        // Don't include remote stats. Size is smaller because of distributed cache
+        List<OAuthClient.AccessTokenResponse> responses = createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, false);
+
+        // Kill node2 now. Around 10 sessions (half of SESSIONS_COUNT) will be lost on Keycloak side. But not on infinispan side
+        stopBackendNode(DC.FIRST, 1);
+
+        channelStatisticsCrossDc.reset();
+
+        // Increase offset a bit to ensure logout happens later then token issued time
+        setTimeOffset(10);
+
+        // Logout user
+        ApiUtil.findUserByUsernameId(getAdminClient().realm(REALM_NAME), "login-test").logout();
+
+        // Assert it's not possible to refresh sessions. Works because user.notBefore
+        int i = 0;
+        for (OAuthClient.AccessTokenResponse response : responses) {
+            i++;
+            OAuthClient.AccessTokenResponse refreshTokenResponse = oauth.doRefreshTokenRequest(response.getRefreshToken(), "password");
+            Assert.assertNull("Failed in iteration " + i, refreshTokenResponse.getRefreshToken());
+            Assert.assertNotNull("Failed in iteration " + i, refreshTokenResponse.getError());
+        }
+    }
+
 
 
     // AUTH SESSIONS
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
index 677430d..d1c7007 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
@@ -154,6 +154,8 @@ public class ExportImportUtil {
 
         Assert.assertNull(realmRsc.users().get(wburke.getId()).roles().getAll().getRealmMappings());
 
+        Assert.assertEquals((Object) 159, wburke.getNotBefore());
+
         UserRepresentation loginclient = findByUsername(realmRsc, "loginclient");
         // user with creation timestamp as string in import
         Assert.assertEquals(new Long(123655), loginclient.getCreatedTimestamp());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
index 7fc3f74..a3a03cb 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
@@ -21,8 +21,11 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.events.Details;
 import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.Assert;
 import org.keycloak.testsuite.AssertEvents;
 import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.admin.ApiUtil;
 import org.keycloak.testsuite.pages.AppPage;
 import org.keycloak.testsuite.pages.LoginPage;
 
@@ -210,4 +213,23 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest {
         events.expectLogout(sessionId2).removeDetail(Details.REDIRECT_URI).assertEvent();
     }
 
+    @Test
+    public void logoutUserByAdmin() {
+        loginPage.open();
+        loginPage.login("test-user@localhost", "password");
+        assertTrue(appPage.isCurrent());
+        String sessionId = events.expectLogin().assertEvent().getSessionId();
+
+        UserRepresentation user = ApiUtil.findUserByUsername(adminClient.realm("test"), "test-user@localhost");
+        Assert.assertEquals((Object) 0, user.getNotBefore());
+
+        adminClient.realm("test").users().get(user.getId()).logout();
+
+        user = adminClient.realm("test").users().get(user.getId()).toRepresentation();
+        Assert.assertTrue(user.getNotBefore() > 0);
+
+        loginPage.open();
+        loginPage.assertCurrent();
+    }
+
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json
index fb1a7e0..a0c5b30 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json
@@ -120,6 +120,7 @@
             "username": "wburke",
             "enabled": true,
             "createdTimestamp" : 123654,
+            "notBefore": 159,
             "attributes": {
                 "email": "bburke@redhat.com"
             },