KEYCLOAK-16616 Limit number of authSessios per rootAuthSession
This commit is contained in:
parent
122fbe1bc6
commit
23aee6c210
16 changed files with 228 additions and 21 deletions
|
@ -291,4 +291,18 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
|
||||||
update();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,13 +51,16 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
private final Cache<String, RootAuthenticationSessionEntity> cache;
|
private final Cache<String, RootAuthenticationSessionEntity> cache;
|
||||||
private final InfinispanKeyGenerator keyGenerator;
|
private final InfinispanKeyGenerator keyGenerator;
|
||||||
|
private final int authSessionsLimit;
|
||||||
protected final InfinispanKeycloakTransaction tx;
|
protected final InfinispanKeycloakTransaction tx;
|
||||||
protected final SessionEventsSenderTransaction clusterEventsSenderTx;
|
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.session = session;
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
this.keyGenerator = keyGenerator;
|
this.keyGenerator = keyGenerator;
|
||||||
|
this.authSessionsLimit = authSessionsLimit;
|
||||||
|
|
||||||
this.tx = new InfinispanKeycloakTransaction();
|
this.tx = new InfinispanKeycloakTransaction();
|
||||||
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
|
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
|
||||||
|
@ -88,7 +91,7 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||||
|
|
||||||
|
|
||||||
private RootAuthenticationSessionAdapter wrap(RealmModel realm, RootAuthenticationSessionEntity entity) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -54,8 +54,14 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
|
||||||
|
|
||||||
private volatile Cache<String, RootAuthenticationSessionEntity> authSessionsCache;
|
private volatile Cache<String, RootAuthenticationSessionEntity> authSessionsCache;
|
||||||
|
|
||||||
|
private int authSessionsLimit;
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "infinispan";
|
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 AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS";
|
||||||
|
|
||||||
public static final String REALM_REMOVED_AUTHSESSION_EVENT = "REALM_REMOVED_EVENT_AUTHSESSIONS";
|
public static final String REALM_REMOVED_AUTHSESSION_EVENT = "REALM_REMOVED_EVENT_AUTHSESSIONS";
|
||||||
|
@ -64,7 +70,10 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
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
|
@Override
|
||||||
public AuthenticationSessionProvider create(KeycloakSession session) {
|
public AuthenticationSessionProvider create(KeycloakSession session) {
|
||||||
lazyInit(session);
|
lazyInit(session);
|
||||||
return new InfinispanAuthenticationSessionProvider(session, keyGenerator, authSessionsCache);
|
return new InfinispanAuthenticationSessionProvider(session, keyGenerator, authSessionsCache, authSessionsLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAuthNotes(ClusterEvent clEvent) {
|
private void updateAuthNotes(ClusterEvent clEvent) {
|
||||||
|
|
|
@ -17,11 +17,13 @@
|
||||||
|
|
||||||
package org.keycloak.models.sessions.infinispan;
|
package org.keycloak.models.sessions.infinispan;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -37,20 +39,26 @@ import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
*/
|
*/
|
||||||
public class RootAuthenticationSessionAdapter implements RootAuthenticationSessionModel {
|
public class RootAuthenticationSessionAdapter implements RootAuthenticationSessionModel {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(RootAuthenticationSessionAdapter.class);
|
||||||
|
|
||||||
private KeycloakSession session;
|
private KeycloakSession session;
|
||||||
private InfinispanAuthenticationSessionProvider provider;
|
private InfinispanAuthenticationSessionProvider provider;
|
||||||
private Cache<String, RootAuthenticationSessionEntity> cache;
|
private Cache<String, RootAuthenticationSessionEntity> cache;
|
||||||
private RealmModel realm;
|
private RealmModel realm;
|
||||||
private RootAuthenticationSessionEntity entity;
|
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,
|
public RootAuthenticationSessionAdapter(KeycloakSession session, InfinispanAuthenticationSessionProvider provider,
|
||||||
Cache<String, RootAuthenticationSessionEntity> cache, RealmModel realm,
|
Cache<String, RootAuthenticationSessionEntity> cache, RealmModel realm,
|
||||||
RootAuthenticationSessionEntity entity) {
|
RootAuthenticationSessionEntity entity, int authSessionsLimt) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
|
this.authSessionsLimit = authSessionsLimt;
|
||||||
}
|
}
|
||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
|
@ -109,14 +117,29 @@ public class RootAuthenticationSessionAdapter implements RootAuthenticationSessi
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationSessionModel createAuthenticationSession(ClientModel client) {
|
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();
|
AuthenticationSessionEntity authSessionEntity = new AuthenticationSessionEntity();
|
||||||
authSessionEntity.setClientUUID(client.getId());
|
authSessionEntity.setClientUUID(client.getId());
|
||||||
|
|
||||||
|
int timestamp = Time.currentTime();
|
||||||
|
authSessionEntity.setTimestamp(timestamp);
|
||||||
|
|
||||||
String tabId = provider.generateTabId();
|
String tabId = provider.generateTabId();
|
||||||
entity.getAuthenticationSessions().put(tabId, authSessionEntity);
|
authenticationSessions.put(tabId, authSessionEntity);
|
||||||
|
|
||||||
// Update our timestamp when adding new authenticationSession
|
// Update our timestamp when adding new authenticationSession
|
||||||
entity.setTimestamp(Time.currentTime());
|
entity.setTimestamp(timestamp);
|
||||||
|
|
||||||
update();
|
update();
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,8 @@ public class AuthenticationSessionEntity implements Serializable {
|
||||||
|
|
||||||
private String authUserId;
|
private String authUserId;
|
||||||
|
|
||||||
|
private int timestamp;
|
||||||
|
|
||||||
private String redirectUri;
|
private String redirectUri;
|
||||||
private String action;
|
private String action;
|
||||||
private Set<String> clientScopes;
|
private Set<String> clientScopes;
|
||||||
|
@ -57,6 +59,17 @@ public class AuthenticationSessionEntity implements Serializable {
|
||||||
public AuthenticationSessionEntity() {
|
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(
|
public AuthenticationSessionEntity(
|
||||||
String clientUUID,
|
String clientUUID,
|
||||||
String authUserId,
|
String authUserId,
|
||||||
|
@ -96,6 +109,14 @@ public class AuthenticationSessionEntity implements Serializable {
|
||||||
this.authUserId = authUserId;
|
this.authUserId = authUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimestamp(int timestamp) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
public String getRedirectUri() {
|
public String getRedirectUri() {
|
||||||
return redirectUri;
|
return redirectUri;
|
||||||
}
|
}
|
||||||
|
@ -171,6 +192,7 @@ public class AuthenticationSessionEntity implements Serializable {
|
||||||
public static class ExternalizerImpl implements Externalizer<AuthenticationSessionEntity> {
|
public static class ExternalizerImpl implements Externalizer<AuthenticationSessionEntity> {
|
||||||
|
|
||||||
private static final int VERSION_1 = 1;
|
private static final int VERSION_1 = 1;
|
||||||
|
private static final int VERSION_2 = 2;
|
||||||
|
|
||||||
public static final ExternalizerImpl INSTANCE = new ExternalizerImpl();
|
public static final ExternalizerImpl INSTANCE = new ExternalizerImpl();
|
||||||
|
|
||||||
|
@ -196,12 +218,14 @@ public class AuthenticationSessionEntity implements Serializable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeObject(ObjectOutput output, AuthenticationSessionEntity value) throws IOException {
|
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.clientUUID, output);
|
||||||
|
|
||||||
MarshallUtil.marshallString(value.authUserId, output);
|
MarshallUtil.marshallString(value.authUserId, output);
|
||||||
|
|
||||||
|
output.writeInt(value.timestamp);
|
||||||
|
|
||||||
MarshallUtil.marshallString(value.redirectUri, output);
|
MarshallUtil.marshallString(value.redirectUri, output);
|
||||||
MarshallUtil.marshallString(value.action, output);
|
MarshallUtil.marshallString(value.action, output);
|
||||||
KeycloakMarshallUtil.writeCollection(value.clientScopes, KeycloakMarshallUtil.STRING_EXT, output);
|
KeycloakMarshallUtil.writeCollection(value.clientScopes, KeycloakMarshallUtil.STRING_EXT, output);
|
||||||
|
@ -220,6 +244,8 @@ public class AuthenticationSessionEntity implements Serializable {
|
||||||
switch (input.readByte()) {
|
switch (input.readByte()) {
|
||||||
case VERSION_1:
|
case VERSION_1:
|
||||||
return readObjectVersion1(input);
|
return readObjectVersion1(input);
|
||||||
|
case VERSION_2:
|
||||||
|
return readObjectVersion2(input);
|
||||||
default:
|
default:
|
||||||
throw new IOException("Unknown version");
|
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
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@ public class MapAuthenticationSessionEntity {
|
||||||
|
|
||||||
private String authUserId;
|
private String authUserId;
|
||||||
|
|
||||||
|
private int timestamp;
|
||||||
|
|
||||||
private String redirectUri;
|
private String redirectUri;
|
||||||
private String action;
|
private String action;
|
||||||
private Set<String> clientScopes = new HashSet<>();
|
private Set<String> clientScopes = new HashSet<>();
|
||||||
|
@ -68,6 +70,14 @@ public class MapAuthenticationSessionEntity {
|
||||||
this.authUserId = authUserId;
|
this.authUserId = authUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimestamp(int timestamp) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
public String getRedirectUri() {
|
public String getRedirectUri() {
|
||||||
return redirectUri;
|
return redirectUri;
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,11 +82,14 @@ public abstract class MapRootAuthenticationSessionAdapter<K> extends AbstractRoo
|
||||||
MapAuthenticationSessionEntity authSessionEntity = new MapAuthenticationSessionEntity();
|
MapAuthenticationSessionEntity authSessionEntity = new MapAuthenticationSessionEntity();
|
||||||
authSessionEntity.setClientUUID(client.getId());
|
authSessionEntity.setClientUUID(client.getId());
|
||||||
|
|
||||||
|
int timestamp = Time.currentTime();
|
||||||
|
authSessionEntity.setTimestamp(timestamp);
|
||||||
|
|
||||||
String tabId = generateTabId();
|
String tabId = generateTabId();
|
||||||
entity.getAuthenticationSessions().put(tabId, authSessionEntity);
|
entity.getAuthenticationSessions().put(tabId, authSessionEntity);
|
||||||
|
|
||||||
// Update our timestamp when adding new authenticationSession
|
// Update our timestamp when adding new authenticationSession
|
||||||
entity.setTimestamp(Time.currentTime());
|
entity.setTimestamp(timestamp);
|
||||||
|
|
||||||
MapAuthenticationSessionAdapter authSession = new MapAuthenticationSessionAdapter(session, this, tabId, authSessionEntity);
|
MapAuthenticationSessionAdapter authSession = new MapAuthenticationSessionAdapter(session, this, tabId, authSessionEntity);
|
||||||
session.getContext().setAuthenticationSession(authSession);
|
session.getContext().setAuthenticationSession(authSession);
|
||||||
|
|
|
@ -204,5 +204,4 @@ public interface AuthenticationSessionModel extends CommonClientSessionModel {
|
||||||
* @param clientScopes {@code Set<String>} Can't be {@code null}.
|
* @param clientScopes {@code Set<String>} Can't be {@code null}.
|
||||||
*/
|
*/
|
||||||
void setClientScopes(Set<String> clientScopes);
|
void setClientScopes(Set<String> clientScopes);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ public interface RootAuthenticationSessionModel {
|
||||||
AuthenticationSessionModel getAuthenticationSession(ClientModel client, String tabId);
|
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}.
|
* @param client {@code ClientModel} Can't be {@code null}.
|
||||||
* @return {@code AuthenticationSessionModel} non-null fresh authentication session. Never returns {@code null}.
|
* @return {@code AuthenticationSessionModel} non-null fresh authentication session. Never returns {@code null}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -31,7 +31,6 @@ import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
import org.keycloak.sessions.StickySessionEncoderProvider;
|
import org.keycloak.sessions.StickySessionEncoderProvider;
|
||||||
|
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
import java.util.AbstractMap.SimpleEntry;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -44,7 +43,7 @@ public class AuthenticationSessionManager {
|
||||||
|
|
||||||
public static final String AUTH_SESSION_ID = "AUTH_SESSION_ID";
|
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);
|
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());
|
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()) {
|
if (authSessionIds.isEmpty()) {
|
||||||
log.debugf("Not found AUTH_SESSION_ID cookie");
|
log.debugf("Not found AUTH_SESSION_ID cookie");
|
||||||
|
|
|
@ -62,12 +62,9 @@ import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.utils.AuthenticationFlowResolver;
|
import org.keycloak.models.utils.AuthenticationFlowResolver;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.protocol.LoginProtocol;
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.LoginProtocolFactory;
|
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.TokenManager;
|
import org.keycloak.protocol.oidc.TokenManager;
|
||||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
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.SamlSessionUtils;
|
||||||
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
|
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
|
|
|
@ -72,7 +72,10 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"authenticationSessions": {
|
"authenticationSessions": {
|
||||||
"provider": "${keycloak.authSession.provider:infinispan}"
|
"provider": "${keycloak.authSession.provider:infinispan}",
|
||||||
|
"infinispan": {
|
||||||
|
"authSessionsLimit": "${keycloak.authSessions.limit:300}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"userSessions": {
|
"userSessions": {
|
||||||
|
|
|
@ -20,8 +20,10 @@ import org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory;
|
||||||
import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory;
|
import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory;
|
||||||
import org.keycloak.connections.infinispan.InfinispanConnectionSpi;
|
import org.keycloak.connections.infinispan.InfinispanConnectionSpi;
|
||||||
import org.keycloak.models.session.UserSessionPersisterSpi;
|
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.InfinispanUserLoginFailureProviderFactory;
|
||||||
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory;
|
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionSpi;
|
||||||
import org.keycloak.sessions.StickySessionEncoderProviderFactory;
|
import org.keycloak.sessions.StickySessionEncoderProviderFactory;
|
||||||
import org.keycloak.sessions.StickySessionEncoderSpi;
|
import org.keycloak.sessions.StickySessionEncoderSpi;
|
||||||
import org.keycloak.testsuite.model.KeycloakModelParameters;
|
import org.keycloak.testsuite.model.KeycloakModelParameters;
|
||||||
|
@ -47,6 +49,7 @@ public class Infinispan extends KeycloakModelParameters {
|
||||||
private static final AtomicInteger NODE_COUNTER = new AtomicInteger();
|
private static final AtomicInteger NODE_COUNTER = new AtomicInteger();
|
||||||
|
|
||||||
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
|
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
|
||||||
|
.add(AuthenticationSessionSpi.class)
|
||||||
.add(CacheRealmProviderSpi.class)
|
.add(CacheRealmProviderSpi.class)
|
||||||
.add(CacheUserProviderSpi.class)
|
.add(CacheUserProviderSpi.class)
|
||||||
.add(InfinispanConnectionSpi.class)
|
.add(InfinispanConnectionSpi.class)
|
||||||
|
@ -56,6 +59,7 @@ public class Infinispan extends KeycloakModelParameters {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
|
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
|
||||||
|
.add(InfinispanAuthenticationSessionProviderFactory.class)
|
||||||
.add(InfinispanCacheRealmProviderFactory.class)
|
.add(InfinispanCacheRealmProviderFactory.class)
|
||||||
.add(InfinispanClusterProviderFactory.class)
|
.add(InfinispanClusterProviderFactory.class)
|
||||||
.add(InfinispanConnectionProviderFactory.class)
|
.add(InfinispanConnectionProviderFactory.class)
|
||||||
|
|
|
@ -25,6 +25,7 @@ import org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderF
|
||||||
import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory;
|
import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory;
|
||||||
import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory;
|
import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory;
|
||||||
import org.keycloak.models.map.userSession.MapUserSessionProviderFactory;
|
import org.keycloak.models.map.userSession.MapUserSessionProviderFactory;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionSpi;
|
||||||
import org.keycloak.testsuite.model.KeycloakModelParameters;
|
import org.keycloak.testsuite.model.KeycloakModelParameters;
|
||||||
import org.keycloak.models.map.client.MapClientProviderFactory;
|
import org.keycloak.models.map.client.MapClientProviderFactory;
|
||||||
import org.keycloak.models.map.clientscope.MapClientScopeProviderFactory;
|
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.models.map.user.MapUserProviderFactory;
|
||||||
import org.keycloak.provider.ProviderFactory;
|
import org.keycloak.provider.ProviderFactory;
|
||||||
import org.keycloak.provider.Spi;
|
import org.keycloak.provider.Spi;
|
||||||
import org.keycloak.sessions.AuthenticationSessionSpi;
|
|
||||||
import org.keycloak.testsuite.model.Config;
|
import org.keycloak.testsuite.model.Config;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -49,6 +49,7 @@ import java.util.Set;
|
||||||
public class Map extends KeycloakModelParameters {
|
public class Map extends KeycloakModelParameters {
|
||||||
|
|
||||||
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
|
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
|
||||||
|
.add(AuthenticationSessionSpi.class)
|
||||||
.add(MapStorageSpi.class)
|
.add(MapStorageSpi.class)
|
||||||
|
|
||||||
.build();
|
.build();
|
||||||
|
@ -76,7 +77,7 @@ public class Map extends KeycloakModelParameters {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateConfig(Config cf) {
|
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("client").defaultProvider(MapClientProviderFactory.PROVIDER_ID)
|
||||||
.spi("clientScope").defaultProvider(MapClientScopeProviderFactory.PROVIDER_ID)
|
.spi("clientScope").defaultProvider(MapClientScopeProviderFactory.PROVIDER_ID)
|
||||||
.spi("group").defaultProvider(MapGroupProviderFactory.PROVIDER_ID)
|
.spi("group").defaultProvider(MapGroupProviderFactory.PROVIDER_ID)
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -46,7 +46,10 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"authenticationSessions": {
|
"authenticationSessions": {
|
||||||
"provider": "${keycloak.authSession.provider:infinispan}"
|
"provider": "${keycloak.authSession.provider:infinispan}",
|
||||||
|
"infinispan": {
|
||||||
|
"authSessionsLimit": "${keycloak.authSessions.limit:300}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"userSessions": {
|
"userSessions": {
|
||||||
|
|
Loading…
Reference in a new issue