Skip to content

Commit c5d9edf

Browse files
gilvansfilhopedroigor
authored andcommitted
add linear strategy to brute force
closes #25917 Signed-off-by: Gilvan Filho <[email protected]>
1 parent 993381c commit c5d9edf

File tree

16 files changed

+182
-8
lines changed

16 files changed

+182
-8
lines changed

core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ public class RealmRepresentation {
9292
protected Boolean bruteForceProtected;
9393
protected Boolean permanentLockout;
9494
protected Integer maxTemporaryLockouts;
95+
protected BruteForceStrategy bruteForceStrategy;
9596
protected Integer maxFailureWaitSeconds;
9697
protected Integer minimumQuickLoginWaitSeconds;
9798
protected Integer waitIncrementSeconds;
@@ -777,6 +778,14 @@ public void setMaxTemporaryLockouts(Integer maxTemporaryLockouts) {
777778
this.maxTemporaryLockouts = maxTemporaryLockouts;
778779
}
779780

781+
public BruteForceStrategy getBruteForceStrategy() {
782+
return this.bruteForceStrategy;
783+
}
784+
785+
public void setBruteForceStrategy(BruteForceStrategy bruteForceStrategy) {
786+
this.bruteForceStrategy = bruteForceStrategy;
787+
}
788+
780789
public Integer getMaxFailureWaitSeconds() {
781790
return maxFailureWaitSeconds;
782791
}
@@ -1450,4 +1459,8 @@ public void addOrganization(OrganizationRepresentation org) {
14501459
}
14511460
organizations.add(org);
14521461
}
1462+
1463+
public enum BruteForceStrategy {
1464+
LINEAR, MULTIPLE;
1465+
}
14531466
}

docs/documentation/server_admin/topics/threat/brute-force.adoc

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,19 @@ wait time will never reach the value you have set to `Max wait`.
7575
.. If the time between this failure and the last failure is greater than _Failure Reset Time_
7676
... Reset `count`
7777
.. Increment `count`
78-
.. Calculate `wait` using _Wait Increment_ * (`count` / _Max Login Failures_). The division is an integer division rounded down to a whole number
79-
.. 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_.
78+
.. Calculate `wait` according the brute force strategy defined (see below Strategies to set Wait Time).
79+
.. If `wait` equals is 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_.
8080
... Temporarily disable the user for the smallest of `wait` and _Max Wait_ seconds
8181
... Increment the temporary lockout counter
8282
8383
`count` does not increment when a temporarily disabled account commits a login failure.
8484
====
8585

86-
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:
86+
*Strategies to set Wait Time*
87+
88+
{project_name} provides two strategies to calculate wait time: By multiples or Linear. By multiples is the first strategy introduced by {project_name}, so that is the default one.
89+
90+
By multiples strategy, wait time is incremented when the number (or count) of failures are multiples of `Max Login Failure`. For instance, if you set `Max Login Failures` to `5` and a `Wait Increment` to `30` seconds, the effective time that an account is disabled after several failed authentication attempts will be:
8791

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

103-
Note that the `Effective Wait Time` at the 5th failed attempt will disable the account for `30` seconds. Only after reaching
104-
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
105-
is only increased when reaching multiples of `Max Login Failures`.
107+
At the fifth failed attempt of the `Effective Wait Time`, the account is disabled for `30` seconds. After reaching the next multiple of `Max Login Failures`, in this case `10`, the time increases from `30` to `60` seconds.
108+
109+
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.
110+
111+
For linear strategy, wait time is incremented when the number (or count) of failures equals or is greater than `Max Login Failure`. For instance, if you have set `Max Login Failures` to `5` and a `Wait Increment` to`30` seconds, the effective time that an account is disabled after several failed authentication attempts will be:
112+
113+
[cols="1,1,1,1"]
114+
|===
115+
|`Number of Failures` | `Wait Increment` | `Max Login Failures` | `Effective Wait Time`
116+
|1 |30 | 5 | 0
117+
|2 |30 | 5 | 0
118+
|3 |30 | 5 | 0
119+
|4 |30 | 5 | 0
120+
|**5** |**30** | 5 | **30**
121+
|**6** |**30** | 5 | **60**
122+
|**7** |**30** | 5 | **90**
123+
|**8** |**30** | 5 | **120**
124+
|**9** |**30** | 5 | **150**
125+
|**10** |**30** | 5 | **180**
126+
|===
127+
128+
At the fifth failed attempt for the `Effective Wait Time`, the account is disabled for `30` seconds. Each new failed attempt increases wait time.
129+
130+
The linear strategy uses the following formula to calculate wait time: _Wait Increment_ * (1 + `count` - _Max Login Failures_).
106131

107132
*Permanent Lockout Parameters*
108133

