add linear strategy to brute force
closes #25917 Signed-off-by: Gilvan Filho <gilvan.sfilho@gmail.com>
This commit is contained in:
parent
6d52520730
commit
c4005d29f0
16 changed files with 182 additions and 8 deletions
|
@ -92,6 +92,7 @@ public class RealmRepresentation {
|
||||||
protected Boolean bruteForceProtected;
|
protected Boolean bruteForceProtected;
|
||||||
protected Boolean permanentLockout;
|
protected Boolean permanentLockout;
|
||||||
protected Integer maxTemporaryLockouts;
|
protected Integer maxTemporaryLockouts;
|
||||||
|
protected BruteForceStrategy bruteForceStrategy;
|
||||||
protected Integer maxFailureWaitSeconds;
|
protected Integer maxFailureWaitSeconds;
|
||||||
protected Integer minimumQuickLoginWaitSeconds;
|
protected Integer minimumQuickLoginWaitSeconds;
|
||||||
protected Integer waitIncrementSeconds;
|
protected Integer waitIncrementSeconds;
|
||||||
|
@ -777,6 +778,14 @@ public class RealmRepresentation {
|
||||||
this.maxTemporaryLockouts = maxTemporaryLockouts;
|
this.maxTemporaryLockouts = maxTemporaryLockouts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BruteForceStrategy getBruteForceStrategy() {
|
||||||
|
return this.bruteForceStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBruteForceStrategy(BruteForceStrategy bruteForceStrategy) {
|
||||||
|
this.bruteForceStrategy = bruteForceStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getMaxFailureWaitSeconds() {
|
public Integer getMaxFailureWaitSeconds() {
|
||||||
return maxFailureWaitSeconds;
|
return maxFailureWaitSeconds;
|
||||||
}
|
}
|
||||||
|
@ -1450,4 +1459,8 @@ public class RealmRepresentation {
|
||||||
}
|
}
|
||||||
organizations.add(org);
|
organizations.add(org);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum BruteForceStrategy {
|
||||||
|
LINEAR, MULTIPLE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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_
|
.. If the time between this failure and the last failure is greater than _Failure Reset Time_
|
||||||
... Reset `count`
|
... Reset `count`
|
||||||
.. Increment `count`
|
.. Increment `count`
|
||||||
.. Calculate `wait` using _Wait Increment_ * (`count` / _Max Login Failures_). The division is an integer division rounded down to a whole number
|
.. Calculate `wait` according brute force strategy defined (see below Strategies to set Wait Time).
|
||||||
.. 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_.
|
.. 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
|
... Temporarily disable the user for the smallest of `wait` and _Max Wait_ seconds
|
||||||
... Increment the temporary lockout counter
|
... Increment the temporary lockout counter
|
||||||
|
|
||||||
`count` does not increment when a temporarily disabled account commits a login failure.
|
`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"]
|
[cols="1,1,1,1"]
|
||||||
|===
|
|===
|
||||||
|
@ -100,9 +104,30 @@ For instance, if you have set `Max Login Failures` to `5` and a `Wait Increment`
|
||||||
|**10** |**30** | 5 | **60**
|
|**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
|
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 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*
|
*Permanent Lockout Parameters*
|
||||||
|
|
||||||
|
|
|
@ -729,6 +729,10 @@ rowSaveBtnAriaLabel=Save edits for {{messageBundle}}
|
||||||
permanentLockout=Permanent lockout
|
permanentLockout=Permanent lockout
|
||||||
maxTemporaryLockouts=Maximum temporary lockouts
|
maxTemporaryLockouts=Maximum temporary lockouts
|
||||||
maxTemporaryLockoutsHelp=The number of temporary lockouts permitted before the user is permanently locked out.
|
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=Linear
|
||||||
|
bruteForceStrategy.MULTIPLE=Multiple
|
||||||
debug=Debug
|
debug=Debug
|
||||||
webAuthnPolicyRequireResidentKey=Require discoverable credential
|
webAuthnPolicyRequireResidentKey=Require discoverable credential
|
||||||
unlockUsersConfirm=All the users that are temporarily locked will be unlocked.
|
unlockUsersConfirm=All the users that are temporarily locked will be unlocked.
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
KeycloakSelect,
|
KeycloakSelect,
|
||||||
NumberControl,
|
NumberControl,
|
||||||
SelectVariant,
|
SelectVariant,
|
||||||
|
SelectControl,
|
||||||
} from "@keycloak/keycloak-ui-shared";
|
} from "@keycloak/keycloak-ui-shared";
|
||||||
import {
|
import {
|
||||||
ActionGroup,
|
ActionGroup,
|
||||||
|
@ -52,6 +53,8 @@ export const BruteForceDetection = ({
|
||||||
BruteForceMode.PermanentAfterTemporaryLockout,
|
BruteForceMode.PermanentAfterTemporaryLockout,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const bruteForceStrategyTypes = ["MULTIPLE", "LINEAR"];
|
||||||
|
|
||||||
const setupForm = () => {
|
const setupForm = () => {
|
||||||
convertToFormValues(realm, setValue);
|
convertToFormValues(realm, setValue);
|
||||||
setIsBruteForceModeUpdated(false);
|
setIsBruteForceModeUpdated(false);
|
||||||
|
@ -155,6 +158,16 @@ export const BruteForceDetection = ({
|
||||||
bruteForceMode ===
|
bruteForceMode ===
|
||||||
BruteForceMode.PermanentAfterTemporaryLockout) && (
|
BruteForceMode.PermanentAfterTemporaryLockout) && (
|
||||||
<>
|
<>
|
||||||
|
<SelectControl
|
||||||
|
name="bruteForceStrategy"
|
||||||
|
label={t("bruteForceStrategy")}
|
||||||
|
labelIcon={t("bruteForceStrategyHelp")}
|
||||||
|
controller={{ defaultValue: "" }}
|
||||||
|
options={bruteForceStrategyTypes.map((key) => ({
|
||||||
|
key,
|
||||||
|
value: t(`bruteForceStrategy.${key}`),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
<Time name="waitIncrementSeconds" />
|
<Time name="waitIncrementSeconds" />
|
||||||
<Time name="maxFailureWaitSeconds" />
|
<Time name="maxFailureWaitSeconds" />
|
||||||
<Time name="maxDeltaTimeSeconds" />
|
<Time name="maxDeltaTimeSeconds" />
|
||||||
|
|
|
@ -74,6 +74,7 @@ export default interface RealmRepresentation {
|
||||||
maxDeltaTimeSeconds?: number;
|
maxDeltaTimeSeconds?: number;
|
||||||
maxFailureWaitSeconds?: number;
|
maxFailureWaitSeconds?: number;
|
||||||
maxTemporaryLockouts?: number;
|
maxTemporaryLockouts?: number;
|
||||||
|
bruteForceStrategy?: "MULTIPLE" | "LINEAR";
|
||||||
minimumQuickLoginWaitSeconds?: number;
|
minimumQuickLoginWaitSeconds?: number;
|
||||||
notBefore?: number;
|
notBefore?: number;
|
||||||
oauth2DeviceCodeLifespan?: number;
|
oauth2DeviceCodeLifespan?: number;
|
||||||
|
|
|
@ -47,6 +47,7 @@ import org.keycloak.models.WebAuthnPolicy;
|
||||||
import org.keycloak.models.cache.CachedRealmModel;
|
import org.keycloak.models.cache.CachedRealmModel;
|
||||||
import org.keycloak.models.cache.UserCache;
|
import org.keycloak.models.cache.UserCache;
|
||||||
import org.keycloak.models.cache.infinispan.entities.CachedRealm;
|
import org.keycloak.models.cache.infinispan.entities.CachedRealm;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.storage.UserStorageProvider;
|
import org.keycloak.storage.UserStorageProvider;
|
||||||
import org.keycloak.storage.UserStorageUtil;
|
import org.keycloak.storage.UserStorageUtil;
|
||||||
import org.keycloak.storage.client.ClientStorageProvider;
|
import org.keycloak.storage.client.ClientStorageProvider;
|
||||||
|
@ -283,6 +284,18 @@ public class RealmAdapter implements CachedRealmModel {
|
||||||
updated.setMaxTemporaryLockouts(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
|
@Override
|
||||||
public int getMaxFailureWaitSeconds() {
|
public int getMaxFailureWaitSeconds() {
|
||||||
if (isUpdated()) return updated.getMaxFailureWaitSeconds();
|
if (isUpdated()) return updated.getMaxFailureWaitSeconds();
|
||||||
|
|
|
@ -52,6 +52,7 @@ import org.keycloak.models.RequiredCredentialModel;
|
||||||
import org.keycloak.models.WebAuthnPolicy;
|
import org.keycloak.models.WebAuthnPolicy;
|
||||||
import org.keycloak.models.cache.infinispan.DefaultLazyLoader;
|
import org.keycloak.models.cache.infinispan.DefaultLazyLoader;
|
||||||
import org.keycloak.models.cache.infinispan.LazyLoader;
|
import org.keycloak.models.cache.infinispan.LazyLoader;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -78,6 +79,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
protected boolean bruteForceProtected;
|
protected boolean bruteForceProtected;
|
||||||
protected boolean permanentLockout;
|
protected boolean permanentLockout;
|
||||||
protected int maxTemporaryLockouts;
|
protected int maxTemporaryLockouts;
|
||||||
|
protected RealmRepresentation.BruteForceStrategy bruteForceStrategy;
|
||||||
protected int maxFailureWaitSeconds;
|
protected int maxFailureWaitSeconds;
|
||||||
protected int minimumQuickLoginWaitSeconds;
|
protected int minimumQuickLoginWaitSeconds;
|
||||||
protected int waitIncrementSeconds;
|
protected int waitIncrementSeconds;
|
||||||
|
@ -193,6 +195,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
bruteForceProtected = model.isBruteForceProtected();
|
bruteForceProtected = model.isBruteForceProtected();
|
||||||
permanentLockout = model.isPermanentLockout();
|
permanentLockout = model.isPermanentLockout();
|
||||||
maxTemporaryLockouts = model.getMaxTemporaryLockouts();
|
maxTemporaryLockouts = model.getMaxTemporaryLockouts();
|
||||||
|
bruteForceStrategy = model.getBruteForceStrategy();
|
||||||
maxFailureWaitSeconds = model.getMaxFailureWaitSeconds();
|
maxFailureWaitSeconds = model.getMaxFailureWaitSeconds();
|
||||||
minimumQuickLoginWaitSeconds = model.getMinimumQuickLoginWaitSeconds();
|
minimumQuickLoginWaitSeconds = model.getMinimumQuickLoginWaitSeconds();
|
||||||
waitIncrementSeconds = model.getWaitIncrementSeconds();
|
waitIncrementSeconds = model.getWaitIncrementSeconds();
|
||||||
|
@ -376,6 +379,10 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
return maxTemporaryLockouts;
|
return maxTemporaryLockouts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RealmRepresentation.BruteForceStrategy getBruteForceStrategy() {
|
||||||
|
return bruteForceStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
public int getMaxFailureWaitSeconds() {
|
public int getMaxFailureWaitSeconds() {
|
||||||
return this.maxFailureWaitSeconds;
|
return this.maxFailureWaitSeconds;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,8 @@ import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import jakarta.persistence.LockModeType;
|
import jakarta.persistence.LockModeType;
|
||||||
import jakarta.persistence.TypedQuery;
|
import jakarta.persistence.TypedQuery;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -268,6 +270,20 @@ public class RealmAdapter implements StorageProviderRealmModel, JpaModel<RealmEn
|
||||||
return getAttribute("maxTemporaryLockouts", 0);
|
return getAttribute("maxTemporaryLockouts", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@Override
|
||||||
public void setMaxTemporaryLockouts(final int val) {
|
public void setMaxTemporaryLockouts(final int val) {
|
||||||
setAttribute("maxTemporaryLockouts", val);
|
setAttribute("maxTemporaryLockouts", val);
|
||||||
|
|
|
@ -186,6 +186,7 @@ public class DefaultExportImportManager implements ExportImportManager {
|
||||||
if (rep.isBruteForceProtected() != null) newRealm.setBruteForceProtected(rep.isBruteForceProtected());
|
if (rep.isBruteForceProtected() != null) newRealm.setBruteForceProtected(rep.isBruteForceProtected());
|
||||||
if (rep.isPermanentLockout() != null) newRealm.setPermanentLockout(rep.isPermanentLockout());
|
if (rep.isPermanentLockout() != null) newRealm.setPermanentLockout(rep.isPermanentLockout());
|
||||||
if (rep.getMaxTemporaryLockouts() != null) newRealm.setMaxTemporaryLockouts(rep.getMaxTemporaryLockouts());
|
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.getMaxFailureWaitSeconds() != null) newRealm.setMaxFailureWaitSeconds(rep.getMaxFailureWaitSeconds());
|
||||||
if (rep.getMinimumQuickLoginWaitSeconds() != null)
|
if (rep.getMinimumQuickLoginWaitSeconds() != null)
|
||||||
newRealm.setMinimumQuickLoginWaitSeconds(rep.getMinimumQuickLoginWaitSeconds());
|
newRealm.setMinimumQuickLoginWaitSeconds(rep.getMinimumQuickLoginWaitSeconds());
|
||||||
|
@ -751,6 +752,7 @@ public class DefaultExportImportManager implements ExportImportManager {
|
||||||
if (rep.isBruteForceProtected() != null) realm.setBruteForceProtected(rep.isBruteForceProtected());
|
if (rep.isBruteForceProtected() != null) realm.setBruteForceProtected(rep.isBruteForceProtected());
|
||||||
if (rep.isPermanentLockout() != null) realm.setPermanentLockout(rep.isPermanentLockout());
|
if (rep.isPermanentLockout() != null) realm.setPermanentLockout(rep.isPermanentLockout());
|
||||||
if (rep.getMaxTemporaryLockouts() != null) realm.setMaxTemporaryLockouts(rep.getMaxTemporaryLockouts());
|
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.getMaxFailureWaitSeconds() != null) realm.setMaxFailureWaitSeconds(rep.getMaxFailureWaitSeconds());
|
||||||
if (rep.getMinimumQuickLoginWaitSeconds() != null)
|
if (rep.getMinimumQuickLoginWaitSeconds() != null)
|
||||||
realm.setMinimumQuickLoginWaitSeconds(rep.getMinimumQuickLoginWaitSeconds());
|
realm.setMinimumQuickLoginWaitSeconds(rep.getMinimumQuickLoginWaitSeconds());
|
||||||
|
|
|
@ -84,6 +84,7 @@ public class ModelToRepresentation {
|
||||||
REALM_EXCLUDED_ATTRIBUTES.add("bruteForceProtected");
|
REALM_EXCLUDED_ATTRIBUTES.add("bruteForceProtected");
|
||||||
REALM_EXCLUDED_ATTRIBUTES.add("permanentLockout");
|
REALM_EXCLUDED_ATTRIBUTES.add("permanentLockout");
|
||||||
REALM_EXCLUDED_ATTRIBUTES.add("maxTemporaryLockouts");
|
REALM_EXCLUDED_ATTRIBUTES.add("maxTemporaryLockouts");
|
||||||
|
REALM_EXCLUDED_ATTRIBUTES.add("bruteForceStrategy");
|
||||||
REALM_EXCLUDED_ATTRIBUTES.add("maxFailureWaitSeconds");
|
REALM_EXCLUDED_ATTRIBUTES.add("maxFailureWaitSeconds");
|
||||||
REALM_EXCLUDED_ATTRIBUTES.add("waitIncrementSeconds");
|
REALM_EXCLUDED_ATTRIBUTES.add("waitIncrementSeconds");
|
||||||
REALM_EXCLUDED_ATTRIBUTES.add("quickLoginCheckMilliSeconds");
|
REALM_EXCLUDED_ATTRIBUTES.add("quickLoginCheckMilliSeconds");
|
||||||
|
@ -372,6 +373,7 @@ public class ModelToRepresentation {
|
||||||
rep.setBruteForceProtected(realm.isBruteForceProtected());
|
rep.setBruteForceProtected(realm.isBruteForceProtected());
|
||||||
rep.setPermanentLockout(realm.isPermanentLockout());
|
rep.setPermanentLockout(realm.isPermanentLockout());
|
||||||
rep.setMaxTemporaryLockouts(realm.getMaxTemporaryLockouts());
|
rep.setMaxTemporaryLockouts(realm.getMaxTemporaryLockouts());
|
||||||
|
rep.setBruteForceStrategy(realm.getBruteForceStrategy());
|
||||||
rep.setMaxFailureWaitSeconds(realm.getMaxFailureWaitSeconds());
|
rep.setMaxFailureWaitSeconds(realm.getMaxFailureWaitSeconds());
|
||||||
rep.setMinimumQuickLoginWaitSeconds(realm.getMinimumQuickLoginWaitSeconds());
|
rep.setMinimumQuickLoginWaitSeconds(realm.getMinimumQuickLoginWaitSeconds());
|
||||||
rep.setWaitIncrementSeconds(realm.getWaitIncrementSeconds());
|
rep.setWaitIncrementSeconds(realm.getWaitIncrementSeconds());
|
||||||
|
|
|
@ -40,6 +40,7 @@ import org.keycloak.models.RequiredCredentialModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
import org.keycloak.models.WebAuthnPolicy;
|
import org.keycloak.models.WebAuthnPolicy;
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -187,6 +188,10 @@ public class RealmModelDelegate implements RealmModel {
|
||||||
delegate.setBruteForceProtected(value);
|
delegate.setBruteForceProtected(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RealmRepresentation.BruteForceStrategy getBruteForceStrategy() { return delegate.getBruteForceStrategy(); }
|
||||||
|
|
||||||
|
public void setBruteForceStrategy(RealmRepresentation.BruteForceStrategy value) { delegate.setBruteForceStrategy(value); }
|
||||||
|
|
||||||
public boolean isPermanentLockout() {
|
public boolean isPermanentLockout() {
|
||||||
return delegate.isPermanentLockout();
|
return delegate.isPermanentLockout();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.keycloak.broker.provider.util;
|
||||||
import org.keycloak.common.enums.SslRequired;
|
import org.keycloak.common.enums.SslRequired;
|
||||||
import org.keycloak.component.ComponentModel;
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.models.*;
|
import org.keycloak.models.*;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -688,6 +689,16 @@ public class IdentityBrokerStateTestHelpers {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RealmRepresentation.BruteForceStrategy getBruteForceStrategy() {
|
||||||
|
return RealmRepresentation.BruteForceStrategy.MULTIPLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBruteForceStrategy(RealmRepresentation.BruteForceStrategy val) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getMaxFailureWaitSeconds() {
|
public int getMaxFailureWaitSeconds() {
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
@ -22,6 +22,7 @@ import org.keycloak.common.enums.SslRequired;
|
||||||
import org.keycloak.component.ComponentModel;
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
import org.keycloak.provider.ProviderEvent;
|
import org.keycloak.provider.ProviderEvent;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -142,6 +143,8 @@ public interface RealmModel extends RoleContainerModel {
|
||||||
void setPermanentLockout(boolean val);
|
void setPermanentLockout(boolean val);
|
||||||
int getMaxTemporaryLockouts();
|
int getMaxTemporaryLockouts();
|
||||||
void setMaxTemporaryLockouts(int val);
|
void setMaxTemporaryLockouts(int val);
|
||||||
|
RealmRepresentation.BruteForceStrategy getBruteForceStrategy();
|
||||||
|
void setBruteForceStrategy(RealmRepresentation.BruteForceStrategy val);
|
||||||
int getMaxFailureWaitSeconds();
|
int getMaxFailureWaitSeconds();
|
||||||
void setMaxFailureWaitSeconds(int val);
|
void setMaxFailureWaitSeconds(int val);
|
||||||
int getWaitIncrementSeconds();
|
int getWaitIncrementSeconds();
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserLoginFailureModel;
|
import org.keycloak.models.UserLoginFailureModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.storage.ReadOnlyException;
|
import org.keycloak.storage.ReadOnlyException;
|
||||||
|
|
||||||
import jakarta.ws.rs.core.HttpHeaders;
|
import jakarta.ws.rs.core.HttpHeaders;
|
||||||
|
@ -90,13 +91,18 @@ public class DefaultBruteForceProtector implements BruteForceProtector {
|
||||||
|
|
||||||
int waitSeconds = 0;
|
int waitSeconds = 0;
|
||||||
if(!(realm.isPermanentLockout() && realm.getMaxTemporaryLockouts() == 0)) {
|
if(!(realm.isPermanentLockout() && realm.getMaxTemporaryLockouts() == 0)) {
|
||||||
|
if(RealmRepresentation.BruteForceStrategy.MULTIPLE.equals(realm.getBruteForceStrategy())) {
|
||||||
waitSeconds = realm.getWaitIncrementSeconds() * (userLoginFailure.getNumFailures() / realm.getFailureFactor());
|
waitSeconds = realm.getWaitIncrementSeconds() * (userLoginFailure.getNumFailures() / realm.getFailureFactor());
|
||||||
|
} else {
|
||||||
|
waitSeconds = realm.getWaitIncrementSeconds() * (1 + userLoginFailure.getNumFailures() - realm.getFailureFactor());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.debugv("waitSeconds: {0}", waitSeconds);
|
logger.debugv("waitSeconds: {0}", waitSeconds);
|
||||||
logger.debugv("deltaTime: {0}", deltaTime);
|
logger.debugv("deltaTime: {0}", deltaTime);
|
||||||
|
|
||||||
boolean quickLoginFailure = false;
|
boolean quickLoginFailure = false;
|
||||||
if (waitSeconds == 0) {
|
if (waitSeconds <= 0) {
|
||||||
if (last > 0 && deltaTime < realm.getQuickLoginCheckMilliSeconds()) {
|
if (last > 0 && deltaTime < realm.getQuickLoginCheckMilliSeconds()) {
|
||||||
logger.debugv("quick login, set min wait seconds");
|
logger.debugv("quick login, set min wait seconds");
|
||||||
waitSeconds = realm.getMinimumQuickLoginWaitSeconds();
|
waitSeconds = realm.getMinimumQuickLoginWaitSeconds();
|
||||||
|
|
|
@ -253,6 +253,7 @@ public class RealmManager {
|
||||||
realm.setBruteForceProtected(false); // default settings off for now todo set it on
|
realm.setBruteForceProtected(false); // default settings off for now todo set it on
|
||||||
realm.setPermanentLockout(false);
|
realm.setPermanentLockout(false);
|
||||||
realm.setMaxTemporaryLockouts(0);
|
realm.setMaxTemporaryLockouts(0);
|
||||||
|
realm.setBruteForceStrategy(RealmRepresentation.BruteForceStrategy.MULTIPLE);
|
||||||
realm.setMaxFailureWaitSeconds(900);
|
realm.setMaxFailureWaitSeconds(900);
|
||||||
realm.setMinimumQuickLoginWaitSeconds(60);
|
realm.setMinimumQuickLoginWaitSeconds(60);
|
||||||
realm.setWaitIncrementSeconds(60);
|
realm.setWaitIncrementSeconds(60);
|
||||||
|
|
|
@ -112,6 +112,7 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest {
|
||||||
UserBuilder.edit(user).totpSecret("totpSecret").emailVerified(true);
|
UserBuilder.edit(user).totpSecret("totpSecret").emailVerified(true);
|
||||||
|
|
||||||
testRealm.setBruteForceProtected(true);
|
testRealm.setBruteForceProtected(true);
|
||||||
|
testRealm.setBruteForceStrategy(RealmRepresentation.BruteForceStrategy.MULTIPLE);
|
||||||
testRealm.setFailureFactor(failureFactor);
|
testRealm.setFailureFactor(failureFactor);
|
||||||
testRealm.setMaxDeltaTimeSeconds(60);
|
testRealm.setMaxDeltaTimeSeconds(60);
|
||||||
testRealm.setMaxFailureWaitSeconds(100);
|
testRealm.setMaxFailureWaitSeconds(100);
|
||||||
|
@ -131,6 +132,7 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest {
|
||||||
clearUserFailures();
|
clearUserFailures();
|
||||||
clearAllUserFailures();
|
clearAllUserFailures();
|
||||||
RealmRepresentation realm = adminClient.realm("test").toRepresentation();
|
RealmRepresentation realm = adminClient.realm("test").toRepresentation();
|
||||||
|
realm.setBruteForceStrategy(RealmRepresentation.BruteForceStrategy.MULTIPLE);
|
||||||
realm.setFailureFactor(failureFactor);
|
realm.setFailureFactor(failureFactor);
|
||||||
realm.setMaxDeltaTimeSeconds(60);
|
realm.setMaxDeltaTimeSeconds(60);
|
||||||
realm.setMaxFailureWaitSeconds(100);
|
realm.setMaxFailureWaitSeconds(100);
|
||||||
|
@ -501,6 +503,56 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest {
|
||||||
testingClient.testing().setTimeOffset(Collections.singletonMap("offset", String.valueOf(0)));
|
testingClient.testing().setTimeOffset(Collections.singletonMap("offset", String.valueOf(0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testByMultipleStrategy() throws Exception {
|
||||||
|
|
||||||
|
try {
|
||||||
|
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0);
|
||||||
|
loginSuccess();
|
||||||
|
loginInvalidPassword();
|
||||||
|
loginInvalidPassword();
|
||||||
|
expectTemporarilyDisabled();
|
||||||
|
assertUserNumberOfFailures(user.getId(), 2);
|
||||||
|
this.setTimeOffset(30);
|
||||||
|
|
||||||
|
loginInvalidPassword();
|
||||||
|
assertUserNumberOfFailures(user.getId(), 3);
|
||||||
|
this.setTimeOffset(60);
|
||||||
|
loginSuccess();
|
||||||
|
} finally {
|
||||||
|
this.resetTimeOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLinearStrategy() throws Exception {
|
||||||
|
RealmRepresentation realm = testRealm().toRepresentation();
|
||||||
|
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0);
|
||||||
|
try {
|
||||||
|
realm.setBruteForceStrategy(RealmRepresentation.BruteForceStrategy.LINEAR);
|
||||||
|
testRealm().update(realm);
|
||||||
|
|
||||||
|
loginSuccess();
|
||||||
|
|
||||||
|
loginInvalidPassword();
|
||||||
|
loginInvalidPassword();
|
||||||
|
expectTemporarilyDisabled();
|
||||||
|
assertUserNumberOfFailures(user.getId(), 2);
|
||||||
|
this.setTimeOffset(30);
|
||||||
|
|
||||||
|
loginInvalidPassword();
|
||||||
|
assertUserNumberOfFailures(user.getId(), 3);
|
||||||
|
this.setTimeOffset(60);
|
||||||
|
expectTemporarilyDisabled();
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
realm.setPermanentLockout(false);
|
||||||
|
realm.setBruteForceStrategy(RealmRepresentation.BruteForceStrategy.MULTIPLE);
|
||||||
|
testRealm().update(realm);
|
||||||
|
this.resetTimeOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBrowserInvalidPasswordDifferentCase() throws Exception {
|
public void testBrowserInvalidPasswordDifferentCase() throws Exception {
|
||||||
loginSuccess("test-user@localhost");
|
loginSuccess("test-user@localhost");
|
||||||
|
|
Loading…
Reference in a new issue