mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-06 06:49:53 -06:00
Create user session expired event
Closes #43942 Signed-off-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com> Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com> Co-authored-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com> Co-authored-by: Alexander Schwartz <alexander.schwartz@ibm.com>
This commit is contained in:
@@ -53,6 +53,7 @@ import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.events.AbstractUserSessionClusterListener;
|
||||
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
|
||||
import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
|
||||
import org.keycloak.models.sessions.infinispan.listeners.EmbeddedUserSessionExpirationListener;
|
||||
import org.keycloak.models.sessions.infinispan.transaction.InfinispanTransactionProvider;
|
||||
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
@@ -89,6 +90,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
||||
private CacheHolder<String, UserSessionEntity> offlineSessionCacheHolder;
|
||||
private CacheHolder<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> clientSessionCacheHolder;
|
||||
private CacheHolder<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> offlineClientSessionCacheHolder;
|
||||
private EmbeddedUserSessionExpirationListener expirationListener;
|
||||
|
||||
private long offlineSessionCacheEntryLifespanOverride;
|
||||
|
||||
@@ -203,7 +205,12 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
||||
offlineSessionCacheHolder = InfinispanChangesUtils.createWithCache(session, OFFLINE_USER_SESSION_CACHE_NAME, this::deriveOfflineSessionCacheEntryLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs);
|
||||
clientSessionCacheHolder = InfinispanChangesUtils.createWithCache(session, CLIENT_SESSION_CACHE_NAME, SessionTimeouts::getClientSessionLifespanMs, SessionTimeouts::getClientSessionMaxIdleMs);
|
||||
offlineClientSessionCacheHolder = InfinispanChangesUtils.createWithCache(session, OFFLINE_CLIENT_SESSION_CACHE_NAME, this::deriveOfflineClientSessionCacheEntryLifespanOverrideMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs);
|
||||
var blockingManager = session.getProvider(InfinispanConnectionProvider.class).getBlockingManager();
|
||||
expirationListener = new EmbeddedUserSessionExpirationListener(session.getKeycloakSessionFactory(), blockingManager);
|
||||
}
|
||||
// Only add the event listener to session caches
|
||||
// The expired events for offline sessions will be triggered by JpaUserSessionPersisterProvider
|
||||
sessionCacheHolder.cache().addListener(expirationListener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,6 +298,10 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
||||
if (persistentSessionsWorker != null) {
|
||||
persistentSessionsWorker.stop();
|
||||
}
|
||||
if (expirationListener != null) {
|
||||
sessionCacheHolder.cache().removeListener(expirationListener);
|
||||
expirationListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -75,14 +75,24 @@ public abstract class BaseUpdater<K, V> implements Updater<K, V> {
|
||||
return state == UpdaterState.READ && isUnchanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isExpired() {
|
||||
return state == UpdaterState.EXPIRED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void markDeleted() {
|
||||
state = switch (state) {
|
||||
case READ, DELETED -> UpdaterState.DELETED;
|
||||
case CREATED, DELETED_TRANSIENT -> UpdaterState.DELETED_TRANSIENT;
|
||||
case CREATED, DELETED_TRANSIENT, EXPIRED -> UpdaterState.DELETED_TRANSIENT;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markExpired() {
|
||||
state = UpdaterState.EXPIRED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTransient() {
|
||||
return state == UpdaterState.DELETED_TRANSIENT;
|
||||
@@ -141,5 +151,10 @@ public abstract class BaseUpdater<K, V> implements Updater<K, V> {
|
||||
* The entity is transient (it won't be updated in the external infinispan cluster) and deleted.
|
||||
*/
|
||||
DELETED_TRANSIENT,
|
||||
/**
|
||||
* The entity is expired (max-idle or lifespan). No changes should be applied.
|
||||
*/
|
||||
EXPIRED,
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,11 +65,28 @@ public interface Updater<K, V> extends BiFunction<K, V, V> {
|
||||
*/
|
||||
boolean isReadOnly();
|
||||
|
||||
/**
|
||||
* @return {@code true} if the entity is expired.
|
||||
*/
|
||||
boolean isExpired();
|
||||
|
||||
/**
|
||||
* @return {@code true} if the entity is not valid and cannot be viewed/accessed from the transaction.
|
||||
*/
|
||||
default boolean isInvalid() {
|
||||
return isExpired() || isDeleted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the entity as deleted.
|
||||
*/
|
||||
void markDeleted();
|
||||
|
||||
/**
|
||||
* Marks the entity as expired when loading from the Infinispan cache.
|
||||
*/
|
||||
void markExpired();
|
||||
|
||||
/**
|
||||
* @return {@code true} if the entity is transient and shouldn't be stored in the Infinispan cache.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2025 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.models.sessions.infinispan.listeners;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
|
||||
import org.infinispan.util.concurrent.BlockingManager;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
|
||||
/**
|
||||
* Base class to handle expired user session.
|
||||
* <p>
|
||||
* It offloads the event creating and sending to a different thread to avoid blocking the caller.
|
||||
*/
|
||||
abstract class BaseUserSessionExpirationListener {
|
||||
|
||||
protected static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass());
|
||||
private final KeycloakSessionFactory factory;
|
||||
private final BlockingManager blockingManager;
|
||||
|
||||
BaseUserSessionExpirationListener(KeycloakSessionFactory factory, BlockingManager blockingManager) {
|
||||
this.factory = factory;
|
||||
this.blockingManager = blockingManager;
|
||||
}
|
||||
|
||||
protected void sendExpirationEvent(String userSessionId, String userId, String realmId) {
|
||||
blockingManager.runBlocking(() -> doSend(userSessionId, userId, realmId), "expired-" + userSessionId);
|
||||
}
|
||||
|
||||
private void doSend(String userSessionId, String userId, String realmId) {
|
||||
KeycloakModelUtils.runJobInTransaction(factory, session -> {
|
||||
logger.debugf("User session expired. sessionId=%s, userId=%s, realmId=%s", userSessionId, userId, realmId);
|
||||
|
||||
RealmModel realm = session.realms().getRealm(realmId);
|
||||
if (realm == null) {
|
||||
return;
|
||||
}
|
||||
session.getContext().setRealm(realm);
|
||||
new EventBuilder(realm, session)
|
||||
.session(userSessionId)
|
||||
.user(userId)
|
||||
.event(EventType.USER_SESSION_DELETED)
|
||||
.detail(Details.REASON, Details.EXPIRED_DETAIL)
|
||||
.success();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 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.models.sessions.infinispan.listeners;
|
||||
|
||||
import org.infinispan.notifications.Listener;
|
||||
import org.infinispan.notifications.cachelistener.annotation.CacheEntryExpired;
|
||||
import org.infinispan.notifications.cachelistener.event.CacheEntryExpiredEvent;
|
||||
import org.infinispan.util.concurrent.BlockingManager;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
||||
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
|
||||
|
||||
/**
|
||||
* A listener for embedded Infinispan caches.
|
||||
* <p>
|
||||
* It listens to the {@link CacheEntryExpired} events for user sessions.
|
||||
*/
|
||||
@Listener(primaryOnly = true)
|
||||
public class EmbeddedUserSessionExpirationListener extends BaseUserSessionExpirationListener {
|
||||
|
||||
public EmbeddedUserSessionExpirationListener(KeycloakSessionFactory factory, BlockingManager blockingManager) {
|
||||
super(factory, blockingManager);
|
||||
}
|
||||
|
||||
@CacheEntryExpired
|
||||
public void onSessionExpired(CacheEntryExpiredEvent<?, SessionEntityWrapper<UserSessionEntity>> event) {
|
||||
UserSessionEntity entity = event.getValue().getEntity();
|
||||
sendExpirationEvent(entity.getId(), entity.getUser(), entity.getRealmId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2025 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.models.sessions.infinispan.listeners;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.infinispan.client.hotrod.annotation.ClientCacheEntryExpired;
|
||||
import org.infinispan.client.hotrod.annotation.ClientListener;
|
||||
import org.infinispan.client.hotrod.event.ClientCacheEntryCustomEvent;
|
||||
import org.infinispan.commons.io.UnsignedNumeric;
|
||||
import org.infinispan.commons.marshall.Marshaller;
|
||||
import org.infinispan.util.concurrent.BlockingManager;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.sessions.infinispan.entities.RemoteUserSessionEntity;
|
||||
|
||||
/**
|
||||
* A listener for remote Infinispan caches.
|
||||
* <p>
|
||||
* It listens to the {@link ClientCacheEntryExpired} events for user sessions.
|
||||
*/
|
||||
@ClientListener(converterFactoryName = "___eager-key-value-version-converter", useRawData = true)
|
||||
public class RemoteUserSessionExpirationListener extends BaseUserSessionExpirationListener {
|
||||
|
||||
private final Marshaller marshaller;
|
||||
|
||||
public RemoteUserSessionExpirationListener(KeycloakSessionFactory factory, BlockingManager blockingManager, Marshaller marshaller) {
|
||||
super(factory, blockingManager);
|
||||
this.marshaller = marshaller;
|
||||
}
|
||||
|
||||
@ClientCacheEntryExpired
|
||||
public void onSessionExpired(ClientCacheEntryCustomEvent<byte[]> entryExpired) {
|
||||
try {
|
||||
RemoteUserSessionEntity entity = extractRemoteUserSessionEntity(entryExpired);
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
sendExpirationEvent(entity.getUserSessionId(), entity.getUserId(), entity.getRealmId());
|
||||
} catch (Exception e) {
|
||||
logger.error("Error handling an expired entry", e);
|
||||
}
|
||||
}
|
||||
|
||||
private RemoteUserSessionEntity extractRemoteUserSessionEntity(ClientCacheEntryCustomEvent<byte[]> event) throws IOException, ClassNotFoundException {
|
||||
ByteBuffer rawData = ByteBuffer.wrap(event.getEventData());
|
||||
|
||||
// skip the key, we don't need it
|
||||
skipElement(rawData);
|
||||
|
||||
// read the value
|
||||
Object value = marshaller.objectFromByteBuffer(readElement(rawData));
|
||||
return value instanceof RemoteUserSessionEntity ruse ? ruse : null;
|
||||
}
|
||||
|
||||
private static void skipElement(ByteBuffer buffer) {
|
||||
int length = UnsignedNumeric.readUnsignedInt(buffer);
|
||||
buffer.position(buffer.position() + length);
|
||||
}
|
||||
|
||||
private static byte[] readElement(ByteBuffer buffer) {
|
||||
int length = UnsignedNumeric.readUnsignedInt(buffer);
|
||||
byte[] element = new byte[length];
|
||||
buffer.get(element);
|
||||
return element;
|
||||
}
|
||||
}
|
||||
@@ -369,7 +369,7 @@ public class RemoteUserSessionProvider implements UserSessionProvider {
|
||||
private UserSessionUpdater initUserSessionFromQuery(UserSessionUpdater updater, RealmModel realm, UserModel user, boolean offline) {
|
||||
assert updater != null;
|
||||
assert realm != null;
|
||||
if (updater.isDeleted()) {
|
||||
if (updater.isInvalid()) {
|
||||
return null;
|
||||
}
|
||||
if (updater.isInitialized()) {
|
||||
@@ -408,7 +408,7 @@ public class RemoteUserSessionProvider implements UserSessionProvider {
|
||||
}
|
||||
|
||||
private AuthenticatedClientSessionModel initClientSessionUpdater(AuthenticatedClientSessionUpdater updater, UserSessionUpdater userSession) {
|
||||
if (updater == null || updater.isDeleted()) {
|
||||
if (updater == null || updater.isInvalid()) {
|
||||
return null;
|
||||
}
|
||||
var client = userSession.getRealm().getClientById(updater.getKey().clientId());
|
||||
@@ -464,7 +464,7 @@ public class RemoteUserSessionProvider implements UserSessionProvider {
|
||||
private static <K, V, T extends BaseUpdater<K, V>> T checkExpiration(T updater) {
|
||||
var expiration = updater.computeExpiration();
|
||||
if (expiration.isExpired()) {
|
||||
updater.markDeleted();
|
||||
updater.markExpired();
|
||||
return null;
|
||||
}
|
||||
return updater;
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.keycloak.models.sessions.infinispan.changes.remote.updater.user.UserS
|
||||
import org.keycloak.models.sessions.infinispan.entities.ClientSessionKey;
|
||||
import org.keycloak.models.sessions.infinispan.entities.RemoteAuthenticatedClientSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.RemoteUserSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.listeners.RemoteUserSessionExpirationListener;
|
||||
import org.keycloak.models.sessions.infinispan.remote.transaction.ClientSessionChangeLogTransaction;
|
||||
import org.keycloak.models.sessions.infinispan.remote.transaction.RemoteChangeLogTransaction;
|
||||
import org.keycloak.models.sessions.infinispan.remote.transaction.UserSessionChangeLogTransaction;
|
||||
@@ -54,6 +55,7 @@ public class RemoteUserSessionProviderFactory implements UserSessionProviderFact
|
||||
private volatile int batchSize = DEFAULT_BATCH_SIZE;
|
||||
private volatile int maxRetries = InfinispanUtils.DEFAULT_MAX_RETRIES;
|
||||
private volatile int backOffBaseTimeMillis = InfinispanUtils.DEFAULT_RETRIES_BASE_TIME_MILLIS;
|
||||
private volatile RemoteUserSessionExpirationListener expirationListener;
|
||||
|
||||
@Override
|
||||
public RemoteUserSessionProvider create(KeycloakSession session) {
|
||||
@@ -80,6 +82,11 @@ public class RemoteUserSessionProviderFactory implements UserSessionProviderFact
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (expirationListener != null) {
|
||||
userSessionState.cache().removeClientListener(expirationListener);
|
||||
offlineUserSessionState.cache().removeClientListener(expirationListener);
|
||||
}
|
||||
expirationListener = null;
|
||||
blockingManager = null;
|
||||
userSessionState = null;
|
||||
offlineUserSessionState = null;
|
||||
@@ -131,7 +138,7 @@ public class RemoteUserSessionProviderFactory implements UserSessionProviderFact
|
||||
|
||||
@Override
|
||||
public Set<Class<? extends Provider>> dependsOn() {
|
||||
return Set.of(InfinispanTransactionProvider.class);
|
||||
return Set.of(InfinispanTransactionProvider.class, InfinispanConnectionProvider.class);
|
||||
}
|
||||
|
||||
private void onUserRemoved(UserModel.UserRemovedEvent event) {
|
||||
@@ -149,6 +156,9 @@ public class RemoteUserSessionProviderFactory implements UserSessionProviderFact
|
||||
clientSessionState = new SharedStateImpl<>(connections.getRemoteCache(CLIENT_SESSION_CACHE_NAME));
|
||||
offlineClientSessionState = new SharedStateImpl<>(connections.getRemoteCache(OFFLINE_CLIENT_SESSION_CACHE_NAME));
|
||||
blockingManager = connections.getBlockingManager();
|
||||
expirationListener = new RemoteUserSessionExpirationListener(session.getKeycloakSessionFactory(), blockingManager, userSessionState.cache().getRemoteCacheContainer().getMarshaller());
|
||||
userSessionState.cache().addClientListener(expirationListener);
|
||||
offlineUserSessionState.cache().addClientListener(expirationListener);
|
||||
}
|
||||
|
||||
private UserSessionTransaction createTransaction(KeycloakSession session) {
|
||||
|
||||
@@ -68,7 +68,7 @@ public class RemoteChangeLogTransaction<K, V, T extends Updater<K, V>, R extends
|
||||
conditionalRemover.executeRemovals(getCache(), stage);
|
||||
|
||||
for (var updater : entityChanges.values()) {
|
||||
if (updater.isReadOnly() || updater.isTransient() || conditionalRemover.willRemove(updater)) {
|
||||
if (updater.isReadOnly() || updater.isExpired() || updater.isTransient() || conditionalRemover.willRemove(updater)) {
|
||||
continue;
|
||||
}
|
||||
if (updater.isDeleted()) {
|
||||
@@ -79,7 +79,7 @@ public class RemoteChangeLogTransaction<K, V, T extends Updater<K, V>, R extends
|
||||
var expiration = updater.computeExpiration();
|
||||
|
||||
if (expiration.isExpired()) {
|
||||
stage.dependsOn(commitRemove(updater));
|
||||
// We need the cache entry expired events from the server, do nothing here.
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ public class RemoteChangeLogTransaction<K, V, T extends Updater<K, V>, R extends
|
||||
public T get(K key) {
|
||||
var updater = entityChanges.get(key);
|
||||
if (updater != null) {
|
||||
return updater.isDeleted() ? null : updater;
|
||||
return updater.isInvalid() ? null : updater;
|
||||
}
|
||||
return onEntityFromCache(key, getCache().getWithMetadata(key));
|
||||
}
|
||||
@@ -135,7 +135,7 @@ public class RemoteChangeLogTransaction<K, V, T extends Updater<K, V>, R extends
|
||||
public CompletionStage<T> getAsync(K key) {
|
||||
var updater = entityChanges.get(key);
|
||||
if (updater != null) {
|
||||
return updater.isDeleted() ? CompletableFutures.completedNull() : CompletableFuture.completedFuture(updater);
|
||||
return updater.isInvalid() ? CompletableFutures.completedNull() : CompletableFuture.completedFuture(updater);
|
||||
}
|
||||
return getCache().getWithMetadataAsync(key).thenApply(e -> onEntityFromCache(key, e));
|
||||
}
|
||||
@@ -189,7 +189,7 @@ public class RemoteChangeLogTransaction<K, V, T extends Updater<K, V>, R extends
|
||||
}
|
||||
var updater = factory.wrapFromCache(key, entity);
|
||||
entityChanges.put(key, updater);
|
||||
return updater.isDeleted() ? null : updater;
|
||||
return updater.isInvalid() ? null : updater;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -17,9 +17,13 @@
|
||||
|
||||
package org.keycloak.models.jpa.session;
|
||||
|
||||
import org.hibernate.jpa.AvailableHints;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.MultiSiteUtils;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
@@ -259,19 +263,32 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||
|
||||
logger.tracef("Trigger removing expired user sessions for realm '%s'", realm.getName());
|
||||
|
||||
int cs = em.createNamedQuery("deleteExpiredClientSessions")
|
||||
TypedQuery<Object[]> query = em.createNamedQuery("findExpiredUserSessions", Object[].class)
|
||||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("lastSessionRefresh", expired)
|
||||
.setParameter("offline", offlineStr)
|
||||
.executeUpdate();
|
||||
.setHint(AvailableHints.HINT_READ_ONLY, true);
|
||||
|
||||
int us = em.createNamedQuery("deleteExpiredUserSessions")
|
||||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("lastSessionRefresh", expired)
|
||||
.setParameter("offline", offlineStr)
|
||||
.executeUpdate();
|
||||
var expiredSessions = query.getResultStream()
|
||||
.map(JpaUserSessionPersisterProvider::userSessionAndUserProjection)
|
||||
.toList();
|
||||
|
||||
sendExpirationEvents(realm, expiredSessions);
|
||||
|
||||
StreamsUtil.chunkedStream(expiredSessions.stream().map(UserSessionAndUser::userSessionId), 100).forEach(chunk -> {
|
||||
// SQL databases only allow a limited number of items in an IN clause. While PostgreSQL allows possibly 32k, we are not sure about the rest
|
||||
int cs = em.createNamedQuery("deleteClientSessionsByUserSessions")
|
||||
.setParameter("userSessionId", chunk)
|
||||
.setParameter("offline", offlineStr)
|
||||
.executeUpdate();
|
||||
|
||||
int us = em.createNamedQuery("deleteUserSessions")
|
||||
.setParameter("userSessionId", chunk)
|
||||
.setParameter("offline", offlineStr)
|
||||
.executeUpdate();
|
||||
logger.debugf("Removed %d expired user sessions and %d expired client sessions in realm '%s'", us, cs, realm.getName());
|
||||
});
|
||||
|
||||
logger.debugf("Removed %d expired user sessions and %d expired client sessions in realm '%s'", us, cs, realm.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -302,12 +319,12 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||
|
||||
String offlineStr = offlineToString(offline);
|
||||
|
||||
TypedQuery<PersistentUserSessionEntity> userSessionQuery = em.createNamedQuery("findUserSession", PersistentUserSessionEntity.class);
|
||||
userSessionQuery.setParameter("realmId", realm.getId());
|
||||
userSessionQuery.setParameter("offline", offlineStr);
|
||||
userSessionQuery.setParameter("userSessionId", userSessionId);
|
||||
userSessionQuery.setParameter("lastSessionRefresh", calculateOldestSessionTime(realm, offline));
|
||||
userSessionQuery.setMaxResults(1);
|
||||
TypedQuery<PersistentUserSessionEntity> userSessionQuery = em.createNamedQuery("findUserSession", PersistentUserSessionEntity.class)
|
||||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("offline", offlineStr)
|
||||
.setParameter("userSessionId", userSessionId)
|
||||
.setParameter("lastSessionRefresh", calculateOldestSessionTime(realm, offline))
|
||||
.setMaxResults(1);
|
||||
|
||||
return handleSingleQuery(userSessionQuery, offlineStr);
|
||||
}
|
||||
@@ -740,7 +757,26 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||
);
|
||||
}
|
||||
|
||||
private void sendExpirationEvents(RealmModel realm, List<UserSessionAndUser> expiredSessions) {
|
||||
expiredSessions.forEach(sessionAndUser -> new EventBuilder(realm, session)
|
||||
.user(sessionAndUser.userId())
|
||||
.session(sessionAndUser.userSessionId())
|
||||
.event(EventType.USER_SESSION_DELETED)
|
||||
.detail(Details.REASON, Details.EXPIRED_DETAIL)
|
||||
.success());
|
||||
}
|
||||
|
||||
private static boolean hasClient(PersistentAuthenticatedClientSessionAdapter clientSession) {
|
||||
return clientSession.getClient() != null;
|
||||
}
|
||||
|
||||
private record UserSessionAndUser(String userSessionId, String userId) {
|
||||
}
|
||||
|
||||
private static UserSessionAndUser userSessionAndUserProjection(Object[] projection) {
|
||||
assert projection.length == 2;
|
||||
assert projection[0] != null;
|
||||
assert projection[1] != null;
|
||||
return new UserSessionAndUser(String.valueOf(projection[0]), String.valueOf(projection[1]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import jakarta.persistence.Version;
|
||||
import org.hibernate.annotations.DynamicUpdate;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
@@ -39,6 +40,8 @@ import java.io.Serializable;
|
||||
@NamedQuery(name="deleteClientSessionsByClientStorageProvider", query="delete from PersistentClientSessionEntity sess where sess.clientStorageProvider = :clientStorageProvider"),
|
||||
@NamedQuery(name="deleteClientSessionsByUser", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.userId = :userId)"),
|
||||
@NamedQuery(name="deleteClientSessionsByUserSession", query="delete from PersistentClientSessionEntity sess where sess.userSessionId = :userSessionId and sess.offline = :offline"),
|
||||
@NamedQuery(name="deleteClientSessionsByUserSessions", query="delete from PersistentClientSessionEntity sess where sess.userSessionId in (:userSessionId) and sess.offline = :offline"),
|
||||
// The query "deleteExpiredClientSessions" is deprecated (since 26.5) and may be removed in the future.
|
||||
@NamedQuery(name="deleteExpiredClientSessions", query="delete from PersistentClientSessionEntity sess where sess.offline = :offline AND sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.realmId = :realmId AND u.offline = :offline AND u.lastSessionRefresh < :lastSessionRefresh)"),
|
||||
@NamedQuery(name="deleteClientSessionsByRealmSessionType", query="delete from PersistentClientSessionEntity sess where sess.offline = :offline AND sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.realmId = :realmId and u.offline = :offline)"),
|
||||
@NamedQuery(name="findClientSessionsByUserSession", query="select sess from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and sess.offline = :offline"),
|
||||
@@ -191,13 +194,11 @@ public class PersistentClientSessionEntity {
|
||||
|
||||
Key key = (Key) o;
|
||||
|
||||
if (this.userSessionId != null ? !this.userSessionId.equals(key.userSessionId) : key.userSessionId != null) return false;
|
||||
if (this.clientId != null ? !this.clientId.equals(key.clientId) : key.clientId != null) return false;
|
||||
if (this.externalClientId != null ? !this.externalClientId.equals(key.externalClientId) : key.externalClientId != null) return false;
|
||||
if (this.clientStorageProvider != null ? !this.clientStorageProvider.equals(key.clientStorageProvider) : key.clientStorageProvider != null) return false;
|
||||
if (this.offline != null ? !this.offline.equals(key.offline) : key.offline != null) return false;
|
||||
|
||||
return true;
|
||||
return Objects.equals(this.userSessionId, key.userSessionId) &&
|
||||
Objects.equals(this.clientId, key.clientId) &&
|
||||
Objects.equals(this.externalClientId, key.externalClientId) &&
|
||||
Objects.equals(this.clientStorageProvider, key.clientStorageProvider) &&
|
||||
Objects.equals(this.offline, key.offline);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -29,6 +29,7 @@ import jakarta.persistence.NamedQueries;
|
||||
import jakarta.persistence.NamedQuery;
|
||||
import jakarta.persistence.Table;
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
@@ -37,7 +38,10 @@ import java.io.Serializable;
|
||||
@NamedQuery(name="deleteUserSessionsByRealm", query="delete from PersistentUserSessionEntity sess where sess.realmId = :realmId"),
|
||||
@NamedQuery(name="deleteUserSessionsByRealmSessionType", query="delete from PersistentUserSessionEntity sess where sess.realmId = :realmId and sess.offline = :offline"),
|
||||
@NamedQuery(name="deleteUserSessionsByUser", query="delete from PersistentUserSessionEntity sess where sess.userId = :userId"),
|
||||
// The query "deleteExpiredUserSessions" is deprecated (since 26.5) and may be removed in the future.
|
||||
@NamedQuery(name="deleteExpiredUserSessions", query="delete from PersistentUserSessionEntity sess where sess.realmId = :realmId AND sess.offline = :offline AND sess.lastSessionRefresh < :lastSessionRefresh"),
|
||||
@NamedQuery(name="deleteUserSessions", query="delete from PersistentUserSessionEntity sess where sess.offline = :offline AND sess.userSessionId IN (:userSessionId)"),
|
||||
@NamedQuery(name="findExpiredUserSessions", query="select sess.userSessionId, sess.userId from PersistentUserSessionEntity sess where sess.realmId = :realmId AND sess.offline = :offline AND sess.lastSessionRefresh < :lastSessionRefresh"),
|
||||
@NamedQuery(name="updateUserSessionLastSessionRefresh", query="update PersistentUserSessionEntity sess set lastSessionRefresh = :lastSessionRefresh where sess.realmId = :realmId" +
|
||||
" AND sess.offline = :offline AND sess.userSessionId IN (:userSessionIds)"),
|
||||
@NamedQuery(name="findUserSessionsCount", query="select count(sess) from PersistentUserSessionEntity sess where sess.offline = :offline"),
|
||||
@@ -192,10 +196,8 @@ public class PersistentUserSessionEntity {
|
||||
|
||||
Key key = (Key) o;
|
||||
|
||||
if (this.userSessionId != null ? !this.userSessionId.equals(key.userSessionId) : key.userSessionId != null) return false;
|
||||
if (this.offline != null ? !this.offline.equals(key.offline) : key.offline != null) return false;
|
||||
|
||||
return true;
|
||||
return Objects.equals(this.userSessionId, key.userSessionId) &&
|
||||
Objects.equals(this.offline, key.offline);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -36,7 +36,7 @@ public class EventOptions {
|
||||
"execute_actions", "execute_action_token", "client_info", "client_register", "client_update", "client_delete", "client_initiated_account_linking", "token_exchange",
|
||||
"oauth2_device_auth", "oauth2_device_verify_user_code", "oauth2_device_code_to_token", "authreqid_to_token", "permission_token", "delete_account", "pushed_authorization_request",
|
||||
"user_disabled_by_permanent_lockout", "user_disabled_by_temporary_lockout", "oauth2_extension_grant", "federated_identity_override_link", "update_credential", "remove_credential",
|
||||
"invite_org", "remove_totp", "update_totp", "update_password"));
|
||||
"invite_org", "remove_totp", "update_totp", "update_password", "user_session_deleted"));
|
||||
events.sort(String::compareToIgnoreCase);
|
||||
return events;
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ public class HelpCommandDistTest {
|
||||
// normalize the output to prevent changes around the feature toggles or events to mark the output to differ
|
||||
String output = cliResult.getOutput()
|
||||
.replaceAll("((Disables|Enables) a set of one or more features. Possible values are: )[^.]{30,}", "$1<...>")
|
||||
.replaceAll("(create a metric.\\s+Possible values are:)[^.]{30,}.(Available|only|when|user|event|metrics|are|enabled.| )*", "$1<...>");
|
||||
.replaceAll("(create a metric.\\s+Possible values are:)[^.]{30,}.[^.]*.", "$1<...>");
|
||||
|
||||
String osName = System.getProperty("os.name");
|
||||
if(osName.toLowerCase(Locale.ROOT).contains("windows")) {
|
||||
|
||||
@@ -114,4 +114,6 @@ public interface Details {
|
||||
|
||||
String CLIENT_POLICY_ERROR = "client_policy_error";
|
||||
String CLIENT_POLICY_ERROR_DETAIL = "client_policy_error_detail";
|
||||
|
||||
String EXPIRED_DETAIL = "expired";
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.keycloak.events;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.keycloak.util.EnumWithStableIndex;
|
||||
|
||||
/**
|
||||
@@ -185,14 +184,17 @@ public enum EventType implements EnumWithStableIndex {
|
||||
REMOVE_CREDENTIAL_ERROR(0x10000 + REMOVE_CREDENTIAL.getStableIndex(), true),
|
||||
|
||||
INVITE_ORG(60, true),
|
||||
INVITE_ORG_ERROR(0x10000 + INVITE_ORG.getStableIndex(), true);
|
||||
INVITE_ORG_ERROR(0x10000 + INVITE_ORG.getStableIndex(), true),
|
||||
|
||||
USER_SESSION_DELETED(61, false),
|
||||
USER_SESSION_DELETED_ERROR(0x10000 + USER_SESSION_DELETED.getStableIndex(), false),
|
||||
;
|
||||
|
||||
private final int stableIndex;
|
||||
private final boolean saveByDefault;
|
||||
private static final Map<Integer, EventType> BY_ID = EnumWithStableIndex.getReverseIndex(values());
|
||||
|
||||
EventType(int stableIndex, boolean saveByDefault) {
|
||||
Objects.requireNonNull(stableIndex);
|
||||
this.stableIndex = stableIndex;
|
||||
this.saveByDefault = saveByDefault;
|
||||
}
|
||||
@@ -204,7 +206,6 @@ public enum EventType implements EnumWithStableIndex {
|
||||
|
||||
/**
|
||||
* Determines whether this event is stored when the admin has not set a specific set of event types to save.
|
||||
* @return
|
||||
*/
|
||||
public boolean isSaveByDefault() {
|
||||
return saveByDefault;
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.protocol.oidc.grants.AuthorizationCodeGrantTypeFactory;
|
||||
import org.keycloak.protocol.oidc.grants.RefreshTokenGrantTypeFactory;
|
||||
@@ -68,7 +69,7 @@ public class AssertEvents implements TestRule {
|
||||
|
||||
public static final String DEFAULT_REDIRECT_URI = getAuthServerContextRoot() + "/auth/realms/master/app/auth";
|
||||
|
||||
private AbstractKeycloakTest context;
|
||||
private final AbstractKeycloakTest context;
|
||||
|
||||
public AssertEvents(AbstractKeycloakTest ctx) {
|
||||
context = ctx;
|
||||
@@ -188,6 +189,44 @@ public class AssertEvents implements TestRule {
|
||||
.session(sessionId);
|
||||
}
|
||||
|
||||
public ExpectedEvent expectSessionExpired(String sessionId, String userId) {
|
||||
return expect(EventType.USER_SESSION_DELETED)
|
||||
.session(sessionId)
|
||||
.user(userId)
|
||||
.detail(Details.REASON, Details.EXPIRED_DETAIL)
|
||||
.client((String) null)
|
||||
.ipAddress((String) null);
|
||||
}
|
||||
|
||||
public void assertRefreshTokenErrorAndMaybeSessionExpired(String sessionId, String userId, String clientId) {
|
||||
// events can be in any order
|
||||
ExpectedEvent expired = expectSessionExpired(sessionId, userId);
|
||||
ExpectedEvent refresh = expect(EventType.REFRESH_TOKEN)
|
||||
.session(sessionId)
|
||||
.client(clientId)
|
||||
.error(Errors.INVALID_TOKEN)
|
||||
.user((String) null);
|
||||
EventRepresentation e = poll(5);
|
||||
if (e.getType().equals(EventType.USER_SESSION_DELETED.name())) {
|
||||
// if we get an expiration event, we must receive the refresh token error event.
|
||||
expired.assertEvent(e);
|
||||
refresh.assertEvent();
|
||||
return;
|
||||
}
|
||||
if (e.getType().equals(EventType.REFRESH_TOKEN_ERROR.name())) {
|
||||
refresh.assertEvent(e);
|
||||
// The session expiration event is optional.
|
||||
// With volatile session send an event because Infinispan sends events on reads.
|
||||
// With persistent session only sends the events during the periodic cleanup task.
|
||||
e = fetchNextEvent();
|
||||
if (e != null) {
|
||||
expired.assertEvent(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Assert.fail("Unexpected event type: " + e.getType());
|
||||
}
|
||||
|
||||
public ExpectedEvent expectLogout(String sessionId) {
|
||||
return expect(EventType.LOGOUT)
|
||||
.detail(Details.REDIRECT_URI, Matchers.equalTo(DEFAULT_REDIRECT_URI))
|
||||
@@ -271,7 +310,7 @@ public class AssertEvents implements TestRule {
|
||||
}
|
||||
|
||||
public class ExpectedEvent {
|
||||
private EventRepresentation expected = new EventRepresentation();
|
||||
private final EventRepresentation expected = new EventRepresentation();
|
||||
private Matcher<String> realmId;
|
||||
private Matcher<String> userId;
|
||||
private Matcher<String> sessionId;
|
||||
@@ -440,23 +479,14 @@ public class AssertEvents implements TestRule {
|
||||
assertThat("session ID", actual.getSessionId(), is(sessionId));
|
||||
|
||||
if (details == null || details.isEmpty()) {
|
||||
// Assert.assertNull(actual.getDetails());
|
||||
} else {
|
||||
Assert.assertNotNull(actual.getDetails());
|
||||
for (Map.Entry<String, Matcher<? super String>> d : details.entrySet()) {
|
||||
String actualValue = actual.getDetails().get(d.getKey());
|
||||
|
||||
assertThat("Unexpected value for " + d.getKey(), actualValue, d.getValue());
|
||||
}
|
||||
/*
|
||||
for (String k : actual.getDetails().keySet()) {
|
||||
if (!details.containsKey(k)) {
|
||||
Assert.fail(k + " was not expected");
|
||||
}
|
||||
}
|
||||
*/
|
||||
return actual;
|
||||
}
|
||||
|
||||
Assert.assertNotNull(actual.getDetails());
|
||||
for (Map.Entry<String, Matcher<? super String>> d : details.entrySet()) {
|
||||
String actualValue = actual.getDetails().get(d.getKey());
|
||||
assertThat("Unexpected value for " + d.getKey(), actualValue, d.getValue());
|
||||
}
|
||||
return actual;
|
||||
}
|
||||
|
||||
|
||||
@@ -82,8 +82,6 @@ import jakarta.ws.rs.NotFoundException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
@@ -128,7 +126,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
@Before
|
||||
public void clientConfiguration() {
|
||||
userId = findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId();
|
||||
oauth.clientId("test-app");
|
||||
oauth.client("test-app");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -174,13 +172,11 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void offlineTokenDisabledForClient() throws Exception {
|
||||
public void offlineTokenDisabledForClient() {
|
||||
// Remove offline-access scope from client
|
||||
ClientScopeRepresentation offlineScope = adminClient.realm("test").clientScopes().findAll().stream().filter((ClientScopeRepresentation clientScope) -> {
|
||||
|
||||
return OAuth2Constants.OFFLINE_ACCESS.equals(clientScope.getName());
|
||||
|
||||
}).findFirst().get();
|
||||
ClientScopeRepresentation offlineScope = adminClient.realm("test").clientScopes().findAll().stream()
|
||||
.filter((ClientScopeRepresentation clientScope) -> OAuth2Constants.OFFLINE_ACCESS.equals(clientScope.getName()))
|
||||
.findFirst().get();
|
||||
|
||||
ClientManager.realm(adminClient.realm("test")).clientId("offline-client")
|
||||
.fullScopeAllowed(false)
|
||||
@@ -201,7 +197,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void offlineTokenUserNotAllowed() throws Exception {
|
||||
public void offlineTokenUserNotAllowed() {
|
||||
String userId = findUserByUsername(adminClient.realm("test"), "keycloak-user@localhost").getId();
|
||||
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
@@ -234,7 +230,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void offlineTokenBrowserFlow() throws Exception {
|
||||
public void offlineTokenBrowserFlow() {
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.client("offline-client", "secret1");
|
||||
oauth.redirectUri(offlineClientAppUri);
|
||||
@@ -264,9 +260,9 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
Assert.assertNull(offlineToken.getExp());
|
||||
|
||||
AccessTokenContext ctx = testingClient.testing("test").getTokenContext(token.getId());
|
||||
Assert.assertEquals(ctx.getSessionType(), AccessTokenContext.SessionType.OFFLINE);
|
||||
Assert.assertEquals(ctx.getTokenType(), AccessTokenContext.TokenType.REGULAR);
|
||||
Assert.assertEquals(ctx.getGrantType(), OAuth2Constants.AUTHORIZATION_CODE);
|
||||
Assert.assertEquals(AccessTokenContext.SessionType.OFFLINE, ctx.getSessionType());
|
||||
Assert.assertEquals(AccessTokenContext.TokenType.REGULAR, ctx.getTokenType());
|
||||
Assert.assertEquals(OAuth2Constants.AUTHORIZATION_CODE, ctx.getGrantType());
|
||||
|
||||
assertTrue(tokenResponse.getScope().contains(OAuth2Constants.OFFLINE_ACCESS));
|
||||
|
||||
@@ -283,18 +279,13 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
Assert.assertEquals(400, response.getStatusCode());
|
||||
assertEquals("invalid_grant", response.getError());
|
||||
|
||||
events.expectRefresh(offlineToken.getId(), newRefreshToken.getSessionState())
|
||||
.client("offline-client")
|
||||
.user((String) null)
|
||||
.error(Errors.INVALID_TOKEN)
|
||||
.clearDetails()
|
||||
.assertEvent();
|
||||
events.assertRefreshTokenErrorAndMaybeSessionExpired(newRefreshToken.getSessionId(), loginEvent.getUserId(), "offline-client");
|
||||
|
||||
setTimeOffset(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlineOfflineTokenBrowserFlow() throws Exception {
|
||||
public void onlineOfflineTokenBrowserFlow() {
|
||||
// request an online token for the client
|
||||
oauth.scope(null);
|
||||
oauth.client("offline-client", "secret1");
|
||||
@@ -373,9 +364,9 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
AccessTokenContext ctx = testingClient.testing("test").getTokenContext(refreshedToken.getId());
|
||||
Assert.assertEquals(ctx.getSessionType(), AccessTokenContext.SessionType.OFFLINE);
|
||||
Assert.assertEquals(ctx.getTokenType(), AccessTokenContext.TokenType.REGULAR);
|
||||
Assert.assertEquals(ctx.getGrantType(), OAuth2Constants.REFRESH_TOKEN);
|
||||
Assert.assertEquals(AccessTokenContext.SessionType.OFFLINE, ctx.getSessionType());
|
||||
Assert.assertEquals(AccessTokenContext.TokenType.REGULAR, ctx.getTokenType());
|
||||
Assert.assertEquals(OAuth2Constants.REFRESH_TOKEN, ctx.getGrantType());
|
||||
|
||||
// Assert new refreshToken in the response
|
||||
String newRefreshToken = response.getRefreshToken();
|
||||
@@ -419,7 +410,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void offlineTokenDirectGrantFlow() throws Exception {
|
||||
public void offlineTokenDirectGrantFlow() {
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.client("offline-client", "secret1");
|
||||
AccessTokenResponse tokenResponse = oauth.doPasswordGrantRequest("test-user@localhost", "password");
|
||||
@@ -456,7 +447,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void offlineTokenDirectGrantFlowWithRefreshTokensRevoked() throws Exception {
|
||||
public void offlineTokenDirectGrantFlowWithRefreshTokensRevoked() {
|
||||
RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(true);
|
||||
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
@@ -470,7 +461,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
events.expectLogin()
|
||||
.client("offline-client")
|
||||
.user(userId)
|
||||
.session(token.getSessionState())
|
||||
.session(token.getSessionId())
|
||||
.detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
|
||||
.detail(Details.TOKEN_ID, token.getId())
|
||||
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
|
||||
@@ -484,13 +475,13 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
|
||||
Assert.assertNull(offlineToken.getExp());
|
||||
|
||||
String offlineTokenString2 = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
|
||||
String offlineTokenString2 = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionId(), userId);
|
||||
RefreshToken offlineToken2 = oauth.parseRefreshToken(offlineTokenString2);
|
||||
|
||||
// Assert second refresh with same refresh token will fail
|
||||
AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString);
|
||||
Assert.assertEquals(400, response.getStatusCode());
|
||||
events.expectRefresh(offlineToken.getId(), token.getSessionState())
|
||||
events.expectRefresh(offlineToken.getId(), token.getSessionId())
|
||||
.client("offline-client")
|
||||
.user((String) null)
|
||||
.error(Errors.INVALID_TOKEN)
|
||||
@@ -500,7 +491,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
// Refresh with new refreshToken fails as well (client session was invalidated because of attempt to refresh with revoked refresh token)
|
||||
AccessTokenResponse response2 = oauth.doRefreshTokenRequest(offlineTokenString2);
|
||||
Assert.assertEquals(400, response2.getStatusCode());
|
||||
events.expectRefresh(offlineToken2.getId(), offlineToken2.getSessionState())
|
||||
events.expectRefresh(offlineToken2.getId(), offlineToken2.getSessionId())
|
||||
.client("offline-client")
|
||||
.user((String) null)
|
||||
.error(Errors.INVALID_TOKEN)
|
||||
@@ -511,7 +502,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void offlineTokenServiceAccountFlow() throws Exception {
|
||||
public void offlineTokenServiceAccountFlow() {
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.client("offline-client", "secret1");
|
||||
AccessTokenResponse tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest();
|
||||
@@ -564,7 +555,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void offlineTokenAllowedWithCompositeRole() throws Exception {
|
||||
public void offlineTokenAllowedWithCompositeRole() {
|
||||
RealmResource appRealm = adminClient.realm("test");
|
||||
UserResource testUser = findUserByUsernameId(appRealm, "test-user@localhost");
|
||||
RoleRepresentation offlineAccess = findRealmRoleByName(adminClient.realm("test"),
|
||||
@@ -591,10 +582,9 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
/**
|
||||
* KEYCLOAK-4201
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void offlineTokenAdminRESTAccess() throws Exception {
|
||||
public void offlineTokenAdminRESTAccess() {
|
||||
// Grant "view-realm" role to user
|
||||
RealmResource appRealm = adminClient.realm("test");
|
||||
ClientResource realmMgmt = ApiUtil.findClientByClientId(appRealm, Constants.REALM_MANAGEMENT_CLIENT_ID);
|
||||
@@ -632,7 +622,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
// KEYCLOAK-4525
|
||||
@Test
|
||||
public void offlineTokenRemoveClientWithTokens() throws Exception {
|
||||
public void offlineTokenRemoveClientWithTokens() {
|
||||
// Create new client
|
||||
RealmResource appRealm = adminClient.realm("test");
|
||||
|
||||
@@ -641,7 +631,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
.directAccessGrants()
|
||||
.secret("secret1").build();
|
||||
|
||||
appRealm.clients().create(clientRep);
|
||||
appRealm.clients().create(clientRep).close();
|
||||
|
||||
// Direct grant login requesting offline token
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
@@ -655,7 +645,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
events.expectLogin()
|
||||
.client("offline-client-2")
|
||||
.user(userId)
|
||||
.session(token.getSessionState())
|
||||
.session(token.getSessionId())
|
||||
.detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
|
||||
.detail(Details.TOKEN_ID, token.getId())
|
||||
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
|
||||
@@ -673,7 +663,8 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
for (Map<String, Object> consent : userConsents) {
|
||||
if (consent.get("clientId").equals("offline-client-2")) {
|
||||
clientId2 = String.valueOf(consent.get("clientId"));
|
||||
offlineAdditionalGrant = String.valueOf(((LinkedHashMap) ((ArrayList) consent.get("additionalGrants")).get(0)).get("key"));
|
||||
//noinspection unchecked
|
||||
offlineAdditionalGrant = String.valueOf((((List<Map<String, ?>>) consent.get("additionalGrants")).get(0)).get("key"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,12 +682,12 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
UserResource user = ApiUtil.findUserByUsernameId(appRealm, "test-user@localhost");
|
||||
List<Map<String, Object>> consents = user.getConsents();
|
||||
for (Map<String, Object> consent : consents) {
|
||||
assertNotEquals(consent.get("clientId"), "offline-client-2");
|
||||
assertNotEquals("offline-client-2", consent.get("clientId"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void offlineTokenLogout() throws Exception {
|
||||
public void offlineTokenLogout() {
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.client("offline-client", "secret1");
|
||||
AccessTokenResponse response = oauth.doPasswordGrantRequest("test-user@localhost", "password");
|
||||
@@ -713,7 +704,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlineOfflineTokenLogout() throws Exception {
|
||||
public void onlineOfflineTokenLogout() {
|
||||
oauth.client("offline-client", "secret1");
|
||||
|
||||
// create online session
|
||||
@@ -747,7 +738,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void browserOfflineTokenLogoutFollowedByLoginSameSession() throws Exception {
|
||||
public void browserOfflineTokenLogoutFollowedByLoginSameSession() {
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.client("offline-client", "secret1");
|
||||
oauth.redirectUri(offlineClientAppUri);
|
||||
@@ -777,7 +768,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
assertNull(offlineToken.getExp());
|
||||
|
||||
String offlineUserSessionId = testingClient.server().fetch((KeycloakSession session) ->
|
||||
session.sessions().getOfflineUserSession(session.realms().getRealmByName("test"), offlineToken.getSessionState()).getId(), String.class);
|
||||
session.sessions().getOfflineUserSession(session.realms().getRealmByName("test"), offlineToken.getSessionId()).getId(), String.class);
|
||||
|
||||
// logout offline session
|
||||
LogoutResponse logoutResponse = oauth.doLogout(offlineTokenString);
|
||||
@@ -804,7 +795,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
codeId = loginEvent.getDetails().get(Details.CODE_ID);
|
||||
|
||||
events.expectCodeToToken(codeId, offlineToken2.getSessionState())
|
||||
events.expectCodeToToken(codeId, offlineToken2.getSessionId())
|
||||
.client("offline-client")
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
|
||||
.assertEvent();
|
||||
@@ -813,7 +804,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
Assert.assertNull(offlineToken.getExp());
|
||||
|
||||
// Assert session changed
|
||||
assertNotEquals(offlineToken.getSessionState(), offlineToken2.getSessionState());
|
||||
assertNotEquals(offlineToken.getSessionId(), offlineToken2.getSessionId());
|
||||
}
|
||||
|
||||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
@@ -843,7 +834,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void offlineTokenBrowserFlowMaxLifespanExpired() throws Exception {
|
||||
public void offlineTokenBrowserFlowMaxLifespanExpired() {
|
||||
// expect that offline session expired by max lifespan
|
||||
final int MAX_LIFESPAN = 3600;
|
||||
final int IDLE_LIFESPAN = 6000;
|
||||
@@ -851,7 +842,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void offlineTokenBrowserFlowIdleTimeExpired() throws Exception {
|
||||
public void offlineTokenBrowserFlowIdleTimeExpired() {
|
||||
// expect that offline session expired by idle time
|
||||
final int MAX_LIFESPAN = 3000;
|
||||
final int IDLE_LIFESPAN = 600;
|
||||
@@ -868,8 +859,8 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
|
||||
int prev[] = null;
|
||||
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(adminClient.realm("test")).setSsoSessionIdleTimeout(900).update()) {
|
||||
int[] prev = null;
|
||||
try (RealmAttributeUpdater ignored = new RealmAttributeUpdater(adminClient.realm("test")).setSsoSessionIdleTimeout(900).update()) {
|
||||
prev = changeOfflineSessionSettings(true, MAX_LIFESPAN, IDLE_LIFESPAN, 0, 0);
|
||||
|
||||
// Step 1 - online login with "tets-app"
|
||||
@@ -917,8 +908,8 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
}
|
||||
|
||||
private RefreshToken assertOfflineToken(AccessTokenResponse tokenResponse) {
|
||||
return assertRefreshToken(tokenResponse, TokenUtil.TOKEN_TYPE_OFFLINE);
|
||||
private void assertOfflineToken(AccessTokenResponse tokenResponse) {
|
||||
assertRefreshToken(tokenResponse, TokenUtil.TOKEN_TYPE_OFFLINE);
|
||||
}
|
||||
|
||||
// Asserts that refresh token in the tokenResponse is of the given type. Return parsed token
|
||||
@@ -977,7 +968,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}, Integer.class);
|
||||
}
|
||||
|
||||
private void removeClientSessionStartedAtNote(final String userSessionId, final String clientId, final String clientSessionId) {
|
||||
private void removeClientSessionStartedAtNote(final String userSessionId, final String clientId) {
|
||||
testingClient.server().run(session -> {
|
||||
RealmModel realmModel = session.realms().getRealmByName("test");
|
||||
session.getContext().setRealm(realmModel);
|
||||
@@ -1027,9 +1018,9 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
setTimeOffset(offsetHalf);
|
||||
|
||||
tokenResponse = oauth.doRefreshTokenRequest(offlineTokenString);
|
||||
AccessToken refreshedToken = oauth.verifyToken(tokenResponse.getAccessToken());
|
||||
oauth.verifyToken(tokenResponse.getAccessToken());
|
||||
offlineTokenString = tokenResponse.getRefreshToken();
|
||||
offlineToken = oauth.parseRefreshToken(offlineTokenString);
|
||||
oauth.parseRefreshToken(offlineTokenString);
|
||||
|
||||
Assert.assertEquals(200, tokenResponse.getStatusCode());
|
||||
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
@@ -1064,7 +1055,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
oauth.client("offline-client", "secret1");
|
||||
AccessTokenResponse tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest();
|
||||
|
||||
JWSHeader header = null;
|
||||
JWSHeader header;
|
||||
String idToken = tokenResponse.getIdToken();
|
||||
String accessToken = tokenResponse.getAccessToken();
|
||||
String refreshToken = tokenResponse.getRefreshToken();
|
||||
@@ -1094,7 +1085,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
events.expectClientLogin()
|
||||
.client("offline-client")
|
||||
.user(serviceAccountUserId)
|
||||
.session(token.getSessionState())
|
||||
.session(token.getSessionId())
|
||||
.detail(Details.TOKEN_ID, token.getId())
|
||||
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
|
||||
@@ -1104,7 +1095,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
|
||||
Assert.assertNull(offlineToken.getExp());
|
||||
|
||||
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
|
||||
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionId(), serviceAccountUserId);
|
||||
|
||||
// Now retrieve another offline token and decode that previous offline token is still valid
|
||||
tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest();
|
||||
@@ -1116,7 +1107,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
events.expectClientLogin()
|
||||
.client("offline-client")
|
||||
.user(serviceAccountUserId)
|
||||
.session(token2.getSessionState())
|
||||
.session(token2.getSessionId())
|
||||
.detail(Details.TOKEN_ID, token2.getId())
|
||||
.detail(Details.REFRESH_TOKEN_ID, offlineToken2.getId())
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
|
||||
@@ -1124,9 +1115,8 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
.assertEvent();
|
||||
|
||||
// Refresh with both offline tokens is fine
|
||||
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
|
||||
testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId);
|
||||
|
||||
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionId(), serviceAccountUserId);
|
||||
testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionId(), serviceAccountUserId);
|
||||
}
|
||||
|
||||
private void offlineTokenRequestWithScopeParameter(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
|
||||
@@ -1136,7 +1126,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
oauth.client("offline-client", "secret1");
|
||||
AccessTokenResponse tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest();
|
||||
|
||||
JWSHeader header = null;
|
||||
JWSHeader header;
|
||||
String idToken = tokenResponse.getIdToken();
|
||||
String accessToken = tokenResponse.getAccessToken();
|
||||
String refreshToken = tokenResponse.getRefreshToken();
|
||||
@@ -1166,7 +1156,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
events.expectClientLogin()
|
||||
.client("offline-client")
|
||||
.user(serviceAccountUserId)
|
||||
.session(token.getSessionState())
|
||||
.session(token.getSessionId())
|
||||
.detail(Details.TOKEN_ID, token.getId())
|
||||
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
|
||||
@@ -1178,7 +1168,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshTokenUserClientMaxLifespanSmallerThanSession() throws Exception {
|
||||
public void refreshTokenUserClientMaxLifespanSmallerThanSession() {
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.client("offline-client", "secret1");
|
||||
oauth.redirectUri(offlineClientAppUri);
|
||||
@@ -1225,7 +1215,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshTokenUserClientMaxLifespanGreaterThanSession() throws Exception {
|
||||
public void refreshTokenUserClientMaxLifespanGreaterThanSession() {
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.client("offline-client", "secret1");
|
||||
oauth.redirectUri(offlineClientAppUri);
|
||||
@@ -1272,7 +1262,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshTokenUserSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception {
|
||||
public void refreshTokenUserSessionMaxLifespanModifiedAfterTokenRefresh() {
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.client("offline-client", "secret1");
|
||||
oauth.redirectUri(offlineClientAppUri);
|
||||
@@ -1306,7 +1296,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
assertEquals(400, tokenResponse.getStatusCode());
|
||||
assertNull(tokenResponse.getAccessToken());
|
||||
assertNull(tokenResponse.getRefreshToken());
|
||||
events.expect(EventType.REFRESH_TOKEN).session(sessionId).client("offline-client").error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
|
||||
events.assertRefreshTokenErrorAndMaybeSessionExpired(sessionId, loginEvent.getUserId(), "offline-client");
|
||||
assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
} finally {
|
||||
changeOfflineSessionSettings(false, prev[0], prev[1], prev[2], prev[3]);
|
||||
@@ -1317,7 +1307,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshTokenClientSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception {
|
||||
public void refreshTokenClientSessionMaxLifespanModifiedAfterTokenRefresh() {
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.client("offline-client", "secret1");
|
||||
oauth.redirectUri(offlineClientAppUri);
|
||||
@@ -1387,7 +1377,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
|
||||
|
||||
JsonNode jsonNode = oauth.doIntrospectionAccessTokenRequest(tokenResponse.getAccessToken()).asJsonNode();
|
||||
Assert.assertEquals(true, jsonNode.get("active").asBoolean());
|
||||
assertTrue(jsonNode.get("active").asBoolean());
|
||||
Assert.assertEquals("test-user@localhost", jsonNode.get("email").asText());
|
||||
assertThat(jsonNode.get("exp").asInt() - getCurrentTime(),
|
||||
allOf(greaterThanOrEqualTo(59), lessThanOrEqualTo(60)));
|
||||
@@ -1399,7 +1389,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientOfflineSessionMaxLifespan() throws Exception {
|
||||
public void testClientOfflineSessionMaxLifespan() {
|
||||
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "offline-client");
|
||||
ClientRepresentation clientRepresentation = client.toRepresentation();
|
||||
|
||||
@@ -1451,7 +1441,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientOfflineSessionIdleTimeout() throws Exception {
|
||||
public void testClientOfflineSessionIdleTimeout() {
|
||||
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "offline-client");
|
||||
ClientRepresentation clientRepresentation = client.toRepresentation();
|
||||
|
||||
@@ -1529,7 +1519,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void offlineRefreshWhenNoStartedAtClientNote() throws Exception {
|
||||
public void offlineRefreshWhenNoStartedAtClientNote() {
|
||||
int prevOfflineSession[] = null;
|
||||
try {
|
||||
prevOfflineSession = changeOfflineSessionSettings(true, 3600, 3600, 0, 0);
|
||||
@@ -1548,8 +1538,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
.assertEvent();
|
||||
|
||||
// remove the started notes that can be missed in previous versions
|
||||
String clientSessionId = getOfflineClientSessionUuid(loginEvent.getSessionId(), loginEvent.getClientId());
|
||||
removeClientSessionStartedAtNote(loginEvent.getSessionId(), loginEvent.getClientId(), clientSessionId);
|
||||
removeClientSessionStartedAtNote(loginEvent.getSessionId(), loginEvent.getClientId());
|
||||
|
||||
// check refresh is successful
|
||||
response = oauth.doRefreshTokenRequest(response.getRefreshToken());
|
||||
@@ -1614,7 +1603,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
||||
.assertEvent();
|
||||
|
||||
// remove offline scope from the client and perform a second refresh
|
||||
try (ClientAttributeUpdater updater = ClientAttributeUpdater.forClient(adminClient, TEST, "offline-client")
|
||||
try (ClientAttributeUpdater ignored = ClientAttributeUpdater.forClient(adminClient, TEST, "offline-client")
|
||||
.removeOptionalClientScope("offline_access").update()) {
|
||||
|
||||
introspectionResponse = oauth.doIntrospectionAccessTokenRequest(response.getAccessToken());
|
||||
|
||||
@@ -38,9 +38,9 @@ import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.cookie.CookieType;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
@@ -76,8 +76,8 @@ import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.AdminClientUtil;
|
||||
import org.keycloak.testsuite.util.BrowserTabUtil;
|
||||
import org.keycloak.testsuite.util.ClientManager;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
import org.keycloak.testsuite.util.RealmManager;
|
||||
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
||||
@@ -85,7 +85,7 @@ import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.testsuite.util.UserInfoClientUtil;
|
||||
import org.keycloak.testsuite.util.UserManager;
|
||||
import org.keycloak.testsuite.util.WaitUtils;
|
||||
import org.keycloak.testsuite.util.BrowserTabUtil;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.util.BasicAuthHelper;
|
||||
import org.openqa.selenium.Cookie;
|
||||
|
||||
@@ -101,26 +101,28 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.hamcrest.Matchers.lessThan;
|
||||
import static org.hamcrest.Matchers.lessThanOrEqualTo;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
|
||||
import static org.keycloak.protocol.oidc.OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT;
|
||||
import static org.keycloak.protocol.oidc.OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN;
|
||||
import static org.keycloak.testsuite.Assert.assertExpiration;
|
||||
import static org.keycloak.testsuite.AbstractAdminTest.loadJson;
|
||||
import static org.keycloak.testsuite.Assert.assertExpiration;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
|
||||
import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.getHttpAuthServerContextRoot;
|
||||
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SSL_REQUIRED;
|
||||
import static org.keycloak.testsuite.util.oauth.OAuthClient.AUTH_SERVER_ROOT;
|
||||
import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.getHttpAuthServerContextRoot;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
@@ -176,16 +178,13 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
URI uri = OIDCLoginProtocolService.tokenUrl(builder).build("test");
|
||||
WebTarget target = client.target(uri);
|
||||
|
||||
org.keycloak.representations.AccessTokenResponse tokenResponse = null;
|
||||
{
|
||||
String header = BasicAuthHelper.createHeader("test-app", "password");
|
||||
Form form = new Form();
|
||||
Response response = target.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, header)
|
||||
.post(Entity.form(form));
|
||||
assertEquals(400, response.getStatus());
|
||||
response.close();
|
||||
}
|
||||
String header = BasicAuthHelper.createHeader("test-app", "password");
|
||||
Form form = new Form();
|
||||
Response response = target.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, header)
|
||||
.post(Entity.form(form));
|
||||
assertEquals(400, response.getStatus());
|
||||
response.close();
|
||||
events.clear();
|
||||
}
|
||||
|
||||
@@ -256,7 +255,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
long actual = refreshToken.getExp() - getCurrentTime();
|
||||
assertThat(actual, allOf(greaterThanOrEqualTo(1799L - ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(1800L + ALLOWED_CLOCK_SKEW)));
|
||||
|
||||
assertEquals(sessionId, refreshToken.getSessionState());
|
||||
assertEquals(sessionId, refreshToken.getSessionId());
|
||||
assertNull(refreshToken.getNonce());
|
||||
|
||||
AccessTokenResponse response = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
|
||||
@@ -265,8 +264,8 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
assertEquals(200, response.getStatusCode());
|
||||
|
||||
assertEquals(sessionId, refreshedToken.getSessionState());
|
||||
assertEquals(sessionId, refreshedRefreshToken.getSessionState());
|
||||
assertEquals(sessionId, refreshedToken.getSessionId());
|
||||
assertEquals(sessionId, refreshedRefreshToken.getSessionId());
|
||||
|
||||
assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
|
||||
assertThat(refreshedToken.getExp() - getCurrentTime(), allOf(greaterThanOrEqualTo(250L - ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(300L + ALLOWED_CLOCK_SKEW)));
|
||||
@@ -303,7 +302,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
assertNotNull(response.getRefreshToken());
|
||||
refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||
assertEquals(sessionId, refreshToken.getSessionState());
|
||||
assertEquals(sessionId, refreshToken.getSessionId());
|
||||
assertNull(refreshToken.getNonce());
|
||||
}
|
||||
|
||||
@@ -360,32 +359,30 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
// Test when neither client nor user session is in the cache
|
||||
testingClient.server().run(session -> {
|
||||
session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).clear();
|
||||
session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).clear();
|
||||
session.getProvider(InfinispanConnectionProvider.class).getCache(USER_SESSION_CACHE_NAME).clear();
|
||||
session.getProvider(InfinispanConnectionProvider.class).getCache(CLIENT_SESSION_CACHE_NAME).clear();
|
||||
});
|
||||
|
||||
response = oauth.doRefreshTokenRequest(refreshTokenString);
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
|
||||
testingClient.server().run(session -> {
|
||||
assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).size(),
|
||||
assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(USER_SESSION_CACHE_NAME).size(),
|
||||
greaterThan(0));
|
||||
assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).size(),
|
||||
assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(CLIENT_SESSION_CACHE_NAME).size(),
|
||||
greaterThan(0));
|
||||
});
|
||||
|
||||
// Test is only the client session is missing
|
||||
testingClient.server().run(session -> {
|
||||
session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).clear();
|
||||
});
|
||||
testingClient.server().run(session -> session.getProvider(InfinispanConnectionProvider.class).getCache(CLIENT_SESSION_CACHE_NAME).clear());
|
||||
|
||||
response = oauth.doRefreshTokenRequest(refreshTokenString);
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
|
||||
testingClient.server().run(session -> {
|
||||
assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).size(),
|
||||
assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(USER_SESSION_CACHE_NAME).size(),
|
||||
greaterThan(0));
|
||||
assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).size(),
|
||||
assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(CLIENT_SESSION_CACHE_NAME).size(),
|
||||
greaterThan(0));
|
||||
});
|
||||
|
||||
@@ -428,20 +425,20 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
.clientId("public-client")
|
||||
.redirectUris("*")
|
||||
.publicClient()
|
||||
.build());
|
||||
.build()).close();
|
||||
|
||||
realmResource.users()
|
||||
.create(UserBuilder.create().username("alice")
|
||||
.firstName("alice")
|
||||
.lastName("alice")
|
||||
.email("alice@keycloak.org")
|
||||
.password("alice").addRoles("offline_access").build());
|
||||
.password("alice").addRoles("offline_access").build()).close();
|
||||
realmResource.users()
|
||||
.create(UserBuilder.create().username("bob")
|
||||
.firstName("bob")
|
||||
.lastName("bob")
|
||||
.email("bob@keycloak.org")
|
||||
.password("bob").addRoles("offline_access").build());
|
||||
.password("bob").addRoles("offline_access").build()).close();
|
||||
|
||||
oauth.realm(realmName);
|
||||
oauth.client("public-client");
|
||||
@@ -493,12 +490,12 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
.clientId("public-client")
|
||||
.redirectUris("*")
|
||||
.publicClient()
|
||||
.build());
|
||||
.build()).close();
|
||||
|
||||
realmResource.users()
|
||||
.create(UserBuilder.create().username("alice").password("alice").addRoles("offline_access").build());
|
||||
.create(UserBuilder.create().username("alice").password("alice").addRoles("offline_access").build()).close();
|
||||
realmResource.users()
|
||||
.create(UserBuilder.create().username("bob").password("bob").addRoles("offline_access").build());
|
||||
.create(UserBuilder.create().username("bob").password("bob").addRoles("offline_access").build()).close();
|
||||
|
||||
oauth.realm(realmName);
|
||||
oauth.client("public-client");
|
||||
@@ -509,7 +506,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
oauth.fillLoginForm("alice", "alice");
|
||||
|
||||
String aliceCode = oauth.parseLoginResponse().getCode();
|
||||
oauth.parseLoginResponse().getCode();
|
||||
// WebClient webClient = DroneHtmlUnitDriver.class.cast(driver).getWebClient();
|
||||
// webClient.getCookieManager().clearCookies();
|
||||
driver.manage().deleteAllCookies();
|
||||
@@ -585,7 +582,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
try {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
|
||||
String code = oauth.parseLoginResponse().getCode();
|
||||
oauth.parseLoginResponse().getCode();
|
||||
|
||||
String optionalScope = "phone address";
|
||||
oauth.scope(optionalScope);
|
||||
@@ -999,7 +996,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
code = oauth.parseLoginResponse().getCode();
|
||||
|
||||
AccessTokenResponse response4 = oauth.doAccessTokenRequest(code);
|
||||
RefreshToken refreshToken4 = oauth.parseRefreshToken(response4.getRefreshToken());
|
||||
oauth.parseRefreshToken(response4.getRefreshToken());
|
||||
events.expectCodeToToken(codeId, sessionId).assertEvent();
|
||||
|
||||
// Client sessions should be available again now after re-authentication
|
||||
@@ -1121,7 +1118,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
assertFalse(loginPage.isCurrent());
|
||||
|
||||
AccessTokenResponse tokenResponse2 = null;
|
||||
AccessTokenResponse tokenResponse2;
|
||||
String code = oauth.parseLoginResponse().getCode();
|
||||
tokenResponse2 = oauth.doAccessTokenRequest(code);
|
||||
|
||||
@@ -1155,7 +1152,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
assertFalse(loginPage.isCurrent());
|
||||
|
||||
AccessTokenResponse tokenResponse2 = null;
|
||||
AccessTokenResponse tokenResponse2;
|
||||
String code = oauth.parseLoginResponse().getCode();
|
||||
tokenResponse2 = oauth.doAccessTokenRequest(code);
|
||||
|
||||
@@ -1188,7 +1185,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
assertFalse(loginPage.isCurrent());
|
||||
|
||||
AccessTokenResponse tokenResponse2 = null;
|
||||
AccessTokenResponse tokenResponse2;
|
||||
String code = oauth.parseLoginResponse().getCode();
|
||||
tokenResponse2 = oauth.doAccessTokenRequest(code);
|
||||
|
||||
@@ -1231,8 +1228,8 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
|
||||
|
||||
AccessToken refreshedToken = oauth.verifyToken(tokenResponse.getAccessToken());
|
||||
RefreshToken refreshedRefreshToken = oauth.parseRefreshToken(tokenResponse.getRefreshToken());
|
||||
oauth.verifyToken(tokenResponse.getAccessToken());
|
||||
oauth.parseRefreshToken(tokenResponse.getRefreshToken());
|
||||
|
||||
assertEquals(200, tokenResponse.getStatusCode());
|
||||
|
||||
@@ -1281,7 +1278,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
public void testUserSessionRefreshAndIdleRememberMe() throws Exception {
|
||||
RealmResource testRealm = adminClient.realm("test");
|
||||
|
||||
try (Closeable realmUpdater = new RealmAttributeUpdater(testRealm)
|
||||
try (Closeable ignored = new RealmAttributeUpdater(testRealm)
|
||||
.updateWith(r -> {
|
||||
r.setRememberMe(true);
|
||||
r.setSsoSessionIdleTimeoutRememberMe(500);
|
||||
@@ -1357,7 +1354,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
public void refreshTokenUserSessionMaxLifespan() throws Exception {
|
||||
RealmResource realmResource = adminClient.realm("test");
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource)
|
||||
try (Closeable ignored = new RealmAttributeUpdater(realmResource)
|
||||
.updateWith(r -> {
|
||||
r.setSsoSessionMaxLifespan(3600);
|
||||
r.setSsoSessionIdleTimeout(7200);
|
||||
@@ -1384,7 +1381,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
events.expectRefresh(refreshId, sessionId).assertEvent();
|
||||
|
||||
setTimeOffset(3700);
|
||||
oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
||||
oauth.parseRefreshToken(tokenResponse.getRefreshToken());
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
|
||||
|
||||
assertEquals(400, tokenResponse.getStatusCode());
|
||||
@@ -1403,7 +1400,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
public void refreshTokenUserClientMaxLifespanSmallerThanSession() throws Exception {
|
||||
RealmResource realmResource = adminClient.realm("test");
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource)
|
||||
try (Closeable ignored = new RealmAttributeUpdater(realmResource)
|
||||
.updateWith(r -> {
|
||||
r.setSsoSessionMaxLifespan(3600);
|
||||
r.setSsoSessionIdleTimeout(7200);
|
||||
@@ -1469,7 +1466,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
public void refreshTokenUserClientMaxLifespanGreaterThanSession() throws Exception {
|
||||
RealmResource realmResource = adminClient.realm("test");
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource)
|
||||
try (Closeable ignored = new RealmAttributeUpdater(realmResource)
|
||||
.updateWith(r -> {
|
||||
r.setSsoSessionMaxLifespan(3600);
|
||||
r.setSsoSessionIdleTimeout(7200);
|
||||
@@ -1516,7 +1513,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
RealmResource realmResource = adminClient.realm("test");
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
|
||||
try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource)
|
||||
try (Closeable ignored = new RealmAttributeUpdater(realmResource)
|
||||
.updateWith(r -> {
|
||||
r.setSsoSessionMaxLifespan(7200);
|
||||
r.setSsoSessionIdleTimeout(7200);
|
||||
@@ -1545,7 +1542,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
assertEquals(400, tokenResponse.getStatusCode());
|
||||
assertNull(tokenResponse.getAccessToken());
|
||||
assertNull(tokenResponse.getRefreshToken());
|
||||
events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).session(sessionId).user((String) null).assertEvent();
|
||||
events.assertRefreshTokenErrorAndMaybeSessionExpired(sessionId, loginEvent.getUserId(), loginEvent.getClientId());
|
||||
assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
} finally {
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
@@ -1559,7 +1556,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
RealmResource realmResource = adminClient.realm("test");
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
|
||||
try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource)
|
||||
try (Closeable ignored = new RealmAttributeUpdater(realmResource)
|
||||
.updateWith(r -> {
|
||||
r.setSsoSessionMaxLifespan(7200);
|
||||
r.setSsoSessionIdleTimeout(7200);
|
||||
@@ -1624,7 +1621,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
RealmResource realmResource = adminClient.realm("test");
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
|
||||
try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource)
|
||||
try (Closeable ignored = new RealmAttributeUpdater(realmResource)
|
||||
.updateWith(r -> {
|
||||
r.setSsoSessionMaxLifespan(7200);
|
||||
r.setSsoSessionIdleTimeout(7200);
|
||||
@@ -1684,7 +1681,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
RealmResource testRealm = adminClient.realm("test");
|
||||
|
||||
try (Closeable realmUpdater = new RealmAttributeUpdater(testRealm)
|
||||
try (Closeable ignored = new RealmAttributeUpdater(testRealm)
|
||||
.updateWith(r -> {
|
||||
r.setRememberMe(true);
|
||||
r.setSsoSessionMaxLifespanRememberMe(100);
|
||||
@@ -1855,8 +1852,8 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
testingClient.server("test").run(session -> {
|
||||
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
|
||||
if (connections != null) {
|
||||
Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionCache = connections.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
|
||||
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache = connections.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
|
||||
Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionCache = connections.getCache(USER_SESSION_CACHE_NAME);
|
||||
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache = connections.getCache(CLIENT_SESSION_CACHE_NAME);
|
||||
if (sessionCache != null) {
|
||||
sessionCache.clear();
|
||||
}
|
||||
@@ -1891,8 +1888,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
@Test
|
||||
public void testCheckSsl() {
|
||||
Client client = AdminClientUtil.createResteasyClient();
|
||||
try {
|
||||
try (Client client = AdminClientUtil.createResteasyClient()) {
|
||||
UriBuilder builder = UriBuilder.fromUri(AUTH_SERVER_ROOT);
|
||||
URI grantUri = OIDCLoginProtocolService.tokenUrl(builder).build("test");
|
||||
WebTarget grantTarget = client.target(grantUri);
|
||||
@@ -1900,7 +1896,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
URI uri = OIDCLoginProtocolService.tokenUrl(builder).build("test");
|
||||
WebTarget refreshTarget = client.target(uri);
|
||||
|
||||
String refreshToken = null;
|
||||
String refreshToken;
|
||||
{
|
||||
Response response = executeGrantAccessTokenRequest(grantTarget);
|
||||
assertEquals(200, response.getStatus());
|
||||
@@ -1932,15 +1928,11 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Response response = executeRefreshToken(refreshTarget, refreshToken);
|
||||
assertEquals(200, response.getStatus());
|
||||
org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class);
|
||||
refreshToken = tokenResponse.getRefreshToken();
|
||||
response.close();
|
||||
}
|
||||
Response response = executeRefreshToken(refreshTarget, refreshToken);
|
||||
assertEquals(200, response.getStatus());
|
||||
response.readEntity(org.keycloak.representations.AccessTokenResponse.class);
|
||||
response.close();
|
||||
} finally {
|
||||
client.close();
|
||||
events.clear();
|
||||
}
|
||||
|
||||
@@ -1993,7 +1985,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
events.expectCodeToToken(codeId, sessionId).user(userId).assertEvent();
|
||||
|
||||
adminClient.realm("test").users().delete(userId);
|
||||
adminClient.realm("test").users().delete(userId).close();
|
||||
|
||||
response = oauth.doRefreshTokenRequest(refreshTokenString);
|
||||
assertEquals(400, response.getStatusCode());
|
||||
@@ -2102,6 +2094,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
@Test // KEYCLOAK-17323
|
||||
public void testRefreshTokenWhenClientSessionTimeoutPassedButRealmDidNot() {
|
||||
//noinspection resource
|
||||
getCleanup()
|
||||
.addCleanup(new RealmAttributeUpdater(adminClient.realm("test"))
|
||||
.setSsoSessionIdleTimeout(2592000) // 30 Days
|
||||
@@ -2219,7 +2212,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
.post(Entity.form(form));
|
||||
}
|
||||
|
||||
private void conductTokenRefreshRequest(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
|
||||
private void conductTokenRefreshRequest(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
|
||||
try {
|
||||
// Realm setting is used for ID Token signature algorithm
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, expectedIdTokenAlg);
|
||||
@@ -2268,7 +2261,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
assertEquals("Bearer", tokenResponse.getTokenType());
|
||||
|
||||
assertEquals(sessionId, refreshToken.getSessionState());
|
||||
assertEquals(sessionId, refreshToken.getSessionId());
|
||||
|
||||
AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString);
|
||||
if (response.getError() != null || response.getErrorDescription() != null) {
|
||||
@@ -2280,8 +2273,8 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||
|
||||
assertEquals(200, response.getStatusCode());
|
||||
|
||||
assertEquals(sessionId, refreshedToken.getSessionState());
|
||||
assertEquals(sessionId, refreshedRefreshToken.getSessionState());
|
||||
assertEquals(sessionId, refreshedToken.getSessionId());
|
||||
assertEquals(sessionId, refreshedRefreshToken.getSessionId());
|
||||
|
||||
Assert.assertNotEquals(token.getId(), refreshedToken.getId());
|
||||
Assert.assertNotEquals(refreshToken.getId(), refreshedRefreshToken.getId());
|
||||
|
||||
@@ -128,6 +128,12 @@
|
||||
<groupId>org.infinispan</groupId>
|
||||
<artifactId>infinispan-remote-query-server</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2025 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 java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.awaitility.Awaitility;
|
||||
import org.infinispan.Cache;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Event;
|
||||
import org.keycloak.events.EventStoreProvider;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RealmProvider;
|
||||
import org.keycloak.models.UserManager;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserProvider;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.UserSessionProvider;
|
||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
import org.keycloak.testsuite.model.HotRodServerRule;
|
||||
import org.keycloak.testsuite.model.KeycloakModelTest;
|
||||
import org.keycloak.testsuite.model.RequireProvider;
|
||||
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME;
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
|
||||
import static org.keycloak.models.utils.SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||
import static org.keycloak.testsuite.model.session.UserSessionPersisterProviderTest.createClients;
|
||||
import static org.keycloak.testsuite.model.session.UserSessionPersisterProviderTest.createSessions;
|
||||
|
||||
@RequireProvider(UserSessionPersisterProvider.class)
|
||||
@RequireProvider(UserSessionProvider.class)
|
||||
@RequireProvider(UserProvider.class)
|
||||
@RequireProvider(RealmProvider.class)
|
||||
@RequireProvider(EventStoreProvider.class)
|
||||
public class UserSessionExpirationTest extends KeycloakModelTest {
|
||||
|
||||
private static final int IDLE_TIMEOUT = 1800;
|
||||
private static final int LIFESPAN_TIMEOUT = 36000;
|
||||
|
||||
private String realmId;
|
||||
|
||||
@Override
|
||||
public void createEnvironment(KeycloakSession s) {
|
||||
RealmModel realm = createRealm(s, "test");
|
||||
realm.setEventsEnabled(true);
|
||||
realm.setEnabledEventTypes(Set.of(EventType.USER_SESSION_DELETED.name()));
|
||||
s.getContext().setRealm(realm);
|
||||
realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName()));
|
||||
realm.setSsoSessionIdleTimeout(IDLE_TIMEOUT);
|
||||
realm.setSsoSessionMaxLifespan(LIFESPAN_TIMEOUT);
|
||||
this.realmId = realm.getId();
|
||||
|
||||
s.users().addUser(realm, "user1").setEmail("user1@localhost");
|
||||
s.users().addUser(realm, "user2").setEmail("user2@localhost");
|
||||
|
||||
createClients(s, realm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanEnvironment(KeycloakSession s) {
|
||||
RealmModel realm = s.realms().getRealm(realmId);
|
||||
s.getContext().setRealm(realm);
|
||||
s.sessions().removeUserSessions(realm);
|
||||
|
||||
UserModel user1 = s.users().getUserByUsername(realm, "user1");
|
||||
UserModel user2 = s.users().getUserByUsername(realm, "user2");
|
||||
|
||||
UserManager um = new UserManager(s);
|
||||
if (user1 != null) {
|
||||
um.removeUser(realm, user1);
|
||||
}
|
||||
if (user2 != null) {
|
||||
um.removeUser(realm, user2);
|
||||
}
|
||||
|
||||
s.realms().removeRealm(realmId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpirationEvents() {
|
||||
UserSessionModel[] userSessions = inComittedTransaction(session -> {
|
||||
return createSessions(session, realmId);
|
||||
});
|
||||
Map<String, String> sessionIdAndUsers = Arrays.stream(userSessions)
|
||||
.collect(Collectors.toUnmodifiableMap(UserSessionModel::getId, s -> s.getUser().getId()));
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
// Time offset is automatically cleaned up in KeycloakModelTest.cleanEnvironment()
|
||||
Time.setOffset(IDLE_TIMEOUT + PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS + 10);
|
||||
RealmModel realm = session.realms().getRealm(realmId);
|
||||
session.getContext().setRealm(realm);
|
||||
session.getProvider(UserSessionPersisterProvider.class).removeExpired(realm);
|
||||
|
||||
var hotRodServer = getParameters(HotRodServerRule.class).findFirst();
|
||||
if (hotRodServer.isEmpty()) {
|
||||
InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class);
|
||||
processExpiration(provider.getCache(USER_SESSION_CACHE_NAME));
|
||||
processExpiration(provider.getCache(OFFLINE_USER_SESSION_CACHE_NAME));
|
||||
} else {
|
||||
hotRodServer.get().streamCacheManagers()
|
||||
.forEach(cacheManager -> {
|
||||
processExpiration(cacheManager.getCache(USER_SESSION_CACHE_NAME));
|
||||
processExpiration(cacheManager.getCache(OFFLINE_USER_SESSION_CACHE_NAME));
|
||||
});
|
||||
}
|
||||
|
||||
// Infinispan events are async, let's ensure it is stored in the database before proceed.
|
||||
Awaitility.await().until(() -> eventsCount(session) == sessionIdAndUsers.size());
|
||||
});
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
RealmModel realm = session.realms().getRealm(realmId);
|
||||
session.getContext().setRealm(realm);
|
||||
// user session id -> user id
|
||||
Map<String, String> eventsData = events(session);
|
||||
Assert.assertEquals(sessionIdAndUsers, eventsData);
|
||||
});
|
||||
}
|
||||
|
||||
private static void processExpiration(Cache<?, ?> cache) {
|
||||
cache.getAdvancedCache().getExpirationManager().processExpiration();
|
||||
}
|
||||
|
||||
private static long eventsCount(KeycloakSession session) {
|
||||
EventStoreProvider provider = session.getProvider(EventStoreProvider.class);
|
||||
return provider.createQuery()
|
||||
.type(EventType.USER_SESSION_DELETED)
|
||||
.getResultStream()
|
||||
.filter(event -> Details.EXPIRED_DETAIL.equals(event.getDetails().get(Details.REASON)))
|
||||
.count();
|
||||
}
|
||||
|
||||
private static Map<String, String> events(KeycloakSession session) {
|
||||
EventStoreProvider provider = session.getProvider(EventStoreProvider.class);
|
||||
return provider.createQuery()
|
||||
.type(EventType.USER_SESSION_DELETED)
|
||||
.getResultStream()
|
||||
.filter(event -> Details.EXPIRED_DETAIL.equals(event.getDetails().get(Details.REASON)))
|
||||
.collect(Collectors.toUnmodifiableMap(Event::getSessionId, Event::getUserId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -557,13 +557,8 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
|
||||
|
||||
createClientSession(session, realmId, realm.getClientByClientId("test-app"), userSession, "http://redirect", "state");
|
||||
userSessionsInner.add(userSession.getId());
|
||||
persistUserSession(session, userSession, true);
|
||||
}
|
||||
|
||||
for (String userSessionId : userSessionsInner) {
|
||||
UserSessionModel userSession2 = session.sessions().getUserSession(realm, userSessionId);
|
||||
persistUserSession(session, userSession2, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user