KEYCLOAK-16616 Limit number of authSessios per rootAuthSession

This commit is contained in:
Martin Kanis 2021-05-25 16:55:41 +02:00 committed by Hynek Mlnařík
parent 122fbe1bc6
commit 23aee6c210
16 changed files with 228 additions and 21 deletions

View file

@ -291,4 +291,18 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
update();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof AuthenticationSessionModel)) return false;
AuthenticationSessionModel that = (AuthenticationSessionModel) o;
return that.getTabId().equals(getTabId());
}
@Override
public int hashCode() {
return getTabId().hashCode();
}
}

View file

@ -51,13 +51,16 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
private final KeycloakSession session;
private final Cache<String, RootAuthenticationSessionEntity> cache;
private final InfinispanKeyGenerator keyGenerator;
private final int authSessionsLimit;
protected final InfinispanKeycloakTransaction tx;
protected final SessionEventsSenderTransaction clusterEventsSenderTx;
public InfinispanAuthenticationSessionProvider(KeycloakSession session, InfinispanKeyGenerator keyGenerator, Cache<String, RootAuthenticationSessionEntity> cache) {
public InfinispanAuthenticationSessionProvider(KeycloakSession session, InfinispanKeyGenerator keyGenerator,
Cache<String, RootAuthenticationSessionEntity> cache, int authSessionsLimit) {
this.session = session;
this.cache = cache;
this.keyGenerator = keyGenerator;
this.authSessionsLimit = authSessionsLimit;
this.tx = new InfinispanKeycloakTransaction();
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
@ -88,7 +91,7 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
private RootAuthenticationSessionAdapter wrap(RealmModel realm, RootAuthenticationSessionEntity entity) {
return entity==null ? null : new RootAuthenticationSessionAdapter(session, this, cache, realm, entity);
return entity==null ? null : new RootAuthenticationSessionAdapter(session, this, cache, realm, entity, authSessionsLimit);
}

View file

@ -54,8 +54,14 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
private volatile Cache<String, RootAuthenticationSessionEntity> authSessionsCache;
private int authSessionsLimit;
public static final String PROVIDER_ID = "infinispan";
public static final String AUTH_SESSIONS_LIMIT = "authSessionsLimit";
public static final int DEFAULT_AUTH_SESSIONS_LIMIT = 300;
public static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS";
public static final String REALM_REMOVED_AUTHSESSION_EVENT = "REALM_REMOVED_EVENT_AUTHSESSIONS";
@ -64,7 +70,10 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
@Override
public void init(Config.Scope config) {
// get auth sessions limit from config or use default if not provided
int configInt = config.getInt(AUTH_SESSIONS_LIMIT, DEFAULT_AUTH_SESSIONS_LIMIT);
// use default if provided value is not a positive number
authSessionsLimit = (configInt <= 0) ? DEFAULT_AUTH_SESSIONS_LIMIT : configInt;
}
@ -115,7 +124,7 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
@Override
public AuthenticationSessionProvider create(KeycloakSession session) {
lazyInit(session);
return new InfinispanAuthenticationSessionProvider(session, keyGenerator, authSessionsCache);
return new InfinispanAuthenticationSessionProvider(session, keyGenerator, authSessionsCache, authSessionsLimit);
}
private void updateAuthNotes(ClusterEvent clEvent) {

View file

@ -17,11 +17,13 @@
package org.keycloak.models.sessions.infinispan;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
@ -37,20 +39,26 @@ import org.keycloak.sessions.RootAuthenticationSessionModel;
*/
public class RootAuthenticationSessionAdapter implements RootAuthenticationSessionModel {
private static final Logger log = Logger.getLogger(RootAuthenticationSessionAdapter.class);
private KeycloakSession session;
private InfinispanAuthenticationSessionProvider provider;
private Cache<String, RootAuthenticationSessionEntity> cache;
private RealmModel realm;
private RootAuthenticationSessionEntity entity;
private final int authSessionsLimit;
private static Comparator<Map.Entry<String, AuthenticationSessionEntity>> TIMESTAMP_COMPARATOR =
Comparator.comparingInt(e -> e.getValue().getTimestamp());
public RootAuthenticationSessionAdapter(KeycloakSession session, InfinispanAuthenticationSessionProvider provider,
Cache<String, RootAuthenticationSessionEntity> cache, RealmModel realm,
RootAuthenticationSessionEntity entity) {
RootAuthenticationSessionEntity entity, int authSessionsLimt) {
this.session = session;
this.provider = provider;
this.cache = cache;
this.realm = realm;
this.entity = entity;
this.authSessionsLimit = authSessionsLimt;
}
void update() {
@ -109,14 +117,29 @@ public class RootAuthenticationSessionAdapter implements RootAuthenticationSessi
@Override
public AuthenticationSessionModel createAuthenticationSession(ClientModel client) {
Map<String, AuthenticationSessionEntity> authenticationSessions = entity.getAuthenticationSessions();
if (authenticationSessions.size() >= authSessionsLimit) {
String tabId = authenticationSessions.entrySet().stream().min(TIMESTAMP_COMPARATOR).map(Map.Entry::getKey).orElse(null);
if (tabId != null) {
log.debugf("Reached limit (%s) of active authentication sessions per a root authentication session. Removing oldest authentication session with TabId %s.", authSessionsLimit, tabId);
// remove the oldest authentication session
authenticationSessions.remove(tabId);
}
}
AuthenticationSessionEntity authSessionEntity = new AuthenticationSessionEntity();
authSessionEntity.setClientUUID(client.getId());
int timestamp = Time.currentTime();
authSessionEntity.setTimestamp(timestamp);
String tabId = provider.generateTabId();
entity.getAuthenticationSessions().put(tabId, authSessionEntity);
authenticationSessions.put(tabId, authSessionEntity);
// Update our timestamp when adding new authenticationSession
entity.setTimestamp(Time.currentTime());
entity.setTimestamp(timestamp);
update();

View file

@ -42,6 +42,8 @@ public class AuthenticationSessionEntity implements Serializable {
private String authUserId;
private int timestamp;
private String redirectUri;
private String action;
private Set<String> clientScopes;
@ -57,6 +59,17 @@ public class AuthenticationSessionEntity implements Serializable {
public AuthenticationSessionEntity() {
}
public AuthenticationSessionEntity(
String clientUUID,
String authUserId,
int timestamp,
String redirectUri, String action, Set<String> clientScopes,
Map<String, AuthenticationSessionModel.ExecutionStatus> executionStatus, String protocol,
Map<String, String> clientNotes, Map<String, String> authNotes, Set<String> requiredActions, Map<String, String> userSessionNotes) {
this(clientUUID, authUserId, redirectUri, action, clientScopes, executionStatus, protocol, clientNotes, authNotes, requiredActions, userSessionNotes);
this.timestamp = timestamp;
}
public AuthenticationSessionEntity(
String clientUUID,
String authUserId,
@ -96,6 +109,14 @@ public class AuthenticationSessionEntity implements Serializable {
this.authUserId = authUserId;
}
public int getTimestamp() {
return timestamp;
}
public void setTimestamp(int timestamp) {
this.timestamp = timestamp;
}
public String getRedirectUri() {
return redirectUri;
}
@ -171,6 +192,7 @@ public class AuthenticationSessionEntity implements Serializable {
public static class ExternalizerImpl implements Externalizer<AuthenticationSessionEntity> {
private static final int VERSION_1 = 1;
private static final int VERSION_2 = 2;
public static final ExternalizerImpl INSTANCE = new ExternalizerImpl();
@ -196,12 +218,14 @@ public class AuthenticationSessionEntity implements Serializable {
@Override
public void writeObject(ObjectOutput output, AuthenticationSessionEntity value) throws IOException {
output.writeByte(VERSION_1);
output.writeByte(VERSION_2);
MarshallUtil.marshallString(value.clientUUID, output);
MarshallUtil.marshallString(value.authUserId, output);
output.writeInt(value.timestamp);
MarshallUtil.marshallString(value.redirectUri, output);
MarshallUtil.marshallString(value.action, output);
KeycloakMarshallUtil.writeCollection(value.clientScopes, KeycloakMarshallUtil.STRING_EXT, output);
@ -220,6 +244,8 @@ public class AuthenticationSessionEntity implements Serializable {
switch (input.readByte()) {
case VERSION_1:
return readObjectVersion1(input);
case VERSION_2:
return readObjectVersion2(input);
default:
throw new IOException("Unknown version");
}
@ -244,5 +270,27 @@ public class AuthenticationSessionEntity implements Serializable {
KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, size -> new ConcurrentHashMap<>(size)) // userSessionNotes
);
}
public AuthenticationSessionEntity readObjectVersion2(ObjectInput input) throws IOException, ClassNotFoundException {
return new AuthenticationSessionEntity(
MarshallUtil.unmarshallString(input), // clientUUID
MarshallUtil.unmarshallString(input), // authUserId
input.readInt(), // timestamp
MarshallUtil.unmarshallString(input), // redirectUri
MarshallUtil.unmarshallString(input), // action
KeycloakMarshallUtil.readCollection(input, KeycloakMarshallUtil.STRING_EXT, ConcurrentHashMap::newKeySet), // clientScopes
KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, EXECUTION_STATUS_EXT, size -> new ConcurrentHashMap<>(size)), // executionStatus
MarshallUtil.unmarshallString(input), // protocol
KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, size -> new ConcurrentHashMap<>(size)), // clientNotes
KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, size -> new ConcurrentHashMap<>(size)), // authNotes
KeycloakMarshallUtil.readCollection(input, KeycloakMarshallUtil.STRING_EXT, ConcurrentHashMap::newKeySet), // requiredActions
KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, size -> new ConcurrentHashMap<>(size)) // userSessionNotes
);
}
}
}

View file

@ -32,6 +32,8 @@ public class MapAuthenticationSessionEntity {
private String authUserId;
private int timestamp;
private String redirectUri;
private String action;
private Set<String> clientScopes = new HashSet<>();
@ -68,6 +70,14 @@ public class MapAuthenticationSessionEntity {
this.authUserId = authUserId;
}
public int getTimestamp() {
return timestamp;
}
public void setTimestamp(int timestamp) {
this.timestamp = timestamp;
}
public String getRedirectUri() {
return redirectUri;
}

View file

@ -82,11 +82,14 @@ public abstract class MapRootAuthenticationSessionAdapter<K> extends AbstractRoo
MapAuthenticationSessionEntity authSessionEntity = new MapAuthenticationSessionEntity();
authSessionEntity.setClientUUID(client.getId());
int timestamp = Time.currentTime();
authSessionEntity.setTimestamp(timestamp);
String tabId = generateTabId();
entity.getAuthenticationSessions().put(tabId, authSessionEntity);
// Update our timestamp when adding new authenticationSession
entity.setTimestamp(Time.currentTime());
entity.setTimestamp(timestamp);
MapAuthenticationSessionAdapter authSession = new MapAuthenticationSessionAdapter(session, this, tabId, authSessionEntity);
session.getContext().setAuthenticationSession(authSession);

View file

@ -204,5 +204,4 @@ public interface AuthenticationSessionModel extends CommonClientSessionModel {
* @param clientScopes {@code Set<String>} Can't be {@code null}.
*/
void setClientScopes(Set<String> clientScopes);
}

View file

@ -78,7 +78,7 @@ public interface RootAuthenticationSessionModel {
AuthenticationSessionModel getAuthenticationSession(ClientModel client, String tabId);
/**
* Create a new authentication session and returns it. Overwrites existing session for particular client if already exists.
* Create a new authentication session and returns it.
* @param client {@code ClientModel} Can't be {@code null}.
* @return {@code AuthenticationSessionModel} non-null fresh authentication session. Never returns {@code null}.
*/

View file

@ -31,7 +31,6 @@ import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.sessions.StickySessionEncoderProvider;
import javax.ws.rs.core.UriInfo;
import java.util.AbstractMap.SimpleEntry;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@ -44,7 +43,7 @@ public class AuthenticationSessionManager {
public static final String AUTH_SESSION_ID = "AUTH_SESSION_ID";
public static final int AUTH_SESSION_LIMIT = 3;
public static final int AUTH_SESSION_COOKIE_LIMIT = 3;
private static final Logger log = Logger.getLogger(AuthenticationSessionManager.class);
@ -189,7 +188,7 @@ public class AuthenticationSessionManager {
AuthenticationManager.expireOldAuthSessionCookie(realm, session.getContext().getUri(), session.getContext().getConnection());
}
List<String> authSessionIds = cookiesVal.stream().limit(AUTH_SESSION_LIMIT).collect(Collectors.toList());
List<String> authSessionIds = cookiesVal.stream().limit(AUTH_SESSION_COOKIE_LIMIT).collect(Collectors.toList());
if (authSessionIds.isEmpty()) {
log.debugf("Not found AUTH_SESSION_ID cookie");

View file

@ -62,12 +62,9 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.AuthenticationFlowResolver;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.SamlService;
import org.keycloak.protocol.saml.SamlSessionUtils;
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
import org.keycloak.representations.AccessToken;

View file

@ -72,7 +72,10 @@
},
"authenticationSessions": {
"provider": "${keycloak.authSession.provider:infinispan}"
"provider": "${keycloak.authSession.provider:infinispan}",
"infinispan": {
"authSessionsLimit": "${keycloak.authSessions.limit:300}"
}
},
"userSessions": {

View file

@ -20,8 +20,10 @@ import org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory;
import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory;
import org.keycloak.connections.infinispan.InfinispanConnectionSpi;
import org.keycloak.models.session.UserSessionPersisterSpi;
import org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory;
import org.keycloak.models.sessions.infinispan.InfinispanUserLoginFailureProviderFactory;
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory;
import org.keycloak.sessions.AuthenticationSessionSpi;
import org.keycloak.sessions.StickySessionEncoderProviderFactory;
import org.keycloak.sessions.StickySessionEncoderSpi;
import org.keycloak.testsuite.model.KeycloakModelParameters;
@ -47,6 +49,7 @@ public class Infinispan extends KeycloakModelParameters {
private static final AtomicInteger NODE_COUNTER = new AtomicInteger();
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
.add(AuthenticationSessionSpi.class)
.add(CacheRealmProviderSpi.class)
.add(CacheUserProviderSpi.class)
.add(InfinispanConnectionSpi.class)
@ -56,6 +59,7 @@ public class Infinispan extends KeycloakModelParameters {
.build();
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
.add(InfinispanAuthenticationSessionProviderFactory.class)
.add(InfinispanCacheRealmProviderFactory.class)
.add(InfinispanClusterProviderFactory.class)
.add(InfinispanConnectionProviderFactory.class)

View file

@ -25,6 +25,7 @@ import org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderF
import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory;
import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory;
import org.keycloak.models.map.userSession.MapUserSessionProviderFactory;
import org.keycloak.sessions.AuthenticationSessionSpi;
import org.keycloak.testsuite.model.KeycloakModelParameters;
import org.keycloak.models.map.client.MapClientProviderFactory;
import org.keycloak.models.map.clientscope.MapClientScopeProviderFactory;
@ -37,7 +38,6 @@ import org.keycloak.models.map.storage.MapStorageSpi;
import org.keycloak.models.map.user.MapUserProviderFactory;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
import org.keycloak.sessions.AuthenticationSessionSpi;
import org.keycloak.testsuite.model.Config;
import com.google.common.collect.ImmutableSet;
import java.util.Set;
@ -49,6 +49,7 @@ import java.util.Set;
public class Map extends KeycloakModelParameters {
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
.add(AuthenticationSessionSpi.class)
.add(MapStorageSpi.class)
.build();
@ -76,7 +77,7 @@ public class Map extends KeycloakModelParameters {
@Override
public void updateConfig(Config cf) {
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).defaultProvider(MapClientProviderFactory.PROVIDER_ID)
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).defaultProvider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID)
.spi("client").defaultProvider(MapClientProviderFactory.PROVIDER_ID)
.spi("clientScope").defaultProvider(MapClientScopeProviderFactory.PROVIDER_ID)
.spi("group").defaultProvider(MapGroupProviderFactory.PROVIDER_ID)

View file

@ -0,0 +1,91 @@
/*
* Copyright 2021 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.testsuite.model.session;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.AuthenticationSessionProvider;
import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.testsuite.model.KeycloakModelTest;
import org.keycloak.testsuite.model.RequireProvider;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.keycloak.testsuite.model.session.UserSessionPersisterProviderTest.createClients;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
@RequireProvider(value = AuthenticationSessionProvider.class, only = InfinispanAuthenticationSessionProviderFactory.PROVIDER_ID)
public class AuthenticationSessionTest extends KeycloakModelTest {
private String realmId;
@Override
public void createEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().createRealm("test");
realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName()));
this.realmId = realm.getId();
createClients(s, realm);
}
@Override
public void cleanEnvironment(KeycloakSession s) {
s.realms().removeRealm(realmId);
}
@Test
public void testLimitAuthSessions() {
RootAuthenticationSessionModel ras = withRealm(realmId, (session, realm) -> session.authenticationSessions().createRootAuthenticationSession(realm));
List<String> tabIds = withRealm(realmId, (session, realm) -> {
ClientModel client = realm.getClientByClientId("test-app");
return IntStream.range(0, 300)
.mapToObj(i -> {
Time.setOffset(i);
return ras.createAuthenticationSession(client);
})
.map(AuthenticationSessionModel::getTabId)
.collect(Collectors.toList());
});
withRealm(realmId, (session, realm) -> {
ClientModel client = realm.getClientByClientId("test-app");
// create 301st auth session
AuthenticationSessionModel as = ras.createAuthenticationSession(client);
Assert.assertEquals(as, ras.getAuthenticationSession(client, as.getTabId()));
// assert the first authentication session was deleted
Assert.assertNull(ras.getAuthenticationSession(client, tabIds.get(0)));
return null;
});
}
}

View file

@ -46,7 +46,10 @@
},
"authenticationSessions": {
"provider": "${keycloak.authSession.provider:infinispan}"
"provider": "${keycloak.authSession.provider:infinispan}",
"infinispan": {
"authSessionsLimit": "${keycloak.authSessions.limit:300}"
}
},
"userSessions": {