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:
Christian Glasmachers
2025-12-10 11:20:19 +01:00
committed by GitHub
parent ef011ea4d2
commit 921b10ee80
14 changed files with 269 additions and 44 deletions

View File

@@ -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[]

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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() {

View File

@@ -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()));
}
}

View File

@@ -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;
}

View File

@@ -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) {};
}

View File

@@ -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());

View File

@@ -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) -> {

View File

@@ -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);

View File

@@ -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();