Add failedLoginNotBefore to AttackDetectionResource

Closes #17574

Signed-off-by: Gilvan Filho <gfilho@redhat.com>
This commit is contained in:
Gilvan Filho 2024-02-13 23:22:26 -03:00 committed by Marek Posolda
parent fb262a05d9
commit 83af01c4c0
3 changed files with 26 additions and 7 deletions

View file

@ -282,6 +282,8 @@ There have been a couple of enhancements to the Brute Protection:
2. In previous versions of Keycloak the Administrator had to choose between whether to disable users temporarily or permanently due to a Brute Force attack on their account. The administrator now has the option to permanently disable a user after a given number of temporary lockouts. 2. In previous versions of Keycloak the Administrator had to choose between whether to disable users temporarily or permanently due to a Brute Force attack on their account. The administrator now has the option to permanently disable a user after a given number of temporary lockouts.
3. The property `failedLoginNotBefore` has been added to the `brute-force/users/{userId}` endpoint
= Authorization Policy = Authorization Policy
In previous versions of Keycloak when the last member of a User, Group or Client policy was deleted then that policy would also be deleted. Unfortunately this could lead to an escalation of privileges if the policy was used in an aggregate policy. To avoid privilege escalation the effect policies are no longer deleted and an administrator will need to update those policies. In previous versions of Keycloak when the last member of a User, Group or Client policy was deleted then that policy would also be deleted. Unfortunately this could lead to an escalation of privileges if the policy was used in an aggregate policy. To avoid privilege escalation the effect policies are no longer deleted and an administrator will need to update those policies.

View file

@ -98,20 +98,21 @@ public class AttackDetectionResource {
data.put("numTemporaryLockouts", 0); data.put("numTemporaryLockouts", 0);
data.put("lastFailure", 0); data.put("lastFailure", 0);
data.put("lastIPFailure", "n/a"); data.put("lastIPFailure", "n/a");
data.put("failedLoginNotBefore", 0);
if (!realm.isBruteForceProtected()) return data; if (!realm.isBruteForceProtected()) return data;
UserLoginFailureModel model = session.loginFailures().getUserLoginFailure(realm, userId); UserLoginFailureModel model = session.loginFailures().getUserLoginFailure(realm, userId);
if (model == null) return data; if (model == null) return data;
boolean disabled; boolean disabled = isUserDisabled(model, user);
if (user == null) {
disabled = Time.currentTime() < model.getFailedLoginNotBefore();
} else {
disabled = session.getProvider(BruteForceProtector.class).isTemporarilyDisabled(session, realm, user);
}
if (disabled) { if (disabled) {
data.put("disabled", true); data.put("disabled", true);
if(session.getProvider(BruteForceProtector.class).isTemporarilyDisabled(session, realm, user)) {
data.put("failedLoginNotBefore", model.getFailedLoginNotBefore());
} else {
data.put("failedLoginNotBefore", Long.MAX_VALUE);
}
} }
data.put("numFailures", model.getNumFailures()); data.put("numFailures", model.getNumFailures());
@ -121,6 +122,19 @@ public class AttackDetectionResource {
return data; return data;
} }
private boolean isUserDisabled(UserLoginFailureModel model, UserModel user) {
if(user == null) {
return Time.currentTime() < model.getFailedLoginNotBefore();
}
return isUserDisabledOrLockedByBruteForce(session, realm, user);
}
private boolean isUserDisabledOrLockedByBruteForce(KeycloakSession session, RealmModel realm, UserModel user) {
return session.getProvider(BruteForceProtector.class).isPermanentlyLockedOut(session, realm, user)
|| session.getProvider(BruteForceProtector.class).isTemporarilyDisabled(session, realm, user);
}
/** /**
* Clear any user login failures for the user * Clear any user login failures for the user
* *

View file

@ -30,6 +30,7 @@ import org.keycloak.testsuite.util.UserBuilder;
import java.util.Map; import java.util.Map;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
@ -83,7 +84,7 @@ public class AttackDetectionResourceTest extends AbstractAdminTest {
} }
private void assertBruteForce(Map<String, Object> status, Integer expectedNumFailures, Integer expectedNumTemporaryLockouts, Boolean expectedFailure, Boolean expectedDisabled) { private void assertBruteForce(Map<String, Object> status, Integer expectedNumFailures, Integer expectedNumTemporaryLockouts, Boolean expectedFailure, Boolean expectedDisabled) {
assertEquals(5, status.size()); assertEquals(6, status.size());
assertEquals(expectedNumFailures, status.get("numFailures")); assertEquals(expectedNumFailures, status.get("numFailures"));
assertEquals(expectedNumTemporaryLockouts, status.get("numTemporaryLockouts")); assertEquals(expectedNumTemporaryLockouts, status.get("numTemporaryLockouts"));
assertEquals(expectedDisabled, status.get("disabled")); assertEquals(expectedDisabled, status.get("disabled"));
@ -91,9 +92,11 @@ public class AttackDetectionResourceTest extends AbstractAdminTest {
assertEquals("127.0.0.1", status.get("lastIPFailure")); assertEquals("127.0.0.1", status.get("lastIPFailure"));
Long lastFailure = (Long) status.get("lastFailure"); Long lastFailure = (Long) status.get("lastFailure");
assertTrue(lastFailure < (System.currentTimeMillis() + 1) && lastFailure > (System.currentTimeMillis() - 10000)); assertTrue(lastFailure < (System.currentTimeMillis() + 1) && lastFailure > (System.currentTimeMillis() - 10000));
assertNotEquals("0", status.get("failedLoginNotBefore").toString());
} else { } else {
assertEquals("n/a", status.get("lastIPFailure")); assertEquals("n/a", status.get("lastIPFailure"));
assertEquals("0", status.get("lastFailure").toString()); assertEquals("0", status.get("lastFailure").toString());
assertEquals("0", status.get("failedLoginNotBefore").toString());
} }
} }