KEYCLOAK-5293 Add notBefore to user

This commit is contained in:
mposolda 2017-08-21 21:02:03 +02:00
parent 8d3384666a
commit fe5891fbdb
27 changed files with 321 additions and 21 deletions

View file

@ -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;

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -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")

View file

@ -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;

View file

@ -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) {

View file

@ -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>

View file

@ -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>

View file

@ -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){

View file

@ -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());
}
}

View file

@ -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);

View file

@ -36,6 +36,7 @@ public interface UserFederatedStorageProvider extends Provider,
UserAttributeFederatedStorage,
UserBrokerLinkFederatedStorage,
UserConsentFederatedStorage,
UserNotBeforeFederatedStorage,
UserGroupMembershipFederatedStorage,
UserRequiredActionsFederatedStorage,
UserRoleMappingsFederatedStorage,

View file

@ -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);
}

View file

@ -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)) {

View file

@ -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;
}
}

View file

@ -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.

View file

@ -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);
}

View file

@ -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);

View file

@ -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);

View file

@ -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.
*

View file

@ -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

View file

@ -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());

View file

@ -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();
}
}

View file

@ -120,6 +120,7 @@
"username": "wburke",
"enabled": true,
"createdTimestamp" : 123654,
"notBefore": 159,
"attributes": {
"email": "bburke@redhat.com"
},

View file

@ -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 {

View file

@ -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)));

View file

@ -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());