FFFF Add linear strategy to brute force wait time increment by gilvansfilho · Pull Request #27674 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public class RealmRepresentation {
protected Boolean bruteForceProtected;
protected Boolean permanentLockout;
protected Integer maxTemporaryLockouts;
protected BruteForceStrategy bruteForceStrategy;
protected Integer maxFailureWaitSeconds;
protected Integer minimumQuickLoginWaitSeconds;
protected Integer waitIncrementSeconds;
Expand Down Expand Up @@ -777,6 +778,14 @@ public void setMaxTemporaryLockouts(Integer maxTemporaryLockouts) {
this.maxTemporaryLockouts = maxTemporaryLockouts;
}

public BruteForceStrategy getBruteForceStrategy() {
return this.bruteForceStrategy;
}

public void setBruteForceStrategy(BruteForceStrategy bruteForceStrategy) {
this.bruteForceStrategy = bruteForceStrategy;
}

public Integer getMaxFailureWaitSeconds() {
return maxFailureWaitSeconds;
}
Expand Down Expand Up @@ -1450,4 +1459,8 @@ public void addOrganization(OrganizationRepresentation org) {
}
organizations.add(org);
}

public enum BruteForceStrategy {
LINEAR, MULTIPLE;
}
}
37 changes: 31 additions & 6 deletions docs/documentation/server_admin/topics/threat/brute-force.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,19 @@ wait time will never reach the value you have set to `Max wait`.
.. If the time between this failure and the last failure is greater than _Failure Reset Time_
... Reset `count`
.. Increment `count`
.. Calculate `wait` using _Wait Increment_ * (`count` / _Max Login Failures_). The division is an integer division rounded down to a whole number
.. If `wait` equals 0 and the time between this failure and the last failure is less than _Quick Login Check Milliseconds_, set `wait` to _Minimum Quick Login Wait_.
.. Calculate `wait` according brute force strategy defined (see below Strategies to set Wait Time).
.. If `wait` equals to or less than 0 and the time between this failure and the last failure is less than _Quick Login Check Milliseconds_, set `wait` to _Minimum Quick Login Wait_.
... Temporarily disable the user for the smallest of `wait` and _Max Wait_ seconds
... Increment the temporary lockout counter

`count` does not increment when a temporarily disabled account commits a login failure.
====

For instance, if you have set `Max Login Failures` to `5` and a `Wait Increment` of `30` seconds, the effective time an account will be disabled after several failed authentication attempts will be:
*Strategies to set Wait Time*

Keycloak provides two strategies to calculate wait time: By multiples or Linear. By multiples is the first strategy introduced by keycloak, so that is the default one.

With by multiples strategy wait time will be incremented when number (or count) of failures are multiple of `Max Login Failure`. For instance, if you have set `Max Login Failures` to `5` and a `Wait Increment` of `30` seconds, the effective time an account will be disabled after several failed authentication attempts will be:

[cols="1,1,1,1"]
|===
Expand All @@ -100,9 +104,30 @@ For instance, if you have set `Max Login Failures` to `5` and a `Wait Increment`
|**10** |**30** | 5 | **60**
|===

Note that the `Effective Wait Time` at the 5th failed attempt will disable the account for `30` seconds. Only after reaching
the next multiple of `Max Login Failures`, in this case `10`, will the time increase from `30` to `60`. The time the account will be disabled
is only increased when reaching multiples of `Max Login Failures`.
Note that the `Effective Wait Time` at the 5th failed attempt will disable the account for `30` seconds. Only after reaching the next multiple of `Max Login Failures`, in this case `10`, will the time increase from `30` to `60`. The time the account will be disabled is only increased when reaching multiples of `Max Login Failures`.

The by multiple strategy uses the following formula to calculate wait time: _Wait Increment_ * (`count` / _Max Login Failures_). The division is an integer division rounded down to a whole number.

With linear strategy wait time will be incremented when number (or count) of failures are equal to or greater than `Max Login Failure`. For instance, if you have set `Max Login Failures` to `5` and a `Wait Increment` of `30` seconds, the effective time an account will be disabled after several failed authentication attempts will be:

[cols="1,1,1,1"]
|===
|`Number of Failures` | `Wait Increment` | `Max Login Failures` | `Effective Wait Time`
|1 |30 | 5 | 0
|2 |30 | 5 | 0
|3 |30 | 5 | 0
|4 |30 | 5 | 0
|**5** |**30** | 5 | **30**
|**6** |**30** | 5 | **60**
|**7** |**30** | 5 | **90**
|**8** |**30** | 5 | **120**
|**9** |**30** | 5 | **150**
|**10** |**30** | 5 | **180**
|===

