mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-16 12:05:49 -06:00
Login failure cache: Evict entries after the configured failure reset time
Closes #44801 Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com> Signed-off-by: Alexander Schwartz <alexander.schwartz@gmx.net> Signed-off-by: Pedro Ruivo <pruivo@redhat.com> Co-authored-by: Christian Glasmachers <Christian.Glasmachers-extern@deutschebahn.com> Co-authored-by: Alexander Schwartz <alexander.schwartz@ibm.com> Co-authored-by: Alexander Schwartz <alexander.schwartz@gmx.net> Co-authored-by: Pedro Ruivo <pruivo@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
ef011ea4d2
commit
921b10ee80
@@ -19,7 +19,7 @@ To enable this protection:
|
||||
. Click *Realm Settings* in the menu
|
||||
. Click the *Security Defenses* tab.
|
||||
. Click the *Brute Force Detection* tab.
|
||||
. Choose the *Brute Force Mode* which best fit to your requirements.
|
||||
. Choose the *Brute Force Mode* which best fit to your requirements.
|
||||
+
|
||||
.Brute force detection
|
||||
image:images/brute-force.png[]
|
||||
@@ -132,7 +132,7 @@ wait time will never reach the value you have set to `Max wait`.
|
||||
|
||||
*Strategies to set Wait Time*
|
||||
|
||||
{project_name} provides two strategies to calculate wait time: By multiples or Linear. By multiples is the first strategy introduced by {project_name}, so that is the default one.
|
||||
{project_name} provides two strategies to calculate wait time: By multiples or Linear. By multiples is the first strategy introduced by {project_name}, so that is the default one.
|
||||
|
||||
By multiples strategy, wait time is incremented when the number (or count) of failures are multiples of `Max Login Failure`. For instance, if you set `Max Login Failures` to `5` and a `Wait Increment` to `30` seconds, the effective time that an account is disabled after several failed authentication attempts will be:
|
||||
|
||||
@@ -151,7 +151,7 @@ By multiples strategy, wait time is incremented when the number (or count) of fa
|
||||
|**10** |**30** | 5 | **60**
|
||||
|===
|
||||
|
||||
At the fifth failed attempt, the account is disabled for `30` seconds. After reaching the next multiple of `Max Login Failures`, in this case `10`, the time increases from `30` to `60` seconds.
|
||||
At the fifth failed attempt, the account is disabled for `30` seconds. After reaching the next multiple of `Max Login Failures`, in this case `10`, the time increases from `30` to `60` seconds.
|
||||
|
||||
The By multiple strategy uses the following formula to calculate wait time: _Wait Increment in Seconds_ * (`count` / _Max Login Failures_). The division is an integer division rounded down to a whole number.
|
||||
|
||||
@@ -177,7 +177,7 @@ At the fifth failed attempt, the account is disabled for `30` seconds. Each new
|
||||
The linear strategy uses the following formula to calculate wait time: _Wait Increment in Seconds_ * (1 + `count` - _Max Login Failures_).
|
||||
|
||||
==== Lockout permanently after temporary lockout
|
||||
Mixed mode. Locks user temporarily for specified number of times and then locks user permanently.
|
||||
Mixed mode. Locks user temporarily for specified number of times and then locks user permanently.
|
||||
|
||||
.Lockout permanently after temporary lockout
|
||||
image:images/brute-force-mixed.png[]
|
||||
|
||||
@@ -81,6 +81,12 @@ If you are running on PostgreSQL as a database for {project_name}, ensure that t
|
||||
This is used during upgrades of {project_name} to determine an estimated number of rows in a table.
|
||||
If {project_name} does not have permissions to access these tables, it will log a warning and proceed with the less efficient `+SELECT COUNT(*) ...+` operation during the upgrade to determine the number of rows in tables affected by schema changes.
|
||||
|
||||
=== Expiration of login failures from the embedded caches
|
||||
|
||||
Previous entries in the `loginFailures` cache never expired, and entries accumulated as users entered wrong credentials, increasing the memory consumption.
|
||||
|
||||
Starting with this release, entries will expire based on the "`Failure reset time`" configured in the "`Brute force detection`" for the modes "`Lockout temporarily`" and "`Lockout permanently after temporary lockout`". For "`Lockout permanently`", entries will not expire as before, as this mode does not have a "`Failure reset time`".
|
||||
|
||||
=== Not recommended to use org.keycloak.credential.UserCredentialManager directly in your extensions
|
||||
|
||||
If you have user storage extension and you reference the class `org.keycloak.credential.UserCredentialManager` from your providers, it is recommended to avoid using this class directly as it might be
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.IdentityProviderQuery;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.OAuth2DeviceConfig;
|
||||
import org.keycloak.models.OTPPolicy;
|
||||
import org.keycloak.models.ParConfig;
|
||||
@@ -53,10 +54,12 @@ import org.keycloak.models.RequiredActionConfigModel;
|
||||
import org.keycloak.models.RequiredActionProviderModel;
|
||||
import org.keycloak.models.RequiredCredentialModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserLoginFailureProvider;
|
||||
import org.keycloak.models.WebAuthnPolicy;
|
||||
import org.keycloak.models.cache.CachedRealmModel;
|
||||
import org.keycloak.models.cache.UserCache;
|
||||
import org.keycloak.models.cache.infinispan.entities.CachedRealm;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.UserStorageUtil;
|
||||
@@ -259,9 +262,44 @@ public class RealmAdapter implements CachedRealmModel {
|
||||
@Override
|
||||
public void setBruteForceProtected(boolean value) {
|
||||
getDelegateForUpdate();
|
||||
if (updated.isBruteForceProtected() != value) {
|
||||
updateBruteForceSettings();
|
||||
}
|
||||
updated.setBruteForceProtected(value);
|
||||
}
|
||||
|
||||
boolean updateBruteForceSettings = false;
|
||||
|
||||
private void updateBruteForceSettings() {
|
||||
// TODO: This should really be an event where the recipient could figure out what has changed and can react accordingly
|
||||
if (!updateBruteForceSettings) {
|
||||
updateBruteForceSettings = true;
|
||||
KeycloakSessionFactory sf = session.getKeycloakSessionFactory();
|
||||
session.getTransactionManager().enlistAfterCompletion(new AbstractKeycloakTransaction() {
|
||||
@Override
|
||||
protected void commitImpl() {
|
||||
runUpdateOfLoginFailureProvider(sf, cached.getId());
|
||||
// Should not be necessary, as the cache entry of the realm will be discarded
|
||||
updateBruteForceSettings = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void rollbackImpl() {
|
||||
updateBruteForceSettings = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void runUpdateOfLoginFailureProvider(KeycloakSessionFactory keycloakSessionFactory, String realmId) {
|
||||
KeycloakModelUtils.runJobInTransaction(keycloakSessionFactory,
|
||||
s -> {
|
||||
UserLoginFailureProvider provider = s.getProvider(UserLoginFailureProvider.class);
|
||||
RealmModel realm = s.realms().getRealm(realmId);
|
||||
provider.updateWithLatestRealmSettings(realm);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPermanentLockout() {
|
||||
if(isUpdated()) return updated.isPermanentLockout();
|
||||
@@ -271,6 +309,9 @@ public class RealmAdapter implements CachedRealmModel {
|
||||
@Override
|
||||
public void setPermanentLockout(final boolean val) {
|
||||
getDelegateForUpdate();
|
||||
if (updated.isPermanentLockout() != val) {
|
||||
updateBruteForceSettings();
|
||||
}
|
||||
updated.setPermanentLockout(val);
|
||||
}
|
||||
|
||||
@@ -283,6 +324,9 @@ public class RealmAdapter implements CachedRealmModel {
|
||||
@Override
|
||||
public void setMaxTemporaryLockouts(final int val) {
|
||||
getDelegateForUpdate();
|
||||
if (updated.getMaxTemporaryLockouts() != val) {
|
||||
updateBruteForceSettings();
|
||||
}
|
||||
updated.setMaxTemporaryLockouts(val);
|
||||
}
|
||||
|
||||
@@ -355,6 +399,9 @@ public class RealmAdapter implements CachedRealmModel {
|
||||
@Override
|
||||
public void setMaxDeltaTimeSeconds(int val) {
|
||||
getDelegateForUpdate();
|
||||
if (updated.getMaxDeltaTimeSeconds() != val) {
|
||||
updateBruteForceSettings();
|
||||
}
|
||||
updated.setMaxDeltaTimeSeconds(val);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
*/
|
||||
package org.keycloak.models.sessions.infinispan;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
@@ -32,10 +34,14 @@ import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
|
||||
import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent;
|
||||
import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
|
||||
import org.keycloak.models.sessions.infinispan.stream.Mappers;
|
||||
import org.keycloak.models.sessions.infinispan.stream.RemoveKeyConsumer;
|
||||
import org.keycloak.models.sessions.infinispan.stream.SessionWrapperPredicate;
|
||||
import org.keycloak.models.sessions.infinispan.util.FuturesHelper;
|
||||
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.infinispan.context.Flag;
|
||||
import org.infinispan.util.function.SerializableBiFunction;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import static org.keycloak.common.util.StackUtil.getShortStackTrace;
|
||||
@@ -118,7 +124,7 @@ public class InfinispanUserLoginFailureProvider implements UserLoginFailureProvi
|
||||
.map(Mappers.loginFailureId())
|
||||
.forEach(loginFailureKey -> {
|
||||
// Remove loginFailure from remoteCache too. Use removeAsync for better perf
|
||||
Future<?> future = localCache.removeAsync(loginFailureKey);
|
||||
Future<?> future = removeKeyFromCache(localCache, loginFailureKey);
|
||||
futures.addTask(future);
|
||||
});
|
||||
|
||||
@@ -145,4 +151,32 @@ public class InfinispanUserLoginFailureProvider implements UserLoginFailureProvi
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateWithLatestRealmSettings(RealmModel realm) {
|
||||
Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> cache = loginFailuresTx.getCache();
|
||||
if (!realm.isBruteForceProtected()) {
|
||||
cache.entrySet().stream()
|
||||
.filter(SessionWrapperPredicate.create(realm.getId()))
|
||||
.forEach(RemoveKeyConsumer.getInstance());
|
||||
} else {
|
||||
final long maxDeltaTimeMillis = realm.getMaxDeltaTimeSeconds() * 1000L;
|
||||
final boolean isPermanentLockout = realm.isPermanentLockout();
|
||||
final int maxTemporaryLockouts = realm.getMaxTemporaryLockouts();
|
||||
cache.entrySet().stream()
|
||||
.filter(SessionWrapperPredicate.create(realm.getId()))
|
||||
.<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>>forEach((c, entry) -> {
|
||||
var entity = entry.getValue().getEntity();
|
||||
long lifespan = SessionTimeouts.getLoginFailuresLifespanMs(isPermanentLockout, maxTemporaryLockouts, maxDeltaTimeMillis, entity);
|
||||
c.getAdvancedCache()
|
||||
.withFlags(Flag.ZERO_LOCK_ACQUISITION_TIMEOUT,Flag.FAIL_SILENTLY, Flag.IGNORE_RETURN_VALUES)
|
||||
.computeIfPresent(entry.getKey(), (SerializableBiFunction<? super LoginFailureKey, ? super SessionEntityWrapper<LoginFailureEntity>, ? extends SessionEntityWrapper<LoginFailureEntity>>) (key, value) -> value, lifespan, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static CompletableFuture<SessionEntityWrapper<LoginFailureEntity>> removeKeyFromCache(Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> cache, LoginFailureKey key) {
|
||||
return cache.removeAsync(key);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserLoginFailureModel;
|
||||
import org.keycloak.models.sessions.infinispan.changes.remote.updater.BaseUpdater;
|
||||
import org.keycloak.models.sessions.infinispan.changes.remote.updater.Expiration;
|
||||
@@ -28,6 +29,7 @@ import org.keycloak.models.sessions.infinispan.changes.remote.updater.Updater;
|
||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
|
||||
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
|
||||
import org.keycloak.utils.KeycloakSessionUtil;
|
||||
|
||||
/**
|
||||
* Implementation of {@link Updater} and {@link UserLoginFailureModel}.
|
||||
@@ -62,9 +64,10 @@ public class LoginFailuresUpdater extends BaseUpdater<LoginFailureKey, LoginFail
|
||||
|
||||
@Override
|
||||
public Expiration computeExpiration() {
|
||||
RealmModel realm = KeycloakSessionUtil.getKeycloakSession().getContext().getRealm();
|
||||
return new Expiration(
|
||||
SessionTimeouts.getLoginFailuresMaxIdleMs(null, null, getValue()),
|
||||
SessionTimeouts.getLoginFailuresLifespanMs(null, null, getValue()));
|
||||
SessionTimeouts.getLoginFailuresMaxIdleMs(realm, null, getValue()),
|
||||
SessionTimeouts.getLoginFailuresLifespanMs(realm, null, getValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2024 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.query;
|
||||
|
||||
import org.keycloak.marshalling.Marshalling;
|
||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
|
||||
|
||||
import org.infinispan.client.hotrod.RemoteCache;
|
||||
import org.infinispan.commons.api.query.Query;
|
||||
|
||||
/**
|
||||
* Util class with Infinispan Ickle Queries for {@link LoginFailureEntity}.
|
||||
*/
|
||||
public final class LoginFailureQueries {
|
||||
|
||||
private LoginFailureQueries() {
|
||||
}
|
||||
|
||||
public static final String LOGIN_FAILURE = Marshalling.protoEntity(LoginFailureEntity.class);
|
||||
|
||||
private static final String BASE_QUERY = "FROM %s as e ".formatted(LOGIN_FAILURE);
|
||||
private static final String BY_REALM_ID = BASE_QUERY + "WHERE e.realmId = :realmId";
|
||||
|
||||
/**
|
||||
* Returns a projection with the login failure session.
|
||||
*/
|
||||
public static Query<LoginFailureEntity> searchByRealmId(RemoteCache<LoginFailureKey, LoginFailureEntity> cache, String realmId) {
|
||||
return cache.<LoginFailureEntity>query(BY_REALM_ID)
|
||||
.setParameter("realmId", realmId);
|
||||
}
|
||||
}
|
||||
@@ -18,14 +18,25 @@ package org.keycloak.models.sessions.infinispan.remote;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserLoginFailureModel;
|
||||
import org.keycloak.models.UserLoginFailureProvider;
|
||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
|
||||
import org.keycloak.models.sessions.infinispan.query.LoginFailureQueries;
|
||||
import org.keycloak.models.sessions.infinispan.query.QueryHelper;
|
||||
import org.keycloak.models.sessions.infinispan.remote.transaction.LoginFailureChangeLogTransaction;
|
||||
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
|
||||
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import org.infinispan.client.hotrod.RemoteCache;
|
||||
import org.infinispan.commons.api.query.Query;
|
||||
import org.infinispan.commons.util.concurrent.CompletionStages;
|
||||
import org.infinispan.util.concurrent.WithinThreadExecutor;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import static org.keycloak.common.util.StackUtil.getShortStackTrace;
|
||||
@@ -77,6 +88,32 @@ public class RemoteUserLoginFailureProvider implements UserLoginFailureProvider
|
||||
transaction.removeByRealmId(realm.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateWithLatestRealmSettings(RealmModel realm) {
|
||||
RemoteCache<LoginFailureKey, LoginFailureEntity> cache = transaction.getCache();
|
||||
if (!realm.isBruteForceProtected()) {
|
||||
removeAllUserLoginFailures(realm);
|
||||
} else {
|
||||
final long maxDeltaTimeMillis = realm.getMaxDeltaTimeSeconds() * 1000L;
|
||||
final boolean isPermanentLockout = realm.isPermanentLockout();
|
||||
final int maxTemporaryLockouts = realm.getMaxTemporaryLockouts();
|
||||
Query<LoginFailureEntity> query = LoginFailureQueries.searchByRealmId(cache, realm.getId());
|
||||
CompletionStages.performConcurrently(
|
||||
QueryHelper.streamAll(query, 20, Function.identity()),
|
||||
20,
|
||||
Schedulers.from(new WithinThreadExecutor()),
|
||||
entry -> updateLifetimeOfCacheEntry(entry, cache, isPermanentLockout, maxTemporaryLockouts, maxDeltaTimeMillis));
|
||||
}
|
||||
}
|
||||
|
||||
private static CompletionStage<?> updateLifetimeOfCacheEntry(LoginFailureEntity entry, RemoteCache<LoginFailureKey, LoginFailureEntity> cache, boolean isPermanentLockout, int maxTemporaryLockouts, long maxDeltaTimeMillis) {
|
||||
long lifespan = SessionTimeouts.getLoginFailuresLifespanMs(isPermanentLockout, maxTemporaryLockouts, maxDeltaTimeMillis, entry);
|
||||
return cache.computeIfPresentAsync(new LoginFailureKey(entry.getRealmId(), entry.getUserId()),
|
||||
// Keep the original value - this should only update the lifespan and idle time
|
||||
(loginFailureKey, loginFailureEntitySessionEntityWrapper) -> loginFailureEntitySessionEntityWrapper,
|
||||
lifespan, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
|
||||
@@ -205,7 +205,23 @@ public class SessionTimeouts {
|
||||
* @return
|
||||
*/
|
||||
public static long getLoginFailuresLifespanMs(RealmModel realm, ClientModel client, LoginFailureEntity loginFailureEntity) {
|
||||
return IMMORTAL_FLAG;
|
||||
return getLoginFailuresLifespanMs(realm.isPermanentLockout(), realm.getMaxTemporaryLockouts(), realm.getMaxDeltaTimeSeconds() * 1000L, loginFailureEntity);
|
||||
}
|
||||
|
||||
public static long getLoginFailuresLifespanMs(boolean isPermanentLockout, int maxTemporaryLockouts, long maxDeltaTimeMillis, LoginFailureEntity loginFailureEntity) {
|
||||
if (loginFailureEntity.getLastFailure() == 0) {
|
||||
// If login failure has been reset, expire the entry.
|
||||
return 0;
|
||||
} else if (isPermanentLockout && maxTemporaryLockouts == 0) {
|
||||
// If mode is permanent lockout only, the "failure reset time" cannot be configured and login failures should never expire.
|
||||
return IMMORTAL_FLAG;
|
||||
} else {
|
||||
// Use realm-specific "failure reset time" configured in the brute force detection settings.
|
||||
// If the time between login failures is greater than the failure reset time,
|
||||
// the brute force detector will reset the failure counter.
|
||||
// So we can safely evict the login failure entry from the cache after this time.
|
||||
return Math.max(0, maxDeltaTimeMillis - (Time.currentTimeMillis() - loginFailureEntity.getLastFailure()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -464,9 +464,9 @@ public final class KeycloakModelUtils {
|
||||
} catch (Throwable t) {
|
||||
session.getTransactionManager().setRollbackOnly();
|
||||
throw t;
|
||||
} finally {
|
||||
KeycloakSessionUtil.setKeycloakSession(existing);
|
||||
}
|
||||
} finally {
|
||||
KeycloakSessionUtil.setKeycloakSession(existing);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -52,4 +52,9 @@ public interface UserLoginFailureProvider extends Provider {
|
||||
*/
|
||||
void removeAllUserLoginFailures(RealmModel realm);
|
||||
|
||||
/**
|
||||
* This is called when the realm settings change in relation to the brute force timeouts.
|
||||
*/
|
||||
default void updateWithLatestRealmSettings(RealmModel realm) {};
|
||||
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.events.email.EmailEventListenerProviderFactory;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserLoginFailureModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
import org.keycloak.models.utils.TimeBasedOTP;
|
||||
@@ -44,6 +46,7 @@ import org.keycloak.services.managers.BruteForceProtector;
|
||||
import org.keycloak.testsuite.AbstractChangeImportedUserPasswordsTest;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.AssertEvents.ExpectedEvent;
|
||||
import org.keycloak.testsuite.model.infinispan.InfinispanTestUtil;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
@@ -80,8 +83,6 @@ import static org.junit.Assert.assertTrue;
|
||||
*/
|
||||
public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
|
||||
private static String userId;
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
@@ -108,8 +109,6 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
|
||||
private TimeBasedOTP totp = new TimeBasedOTP();
|
||||
|
||||
private int lifespan;
|
||||
|
||||
private static final Integer failureFactor = 2;
|
||||
|
||||
@Override
|
||||
@@ -125,9 +124,6 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
testRealm.setMaxFailureWaitSeconds(100);
|
||||
testRealm.setWaitIncrementSeconds(20);
|
||||
testRealm.setOtpPolicyCodeReusable(true);
|
||||
//testRealm.setQuickLoginCheckMilliSeconds(0L);
|
||||
|
||||
userId = user.getId();
|
||||
|
||||
RealmRepUtil.findClientByClientId(testRealm, "test-app").setDirectAccessGrantsEnabled(true);
|
||||
testRealm.getUsers().add(UserBuilder.create().username("user2").email("user2@localhost").password(generatePassword("user2")).build());
|
||||
@@ -136,6 +132,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
@Before
|
||||
public void config() {
|
||||
try {
|
||||
testingClient.server().run(InfinispanTestUtil::setTestingTimeService);
|
||||
clearUserFailures();
|
||||
clearAllUserFailures();
|
||||
RealmRepresentation realm = adminClient.realm("test").toRepresentation();
|
||||
@@ -156,6 +153,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
@After
|
||||
public void slowItDown() throws Exception {
|
||||
try {
|
||||
testingClient.server().run(InfinispanTestUtil::revertTimeService);
|
||||
clearUserFailures();
|
||||
clearAllUserFailures();
|
||||
RealmRepresentation realm = adminClient.realm("test").toRepresentation();
|
||||
@@ -178,7 +176,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
totp = new TimeBasedOTP();
|
||||
}
|
||||
|
||||
public String getAdminToken() throws Exception {
|
||||
public String getAdminToken() {
|
||||
return oauth.realm("master").client(Constants.ADMIN_CLI_CLIENT_ID).doPasswordGrantRequest( "admin", "admin").getAccessToken();
|
||||
}
|
||||
|
||||
@@ -186,16 +184,16 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
return oauth.passwordGrantRequest("test-user@localhost", password).otp(totp).send();
|
||||
}
|
||||
|
||||
protected void clearUserFailures() throws Exception {
|
||||
protected void clearUserFailures() {
|
||||
adminClient.realm("test").attackDetection().clearBruteForceForUser(findUser("test-user@localhost").getId());
|
||||
}
|
||||
|
||||
protected void clearAllUserFailures() throws Exception {
|
||||
protected void clearAllUserFailures() {
|
||||
adminClient.realm("test").attackDetection().clearAllBruteForce();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidConfiguration() throws Exception {
|
||||
public void testInvalidConfiguration() {
|
||||
RealmRepresentation realm = testRealm().toRepresentation();
|
||||
realm.setFailureFactor(-1);
|
||||
try {
|
||||
@@ -261,7 +259,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrantInvalidPassword() throws Exception {
|
||||
public void testGrantInvalidPassword() {
|
||||
{
|
||||
String totpSecret = totp.generateTOTP("totpSecret");
|
||||
AccessTokenResponse response = getTestToken(getPassword("test-user@localhost"), totpSecret);
|
||||
@@ -307,7 +305,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrantInvalidOtp() throws Exception {
|
||||
public void testGrantInvalidOtp() {
|
||||
{
|
||||
String totpSecret = totp.generateTOTP("totpSecret");
|
||||
AccessTokenResponse response = getTestToken(getPassword("test-user@localhost"), totpSecret);
|
||||
@@ -355,7 +353,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrantMissingOtp() throws Exception {
|
||||
public void testGrantMissingOtp() {
|
||||
{
|
||||
String totpSecret = totp.generateTOTP("totpSecret");
|
||||
AccessTokenResponse response = getTestToken(getPassword("test-user@localhost"), totpSecret);
|
||||
@@ -399,7 +397,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNumberOfFailuresForDisabledUsersWithPasswordGrantType() throws Exception {
|
||||
public void testNumberOfFailuresForDisabledUsersWithPasswordGrantType() {
|
||||
try {
|
||||
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0);
|
||||
assertUserNumberOfFailures(user.getId(), 0);
|
||||
@@ -466,7 +464,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBrowserInvalidPassword() throws Exception {
|
||||
public void testBrowserInvalidPassword() {
|
||||
loginSuccess();
|
||||
loginInvalidPassword();
|
||||
loginInvalidPassword();
|
||||
@@ -483,7 +481,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailureResetForTemporaryLockout() throws Exception {
|
||||
public void testFailureResetForTemporaryLockout() {
|
||||
RealmRepresentation realm = testRealm().toRepresentation();
|
||||
try {
|
||||
realm.setMaxDeltaTimeSeconds(5);
|
||||
@@ -503,7 +501,25 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFailureResetForPermanentLockout() throws Exception {
|
||||
public void testCacheExpiryForTemporaryLockout() {
|
||||
RealmRepresentation realm = testRealm().toRepresentation();
|
||||
loginInvalidPassword();
|
||||
|
||||
//Wait for brute force executor to process the login and then wait for delta time
|
||||
WaitUtils.waitForBruteForceExecutors(testingClient);
|
||||
setTimeOffset(realm.getMaxDeltaTimeSeconds());
|
||||
|
||||
String realmId = realm.getId();
|
||||
testingClient.server().run(session -> {
|
||||
RealmModel realmModel = session.realms().getRealm(realmId);
|
||||
UserModel userModel = session.users().getUserByEmail(realmModel, "test-user@localhost");
|
||||
UserLoginFailureModel userLoginFailure = session.loginFailures().getUserLoginFailure(realmModel, userModel.getId());
|
||||
Assert.assertNull("cache entry should have expired", userLoginFailure);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFailureResetForPermanentLockout() {
|
||||
RealmRepresentation realm = testRealm().toRepresentation();
|
||||
try {
|
||||
realm.setMaxDeltaTimeSeconds(5);
|
||||
@@ -528,7 +544,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWait() throws Exception {
|
||||
public void testWait() {
|
||||
loginSuccess();
|
||||
loginInvalidPassword();
|
||||
loginInvalidPassword();
|
||||
@@ -562,7 +578,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testByMultipleStrategy() throws Exception {
|
||||
public void testByMultipleStrategy() {
|
||||
|
||||
try {
|
||||
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0);
|
||||
@@ -583,7 +599,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLinearStrategy() throws Exception {
|
||||
public void testLinearStrategy() {
|
||||
RealmRepresentation realm = testRealm().toRepresentation();
|
||||
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0);
|
||||
try {
|
||||
@@ -612,7 +628,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBrowserInvalidPasswordDifferentCase() throws Exception {
|
||||
public void testBrowserInvalidPasswordDifferentCase() {
|
||||
loginSuccess("test-user@localhost");
|
||||
loginInvalidPassword("test-User@localhost");
|
||||
loginInvalidPassword("Test-user@localhost");
|
||||
@@ -622,7 +638,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmail() throws Exception {
|
||||
public void testEmail() {
|
||||
String userId = adminClient.realm("test").users().search("user2", null, null, null, 0, 1).get(0).getId();
|
||||
|
||||
loginInvalidPassword("user2");
|
||||
@@ -632,7 +648,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserDisabledTemporaryLockout() throws Exception {
|
||||
public void testUserDisabledTemporaryLockout() {
|
||||
String userId = adminClient.realm("test").users().search("test-user@localhost", null, null, null, 0, 1).get(0).getId();
|
||||
|
||||
loginInvalidPassword();
|
||||
@@ -645,7 +661,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserDisabledAfterSwitchFromMixedToPermanentLockout() throws Exception {
|
||||
public void testUserDisabledAfterSwitchFromMixedToPermanentLockout() {
|
||||
UsersResource users = testRealm().users();
|
||||
UserRepresentation user = users.search("test-user@localhost", null, null, null, 0, 1).get(0);
|
||||
|
||||
@@ -696,7 +712,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBrowserMissingPassword() throws Exception {
|
||||
public void testBrowserMissingPassword() {
|
||||
loginSuccess();
|
||||
loginMissingPassword();
|
||||
loginMissingPassword();
|
||||
@@ -704,7 +720,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBrowserInvalidTotp() throws Exception {
|
||||
public void testBrowserInvalidTotp() {
|
||||
loginSuccess();
|
||||
loginInvalidPassword();
|
||||
loginWithTotpFailure();
|
||||
@@ -712,7 +728,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBrowserMissingTotp() throws Exception {
|
||||
public void testBrowserMissingTotp() {
|
||||
loginSuccess();
|
||||
loginWithMissingTotp();
|
||||
loginWithMissingTotp();
|
||||
@@ -720,7 +736,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBrowserTotpSessionInvalidAfterLockout() throws Exception {
|
||||
public void testBrowserTotpSessionInvalidAfterLockout() {
|
||||
long start = System.currentTimeMillis();
|
||||
loginWithTotpFailure();
|
||||
continueLoginWithInvalidTotp();
|
||||
@@ -916,7 +932,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailureCountResetWithPasswordGrantType() throws Exception {
|
||||
public void testFailureCountResetWithPasswordGrantType() {
|
||||
String totpSecret = totp.generateTOTP("totpSecret");
|
||||
AccessTokenResponse response = getTestToken("invalid", totpSecret);
|
||||
Assert.assertNull(response.getAccessToken());
|
||||
@@ -937,7 +953,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonExistingAccounts() throws Exception {
|
||||
public void testNonExistingAccounts() {
|
||||
|
||||
loginInvalidPassword("non-existent-user");
|
||||
loginInvalidPassword("non-existent-user");
|
||||
@@ -1208,7 +1224,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
events.clear();
|
||||
}
|
||||
|
||||
public void loginWithMissingTotp() throws Exception {
|
||||
public void loginWithMissingTotp() {
|
||||
loginPage.open();
|
||||
loginPage.login("test-user@localhost", getPassword("test-user@localhost"));
|
||||
|
||||
@@ -1286,7 +1302,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
MatcherAssert.assertThat((Integer) userAttackInfo.get("numFailures"), is(numberOfFailures));
|
||||
}
|
||||
|
||||
private void sendInvalidPasswordPasswordGrant() throws Exception {
|
||||
private void sendInvalidPasswordPasswordGrant() {
|
||||
String totpSecret = totp.generateTOTP("totpSecret");
|
||||
AccessTokenResponse response = getTestToken("invalid", totpSecret);
|
||||
Assert.assertNull(response.getAccessToken());
|
||||
@@ -1295,7 +1311,7 @@ public class BruteForceTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
events.clear();
|
||||
}
|
||||
|
||||
private void lockUserWithPasswordGrant() throws Exception {
|
||||
private void lockUserWithPasswordGrant() {
|
||||
String totpSecret = totp.generateTOTP("totpSecret");
|
||||
AccessTokenResponse response = getTestToken(getPassword("test-user@localhost"), totpSecret);
|
||||
Assert.assertNotNull(response.getAccessToken());
|
||||
|
||||
@@ -800,10 +800,12 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
|
||||
kcSession.getContext().setRealm(realm);
|
||||
UserLoginFailureModel failure1 = kcSession.loginFailures().addUserLoginFailure(realm, "user1");
|
||||
failure1.incrementFailures();
|
||||
failure1.setLastFailure(Time.currentTimeMillis());
|
||||
|
||||
UserLoginFailureModel failure2 = kcSession.loginFailures().addUserLoginFailure(realm, "user2");
|
||||
failure2.incrementFailures();
|
||||
failure2.incrementFailures();
|
||||
failure2.setLastFailure(Time.currentTimeMillis());
|
||||
});
|
||||
|
||||
testingClient.server().run((KeycloakSession kcSession) -> {
|
||||
|
||||
@@ -97,6 +97,7 @@ public class RetryAndBackOffTest extends KeycloakModelTest {
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
var realm = session.realms().getRealm(realmId);
|
||||
session.getContext().setRealm(realm);
|
||||
var loginFailures = session.loginFailures().addUserLoginFailure(realm, userId);
|
||||
loginFailures.incrementFailures();
|
||||
});
|
||||
@@ -108,6 +109,7 @@ public class RetryAndBackOffTest extends KeycloakModelTest {
|
||||
public void testRetryWithReplace() {
|
||||
inComittedTransaction(session -> {
|
||||
var realm = session.realms().getRealm(realmId);
|
||||
session.getContext().setRealm(realm);
|
||||
var loginFailures = session.loginFailures().addUserLoginFailure(realm, userId);
|
||||
loginFailures.incrementFailures();
|
||||
});
|
||||
@@ -116,6 +118,7 @@ public class RetryAndBackOffTest extends KeycloakModelTest {
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
var realm = session.realms().getRealm(realmId);
|
||||
session.getContext().setRealm(realm);
|
||||
var loginFailures = session.loginFailures().getUserLoginFailure(realm, userId);
|
||||
loginFailures.incrementFailures();
|
||||
});
|
||||
@@ -127,6 +130,7 @@ public class RetryAndBackOffTest extends KeycloakModelTest {
|
||||
public void testRetryWithRemove() {
|
||||
inComittedTransaction(session -> {
|
||||
var realm = session.realms().getRealm(realmId);
|
||||
session.getContext().setRealm(realm);
|
||||
var loginFailures = session.loginFailures().addUserLoginFailure(realm, userId);
|
||||
loginFailures.incrementFailures();
|
||||
});
|
||||
@@ -135,6 +139,7 @@ public class RetryAndBackOffTest extends KeycloakModelTest {
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
var realm = session.realms().getRealm(realmId);
|
||||
session.getContext().setRealm(realm);
|
||||
session.loginFailures().removeUserLoginFailure(realm, userId);
|
||||
});
|
||||
|
||||
@@ -146,6 +151,7 @@ public class RetryAndBackOffTest extends KeycloakModelTest {
|
||||
// compute is implemented with get() and replace()
|
||||
inComittedTransaction(session -> {
|
||||
var realm = session.realms().getRealm(realmId);
|
||||
session.getContext().setRealm(realm);
|
||||
var loginFailures = session.loginFailures().addUserLoginFailure(realm, userId);
|
||||
loginFailures.incrementFailures();
|
||||
});
|
||||
@@ -156,6 +162,7 @@ public class RetryAndBackOffTest extends KeycloakModelTest {
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
var realm = session.realms().getRealm(realmId);
|
||||
session.getContext().setRealm(realm);
|
||||
var loginFailures = session.loginFailures().getUserLoginFailure(realm, userId);
|
||||
loginFailures.incrementFailures();
|
||||
});
|
||||
@@ -172,6 +179,7 @@ public class RetryAndBackOffTest extends KeycloakModelTest {
|
||||
var ce = assertThrows(CompletionException.class,
|
||||
() -> inComittedTransaction(session -> {
|
||||
var realm = session.realms().getRealm(realmId);
|
||||
session.getContext().setRealm(realm);
|
||||
session.loginFailures().addUserLoginFailure(realm, userId);
|
||||
}));
|
||||
assertTrue(String.valueOf(ce.getCause()), ce.getCause() instanceof HotRodClientException);
|
||||
|
||||
@@ -80,6 +80,7 @@ public class RemoteLoginFailureTest extends KeycloakModelTest {
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
var realm = session.realms().getRealm(realmId);
|
||||
session.getContext().setRealm(realm);
|
||||
var loginFailures = session.loginFailures().addUserLoginFailure(realm, userIds.get(0));
|
||||
loginFailures.incrementFailures();
|
||||
});
|
||||
@@ -104,6 +105,7 @@ public class RemoteLoginFailureTest extends KeycloakModelTest {
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
var realm = session.realms().getRealm(realmId);
|
||||
session.getContext().setRealm(realm);
|
||||
var loginFailures = session.loginFailures().getUserLoginFailure(realm, userIds.get(0));
|
||||
|
||||
// update all fields
|
||||
@@ -134,6 +136,7 @@ public class RemoteLoginFailureTest extends KeycloakModelTest {
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
var realm = session.realms().getRealm(realmId);
|
||||
session.getContext().setRealm(realm);
|
||||
var loginFailures = session.loginFailures().getUserLoginFailure(realm, userIds.get(0));
|
||||
|
||||
// update all fields
|
||||
@@ -166,6 +169,7 @@ public class RemoteLoginFailureTest extends KeycloakModelTest {
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
var realm = session.realms().getRealm(realmId);
|
||||
session.getContext().setRealm(realm);
|
||||
var loginFailures = session.loginFailures().getUserLoginFailure(realm, userIds.get(0));
|
||||
loginFailures.incrementTemporaryLockouts();
|
||||
loginFailures.clearFailures();
|
||||
|
||||
Reference in New Issue
Block a user