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 4dcea953df..44458fd7e9 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 realmRoles; protected Map> clientRoles; protected List clientConsents; + protected Integer notBefore; @Deprecated protected Map> 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> getApplicationRoles() { return applicationRoles; 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 0d971f70b9..390c25c158 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/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 1bf6e426a0..68dfc372cb 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 requiredActions = new HashSet<>(); private Set roleMappings = new HashSet<>(); private Set 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/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index b9352c0588..b543d6b2f4 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; @@ -363,6 +362,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") 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 50b77607a6..771487f754 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/storage/jpa/JpaUserFederatedStorageProvider.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java index cded4e997b..f6de4312a3 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 attrs = getAttributes(realm, userId); + String notBeforeStr = attrs.getFirst("fedNotBefore"); + + return notBeforeStr==null ? 0 : Integer.parseInt(notBeforeStr); + } @Override public Set 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 0000000000..c6d201eb49 --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.3.0.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + 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 ae7d98b4e4..96b9a18dd0 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 @@ + 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 6b7016ff5f..ef95c0ae6a 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 reqActions = new ArrayList(); Set 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 fe27fae666..3fdddde16a 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/server-spi/src/main/java/org/keycloak/models/UserProvider.java b/server-spi/src/main/java/org/keycloak/models/UserProvider.java index 46bb4c3117..c337f3ac87 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 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 1d12d3626f..da42c4c868 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 0000000000..4b18026645 --- /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 Marek Posolda + */ +public interface UserNotBeforeFederatedStorage { + + void setNotBeforeForUser(RealmModel realm, String userId, int notBefore); + int getNotBeforeOfUser(RealmModel realm, String userId); +} 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 facce6ca2d..fa1e238b04 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 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 6a26c692d5..769947a0b8 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 bc28fc4afd..02351204aa 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 3d71c2a663..dbb0e78c4b 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 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 b814abdb9c..9b26b18c07 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 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 fbd318ae74..21943ccd79 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 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 2baeb8ceb2..8c5b633f89 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-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 19e45d1b62..a7f955924e 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 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 responses = new ArrayList<>(); for (int i=0 ; i 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 677430d23a..d1c70075c3 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 7fc3f743d0..a3a03cbe68 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 fb1a7e0002..a0c5b3011e 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" }, 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 e006820549..98215f7bfb 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 8a1bf5dbeb..a1e4411359 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 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 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 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 191a39af89..4e0dca51c1 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> 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());