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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 269 additions and 44 deletions

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