js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,10 @@ rowSaveBtnAriaLabel=Save edits for {{messageBundle}}
729729
permanentLockout=Permanent lockout
730730
maxTemporaryLockouts=Maximum temporary lockouts
731731
maxTemporaryLockoutsHelp=The number of temporary lockouts permitted before the user is permanently locked out.
732+
bruteForceStrategy=Strategy to increase wait time
733+
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.
734+
bruteForceStrategy.LINEAR=Linear
735+
bruteForceStrategy.MULTIPLE=Multiple
732736
debug=Debug
733737
webAuthnPolicyRequireResidentKey=Require discoverable credential
734738
unlockUsersConfirm=All the users that are temporarily locked will be unlocked.

js/apps/admin-ui/src/realm-settings/security-defences/BruteForceDetection.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
KeycloakSelect,
55
NumberControl,
66
SelectVariant,
7+
SelectControl,
78
} from "@keycloak/keycloak-ui-shared";
89
import {
910
ActionGroup,
@@ -52,6 +53,8 @@ export const BruteForceDetection = ({
5253
BruteForceMode.PermanentAfterTemporaryLockout,
5354
];
5455

56+
const bruteForceStrategyTypes = ["MULTIPLE", "LINEAR"];
57+
5558
const setupForm = () => {
5659
convertToFormValues(realm, setValue);
5760
setIsBruteForceModeUpdated(false);
@@ -155,6 +158,16 @@ export const BruteForceDetection = ({
155158
bruteForceMode ===
156159
BruteForceMode.PermanentAfterTemporaryLockout) && (
157160
<>
161+
<SelectControl
162+
name="bruteForceStrategy"
163+
label={t("bruteForceStrategy")}
164+
labelIcon={t("bruteForceStrategyHelp")}
165+
controller={{ defaultValue: "" }}
166+
options={bruteForceStrategyTypes.map((key) => ({
167+
key,
168+
value: t(`bruteForceStrategy.${key}`),
169+
}))}
170+
/>
158171
<Time name="waitIncrementSeconds" />
159172
<Time name="maxFailureWaitSeconds" />
160173
<Time name="maxDeltaTimeSeconds" />

js/libs/keycloak-admin-client/src/defs/realmRepresentation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export default interface RealmRepresentation {
7474
maxDeltaTimeSeconds?: number;
7575
maxFailureWaitSeconds?: number;
7676
maxTemporaryLockouts?: number;
77+
bruteForceStrategy?: "MULTIPLE" | "LINEAR";
7778
minimumQuickLoginWaitSeconds?: number;
7879
notBefore?: number;
7980
oauth2DeviceCodeLifespan?: number;

model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.keycloak.models.cache.CachedRealmModel;
4848
import org.keycloak.models.cache.UserCache;
4949
import org.keycloak.models.cache.infinispan.entities.CachedRealm;
50+
import org.keycloak.representations.idm.RealmRepresentation;
5051
import org.keycloak.storage.UserStorageProvider;
5152
import org.keycloak.storage.UserStorageUtil;
5253
import org.keycloak.storage.client.ClientStorageProvider;
@@ -283,6 +284,18 @@ public void setMaxTemporaryLockouts(final int val) {
283284
updated.setMaxTemporaryLockouts(val);
284285
}
285286

287+
@Override
288+
public RealmRepresentation.BruteForceStrategy getBruteForceStrategy() {
289+
if(isUpdated()) return updated.getBruteForceStrategy();
290+
return cached.getBruteForceStrategy();
291+
}
292+
293+
@Override
294+
public void setBruteForceStrategy(final RealmRepresentation.BruteForceStrategy val) {
295+
getDelegateForUpdate();
296+
updated.setBruteForceStrategy(val);
297+
}
298+
286299
@Override
287300
public int getMaxFailureWaitSeconds() {
288301
if (isUpdated()) return updated.getMaxFailureWaitSeconds();

model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.keycloak.models.WebAuthnPolicy;
5353
import org.keycloak.models.cache.infinispan.DefaultLazyLoader;
5454
import org.keycloak.models.cache.infinispan.LazyLoader;
55+
import org.keycloak.representations.idm.RealmRepresentation;
5556

5657
/**
5758
* @author <a href="mailto:[email protected]">Bill Burke</a>
@@ -78,6 +79,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
7879
protected boolean bruteForceProtected;
7980
protected boolean permanentLockout;
8081
protected int maxTemporaryLockouts;
82+
protected RealmRepresentation.BruteForceStrategy bruteForceStrategy;
8183
protected int maxFailureWaitSeconds;
8284
protected int minimumQuickLoginWaitSeconds;
8385
protected int waitIncrementSeconds;
@@ -193,6 +195,7 @@ public CachedRealm(Long revision, RealmModel model) {
193195
bruteForceProtected = model.isBruteForceProtected();
194196
permanentLockout = model.isPermanentLockout();
195197
maxTemporaryLockouts = model.getMaxTemporaryLockouts();
198+
bruteForceStrategy = model.getBruteForceStrategy();
196199
maxFailureWaitSeconds = model.getMaxFailureWaitSeconds();
197200
minimumQuickLoginWaitSeconds = model.getMinimumQuickLoginWaitSeconds();
198201
waitIncrementSeconds = model.getWaitIncrementSeconds();
@@ -376,6 +379,10 @@ public int getMaxTemporaryLockouts() {
376379
return maxTemporaryLockouts;
377380
}
378381

382+
public RealmRepresentation.BruteForceStrategy getBruteForceStrategy() {
383+
return bruteForceStrategy;
384+
}
385+
379386
public int getMaxFailureWaitSeconds() {
380387
return this.maxFailureWaitSeconds;
381388
}

model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
import jakarta.persistence.EntityManager;
3737
import jakarta.persistence.LockModeType;
3838
import jakarta.persistence.TypedQuery;
39+
import org.keycloak.representations.idm.RealmRepresentation;
40+
3941
import java.util.HashMap;
4042
import java.util.Iterator;
4143
import java.util.Map;
@@ -268,6 +270,20 @@ public int getMaxTemporaryLockouts() {
268270
return getAttribute("maxTemporaryLockouts", 0);
269271
}
270272

273+
@Override
274+
public RealmRepresentation.BruteForceStrategy getBruteForceStrategy() {
275+
String name = getAttribute("bruteForceStrategy");
276+
if(name == null)
277+
return RealmRepresentation.BruteForceStrategy.MULTIPLE;
278+
279+
return RealmRepresentation.BruteForceStrategy.valueOf(name);
280+
}
281+
282+
@Override
283+
public void setBruteForceStrategy(final RealmRepresentation.BruteForceStrategy val) {
284+
setAttribute("bruteForceStrategy", val.toString());
285+
}
286+
271287
@Override
272288
public void setMaxTemporaryLockouts(final int val) {
273289
setAttribute("maxTemporaryLockouts", val);

model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ public void importRealm(RealmRepresentation rep, RealmModel newRealm, boolean sk
186186
if (rep.isBruteForceProtected() != null) newRealm.setBruteForceProtected(rep.isBruteForceProtected());
187187
if (rep.isPermanentLockout() != null) newRealm.setPermanentLockout(rep.isPermanentLockout());
188188
if (rep.getMaxTemporaryLockouts() != null) newRealm.setMaxTemporaryLockouts(rep.getMaxTemporaryLockouts());
189+
if (rep.getBruteForceStrategy() != null) newRealm.setBruteForceStrategy(rep.getBruteForceStrategy());
189190
if (rep.getMaxFailureWaitSeconds() != null) newRealm.setMaxFailureWaitSeconds(rep.getMaxFailureWaitSeconds());
190191
if (rep.getMinimumQuickLoginWaitSeconds() != null)
191192
newRealm.setMinimumQuickLoginWaitSeconds(rep.getMinimumQuickLoginWaitSeconds());
@@ -751,6 +752,7 @@ public void updateRealm(RealmRepresentation rep, RealmModel realm) {
751752
if (rep.isBruteForceProtected() != null) realm.setBruteForceProtected(rep.isBruteForceProtected());
752753
if (rep.isPermanentLockout() != null) realm.setPermanentLockout(rep.isPermanentLockout());
753754
if (rep.getMaxTemporaryLockouts() != null) realm.setMaxTemporaryLockouts(rep.getMaxTemporaryLockouts());
755+
if (rep.getBruteForceStrategy() != null) realm.setBruteForceStrategy(rep.getBruteForceStrategy());
754756
if (rep.getMaxFailureWaitSeconds() != null) realm.setMaxFailureWaitSeconds(rep.getMaxFailureWaitSeconds());
755757
if (rep.getMinimumQuickLoginWaitSeconds() != null)
756758
realm.setMinimumQuickLoginWaitSeconds(rep.getMinimumQuickLoginWaitSeconds());

server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public class ModelToRepresentation {
8484
REALM_EXCLUDED_ATTRIBUTES.add("bruteForceProtected");
8585
REALM_EXCLUDED_ATTRIBUTES.add("permanentLockout");
8686
REALM_EXCLUDED_ATTRIBUTES.add("maxTemporaryLockouts");
87+
REALM_EXCLUDED_ATTRIBUTES.add("bruteForceStrategy");
8788
REALM_EXCLUDED_ATTRIBUTES.add("maxFailureWaitSeconds");
8889
REALM_EXCLUDED_ATTRIBUTES.add("waitIncrementSeconds");
8990
REALM_EXCLUDED_ATTRIBUTES.add("quickLoginCheckMilliSeconds");
@@ -372,6 +373,7 @@ public static RealmRepresentation toRepresentation(KeycloakSession session, Real
372373
rep.setBruteForceProtected(realm.isBruteForceProtected());
373374
rep.setPermanentLockout(realm.isPermanentLockout());
374375
rep.setMaxTemporaryLockouts(realm.getMaxTemporaryLockouts());
376+
rep.setBruteForceStrategy(realm.getBruteForceStrategy());
375377
rep.setMaxFailureWaitSeconds(realm.getMaxFailureWaitSeconds());
376378
rep.setMinimumQuickLoginWaitSeconds(realm.getMinimumQuickLoginWaitSeconds());
377379
rep.setWaitIncrementSeconds(realm.getWaitIncrementSeconds());

0 commit comments

Comments
 (0)