keycloak-memoizeit
Changes
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java 7(+6 -1)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java 32(+30 -2)
server-spi/src/main/java/org/keycloak/storage/federated/UserNotBeforeFederatedStorage.java 30(+30 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java 8(+8 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/FederatedStorageExportImportTest.java 6(+4 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java 65(+52 -13)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java 2(+2 -0)
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"
},