Merge pull request #2122 from abstractj/KEYCLOAK-2151
Brute force detector active for non-existing accounts
This commit is contained in:
commit
39f12549a3
3 changed files with 79 additions and 34 deletions
|
@ -21,6 +21,7 @@ import org.keycloak.common.ClientConnection;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UsernameLoginFailureModel;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
|
||||
|
@ -91,44 +92,49 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
|
|||
logger.debug("failure");
|
||||
RealmModel realm = getRealmModel(session, event);
|
||||
logFailure(event);
|
||||
UsernameLoginFailureModel user = getUserModel(session, event);
|
||||
if (user == null) {
|
||||
user = session.sessions().addUserLoginFailure(realm, event.username.toLowerCase());
|
||||
}
|
||||
user.setLastIPFailure(event.ip);
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long last = user.getLastFailure();
|
||||
long deltaTime = 0;
|
||||
if (last > 0) {
|
||||
deltaTime = currentTime - last;
|
||||
}
|
||||
user.setLastFailure(currentTime);
|
||||
if (deltaTime > 0) {
|
||||
// if last failure was more than MAX_DELTA clear failures
|
||||
if (deltaTime > (long)realm.getMaxDeltaTimeSeconds() *1000L) {
|
||||
user.clearFailures();
|
||||
UserModel user = session.users().getUserByUsername(event.username.toString(), realm);
|
||||
UsernameLoginFailureModel userLoginFailure = getUserModel(session, event);
|
||||
if (user != null) {
|
||||
if (userLoginFailure == null) {
|
||||
userLoginFailure = session.sessions().addUserLoginFailure(realm, event.username.toLowerCase());
|
||||
}
|
||||
}
|
||||
user.incrementFailures();
|
||||
logger.debugv("new num failures: {0}" , user.getNumFailures());
|
||||
userLoginFailure.setLastIPFailure(event.ip);
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long last = userLoginFailure.getLastFailure();
|
||||
long deltaTime = 0;
|
||||
if (last > 0) {
|
||||
deltaTime = currentTime - last;
|
||||
}
|
||||
userLoginFailure.setLastFailure(currentTime);
|
||||
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() * (user.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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (waitSeconds > 0) {
|
||||
waitSeconds = Math.min(realm.getMaxFailureWaitSeconds(), waitSeconds);
|
||||
int notBefore = (int) (currentTime / 1000) + waitSeconds;
|
||||
logger.debugv("set notBefore: {0}", notBefore);
|
||||
user.setFailedLoginNotBefore(notBefore);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected UsernameLoginFailureModel getUserModel(KeycloakSession session, LoginEvent event) {
|
||||
RealmModel realm = getRealmModel(session, event);
|
||||
if (realm == null) return null;
|
||||
|
|
|
@ -41,6 +41,7 @@ import org.keycloak.testsuite.pages.AppPage;
|
|||
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.pages.LoginTotpPage;
|
||||
import org.keycloak.testsuite.pages.RegisterPage;
|
||||
import org.keycloak.testsuite.rule.GreenMailRule;
|
||||
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||
import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup;
|
||||
|
@ -101,13 +102,15 @@ public class BruteForceTest {
|
|||
@WebResource
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@WebResource
|
||||
private RegisterPage registerPage;
|
||||
|
||||
@WebResource
|
||||
protected LoginTotpPage loginTotpPage;
|
||||
|
||||
@WebResource
|
||||
protected OAuthClient oauth;
|
||||
|
||||
|
||||
private TimeBasedOTP totp = new TimeBasedOTP();
|
||||
|
||||
private int lifespan;
|
||||
|
@ -340,6 +343,17 @@ public class BruteForceTest {
|
|||
loginSuccess();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonExistingAccounts() throws Exception {
|
||||
|
||||
loginInvalidPassword("non-existent-user");
|
||||
loginInvalidPassword("non-existent-user");
|
||||
loginInvalidPassword("non-existent-user");
|
||||
|
||||
registerUser("non-existent-user");
|
||||
|
||||
}
|
||||
|
||||
public void expectTemporarilyDisabled() throws Exception {
|
||||
expectTemporarilyDisabled("test-user@localhost");
|
||||
}
|
||||
|
@ -430,4 +444,16 @@ public class BruteForceTest {
|
|||
events.clear();
|
||||
}
|
||||
|
||||
public void registerUser(String username){
|
||||
loginPage.open();
|
||||
loginPage.clickRegister();
|
||||
registerPage.assertCurrent();
|
||||
|
||||
registerPage.register("user", "name", username + "@localhost", username, "password", "password");
|
||||
|
||||
Assert.assertNull(registerPage.getInstruction());
|
||||
|
||||
events.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -57,6 +57,10 @@ public class RegisterPage extends AbstractPage {
|
|||
@FindBy(className = "alert-error")
|
||||
private WebElement loginErrorMessage;
|
||||
|
||||
@FindBy(className = "instruction")
|
||||
private WebElement loginInstructionMessage;
|
||||
|
||||
|
||||
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm) {
|
||||
firstNameInput.clear();
|
||||
if (firstName != null) {
|
||||
|
@ -131,6 +135,15 @@ public class RegisterPage extends AbstractPage {
|
|||
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
|
||||
}
|
||||
|
||||
public String getInstruction() {
|
||||
try {
|
||||
return loginInstructionMessage != null ? loginInstructionMessage.getText() : null;
|
||||
} catch (NoSuchElementException e){
|
||||
// OK
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstNameInput.getAttribute("value");
|
||||
}
|
||||
|
@ -164,4 +177,4 @@ public class RegisterPage extends AbstractPage {
|
|||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue