Add expiration field to root authentication session

This commit is contained in:
Martin Kanis 2022-03-14 10:02:31 +01:00 committed by Hynek Mlnařík
parent f8ded02bef
commit e493b08fa7
15 changed files with 86 additions and 51 deletions

View file

@ -36,7 +36,7 @@ import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
import org.keycloak.models.sessions.infinispan.stream.RootAuthenticationSessionPredicate;
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
import org.keycloak.models.utils.RealmInfoUtil;
import org.keycloak.models.utils.SessionExpiration;
import org.keycloak.sessions.AuthenticationSessionCompoundId;
import org.keycloak.sessions.AuthenticationSessionProvider;
import org.keycloak.sessions.RootAuthenticationSessionModel;
@ -83,7 +83,7 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
entity.setRealmId(realm.getId());
entity.setTimestamp(Time.currentTime());
int expirationSeconds = RealmInfoUtil.getDettachedClientSessionLifespan(realm);
int expirationSeconds = SessionExpiration.getAuthSessionLifespan(realm);
tx.put(cache, id, entity, expirationSeconds, TimeUnit.SECONDS);
return wrap(realm, entity);

View file

@ -31,7 +31,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
import org.keycloak.models.utils.RealmInfoUtil;
import org.keycloak.models.utils.SessionExpiration;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
@ -63,7 +63,7 @@ public class RootAuthenticationSessionAdapter implements RootAuthenticationSessi
}
void update() {
int expirationSeconds = RealmInfoUtil.getDettachedClientSessionLifespan(realm);
int expirationSeconds = SessionExpiration.getAuthSessionLifespan(realm);
provider.tx.replace(cache, entity.getId(), entity, expirationSeconds, TimeUnit.SECONDS);
}

View file

@ -48,11 +48,14 @@ public class HotRodRootAuthenticationSessionEntity extends AbstractHotRodEntity
@ProtoField(number = 3)
public String realmId;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 4)
public Integer timestamp;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 5)
public Long expiration;
@ProtoField(number = 6)
public Set<HotRodAuthenticationSessionEntity> authenticationSessions;
public static abstract class AbstractHotRodRootAuthenticationSessionEntityDelegate extends UpdatableHotRodEntityDelegateImpl<HotRodRootAuthenticationSessionEntity> implements MapRootAuthenticationSessionEntity {

View file

@ -22,6 +22,7 @@ import org.keycloak.common.util.Time;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.SessionExpiration;
import org.keycloak.sessions.AuthenticationSessionModel;
import java.util.Collections;
@ -57,6 +58,7 @@ public class MapRootAuthenticationSessionAdapter extends AbstractRootAuthenticat
@Override
public void setTimestamp(int timestamp) {
entity.setTimestamp(timestamp);
entity.setExpiration(SessionExpiration.getAuthSessionExpiration(realm, timestamp));
}
@Override
@ -90,6 +92,7 @@ public class MapRootAuthenticationSessionAdapter extends AbstractRootAuthenticat
// Update our timestamp when adding new authenticationSession
entity.setTimestamp(timestamp);
entity.setExpiration(SessionExpiration.getAuthSessionExpiration(realm, timestamp));
return entity.getAuthenticationSession(tabId).map(this::toAdapter).map(this::setAuthContext).orElse(null);
}
@ -101,7 +104,9 @@ public class MapRootAuthenticationSessionAdapter extends AbstractRootAuthenticat
if (entity.getAuthenticationSessions().isEmpty()) {
session.authenticationSessions().removeRootAuthenticationSession(realm, this);
} else {
entity.setTimestamp(Time.currentTime());
int timestamp = Time.currentTime();
entity.setTimestamp(timestamp);
entity.setExpiration(SessionExpiration.getAuthSessionExpiration(realm, timestamp));
}
}
}
@ -109,7 +114,9 @@ public class MapRootAuthenticationSessionAdapter extends AbstractRootAuthenticat
@Override
public void restartSession(RealmModel realm) {
entity.setAuthenticationSessions(null);
entity.setTimestamp(Time.currentTime());
int timestamp = Time.currentTime();
entity.setTimestamp(timestamp);
entity.setExpiration(SessionExpiration.getAuthSessionExpiration(realm, timestamp));
}
private String generateTabId() {

View file

@ -87,6 +87,9 @@ public interface MapRootAuthenticationSessionEntity extends AbstractEntity, Upda
Integer getTimestamp();
void setTimestamp(Integer timestamp);
Long getExpiration();
void setExpiration(Long expiration);
Set<MapAuthenticationSessionEntity> getAuthenticationSessions();
void setAuthenticationSessions(Set<MapAuthenticationSessionEntity> authenticationSessions);
Optional<MapAuthenticationSessionEntity> getAuthenticationSession(String tabId);

View file

@ -27,12 +27,13 @@ import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
import org.keycloak.models.utils.RealmInfoUtil;
import org.keycloak.models.utils.SessionExpiration;
import org.keycloak.sessions.AuthenticationSessionCompoundId;
import org.keycloak.sessions.AuthenticationSessionProvider;
import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel.SearchableFields;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
@ -63,7 +64,15 @@ public class MapRootAuthenticationSessionProvider implements AuthenticationSessi
private Function<MapRootAuthenticationSessionEntity, RootAuthenticationSessionModel> entityToAdapterFunc(RealmModel realm) {
// Clone entity before returning back, to avoid giving away a reference to the live object to the caller
return origEntity -> new MapRootAuthenticationSessionAdapter(session, realm, origEntity);
return origEntity -> {
//return new MapRootAuthenticationSessionAdapter(session, realm, origEntity);
if (Time.currentTime() < origEntity.getExpiration()) {
return new MapRootAuthenticationSessionAdapter(session, realm, origEntity);
} else {
tx.delete(origEntity.getId());
return null;
}
};
}
private Predicate<MapRootAuthenticationSessionEntity> entityRealmFilter(String realmId) {
@ -89,7 +98,9 @@ public class MapRootAuthenticationSessionProvider implements AuthenticationSessi
MapRootAuthenticationSessionEntity entity = new MapRootAuthenticationSessionEntityImpl();
entity.setId(id);
entity.setRealmId(realm.getId());
entity.setTimestamp(Time.currentTime());
int timestamp = Time.currentTime();
entity.setTimestamp(timestamp);
entity.setExpiration(SessionExpiration.getAuthSessionExpiration(realm, timestamp));
if (id != null && tx.read(id) != null) {
throw new ModelDuplicateException("Root authentication session exists: " + entity.getId());
@ -131,11 +142,9 @@ public class MapRootAuthenticationSessionProvider implements AuthenticationSessi
Objects.requireNonNull(realm, "The provided realm can't be null!");
LOG.debugf("Removing expired sessions");
int expired = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm);
DefaultModelCriteria<RootAuthenticationSessionModel> mcb = criteria();
mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId())
.compare(SearchableFields.TIMESTAMP, Operator.LT, expired);
.compare(SearchableFields.EXPIRATION, Operator.LT, Time.currentTime());
long deletedCount = tx.delete(withCriteria(mcb));

View file

@ -141,7 +141,7 @@ public class MapFieldPredicates {
put(USER_PREDICATES, UserModel.SearchableFields.SERVICE_ACCOUNT_CLIENT, MapUserEntity::getServiceAccountClientLink);
put(AUTHENTICATION_SESSION_PREDICATES, RootAuthenticationSessionModel.SearchableFields.REALM_ID, MapRootAuthenticationSessionEntity::getRealmId);
put(AUTHENTICATION_SESSION_PREDICATES, RootAuthenticationSessionModel.SearchableFields.TIMESTAMP, MapRootAuthenticationSessionEntity::getTimestamp);
put(AUTHENTICATION_SESSION_PREDICATES, RootAuthenticationSessionModel.SearchableFields.EXPIRATION, MapRootAuthenticationSessionEntity::getExpiration);
put(AUTHZ_RESOURCE_SERVER_PREDICATES, ResourceServer.SearchableFields.ID, predicateForKeyField(MapResourceServerEntity::getId));

View file

@ -22,9 +22,9 @@ import org.keycloak.models.RealmModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RealmInfoUtil {
public class SessionExpiration {
public static int getDettachedClientSessionLifespan(RealmModel realm) {
public static int getAuthSessionLifespan(RealmModel realm) {
int lifespan = realm.getAccessCodeLifespanLogin();
if (realm.getAccessCodeLifespanUserAction() > lifespan) {
lifespan = realm.getAccessCodeLifespanUserAction();
@ -35,4 +35,8 @@ public class RealmInfoUtil {
return lifespan;
}
public static long getAuthSessionExpiration(RealmModel realm, int timestamp) {
return (long) timestamp + getAuthSessionLifespan(realm);
}
}

View file

@ -34,7 +34,7 @@ public interface RootAuthenticationSessionModel {
public static class SearchableFields {
public static final SearchableModelField<RootAuthenticationSessionModel> ID = new SearchableModelField<>("id", String.class);
public static final SearchableModelField<RootAuthenticationSessionModel> REALM_ID = new SearchableModelField<>("realmId", String.class);
public static final SearchableModelField<RootAuthenticationSessionModel> TIMESTAMP = new SearchableModelField<>("timestamp", Long.class);
public static final SearchableModelField<RootAuthenticationSessionModel> EXPIRATION = new SearchableModelField<>("expiration", Long.class);
}
/**
@ -57,6 +57,7 @@ public interface RootAuthenticationSessionModel {
/**
* Sets a timestamp when the root authentication session was created or updated.
* It also updates the expiration time for the root authentication session entity.
* @param timestamp {@code int}
*/
void setTimestamp(int timestamp);

View file

@ -52,6 +52,7 @@ import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.updaters.UserAttributeUpdater;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.testsuite.util.MailUtils;
import org.keycloak.testsuite.util.SecondBrowser;
import org.keycloak.testsuite.util.UserActionTokenBuilder;
@ -91,6 +92,9 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
@Rule
public GreenMailRule greenMail = new GreenMailRule();
@Rule
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
@Page
protected AppPage appPage;
@ -454,7 +458,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
events.poll();
try {
setTimeOffset(3600);
setTimeOffset(360);
driver.navigate().to(verificationUrl.trim());
@ -990,7 +994,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
String verificationUrl = getPasswordResetEmailLink(message);
try {
setTimeOffset(3600);
setTimeOffset(360);
driver.navigate().to(verificationUrl.trim());

View file

@ -8,13 +8,18 @@ import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
import java.util.List;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
public class KcOidcBrokerWithConsentTest extends AbstractInitializedBaseBrokerTest {
@Rule
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return KcOidcBrokerConfiguration.INSTANCE;
@ -35,8 +40,8 @@ public class KcOidcBrokerWithConsentTest extends AbstractInitializedBaseBrokerTe
// Change timeouts on realm-with-broker to lower values
RealmResource realmWithBroker = adminClient.realm(bc.consumerRealmName());
RealmRepresentation realmRep = realmWithBroker.toRepresentation();
realmRep.setAccessCodeLifespanLogin(30);;
realmRep.setAccessCodeLifespan(30);
realmRep.setAccessCodeLifespanLogin(30);
realmRep.setAccessCodeLifespan(300);
realmRep.setAccessCodeLifespanUserAction(30);
realmWithBroker.update(realmRep);
}

View file

@ -60,6 +60,7 @@ import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.ContainerAssume;
import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.testsuite.util.JavascriptBrowser;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.Matchers;
@ -87,7 +88,6 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.keycloak.common.Profile.Feature.AUTHORIZATION;
import static org.keycloak.common.Profile.Feature.DYNAMIC_SCOPES;
import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
@ -165,6 +165,9 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
@Page
protected LoginPasswordUpdatePage updatePasswordPage;
@Rule
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
private static String userId;
private static String user2Id;
@ -762,9 +765,8 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
@Test
public void loginExpiredCode() {
loginPage.open();
// authSession expired and removed from the storage
setTimeOffset(5000);
// No explicitly call "removeExpired". Hence authSession will still exists, but will be expired
//testingClient.testing().removeExpired("test");
loginPage.login("login@test.com", "password");
loginPage.assertCurrent();
@ -772,35 +774,28 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError());
setTimeOffset(0);
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
events.expectLogin().client((String) null).user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
.assertEvent();
}
// KEYCLOAK-1037
@Test
public void loginExpiredCodeWithExplicitRemoveExpired() {
getTestingClient().testing().setTestingInfinispanTimeService();
loginPage.open();
setTimeOffset(5000);
try {
loginPage.open();
setTimeOffset(5000);
// Explicitly call "removeExpired". Hence authSession won't exist, but will be restarted from the KC_RESTART
testingClient.testing().removeExpired("test");
loginPage.login("login@test.com", "password");
loginPage.login("login@test.com", "password");
loginPage.assertCurrent();
//loginPage.assertCurrent();
loginPage.assertCurrent();
Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError());
Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError());
setTimeOffset(0);
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
.client((String) null)
.assertEvent();
} finally {
getTestingClient().testing().revertTestingInfinispanTimeService();
}
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
.client((String) null)
.assertEvent();
}
@Test

View file

@ -51,6 +51,7 @@ import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.util.BrowserTabUtil;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.testsuite.util.MailUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
@ -92,6 +93,9 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
private String userId;
private UserRepresentation defaultUser;
@Rule
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
@Drone
@SecondBrowser
protected WebDriver driver2;
@ -409,7 +413,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
String changePasswordUrl = MailUtils.getPasswordResetEmailLink(message);
try {
setTimeOffset(1800 + 23);
setTimeOffset(360);
driver.navigate().to(changePasswordUrl.trim());

View file

@ -93,6 +93,7 @@ public class AuthenticationSessionProviderTest extends AbstractTestRealmKeycloak
public void testLoginSessionsCRUD(KeycloakSession session) {
AtomicReference<String> rootAuthSessionID = new AtomicReference<>();
AtomicReference<String> tabID = new AtomicReference<>();
final int timestamp = Time.currentTime();
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCRUD1) -> {
KeycloakSession currentSession = sessionCRUD1;
@ -107,7 +108,7 @@ public class AuthenticationSessionProviderTest extends AbstractTestRealmKeycloak
tabID.set(authSession.getTabId());
authSession.setAction("foo");
rootAuthSession.setTimestamp(100);
rootAuthSession.setTimestamp(timestamp);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCRUD2) -> {
@ -121,11 +122,11 @@ public class AuthenticationSessionProviderTest extends AbstractTestRealmKeycloak
AuthenticationSessionModel authSession = rootAuthSession.getAuthenticationSession(client1, tabID.get());
testAuthenticationSession(authSession, client1.getId(), null, "foo");
assertThat(rootAuthSession.getTimestamp(), is(100));
assertThat(rootAuthSession.getTimestamp(), is(timestamp));
// Update and commit
authSession.setAction("foo-updated");
rootAuthSession.setTimestamp(200);
rootAuthSession.setTimestamp(timestamp + 1000);
authSession.setAuthenticatedUser(currentSession.users().getUserByUsername(realm, "user1"));
});
@ -141,7 +142,7 @@ public class AuthenticationSessionProviderTest extends AbstractTestRealmKeycloak
testAuthenticationSession(authSession, client1.getId(), user1.getId(), "foo-updated");
assertThat(rootAuthSession.getTimestamp(), is(200));
assertThat(rootAuthSession.getTimestamp(), is(timestamp + 1000));
// Remove and commit
currentSession.authenticationSessions().removeRootAuthenticationSession(realm, rootAuthSession);
@ -161,6 +162,7 @@ public class AuthenticationSessionProviderTest extends AbstractTestRealmKeycloak
public void testAuthenticationSessionRestart(KeycloakSession session) {
AtomicReference<String> parentAuthSessionID = new AtomicReference<>();
AtomicReference<String> tabID = new AtomicReference<>();
final int timestamp = Time.currentTime();
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionRestart1) -> {
KeycloakSession currentSession = sessionRestart1;
@ -176,7 +178,7 @@ public class AuthenticationSessionProviderTest extends AbstractTestRealmKeycloak
tabID.set(authSession.getTabId());
authSession.setAction("foo");
authSession.getParentSession().setTimestamp(100);
authSession.getParentSession().setTimestamp(timestamp);
authSession.setAuthenticatedUser(user1);
authSession.setAuthNote("foo", "bar");
@ -256,7 +258,7 @@ public class AuthenticationSessionProviderTest extends AbstractTestRealmKeycloak
RealmModel realm = currentSession.realms().getRealm("test");
RealmModel fooRealm = currentSession.realms().createRealm("foo-realm");
fooRealm.setDefaultRole(currentSession.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + fooRealm.getName()));
fooRealm.setAccessCodeLifespanLogin(1800);
fooRealm.addClient("foo-client");
authSessionID.set(currentSession.authenticationSessions().createRootAuthenticationSession(realm).getId());

View file

@ -179,8 +179,6 @@ public class AuthenticationSessionTest extends KeycloakModelTest {
Assert.assertNotNull(rootAuthSession);
Time.setOffset(1900);
// not needed with Infinispan where expiration handles Infinispan itself
session.authenticationSessions().removeExpired(realm);
return null;
});