KEYCLOAK-4204 Extend brute force protection with permanent lockout on failed attempts
- Can still use temporary brute force protection. - After X-1 failed login attempt, if the user successfully logs in his/her fail login count is reset.
This commit is contained in:
parent
7bcbc9a6af
commit
ca1152c3e5
14 changed files with 233 additions and 33 deletions
|
@ -66,6 +66,7 @@ public class RealmRepresentation {
|
||||||
|
|
||||||
//--- brute force settings
|
//--- brute force settings
|
||||||
protected Boolean bruteForceProtected;
|
protected Boolean bruteForceProtected;
|
||||||
|
protected Boolean permanentLockout;
|
||||||
protected Integer maxFailureWaitSeconds;
|
protected Integer maxFailureWaitSeconds;
|
||||||
protected Integer minimumQuickLoginWaitSeconds;
|
protected Integer minimumQuickLoginWaitSeconds;
|
||||||
protected Integer waitIncrementSeconds;
|
protected Integer waitIncrementSeconds;
|
||||||
|
@ -558,6 +559,14 @@ public class RealmRepresentation {
|
||||||
this.bruteForceProtected = bruteForceProtected;
|
this.bruteForceProtected = bruteForceProtected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean isPermanentLockout() {
|
||||||
|
return permanentLockout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPermanentLockout(Boolean permanentLockout) {
|
||||||
|
this.permanentLockout = permanentLockout;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getMaxFailureWaitSeconds() {
|
public Integer getMaxFailureWaitSeconds() {
|
||||||
return maxFailureWaitSeconds;
|
return maxFailureWaitSeconds;
|
||||||
}
|
}
|
||||||
|
|
|
@ -221,6 +221,18 @@ public class RealmAdapter implements CachedRealmModel {
|
||||||
updated.setBruteForceProtected(value);
|
updated.setBruteForceProtected(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPermanentLockout() {
|
||||||
|
if(isUpdated()) return updated.isPermanentLockout();
|
||||||
|
return cached.isPermanentLockout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPermanentLockout(final boolean val) {
|
||||||
|
getDelegateForUpdate();
|
||||||
|
updated.setPermanentLockout(val);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getMaxFailureWaitSeconds() {
|
public int getMaxFailureWaitSeconds() {
|
||||||
if (isUpdated()) return updated.getMaxFailureWaitSeconds();
|
if (isUpdated()) return updated.getMaxFailureWaitSeconds();
|
||||||
|
|
|
@ -65,6 +65,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
protected boolean editUsernameAllowed;
|
protected boolean editUsernameAllowed;
|
||||||
//--- brute force settings
|
//--- brute force settings
|
||||||
protected boolean bruteForceProtected;
|
protected boolean bruteForceProtected;
|
||||||
|
protected boolean permanentLockout;
|
||||||
protected int maxFailureWaitSeconds;
|
protected int maxFailureWaitSeconds;
|
||||||
protected int minimumQuickLoginWaitSeconds;
|
protected int minimumQuickLoginWaitSeconds;
|
||||||
protected int waitIncrementSeconds;
|
protected int waitIncrementSeconds;
|
||||||
|
@ -156,6 +157,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
editUsernameAllowed = model.isEditUsernameAllowed();
|
editUsernameAllowed = model.isEditUsernameAllowed();
|
||||||
//--- brute force settings
|
//--- brute force settings
|
||||||
bruteForceProtected = model.isBruteForceProtected();
|
bruteForceProtected = model.isBruteForceProtected();
|
||||||
|
permanentLockout = model.isPermanentLockout();
|
||||||
maxFailureWaitSeconds = model.getMaxFailureWaitSeconds();
|
maxFailureWaitSeconds = model.getMaxFailureWaitSeconds();
|
||||||
minimumQuickLoginWaitSeconds = model.getMinimumQuickLoginWaitSeconds();
|
minimumQuickLoginWaitSeconds = model.getMinimumQuickLoginWaitSeconds();
|
||||||
waitIncrementSeconds = model.getWaitIncrementSeconds();
|
waitIncrementSeconds = model.getWaitIncrementSeconds();
|
||||||
|
@ -314,6 +316,10 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
return bruteForceProtected;
|
return bruteForceProtected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPermanentLockout() {
|
||||||
|
return permanentLockout;
|
||||||
|
}
|
||||||
|
|
||||||
public int getMaxFailureWaitSeconds() {
|
public int getMaxFailureWaitSeconds() {
|
||||||
return this.maxFailureWaitSeconds;
|
return this.maxFailureWaitSeconds;
|
||||||
}
|
}
|
||||||
|
|
|
@ -278,6 +278,16 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
||||||
setAttribute("bruteForceProtected", value);
|
setAttribute("bruteForceProtected", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPermanentLockout() {
|
||||||
|
return getAttribute("permanentLockout", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPermanentLockout(final boolean val) {
|
||||||
|
setAttribute("permanentLockout", val);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getMaxFailureWaitSeconds() {
|
public int getMaxFailureWaitSeconds() {
|
||||||
return getAttribute("maxFailureWaitSeconds", 0);
|
return getAttribute("maxFailureWaitSeconds", 0);
|
||||||
|
|
|
@ -267,6 +267,7 @@ public class ModelToRepresentation {
|
||||||
rep.setRegistrationEmailAsUsername(realm.isRegistrationEmailAsUsername());
|
rep.setRegistrationEmailAsUsername(realm.isRegistrationEmailAsUsername());
|
||||||
rep.setRememberMe(realm.isRememberMe());
|
rep.setRememberMe(realm.isRememberMe());
|
||||||
rep.setBruteForceProtected(realm.isBruteForceProtected());
|
rep.setBruteForceProtected(realm.isBruteForceProtected());
|
||||||
|
rep.setPermanentLockout(realm.isPermanentLockout());
|
||||||
rep.setMaxFailureWaitSeconds(realm.getMaxFailureWaitSeconds());
|
rep.setMaxFailureWaitSeconds(realm.getMaxFailureWaitSeconds());
|
||||||
rep.setMinimumQuickLoginWaitSeconds(realm.getMinimumQuickLoginWaitSeconds());
|
rep.setMinimumQuickLoginWaitSeconds(realm.getMinimumQuickLoginWaitSeconds());
|
||||||
rep.setWaitIncrementSeconds(realm.getWaitIncrementSeconds());
|
rep.setWaitIncrementSeconds(realm.getWaitIncrementSeconds());
|
||||||
|
|
|
@ -141,6 +141,7 @@ public class RepresentationToModel {
|
||||||
if (rep.getDisplayNameHtml() != null) newRealm.setDisplayNameHtml(rep.getDisplayNameHtml());
|
if (rep.getDisplayNameHtml() != null) newRealm.setDisplayNameHtml(rep.getDisplayNameHtml());
|
||||||
if (rep.isEnabled() != null) newRealm.setEnabled(rep.isEnabled());
|
if (rep.isEnabled() != null) newRealm.setEnabled(rep.isEnabled());
|
||||||
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.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());
|
||||||
|
@ -787,6 +788,7 @@ public class RepresentationToModel {
|
||||||
if (rep.getDisplayNameHtml() != null) realm.setDisplayNameHtml(rep.getDisplayNameHtml());
|
if (rep.getDisplayNameHtml() != null) realm.setDisplayNameHtml(rep.getDisplayNameHtml());
|
||||||
if (rep.isEnabled() != null) realm.setEnabled(rep.isEnabled());
|
if (rep.isEnabled() != null) realm.setEnabled(rep.isEnabled());
|
||||||
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.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());
|
||||||
|
|
|
@ -30,5 +30,7 @@ import org.keycloak.provider.Provider;
|
||||||
public interface BruteForceProtector extends Provider {
|
public interface BruteForceProtector extends Provider {
|
||||||
void failedLogin(RealmModel realm, UserModel user, ClientConnection clientConnection);
|
void failedLogin(RealmModel realm, UserModel user, ClientConnection clientConnection);
|
||||||
|
|
||||||
|
void successfulLogin(RealmModel realm, UserModel user, ClientConnection clientConnection);
|
||||||
|
|
||||||
boolean isTemporarilyDisabled(KeycloakSession session, RealmModel realm, UserModel user);
|
boolean isTemporarilyDisabled(KeycloakSession session, RealmModel realm, UserModel user);
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,6 +127,8 @@ public interface RealmModel extends RoleContainerModel {
|
||||||
//--- brute force settings
|
//--- brute force settings
|
||||||
boolean isBruteForceProtected();
|
boolean isBruteForceProtected();
|
||||||
void setBruteForceProtected(boolean value);
|
void setBruteForceProtected(boolean value);
|
||||||
|
boolean isPermanentLockout();
|
||||||
|
void setPermanentLockout(boolean val);
|
||||||
int getMaxFailureWaitSeconds();
|
int getMaxFailureWaitSeconds();
|
||||||
void setMaxFailureWaitSeconds(int val);
|
void setMaxFailureWaitSeconds(int val);
|
||||||
int getWaitIncrementSeconds();
|
int getWaitIncrementSeconds();
|
||||||
|
|
|
@ -552,6 +552,22 @@ public class AuthenticationProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void logSuccess() {
|
||||||
|
if (realm.isBruteForceProtected()) {
|
||||||
|
String username = clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
|
||||||
|
// TODO: as above, need to handle non form success
|
||||||
|
|
||||||
|
if(username == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(session, realm, username);
|
||||||
|
if (user != null) {
|
||||||
|
getBruteForceProtector().successfulLogin(realm, user, connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSuccessful(AuthenticationExecutionModel model) {
|
public boolean isSuccessful(AuthenticationExecutionModel model) {
|
||||||
ClientSessionModel.ExecutionStatus status = clientSession.getExecutionStatus().get(model.getId());
|
ClientSessionModel.ExecutionStatus status = clientSession.getExecutionStatus().get(model.getId());
|
||||||
if (status == null) return false;
|
if (status == null) return false;
|
||||||
|
@ -853,7 +869,7 @@ public class AuthenticationProcessor {
|
||||||
public void validateUser(UserModel authenticatedUser) {
|
public void validateUser(UserModel authenticatedUser) {
|
||||||
if (authenticatedUser == null) return;
|
if (authenticatedUser == null) return;
|
||||||
if (!authenticatedUser.isEnabled()) throw new AuthenticationFlowException(AuthenticationFlowError.USER_DISABLED);
|
if (!authenticatedUser.isEnabled()) throw new AuthenticationFlowException(AuthenticationFlowError.USER_DISABLED);
|
||||||
if (realm.isBruteForceProtected()) {
|
if (realm.isBruteForceProtected() && !realm.isPermanentLockout()) {
|
||||||
if (getBruteForceProtector().isTemporarilyDisabled(session, realm, authenticatedUser)) {
|
if (getBruteForceProtector().isTemporarilyDisabled(session, realm, authenticatedUser)) {
|
||||||
throw new AuthenticationFlowException(AuthenticationFlowError.USER_TEMPORARILY_DISABLED);
|
throw new AuthenticationFlowException(AuthenticationFlowError.USER_TEMPORARILY_DISABLED);
|
||||||
}
|
}
|
||||||
|
@ -866,6 +882,8 @@ public class AuthenticationProcessor {
|
||||||
return redirectToRequiredActions(session, realm, clientSession, uriInfo);
|
return redirectToRequiredActions(session, realm, clientSession, uriInfo);
|
||||||
} else {
|
} else {
|
||||||
event.detail(Details.CODE_ID, clientSession.getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set
|
event.detail(Details.CODE_ID, clientSession.getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set
|
||||||
|
// the user has successfully logged in and we can clear his/her previous login failure attempts.
|
||||||
|
logSuccess();
|
||||||
return AuthenticationManager.finishedRequiredActions(session, userSession, clientSession, connection, request, uriInfo, event);
|
return AuthenticationManager.finishedRequiredActions(session, userSession, clientSession, connection, request, uriInfo, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,14 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected class SuccessfulLogin extends LoginEvent {
|
||||||
|
protected final CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
public SuccessfulLogin(String realmId, String userId, String ip) {
|
||||||
|
super(realmId, userId, ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public DefaultBruteForceProtector(KeycloakSessionFactory factory) {
|
public DefaultBruteForceProtector(KeycloakSessionFactory factory) {
|
||||||
this.factory = factory;
|
this.factory = factory;
|
||||||
}
|
}
|
||||||
|
@ -96,44 +104,67 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
|
||||||
|
|
||||||
String userId = event.userId;
|
String userId = event.userId;
|
||||||
UserModel user = session.users().getUserById(userId, realm);
|
UserModel user = session.users().getUserById(userId, realm);
|
||||||
|
if (user == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
UserLoginFailureModel userLoginFailure = getUserModel(session, event);
|
UserLoginFailureModel userLoginFailure = getUserModel(session, event);
|
||||||
if (user != null) {
|
if (userLoginFailure == null) {
|
||||||
if (userLoginFailure == null) {
|
userLoginFailure = session.sessions().addUserLoginFailure(realm, userId);
|
||||||
userLoginFailure = session.sessions().addUserLoginFailure(realm, userId);
|
}
|
||||||
}
|
userLoginFailure.setLastIPFailure(event.ip);
|
||||||
userLoginFailure.setLastIPFailure(event.ip);
|
long currentTime = Time.currentTimeMillis();
|
||||||
long currentTime = Time.currentTimeMillis();
|
long last = userLoginFailure.getLastFailure();
|
||||||
long last = userLoginFailure.getLastFailure();
|
long deltaTime = 0;
|
||||||
long deltaTime = 0;
|
if (last > 0) {
|
||||||
if (last > 0) {
|
deltaTime = currentTime - last;
|
||||||
deltaTime = currentTime - last;
|
}
|
||||||
}
|
userLoginFailure.setLastFailure(currentTime);
|
||||||
userLoginFailure.setLastFailure(currentTime);
|
|
||||||
if (deltaTime > 0) {
|
if(realm.isPermanentLockout()) {
|
||||||
// if last failure was more than MAX_DELTA clear failures
|
|
||||||
if (deltaTime > (long) realm.getMaxDeltaTimeSeconds() * 1000L) {
|
|
||||||
userLoginFailure.clearFailures();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
userLoginFailure.incrementFailures();
|
userLoginFailure.incrementFailures();
|
||||||
logger.debugv("new num failures: {0}", userLoginFailure.getNumFailures());
|
logger.debugv("new num failures: {0}", userLoginFailure.getNumFailures());
|
||||||
|
|
||||||
int waitSeconds = realm.getWaitIncrementSeconds() * (userLoginFailure.getNumFailures() / realm.getFailureFactor());
|
if(userLoginFailure.getNumFailures() == realm.getFailureFactor()) {
|
||||||
logger.debugv("waitSeconds: {0}", waitSeconds);
|
logger.debugv("user {0} locked permanently due to too many login attempts", user.getUsername());
|
||||||
logger.debugv("deltaTime: {0}", deltaTime);
|
user.setEnabled(false);
|
||||||
|
return;
|
||||||
if (waitSeconds == 0) {
|
|
||||||
if (last > 0 && deltaTime < realm.getQuickLoginCheckMilliSeconds()) {
|
|
||||||
logger.debugv("quick login, set min wait seconds");
|
|
||||||
waitSeconds = realm.getMinimumQuickLoginWaitSeconds();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (waitSeconds > 0) {
|
|
||||||
waitSeconds = Math.min(realm.getMaxFailureWaitSeconds(), waitSeconds);
|
if (last > 0 && deltaTime < realm.getQuickLoginCheckMilliSeconds()) {
|
||||||
|
logger.debugv("quick login, set min wait seconds");
|
||||||
|
int waitSeconds = realm.getMinimumQuickLoginWaitSeconds();
|
||||||
int notBefore = (int) (currentTime / 1000) + waitSeconds;
|
int notBefore = (int) (currentTime / 1000) + waitSeconds;
|
||||||
logger.debugv("set notBefore: {0}", notBefore);
|
logger.debugv("set notBefore: {0}", notBefore);
|
||||||
userLoginFailure.setFailedLoginNotBefore(notBefore);
|
userLoginFailure.setFailedLoginNotBefore(notBefore);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deltaTime > 0) {
|
||||||
|
// if last failure was more than MAX_DELTA clear failures
|
||||||
|
if (deltaTime > (long) realm.getMaxDeltaTimeSeconds() * 1000L) {
|
||||||
|
userLoginFailure.clearFailures();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userLoginFailure.incrementFailures();
|
||||||
|
logger.debugv("new num failures: {0}", userLoginFailure.getNumFailures());
|
||||||
|
|
||||||
|
int waitSeconds = realm.getWaitIncrementSeconds() * (userLoginFailure.getNumFailures() / realm.getFailureFactor());
|
||||||
|
logger.debugv("waitSeconds: {0}", waitSeconds);
|
||||||
|
logger.debugv("deltaTime: {0}", deltaTime);
|
||||||
|
|
||||||
|
if (waitSeconds == 0) {
|
||||||
|
if (last > 0 && deltaTime < realm.getQuickLoginCheckMilliSeconds()) {
|
||||||
|
logger.debugv("quick login, set min wait seconds");
|
||||||
|
waitSeconds = realm.getMinimumQuickLoginWaitSeconds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (waitSeconds > 0) {
|
||||||
|
waitSeconds = Math.min(realm.getMaxFailureWaitSeconds(), waitSeconds);
|
||||||
|
int notBefore = (int) (currentTime / 1000) + waitSeconds;
|
||||||
|
logger.debugv("set notBefore: {0}", notBefore);
|
||||||
|
userLoginFailure.setFailedLoginNotBefore(notBefore);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +216,8 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
|
||||||
for (LoginEvent event : events) {
|
for (LoginEvent event : events) {
|
||||||
if (event instanceof FailedLogin) {
|
if (event instanceof FailedLogin) {
|
||||||
failure(session, event);
|
failure(session, event);
|
||||||
|
} else if (event instanceof SuccessfulLogin) {
|
||||||
|
success(session, event);
|
||||||
} else if (event instanceof ShutdownEvent) {
|
} else if (event instanceof ShutdownEvent) {
|
||||||
run = false;
|
run = false;
|
||||||
}
|
}
|
||||||
|
@ -197,6 +230,8 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
|
||||||
for (LoginEvent event : events) {
|
for (LoginEvent event : events) {
|
||||||
if (event instanceof FailedLogin) {
|
if (event instanceof FailedLogin) {
|
||||||
((FailedLogin) event).latch.countDown();
|
((FailedLogin) event).latch.countDown();
|
||||||
|
} else if (event instanceof SuccessfulLogin) {
|
||||||
|
((SuccessfulLogin) event).latch.countDown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
events.clear();
|
events.clear();
|
||||||
|
@ -214,6 +249,17 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void success(KeycloakSession session, LoginEvent event) {
|
||||||
|
String userId = event.userId;
|
||||||
|
UserModel model = session.users().getUserById(userId, getRealmModel(session, event));
|
||||||
|
|
||||||
|
UserLoginFailureModel user = getUserModel(session, event);
|
||||||
|
if(user == null) return;
|
||||||
|
|
||||||
|
logger.debugv("user {0} successfully logged in, clearing all failures", model.getUsername());
|
||||||
|
user.clearFailures();
|
||||||
|
}
|
||||||
|
|
||||||
protected void logFailure(LoginEvent event) {
|
protected void logFailure(LoginEvent event) {
|
||||||
ServicesLogger.LOGGER.loginFailure(event.userId, event.ip);
|
ServicesLogger.LOGGER.loginFailure(event.userId, event.ip);
|
||||||
failures++;
|
failures++;
|
||||||
|
@ -243,6 +289,18 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
|
||||||
logger.trace("sent failure event");
|
logger.trace("sent failure event");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void successfulLogin(final RealmModel realm, final UserModel user, final ClientConnection clientConnection) {
|
||||||
|
try {
|
||||||
|
SuccessfulLogin event = new SuccessfulLogin(realm.getId(), user.getId(), clientConnection.getRemoteAddr());
|
||||||
|
queue.offer(event);
|
||||||
|
|
||||||
|
event.latch.await(5, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
}
|
||||||
|
logger.trace("sent success event");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isTemporarilyDisabled(KeycloakSession session, RealmModel realm, UserModel user) {
|
public boolean isTemporarilyDisabled(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
UserLoginFailureModel failure = session.sessions().getUserLoginFailure(realm, user.getId());
|
UserLoginFailureModel failure = session.sessions().getUserLoginFailure(realm, user.getId());
|
||||||
|
|
|
@ -213,6 +213,7 @@ public class RealmManager {
|
||||||
|
|
||||||
// brute force
|
// brute force
|
||||||
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.setMaxFailureWaitSeconds(900);
|
realm.setMaxFailureWaitSeconds(900);
|
||||||
realm.setMinimumQuickLoginWaitSeconds(60);
|
realm.setMinimumQuickLoginWaitSeconds(60);
|
||||||
realm.setWaitIncrementSeconds(60);
|
realm.setWaitIncrementSeconds(60);
|
||||||
|
|
|
@ -325,6 +325,54 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest {
|
||||||
loginSuccess();
|
loginSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPermanentLockout() throws Exception {
|
||||||
|
RealmRepresentation realm = testRealm().toRepresentation();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// arrange
|
||||||
|
realm.setPermanentLockout(true);
|
||||||
|
testRealm().update(realm);
|
||||||
|
|
||||||
|
// act
|
||||||
|
loginInvalidPassword();
|
||||||
|
loginInvalidPassword();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPermanentlyDisabled();
|
||||||
|
Assert.assertFalse(adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0).isEnabled());
|
||||||
|
} finally {
|
||||||
|
realm.setPermanentLockout(false);
|
||||||
|
testRealm().update(realm);
|
||||||
|
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0);
|
||||||
|
user.setEnabled(true);
|
||||||
|
updateUser(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResetLoginFailureCount() throws Exception {
|
||||||
|
RealmRepresentation realm = testRealm().toRepresentation();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// arrange
|
||||||
|
realm.setPermanentLockout(true);
|
||||||
|
testRealm().update(realm);
|
||||||
|
|
||||||
|
// act
|
||||||
|
loginInvalidPassword();
|
||||||
|
loginSuccess();
|
||||||
|
loginInvalidPassword();
|
||||||
|
loginSuccess();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.assertTrue(adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0).isEnabled());
|
||||||
|
} finally {
|
||||||
|
realm.setPermanentLockout(false);
|
||||||
|
testRealm().update(realm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNonExistingAccounts() throws Exception {
|
public void testNonExistingAccounts() throws Exception {
|
||||||
|
|
||||||
|
@ -358,6 +406,27 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest {
|
||||||
event.assertEvent();
|
event.assertEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void expectPermanentlyDisabled() throws Exception {
|
||||||
|
expectPermanentlyDisabled("test-user@localhost", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void expectPermanentlyDisabled(String username, String userId) throws Exception {
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login(username, "password");
|
||||||
|
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
Assert.assertEquals("Account is disabled, contact admin.", loginPage.getError());
|
||||||
|
ExpectedEvent event = events.expectLogin()
|
||||||
|
.session((String) null)
|
||||||
|
.error(Errors.USER_DISABLED)
|
||||||
|
.detail(Details.USERNAME, username)
|
||||||
|
.removeDetail(Details.CONSENT);
|
||||||
|
if (userId != null) {
|
||||||
|
event.user(userId);
|
||||||
|
}
|
||||||
|
event.assertEvent();
|
||||||
|
}
|
||||||
|
|
||||||
public void loginSuccess() throws Exception {
|
public void loginSuccess() throws Exception {
|
||||||
loginSuccess("test-user@localhost");
|
loginSuccess("test-user@localhost");
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,6 +126,8 @@ robots-tag=X-Robots-Tag
|
||||||
robots-tag-tooltip=Prevent pages from appearing in search engines (click label for more information)
|
robots-tag-tooltip=Prevent pages from appearing in search engines (click label for more information)
|
||||||
x-xss-protection=X-XSS-Protection
|
x-xss-protection=X-XSS-Protection
|
||||||
x-xss-protection-tooltip=This header configures the Cross-site scripting (XSS) filter in your browser. Using the default behavior, the browser will prevent rendering of the page when a XSS attack is detected (click label for more information)
|
x-xss-protection-tooltip=This header configures the Cross-site scripting (XSS) filter in your browser. Using the default behavior, the browser will prevent rendering of the page when a XSS attack is detected (click label for more information)
|
||||||
|
permanent-lockout=Permanent Lockout
|
||||||
|
permanent-lockout.tooltip=Lock the user permanently when the user exceeds the maximum login failures.
|
||||||
max-login-failures=Max Login Failures
|
max-login-failures=Max Login Failures
|
||||||
max-login-failures.tooltip=How many failures before wait is triggered.
|
max-login-failures.tooltip=How many failures before wait is triggered.
|
||||||
wait-increment=Wait Increment
|
wait-increment=Wait Increment
|
||||||
|
|
|
@ -14,6 +14,14 @@
|
||||||
<input ng-model="realm.bruteForceProtected" name="bruteForceProtected" id="bruteForceProtected" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
<input ng-model="realm.bruteForceProtected" name="bruteForceProtected" id="bruteForceProtected" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" data-ng-show="realm.bruteForceProtected">
|
||||||
|
<label class="col-md-2 control-label" for="permanentLockout">{{:: 'permanent-lockout' | translate}}</label>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input ng-model="realm.permanentLockout" name="permanentLockout" id="permanentLockout" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'permanent-lockout.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group" data-ng-show="realm.bruteForceProtected">
|
<div class="form-group" data-ng-show="realm.bruteForceProtected">
|
||||||
<label class="col-md-2 control-label" for="failureFactor">{{:: 'max-login-failures' | translate}}</label>
|
<label class="col-md-2 control-label" for="failureFactor">{{:: 'max-login-failures' | translate}}</label>
|
||||||
|
|
||||||
|
@ -23,7 +31,7 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'max-login-failures.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'max-login-failures.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" data-ng-show="realm.bruteForceProtected">
|
<div class="form-group" data-ng-show="realm.bruteForceProtected && !realm.permanentLockout">
|
||||||
<label class="col-md-2 control-label" for="waitIncrement">{{:: 'wait-increment' | translate}}</label>
|
<label class="col-md-2 control-label" for="waitIncrement">{{:: 'wait-increment' | translate}}</label>
|
||||||
<div class="col-md-6 time-selector">
|
<div class="col-md-6 time-selector">
|
||||||
<input class="form-control" type="number" required min="1"
|
<input class="form-control" type="number" required min="1"
|
||||||
|
@ -62,7 +70,7 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'min-quick-login-wait.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'min-quick-login-wait.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" data-ng-show="realm.bruteForceProtected">
|
<div class="form-group" data-ng-show="realm.bruteForceProtected && !realm.permanentLockout">
|
||||||
<label class="col-md-2 control-label" for="maxFailureWait">{{:: 'max-wait' | translate}}</label>
|
<label class="col-md-2 control-label" for="maxFailureWait">{{:: 'max-wait' | translate}}</label>
|
||||||
<div class="col-md-6 time-selector">
|
<div class="col-md-6 time-selector">
|
||||||
<input class="form-control" type="number" required min="1"
|
<input class="form-control" type="number" required min="1"
|
||||||
|
@ -77,7 +85,7 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'max-wait.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'max-wait.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" data-ng-show="realm.bruteForceProtected">
|
<div class="form-group" data-ng-show="realm.bruteForceProtected && !realm.permanentLockout">
|
||||||
<label class="col-md-2 control-label" for="maxDeltaTime">{{:: 'failure-reset-time' | translate}}</label>
|
<label class="col-md-2 control-label" for="maxDeltaTime">{{:: 'failure-reset-time' | translate}}</label>
|
||||||
<div class="col-md-6 time-selector">
|
<div class="col-md-6 time-selector">
|
||||||
<input class="form-control" type="number" required min="1"
|
<input class="form-control" type="number" required min="1"
|
||||||
|
|
Loading…
Reference in a new issue