Note that the `Effective Wait Time` at the 5th failed attempt will disable the account for `30` seconds. Each new failed attempt will increase wait time.

The linear strategy uses the following formula to calculate wait time: _Wait Increment_ * (1 + `count` - _Max Login Failures_).

*Permanent Lockout Parameters*

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ describe("Realm settings events tab tests", () => {
cy.findByTestId("select-brute-force-mode")
.contains("Lockout temporarily")
.click();
cy.get('[name="bruteForceStrategy"]').contains("MULTIPLE").click();
cy.findByTestId("waitIncrementSeconds").type("1");
cy.findByTestId("maxFailureWaitSeconds").type("1");
cy.findByTestId("maxDeltaTimeSeconds").type("1");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ describe("Realm settings tabs tests", () => {
cy.findByTestId("select-brute-force-mode")
.contains("Lockout temporarily")
.click();
cy.get('[name="bruteForceStrategy"]').contains("MULTIPLE").click();
cy.findByTestId("waitIncrementSeconds").type("1");
cy.findByTestId("maxFailureWaitSeconds").type("1");
cy.findByTestId("maxDeltaTimeSeconds").type("1");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,10 @@ rowSaveBtnAriaLabel=Save edits for {{messageBundle}}
permanentLockout=Permanent lockout
maxTemporaryLockouts=Maximum temporary lockouts
maxTemporaryLockoutsHelp=The number of temporary lockouts permitted before the user is permanently locked out.
bruteForceStrategy=Strategy to increase wait time
bruteForceStrategyHelp=Multiple means wait time will be increased only when number of failures are multiples of {{failureFactor}}. Linear means each new failure starting at {{failureFactor}} will increase wait time.
bruteForceStrategy.Linear=Each new failure starting at {{failureFactor}} will increase wait time.
bruteForceStrategy.Multiple=Wait time will be increased only when number of failures are multiples of {{failureFactor}}
debug=Debug
webAuthnPolicyRequireResidentKey=Require discoverable credential
unlockUsersConfirm=All the users that are temporarily locked will be unlocked.
Expand Down
Comment thread
jonkoops marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
KeycloakSelect,
NumberControl,
SelectVariant,
SelectControl,
} from "@keycloak/keycloak-ui-shared";
import {
ActionGroup,
Expand Down Expand Up @@ -52,6 +53,8 @@ export const BruteForceDetection = ({
BruteForceMode.PermanentAfterTemporaryLockout,
];

const bruteForceStrategyTypes = ["MULTIPLE", "LINEAR"];

const setupForm = () => {
convertToFormValues(realm, setValue);
setIsBruteForceModeUpdated(false);
Expand Down Expand Up @@ -155,6 +158,14 @@ export const BruteForceDetection = ({
10BC8 bruteForceMode ===
BruteForceMode.PermanentAfterTemporaryLockout) && (
<>
<SelectControl
name="bruteForceStrategy"
label={t("bruteForceStrategy")}
labelIcon={t("bruteForceStrategyHelp")}
controller={{ defaultValue: bruteForceStrategyTypes[0] }}
options={bruteForceStrategyTypes}
/>

<Time name="waitIncrementSeconds" />
<Time name="maxFailureWaitSeconds" />
<Time name="maxDeltaTimeSeconds" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default interface RealmRepresentation {
maxDeltaTimeSeconds?: number;
maxFailureWaitSeconds?: number;
maxTemporaryLockouts?: number;
bruteForceStrategy?: "MULTIPLE" | "LINEAR";
minimumQuickLoginWaitSeconds?: number;
notBefore?: number;
oauth2DeviceCodeLifespan?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.keycloak.models.cache.CachedRealmModel;
import org.keycloak.models.cache.UserCache;
import org.keycloak.models.cache.infinispan.entities.CachedRealm;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageUtil;
import org.keycloak.storage.client.ClientStorageProvider;
Expand Down Expand Up @@ -285,6 +286,18 @@ public void setMaxTemporaryLockouts(final int val) {
updated.setMaxTemporaryLockouts(val);
}

@Override
public RealmRepresentation.BruteForceStrategy getBruteForceStrategy() {
if(isUpdated()) return updated.getBruteForceStrategy();
return cached.getBruteForceStrategy();
}

@Override
public void setBruteForceStrategy(final RealmRepresentation.BruteForceStrategy val) {
getDelegateForUpdate();
updated.setBruteForceStrategy(val);
}

@Override
public int getMaxFailureWaitSeconds() {
if (isUpdated()) return updated.getMaxFailureWaitSeconds();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.keycloak.models.WebAuthnPolicy;
import org.keycloak.models.cache.infinispan.DefaultLazyLoader;
import org.keycloak.models.cache.infinispan.LazyLoader;
import org.keycloak.representations.idm.RealmRepresentation;

/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
Expand All @@ -77,6 +78,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected boolean bruteForceProtected;
protected boolean permanentLockout;
protected int maxTemporaryLockouts;
protected RealmRepresentation.BruteForceStrategy bruteForceStrategy;
protected int maxFailureWaitSeconds;
protected int minimumQuickLoginWaitSeconds;
protected int waitIncrementSeconds;
Expand Down Expand Up @@ -198,6 +200,7 @@ public CachedRealm(Long revision, RealmModel model) {
bruteForceProtected = model.isBruteForceProtected();
permanentLockout = model.isPermanentLockout();
maxTemporaryLockouts = model.getMaxTemporaryLockouts();
bruteForceStrategy = model.getBruteForceStrategy();
maxFailureWaitSeconds = model.getMaxFailureWaitSeconds();
minimumQuickLoginWaitSeconds = model.getMinimumQuickLoginWaitSeconds();
waitIncrementSeconds = model.getWaitIncrementSeconds();
Expand Down Expand Up @@ -384,6 +387,10 @@ public int getMaxTemporaryLockouts() {
return maxTemporaryLockouts;
}

public RealmRepresentation.BruteForceStrategy getBruteForceStrategy() {
return bruteForceStrategy;
}

public int getMaxFailureWaitSeconds() {
return this.maxFailureWaitSeconds;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import org.keycloak.models.utils.ComponentUtil;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderConfigProperty;

import org.keycloak.representations.idm.RealmRepresentation;
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import jakarta.persistence.TypedQuery;
Expand Down Expand Up @@ -272,6 +272,20 @@ public void setMaxTemporaryLockouts(final int val) {
setAttribute("maxTemporaryLockouts", val);
}

@Override
public RealmRepresentation.BruteForceStrategy getBruteForceStrategy() {
String name = getAttribute("bruteForceStrategy");
if(name == null)
return RealmRepresentation.BruteForceStrategy.MULTIPLE;

return RealmRepresentation.BruteForceStrategy.valueOf(name);
}

@Override
public void setBruteForceStrategy(final RealmRepresentation.BruteForceStrategy val) {
setAttribute("bruteForceStrategy", val.toString());
}

@Override
public int getMaxFailureWaitSeconds() {
return getAttribute("maxFailureWaitSeconds", 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ public void importRealm(RealmRepresentation rep, RealmModel newRealm, boolean sk
if (rep.isBruteForceProtected() != null) newRealm.setBruteForceProtected(rep.isBruteForceProtected());
if (rep.isPermanentLockout() != null) newRealm.setPermanentLockout(rep.isPermanentLockout());
if (rep.getMaxTemporaryLockouts() != null) newRealm.setMaxTemporaryLockouts(rep.getMaxTemporaryLockouts());
if (rep.getBruteForceStrategy() != null) newRealm.setBruteForceStrategy(rep.getBruteForceStrategy());
if (rep.getMaxFailureWaitSeconds() != null) newRealm.setMaxFailureWaitSeconds(rep.getMaxFailureWaitSeconds());
if (rep.getMinimumQuickLoginWaitSeconds() != null)
newRealm.setMinimumQuickLoginWaitSeconds(rep.getMinimumQuickLoginWaitSeconds());
Expand Down Expand Up @@ -751,6 +752,7 @@ public void updateRealm(RealmRepresentation rep, RealmModel realm) {
if (rep.isBruteForceProtected() != null) realm.setBruteForceProtected(rep.isBruteForceProtected());
if (rep.isPermanentLockout() != null) realm.setPermanentLockout(rep.isPermanentLockout());
if (rep.getMaxTemporaryLockouts() != null) realm.setMaxTemporaryLockouts(rep.getMaxTemporaryLockouts());
if (rep.getBruteForceStrategy() != null) realm.setBruteForceStrategy(rep.getBruteForceStrategy());
if (rep.getMaxFailureWaitSeconds() != null) realm.setMaxFailureWaitSeconds(rep.getMaxFailureWaitSeconds());
if (rep.getMinimumQuickLoginWaitSeconds() != null)
realm.setMinimumQuickLoginWaitSeconds(rep.getMinimumQuickLoginWaitSeconds());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public class ModelToRepresentation {
REALM_EXCLUDED_ATTRIBUTES.add("bruteForceProtected");
REALM_EXCLUDED_ATTRIBUTES.add("permanentLockout");
REALM_EXCLUDED_ATTRIBUTES.add("maxTemporaryLockouts");
REALM_EXCLUDED_ATTRIBUTES.add("bruteForceStrategy");
REALM_EXCLUDED_ATTRIBUTES.add("maxFailureWaitSeconds");
REALM_EXCLUDED_ATTRIBUTES.add("waitIncrementSeconds");
REALM_EXCLUDED_ATTRIBUTES.add("quickLoginCheckMilliSeconds");
Expand Down Expand Up @@ -362,6 +363,7 @@ public static RealmRepresentation toRepresentation(KeycloakSession session, Real
rep.setBruteForceProtected(realm.isBruteForceProtected());
rep.setPermanentLockout(realm.isPermanentLockout());
rep.setMaxTemporaryLockouts(realm.getMaxTemporaryLockouts());
rep.setBruteForceStrategy(realm.getBruteForceStrategy());
rep.setMaxFailureWaitSeconds(realm.getMaxFailureWaitSeconds());
rep.setMinimumQuickLoginWaitSeconds(realm.getMinimumQuickLoginWaitSeconds());
rep.setWaitIncrementSeconds(realm.getWaitIncrementSeconds());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.keycloak.models.RoleModel;
import org.keycloak.models.WebAuthnPolicy;
import org.keycloak.provider.Provider;
import org.keycloak.representations.idm.RealmRepresentation;

import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -187,6 +188,14 @@ public void setBruteForceProtected(boolean value) {
delegate.setBruteForceProtected(value);
}

public RealmRepresentation.BruteForceStrategy getBruteForceStrategy() {
return delegate.getBruteForceStrategy();
}

public void setBruteForceStrategy(RealmRepresentation.BruteForceStrategy value) {
delegate.setBruteForceStrategy(value);
}

public boolean isPermanentLockout() {
return delegate.isPermanentLockout();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import org.keycloak.common.enums.SslRequired;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.*;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RealmRepresentation.BruteForceStrategy;

import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -688,6 +690,16 @@ public void setMaxTemporaryLockouts(int val) {

}

@Override
public RealmRepresentation.BruteForceStrategy getBruteForceStrategy() {
return RealmRepresentation.BruteForceStrategy.MULTIPLE;
}

@Override
public void setBruteForceStrategy(RealmRepresentation.BruteForceStrategy val) {

}

@Override
public int getMaxFailureWaitSeconds() {
return 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.keycloak.component.ComponentModel;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.representations.idm.RealmRepresentation;

import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -142,6 +143,8 @@ default Boolean getAttribute(String name, Boolean defaultValue) {
void setPermanentLockout(boolean val);
int getMaxTemporaryLockouts();
void setMaxTemporaryLockouts(int val);
RealmRepresentation.BruteForceStrategy getBruteForceStrategy();
void setBruteForceStrategy(RealmRepresentation.BruteForceStrategy val);
int getMaxFailureWaitSeconds();
void setMaxFailureWaitSeconds(int val);
int getWaitIncrementSeconds();
Expand Down
10BC8
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.ServicesLogger;

import java.time.Instant;
import java.time.LocalDateTime;
Expand Down Expand Up @@ -79,12 +81,18 @@ protected void failure(KeycloakSession session, RealmModel realm, String userId,
userLoginFailure.incrementFailures();
logger.debugv("new num failures: {0}", userLoginFailure.getNumFailures());

int waitSeconds = realm.getWaitIncrementSeconds() * (userLoginFailure.getNumFailures() / realm.getFailureFactor());
int waitSeconds;
if(RealmRepresentation.BruteForceStrategy.MULTIPLE.equals(realm.getBruteForceStrategy())) {
waitSeconds = realm.getWaitIncrementSeconds() * (userLoginFailure.getNumFailures() / realm.getFailureFactor());
} else {
waitSeconds = realm.getWaitIncrementSeconds() * (1 + userLoginFailure.getNumFailures() - realm.getFailureFactor());
}

logger.debugv("waitSeconds: {0}", waitSeconds);
logger.debugv("deltaTime: {0}", deltaTime);

boolean quickLoginFailure = false;
if (waitSeconds == 0) {
if (waitSeconds <= 0) {
if (last > 0 && deltaTime < realm.getQuickLoginCheckMilliSeconds()) {
logger.debugv("quick login, set min wait seconds");
waitSeconds = realm.getMinimumQuickLoginWaitSeconds();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ protected void setupRealmDefaults(RealmModel realm) {
realm.setBruteForceProtected(false); // default settings off for now todo set it on
realm.setPermanentLockout(false);
realm.setMaxTemporaryLockouts(0);
realm.setBruteForceStrategy(RealmRepresentation.BruteForceStrategy.MULTIPLE);
realm.setMaxFailureWaitSeconds(900);
realm.setMinimumQuickLoginWaitSeconds(60);
realm.setWaitIncrementSeconds(60);
Expand Down
Loading
0