Skip to content

Commit ecbd856

Browse files
douglaspalmerahus1
authored andcommitted
Brute force protection: Lockout permanently uses parameters configured under lockout temporarily
Closes keycloak#30969 Signed-off-by: Douglas Palmer <[email protected]>
1 parent fff5087 commit ecbd856

File tree

2 files changed

+49
-2
lines changed

2 files changed

+49
-2
lines changed

services/src/main/java/org/keycloak/services/managers/DefaultBruteForceProtector.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ protected void failure(KeycloakSession session, RealmModel realm, String userId,
7979
userLoginFailure.incrementFailures();
8080
logger.debugv("new num failures: {0}", userLoginFailure.getNumFailures());
8181

82-
int waitSeconds = realm.getWaitIncrementSeconds() * (userLoginFailure.getNumFailures() / realm.getFailureFactor());
82+
int waitSeconds = 0;
83+
if(!(realm.isPermanentLockout() && realm.getMaxTemporaryLockouts() == 0)) {
84+
waitSeconds = realm.getWaitIncrementSeconds() * (userLoginFailure.getNumFailures() / realm.getFailureFactor());
85+
}
8386
logger.debugv("waitSeconds: {0}", waitSeconds);
8487
logger.debugv("deltaTime: {0}", deltaTime);
8588

@@ -110,7 +113,8 @@ protected void failure(KeycloakSession session, RealmModel realm, String userId,
110113
return;
111114
}
112115

113-
if(userLoginFailure.getNumTemporaryLockouts() > realm.getMaxTemporaryLockouts()) {
116+
if(userLoginFailure.getNumTemporaryLockouts() > realm.getMaxTemporaryLockouts() ||
117+
(realm.getMaxTemporaryLockouts() == 0 && userLoginFailure.getNumFailures() >= realm.getFailureFactor())) {
114118
UserModel user = session.users().getUserById(realm, userId);
115119
if (user == null) {
116120
return;

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,49 @@ public void testPermanentLockout() {
564564
loginInvalidPassword("test-user@localhost");
565565
loginInvalidPassword("test-user@localhost", false);
566566

567+
// As of now, there are two events: USER_DISABLED_BY_PERMANENT_LOCKOUT and LOGIN_ERROR but Order is not
568+
// guarantee though since the brute force detector is running separately "in its own thread" named
569+
// "Brute Force Protector".
570+
List<EventRepresentation> actualEvents = Arrays.asList(events.poll(), events.poll(), events.poll());
571+
assertIsContained(events.expect(EventType.USER_DISABLED_BY_PERMANENT_LOCKOUT).client((String) null).detail(Details.REASON, "brute_force_attack detected"), actualEvents);
572+
assertIsContained(events.expect(EventType.LOGIN_ERROR).error(Errors.INVALID_USER_CREDENTIALS), actualEvents);
573+
574+
// assert
575+
expectPermanentlyDisabled();
576+
577+
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0);
578+
assertFalse(user.isEnabled());
579+
assertUserDisabledReason(BruteForceProtector.DISABLED_BY_PERMANENT_LOCKOUT);
580+
581+
user.setEnabled(true);
582+
updateUser(user);
583+
assertUserDisabledReason(null);
584+
} finally {
585+
realm.setPermanentLockout(false);
586+
testRealm().update(realm);
587+
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0);
588+
user.setEnabled(true);
589+
updateUser(user);
590+
}
591+
}
592+
593+
// https://github.com/keycloak/keycloak/issues/30969
594+
@Test
595+
public void testPermanentLockoutWithTempLockoutParamsSet()
596+
{
597+
RealmRepresentation realm = testRealm().toRepresentation();
598+
try {
599+
// arrange
600+
realm.setWaitIncrementSeconds(0);
601+
realm.setPermanentLockout(true);
602+
realm.setMaxTemporaryLockouts(0);
603+
realm.setQuickLoginCheckMilliSeconds(0L);
604+
testRealm().update(realm);
605+
606+
// act
607+
loginInvalidPassword("test-user@localhost");
608+
loginInvalidPassword("test-user@localhost", false);
609+
567610
// As of now, there are two events: USER_DISABLED_BY_PERMANENT_LOCKOUT and LOGIN_ERROR but Order is not
568611
// guarantee though since the brute force detector is running separately "in its own thread" named
569612
// "Brute Force Protector".

0 commit comments

Comments
 (0)