Fix brute force detection for LDAP read-only users

Closes #28579

Signed-off-by: devjos <github_11837948@feido.de>
This commit is contained in:
devjos 2024-04-10 09:45:21 +02:00 committed by Marek Posolda
parent ce8e925c1a
commit cccddc0810
2 changed files with 65 additions and 1 deletions

View file

@ -29,6 +29,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
import org.keycloak.storage.ReadOnlyException;
import java.time.Instant;
import java.time.LocalDateTime;
@ -215,7 +216,11 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
}
logger.debugv("user {0} locked permanently due to too many login attempts", user.getUsername());
user.setEnabled(false);
user.setSingleAttribute(DISABLED_REASON, DISABLED_BY_PERMANENT_LOCKOUT);
try {
user.setSingleAttribute(DISABLED_REASON, DISABLED_BY_PERMANENT_LOCKOUT);
}catch (ReadOnlyException e){
logger.debug("Cannot set disabled reason on read only user");
}
// Send event
sendEvent(session, realm, userLoginFailure, EventType.USER_DISABLED_BY_PERMANENT_LOCKOUT);
}

View file

@ -18,6 +18,8 @@
package org.keycloak.testsuite.federation.ldap;
import java.util.Map;
import org.hamcrest.MatcherAssert;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.ClassRule;
@ -26,17 +28,21 @@ import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginConfigTotpPage;
@ -48,6 +54,7 @@ import jakarta.ws.rs.ClientErrorException;
import java.util.Collections;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@ -155,6 +162,58 @@ public class LDAPReadOnlyTest extends AbstractLDAPTest {
user.update(userRepresentation);
}
// issue #28580
@Test
public void testReadOnlyUserGetsPermanentlyLocked(){
int failureFactor = 2;
RealmRepresentation realm = testRealm().toRepresentation();
try {
// Set permanent lockout for the test
realm.setBruteForceProtected(true);
realm.setPermanentLockout(true);
realm.setFailureFactor(failureFactor);
testRealm().update(realm);
UserRepresentation user = adminClient.realm("test").users().search("johnkeycloak", 0, 1).get(0);
assertTrue(user.isEnabled());
// Lock user (permanently) and make sure the number of failures matches failure factor
loginInvalidPassword("johnkeycloak");
loginInvalidPassword("johnkeycloak");
assertUserNumberOfFailures(user.getId(), failureFactor);
// Make sure user is now disabled
user = adminClient.realm("test").users().search("johnkeycloak", 0, 1).get(0);
assertFalse(user.isEnabled());
events.clear();
} finally {
realm.setBruteForceProtected(false);
realm.setPermanentLockout(false);
realm.setFailureFactor(30);
testRealm().update(realm);
UserRepresentation user = adminClient.realm("test").users().search("johnkeycloak", 0, 1).get(0);
user.setEnabled(true);
updateUser(user);
}
}
public void loginInvalidPassword(String username) {
loginPage.open();
loginPage.login(username, "invalid");
loginPage.assertCurrent();
Assert.assertEquals("Invalid username or password.", loginPage.getInputError());
events.clear();
}
private void assertUserNumberOfFailures(String userId, Integer numberOfFailures) {
Map<String, Object> userAttackInfo = adminClient.realm("test").attackDetection().bruteForceUserStatus(userId);
MatcherAssert.assertThat((Integer) userAttackInfo.get("numFailures"), is(numberOfFailures));
}
private void setTotpRequirementExecutionForRealm(AuthenticationExecutionModel.Requirement requirement) {
adminClient.realm("test").flows().getExecutions("browser").
stream().filter(execution -> execution.getDisplayName().equals("Browser - Conditional OTP"))