diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js
index 4802f1fcd1..14b58b51ab 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js
@@ -639,6 +639,15 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'RealmRevocationCtrl'
})
+ .when('/realms/:realm/sessions/brute-force', {
+ templateUrl : 'partials/session-brute-force.html',
+ resolve : {
+ realm : function(RealmLoader) {
+ return RealmLoader();
+ }
+ },
+ controller : 'RealmBruteForceCtrl'
+ })
.when('/realms/:realm/sessions/realm', {
templateUrl : 'partials/session-realm.html',
resolve : {
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js
index 9bbea17fc8..04be1273b1 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js
@@ -1118,3 +1118,33 @@ module.controller('RealmAuditEventsCtrl', function($scope, RealmAuditEvents, rea
$scope.update();
});
+
+module.controller('RealmBruteForceCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications) {
+ console.log('RealmBruteForceCtrl');
+
+ $scope.realm = realm;
+
+ var oldCopy = angular.copy($scope.realm);
+ $scope.changed = false;
+
+ $scope.$watch('realm', function() {
+ if (!angular.equals($scope.realm, oldCopy)) {
+ $scope.changed = true;
+ }
+ }, true);
+
+ $scope.save = function() {
+ var realmCopy = angular.copy($scope.realm);
+ $scope.changed = false;
+ Realm.update(realmCopy, function () {
+ $location.url("/realms/" + realm.realm + "/sessions/brute-force");
+ Notifications.success("Your changes have been saved to the realm.");
+ });
+ };
+
+ $scope.reset = function() {
+ $scope.realm = angular.copy(oldCopy);
+ $scope.changed = false;
+ };
+});
+
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-tokens.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-tokens.html
index 2e8d158f7e..ba455e4e01 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-tokens.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-tokens.html
@@ -4,6 +4,7 @@
Realm Sessions
Token Settings
Revocation
+ Brute Force
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-brute-force.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-brute-force.html
new file mode 100755
index 0000000000..ae4571284f
--- /dev/null
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-brute-force.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+ - {{realm.realm}}
+ - Brute Force
+
+
{{realm.realm}} Brute Force Protection Settings
+
+
+
\ No newline at end of file
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-realm.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-realm.html
index addf466dbf..0d0ba49afa 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-realm.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-realm.html
@@ -4,6 +4,7 @@
- Realm Sessions
- Token Settings
- Revocation
+ - Brute Force
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-revocation.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-revocation.html
index c1d451d699..285903c324 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-revocation.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/session-revocation.html
@@ -4,6 +4,7 @@
- Realm Sessions
- Token Settings
- Revocation
+ - Brute Force
diff --git a/audit/api/src/main/java/org/keycloak/audit/Errors.java b/audit/api/src/main/java/org/keycloak/audit/Errors.java
old mode 100644
new mode 100755
index b34371419f..1ab7d4aa73
--- a/audit/api/src/main/java/org/keycloak/audit/Errors.java
+++ b/audit/api/src/main/java/org/keycloak/audit/Errors.java
@@ -13,6 +13,7 @@ public interface Errors {
String USER_NOT_FOUND = "user_not_found";
String USER_DISABLED = "user_disabled";
+ String USER_TEMPORARILY_DISABLED = "user_temporarily_disabled";
String INVALID_USER_CREDENTIALS = "invalid_user_credentials";
String USERNAME_MISSING = "username_missing";
diff --git a/bundled-war-example/src/main/resources/META-INF/persistence.xml b/bundled-war-example/src/main/resources/META-INF/persistence.xml
index b9dbe7bec8..3eeed1f7f8 100755
--- a/bundled-war-example/src/main/resources/META-INF/persistence.xml
+++ b/bundled-war-example/src/main/resources/META-INF/persistence.xml
@@ -15,6 +15,7 @@
org.keycloak.models.jpa.entities.SocialLinkEntity
org.keycloak.models.jpa.entities.AuthenticationLinkEntity
org.keycloak.models.jpa.entities.UserEntity
+ org.keycloak.models.jpa.entities.UsernameLoginFailureEntity
org.keycloak.models.jpa.entities.UserRoleMappingEntity
org.keycloak.models.jpa.entities.ScopeMappingEntity
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index ac57c7ea1c..0228a8ba7f 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -28,6 +28,7 @@ public class RealmRepresentation {
protected Boolean resetPasswordAllowed;
protected Boolean social;
protected Boolean updateProfileOnInitialSocialLogin;
+ protected Boolean bruteForceProtected;
protected String privateKey;
protected String publicKey;
protected RolesRepresentation roles;
@@ -375,4 +376,12 @@ public class RealmRepresentation {
public void setNotBefore(Integer notBefore) {
this.notBefore = notBefore;
}
+
+ public Boolean isBruteForceProtected() {
+ return bruteForceProtected;
+ }
+
+ public void setBruteForceProtected(Boolean bruteForceProtected) {
+ this.bruteForceProtected = bruteForceProtected;
+ }
}
diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json
index 81af756756..2e35b1218d 100755
--- a/examples/demo-template/testrealm.json
+++ b/examples/demo-template/testrealm.json
@@ -7,6 +7,7 @@
"sslNotRequired": true,
"registrationAllowed": false,
"social": false,
+ "bruteForceProtected": true,
"updateProfileOnInitialSocialLogin": false,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
diff --git a/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties
old mode 100644
new mode 100755
index b68a25ebfc..1945c64508
--- a/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties
+++ b/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties
@@ -33,4 +33,5 @@ socialLinkNotActive=This social link is not active anymore
socialRedirectError=Failed to redirect to social provider
socialProviderRemoved=Social provider removed successfully
-accountDisabled=Account is disabled, contact admin
\ No newline at end of file
+accountDisabled=Account is disabled, contact admin\
+accountTemporarilyDisabled=Account is temporarily disabled, contact admin or try again later
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties
index 9aa907ee23..5bac22e986 100755
--- a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties
+++ b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties
@@ -30,6 +30,7 @@ clientCertificate=Client Certificate
invalidUser=Invalid username or password.
invalidPassword=Invalid username or password.
accountDisabled=Account is disabled, contact admin
+accountTemporarilyDisabled=Account is temporarily disabled, contact admin or try again later
missingFirstName=Please specify first name
missingLastName=Please specify last name
diff --git a/model/jpa/src/test/resources/META-INF/persistence.xml b/model/jpa/src/test/resources/META-INF/persistence.xml
index e84acc8849..558aa89032 100755
--- a/model/jpa/src/test/resources/META-INF/persistence.xml
+++ b/model/jpa/src/test/resources/META-INF/persistence.xml
@@ -17,6 +17,7 @@
org.keycloak.models.jpa.entities.AuthenticationLinkEntity
org.keycloak.models.jpa.entities.UserEntity
org.keycloak.models.jpa.entities.UserRoleMappingEntity
+ org.keycloak.models.jpa.entities.UsernameLoginFailureEntity
org.keycloak.models.jpa.entities.ScopeMappingEntity
true
diff --git a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java
index 58bb2947e7..5d28b60282 100755
--- a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java
+++ b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java
@@ -45,7 +45,10 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest {
// Create 2 realms and user in realm1
realm1 = realmManager.createRealm("realm1");
+ realm1.setBruteForceProtected(false);
realm2 = realmManager.createRealm("realm2");
+ realm2.setBruteForceProtected(false);
+
realm1.addRequiredCredential(CredentialRepresentation.PASSWORD);
realm2.addRequiredCredential(CredentialRepresentation.PASSWORD);
realm1.setAuthenticationProviders(Arrays.asList(AuthenticationProviderModel.DEFAULT_PROVIDER));
diff --git a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java
index dae3862317..5498f78c93 100755
--- a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java
+++ b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java
@@ -48,6 +48,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
// Create realm and configure ldap
realm = realmManager.createRealm("realm");
+ realm.setBruteForceProtected(false);
realm.addRequiredCredential(CredentialRepresentation.PASSWORD);
this.embeddedServer.setupLdapInRealm(realm);
diff --git a/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java
index b6f841358d..880ce1462c 100755
--- a/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java
+++ b/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java
@@ -1,6 +1,7 @@
package org.keycloak.model.test;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
+import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@@ -11,8 +12,10 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.ClientConnection;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
+import org.keycloak.services.managers.BruteForceProtector;
import javax.ws.rs.core.MultivaluedMap;
@@ -26,10 +29,29 @@ public class AuthenticationManagerTest extends AbstractModelTest {
private TimeBasedOTP otp;
private RealmModel realm;
private UserModel user;
+ private BruteForceProtector protector;
+ private ClientConnection dummyConnection = new ClientConnection() {
+ @Override
+ public String getRemoteAddr() {
+ return "127.0.0.1";
+
+ }
+
+ @Override
+ public String getRemoteHost() {
+ return "localhost";
+
+ }
+
+ @Override
+ public int getReportPort() {
+ return 8080;
+ }
+ };
@Test
public void authForm() {
- AuthenticationStatus status = am.authenticateForm(null, realm, formData);
+ AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
Assert.assertEquals(AuthenticationStatus.SUCCESS, status);
}
@@ -38,7 +60,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
formData.remove(CredentialRepresentation.PASSWORD);
formData.add(CredentialRepresentation.PASSWORD, "invalid");
- AuthenticationStatus status = am.authenticateForm(null, realm, formData);
+ AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
}
@@ -46,7 +68,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
public void authFormMissingUsername() {
formData.remove("username");
- AuthenticationStatus status = am.authenticateForm(null, realm, formData);
+ AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
Assert.assertEquals(AuthenticationStatus.INVALID_USER, status);
}
@@ -54,7 +76,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
public void authFormMissingPassword() {
formData.remove(CredentialRepresentation.PASSWORD);
- AuthenticationStatus status = am.authenticateForm(null, realm, formData);
+ AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
Assert.assertEquals(AuthenticationStatus.MISSING_PASSWORD, status);
}
@@ -62,8 +84,8 @@ public class AuthenticationManagerTest extends AbstractModelTest {
public void authFormRequiredAction() {
realm.addRequiredCredential(CredentialRepresentation.TOTP);
user.addRequiredAction(RequiredAction.CONFIGURE_TOTP);
-
- AuthenticationStatus status = am.authenticateForm(null, realm, formData);
+
+ AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
Assert.assertEquals(AuthenticationStatus.ACTIONS_REQUIRED, status);
}
@@ -71,7 +93,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
public void authFormUserDisabled() {
user.setEnabled(false);
- AuthenticationStatus status = am.authenticateForm(null, realm, formData);
+ AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
Assert.assertEquals(AuthenticationStatus.ACCOUNT_DISABLED, status);
}
@@ -93,7 +115,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
formData.add(CredentialRepresentation.TOTP, token);
- AuthenticationStatus status = am.authenticateForm(null, realm, formData);
+ AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
Assert.assertEquals(AuthenticationStatus.SUCCESS, status);
}
@@ -104,7 +126,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
formData.remove(CredentialRepresentation.PASSWORD);
formData.add(CredentialRepresentation.PASSWORD, "invalid");
- AuthenticationStatus status = am.authenticateForm(null, realm, formData);
+ AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
}
@@ -115,7 +137,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
formData.remove(CredentialRepresentation.TOTP);
formData.add(CredentialRepresentation.TOTP, "invalid");
- AuthenticationStatus status = am.authenticateForm(null, realm, formData);
+ AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
}
@@ -125,7 +147,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
formData.remove(CredentialRepresentation.TOTP);
- AuthenticationStatus status = am.authenticateForm(null, realm, formData);
+ AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
Assert.assertEquals(AuthenticationStatus.MISSING_TOTP, status);
}
@@ -142,8 +164,9 @@ public class AuthenticationManagerTest extends AbstractModelTest {
realm.setAccessTokenLifespan(1000);
realm.addRequiredCredential(CredentialRepresentation.PASSWORD);
realm.setAuthenticationProviders(Arrays.asList(AuthenticationProviderModel.DEFAULT_PROVIDER));
-
- am = new AuthenticationManager(providerSession);
+ protector = new BruteForceProtector(factory);
+ protector.start();
+ am = new AuthenticationManager(providerSession, protector);
user = realm.addUser("test");
user.setEnabled(true);
@@ -161,4 +184,12 @@ public class AuthenticationManagerTest extends AbstractModelTest {
otp = new TimeBasedOTP();
}
+ @After
+ public void after() throws Exception {
+ protector.shutdown();
+ super.after();
+
+
+ }
+
}
diff --git a/server/src/main/resources/META-INF/persistence.xml b/server/src/main/resources/META-INF/persistence.xml
index b9dbe7bec8..3eeed1f7f8 100755
--- a/server/src/main/resources/META-INF/persistence.xml
+++ b/server/src/main/resources/META-INF/persistence.xml
@@ -15,6 +15,7 @@
org.keycloak.models.jpa.entities.SocialLinkEntity
org.keycloak.models.jpa.entities.AuthenticationLinkEntity
org.keycloak.models.jpa.entities.UserEntity
+ org.keycloak.models.jpa.entities.UsernameLoginFailureEntity
org.keycloak.models.jpa.entities.UserRoleMappingEntity
org.keycloak.models.jpa.entities.ScopeMappingEntity
diff --git a/services/src/main/java/org/keycloak/services/listeners/KeycloakSessionDestroyListener.java b/services/src/main/java/org/keycloak/services/listeners/KeycloakSessionDestroyListener.java
old mode 100644
new mode 100755
index 7825c04027..0c74c9d733
--- a/services/src/main/java/org/keycloak/services/listeners/KeycloakSessionDestroyListener.java
+++ b/services/src/main/java/org/keycloak/services/listeners/KeycloakSessionDestroyListener.java
@@ -2,6 +2,7 @@ package org.keycloak.services.listeners;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderSessionFactory;
+import org.keycloak.services.managers.BruteForceProtector;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
@@ -17,6 +18,10 @@ public class KeycloakSessionDestroyListener implements ServletContextListener {
@Override
public void contextDestroyed(ServletContextEvent sce) {
+ BruteForceProtector protector = (BruteForceProtector) sce.getServletContext().getAttribute(BruteForceProtector.class.getName());
+ if (protector != null) {
+ protector.shutdown();
+ }
ProviderSessionFactory providerSessionFactory = (ProviderSessionFactory) sce.getServletContext().getAttribute(ProviderSessionFactory.class.getName());
KeycloakSessionFactory kcSessionFactory = (KeycloakSessionFactory) sce.getServletContext().getAttribute(KeycloakSessionFactory.class.getName());
if (providerSessionFactory != null) {
@@ -25,6 +30,7 @@ public class KeycloakSessionDestroyListener implements ServletContextListener {
if (kcSessionFactory != null) {
kcSessionFactory.close();
}
+
}
}
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 3be892c040..f276f8f999 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -50,7 +50,8 @@ public class AuthenticationManager {
this.providerSession = providerSession;
}
- public AuthenticationManager(BruteForceProtector protector) {
+ public AuthenticationManager(ProviderSession providerSession, BruteForceProtector protector) {
+ this.providerSession = providerSession;
this.protector = protector;
}
@@ -199,6 +200,12 @@ public class AuthenticationManager {
return AuthenticationStatus.INVALID_USER;
}
+ if (realm.isBruteForceProtected()) {
+ if (protector.isTemporarilyDisabled(realm, username)) {
+ return AuthenticationStatus.ACCOUNT_TEMPORARILY_DISABLED;
+ }
+ }
+
AuthenticationStatus status = authenticateInternal(realm, formData, username);
if (realm.isBruteForceProtected()) {
switch (status) {
@@ -313,7 +320,7 @@ public class AuthenticationManager {
}
public enum AuthenticationStatus {
- SUCCESS, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED
+ SUCCESS, ACCOUNT_TEMPORARILY_DISABLED, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED
}
}
diff --git a/services/src/main/java/org/keycloak/services/managers/BruteForceProtector.java b/services/src/main/java/org/keycloak/services/managers/BruteForceProtector.java
index c8190f49a0..8692bf183b 100755
--- a/services/src/main/java/org/keycloak/services/managers/BruteForceProtector.java
+++ b/services/src/main/java/org/keycloak/services/managers/BruteForceProtector.java
@@ -5,7 +5,6 @@ import org.jboss.resteasy.logging.Logger;
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.ClientConnection;
@@ -28,8 +27,8 @@ public class BruteForceProtector implements Runnable {
protected int minimumQuickLoginWaitSeconds = 60;
protected int waitIncrementSeconds = 60;
protected long quickLoginCheckMilliSeconds = 1000;
- protected int maxDeltaTime = 60 * 60 * 24 * 1000;
- protected int failureFactor = 10;
+ protected long maxDeltaTimeMilliSeconds = 60 * 60 * 12 * 1000; // 12 hours
+ protected int failureFactor = 30;
protected volatile boolean run = true;
protected KeycloakSessionFactory factory;
protected CountDownLatch shutdownLatch = new CountDownLatch(1);
@@ -65,6 +64,12 @@ public class BruteForceProtector implements Runnable {
}
}
+ protected class ShutdownEvent extends LoginEvent {
+ public ShutdownEvent() {
+ super(null, null, null);
+ }
+ }
+
protected class FailedLogin extends LoginEvent {
protected final CountDownLatch latch = new CountDownLatch(1);
@@ -90,7 +95,7 @@ public class BruteForceProtector implements Runnable {
user.setLastFailure(currentTime);
if (deltaTime > 0) {
// if last failure was more than MAX_DELTA clear failures
- if (deltaTime > maxDeltaTime) {
+ if (deltaTime > maxDeltaTimeMilliSeconds) {
user.clearFailures();
}
}
@@ -109,20 +114,27 @@ public class BruteForceProtector implements Runnable {
}
protected UsernameLoginFailureModel getUserModel(KeycloakSession session, LoginEvent event) {
- RealmModel realm = session.getRealm(event.realmId);
+ RealmModel realm = getRealmModel(session, event);
if (realm == null) return null;
UsernameLoginFailureModel user = realm.getUserLoginFailure(event.username);
if (user == null) return null;
return user;
}
+ protected RealmModel getRealmModel(KeycloakSession session, LoginEvent event) {
+ RealmModel realm = session.getRealm(event.realmId);
+ if (realm == null) return null;
+ return realm;
+ }
+
public void start() {
- new Thread(this).start();
+ new Thread(this, "Brute Force Protector").start();
}
public void shutdown() {
run = false;
try {
+ queue.offer(new ShutdownEvent());
shutdownLatch.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
@@ -144,7 +156,7 @@ public class BruteForceProtector implements Runnable {
for (LoginEvent event : events) {
if (event instanceof FailedLogin) {
logFailure(event);
- } else {
+ } else if (event instanceof SuccessfulLogin) {
logSuccess(event);
}
}
@@ -191,7 +203,7 @@ public class BruteForceProtector implements Runnable {
long delta = 0;
if (lastFailure > 0) {
delta = System.currentTimeMillis() - lastFailure;
- if (delta > maxDeltaTime) {
+ if (delta > maxDeltaTimeMilliSeconds) {
totalTime = 0;
} else {
@@ -221,4 +233,73 @@ public class BruteForceProtector implements Runnable {
} catch (InterruptedException e) {
}
}
+
+ public boolean isTemporarilyDisabled(RealmModel realm, String username) {
+ UsernameLoginFailureModel failure = realm.getUserLoginFailure(username);
+ if (failure == null) {
+ return false;
+ }
+
+ int currTime = (int)(System.currentTimeMillis()/1000);
+ if (currTime < failure.getFailedLoginNotBefore()) {
+ return true;
+ }
+ return false;
+ }
+
+ public long getFailures() {
+ return failures;
+ }
+
+ public long getLastFailure() {
+ return lastFailure;
+ }
+
+ public int getMaxFailureWaitSeconds() {
+ return maxFailureWaitSeconds;
+ }
+
+ public void setMaxFailureWaitSeconds(int maxFailureWaitSeconds) {
+ this.maxFailureWaitSeconds = maxFailureWaitSeconds;
+ }
+
+ public int getMinimumQuickLoginWaitSeconds() {
+ return minimumQuickLoginWaitSeconds;
+ }
+
+ public void setMinimumQuickLoginWaitSeconds(int minimumQuickLoginWaitSeconds) {
+ this.minimumQuickLoginWaitSeconds = minimumQuickLoginWaitSeconds;
+ }
+
+ public int getWaitIncrementSeconds() {
+ return waitIncrementSeconds;
+ }
+
+ public void setWaitIncrementSeconds(int waitIncrementSeconds) {
+ this.waitIncrementSeconds = waitIncrementSeconds;
+ }
+
+ public long getQuickLoginCheckMilliSeconds() {
+ return quickLoginCheckMilliSeconds;
+ }
+
+ public void setQuickLoginCheckMilliSeconds(long quickLoginCheckMilliSeconds) {
+ this.quickLoginCheckMilliSeconds = quickLoginCheckMilliSeconds;
+ }
+
+ public long getMaxDeltaTimeMilliSeconds() {
+ return maxDeltaTimeMilliSeconds;
+ }
+
+ public void setMaxDeltaTimeMilliSeconds(long maxDeltaTimeMilliSeconds) {
+ this.maxDeltaTimeMilliSeconds = maxDeltaTimeMilliSeconds;
+ }
+
+ public int getFailureFactor() {
+ return failureFactor;
+ }
+
+ public void setFailureFactor(int failureFactor) {
+ this.failureFactor = failureFactor;
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java b/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java
index 8562bd850f..24c09b2016 100755
--- a/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java
+++ b/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java
@@ -78,6 +78,7 @@ public class ModelToRepresentation {
rep.setPrivateKey(realm.getPrivateKeyPem());
rep.setRegistrationAllowed(realm.isRegistrationAllowed());
rep.setRememberMe(realm.isRememberMe());
+ rep.setBruteForceProtected(realm.isBruteForceProtected());
rep.setVerifyEmail(realm.isVerifyEmail());
rep.setResetPasswordAllowed(realm.isResetPasswordAllowed());
rep.setAccessTokenLifespan(realm.getAccessTokenLifespan());
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index 1e5bb84191..b710af6cc4 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -79,6 +79,7 @@ public class RealmManager {
if (id == null) id = KeycloakModelUtils.generateId();
RealmModel realm = identitySession.createRealm(id, name);
realm.setName(name);
+ realm.setBruteForceProtected(false); // default settings off for now todo set it on
setupAdminManagement(realm);
setupAccountManagement(realm);
@@ -121,6 +122,7 @@ public class RealmManager {
}
if (rep.isEnabled() != null) realm.setEnabled(rep.isEnabled());
if (rep.isSocial() != null) realm.setSocial(rep.isSocial());
+ if (rep.isBruteForceProtected() != null) realm.setBruteForceProtected(rep.isBruteForceProtected());
if (rep.isRegistrationAllowed() != null) realm.setRegistrationAllowed(rep.isRegistrationAllowed());
if (rep.isRememberMe() != null) realm.setRememberMe(rep.isRememberMe());
if (rep.isVerifyEmail() != null) realm.setVerifyEmail(rep.isVerifyEmail());
@@ -227,6 +229,7 @@ public class RealmManager {
newRealm.setName(rep.getRealm());
if (rep.isEnabled() != null) newRealm.setEnabled(rep.isEnabled());
if (rep.isSocial() != null) newRealm.setSocial(rep.isSocial());
+ if (rep.isBruteForceProtected() != null) newRealm.setBruteForceProtected(rep.isBruteForceProtected());
if (rep.getNotBefore() != null) newRealm.setNotBefore(rep.getNotBefore());
diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java
old mode 100644
new mode 100755
index e5a9e596c8..cc0ab1df60
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -27,6 +27,7 @@ package org.keycloak.services.messages;
public class Messages {
public static final String ACCOUNT_DISABLED = "accountDisabled";
+ public static final String ACCOUNT_TEMPORARILY_DISABLED = "accountTemporarilyDisabled";
public static final String INVALID_PASSWORD = "invalidPassword";
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index 7b8b3ddfba..7f49ff5ed4 100755
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -2,6 +2,7 @@ package org.keycloak.services.resources;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.logging.Logger;
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.SkeletonKeyContextResolver;
import org.keycloak.audit.AuditListener;
import org.keycloak.audit.AuditListenerFactory;
@@ -22,6 +23,7 @@ import org.keycloak.picketlink.IdentityManagerProvider;
import org.keycloak.picketlink.IdentityManagerProviderFactory;
import org.keycloak.provider.ProviderSessionFactory;
import org.keycloak.services.managers.ApplianceBootstrap;
+import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.SocialRequestManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.resources.admin.AdminService;
@@ -57,6 +59,11 @@ public class KeycloakApplication extends Application {
dispatcher.getDefaultContextObjects().put(KeycloakApplication.class, this);
this.contextPath = context.getContextPath();
this.factory = createSessionFactory();
+ BruteForceProtector protector = new BruteForceProtector(factory);
+ dispatcher.getDefaultContextObjects().put(BruteForceProtector.class, protector);
+ ResteasyProviderFactory.pushContext(BruteForceProtector.class, protector); // for injection
+ protector.start();
+ context.setAttribute(BruteForceProtector.class.getName(), protector);
this.providerSessionFactory = createProviderSessionFactory();
context.setAttribute(KeycloakSessionFactory.class.getName(), factory);
//classes.add(KeycloakSessionCleanupFilter.class);
diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index cea5959277..0b8126c76e 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -12,6 +12,7 @@ import org.keycloak.services.ClientConnection;
import org.keycloak.provider.ProviderSession;
import org.keycloak.services.managers.AuditManager;
import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.SocialRequestManager;
import org.keycloak.services.managers.TokenManager;
@@ -51,6 +52,9 @@ public class RealmsResource {
@Context
protected ClientConnection clientConnection;
+ @Context
+ protected BruteForceProtector protector;
+
protected TokenManager tokenManager;
protected SocialRequestManager socialRequestManager;
@@ -68,7 +72,7 @@ public class RealmsResource {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = locateRealm(name, realmManager);
Audit audit = new AuditManager(realm, providers, clientConnection).createAudit();
- AuthenticationManager authManager = new AuthenticationManager(providers);
+ AuthenticationManager authManager = new AuthenticationManager(providers, protector);
TokenService tokenService = new TokenService(realm, tokenManager, audit, authManager);
ResteasyProviderFactory.getInstance().injectProperties(tokenService);
//resourceContext.initResource(tokenService);
diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java
index c00b09cd05..ed223cea58 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -179,9 +179,20 @@ public class TokenService {
throw new UnauthorizedException("Disabled realm");
}
- if (authManager.authenticateForm(clientConnection, realm, form) != AuthenticationStatus.SUCCESS) {
- audit.error(Errors.INVALID_USER_CREDENTIALS);
- throw new UnauthorizedException("Auth failed");
+ AuthenticationStatus authenticationStatus = authManager.authenticateForm(clientConnection, realm, form);
+
+ switch (authenticationStatus) {
+ case SUCCESS:
+ break;
+ case ACCOUNT_TEMPORARILY_DISABLED:
+ case ACTIONS_REQUIRED:
+ audit.error(Errors.USER_TEMPORARILY_DISABLED);
+ return Response.status(503).type(MediaType.TEXT_PLAIN).entity("Account temporarily disabled").build();
+ case ACCOUNT_DISABLED:
+ return Response.status(403).type(MediaType.TEXT_PLAIN).entity("Account disabled").build();
+ default:
+ audit.error(Errors.INVALID_USER_CREDENTIALS);
+ throw new UnauthorizedException("Auth failed");
}
UserModel user = realm.getUser(form.getFirst(AuthenticationManager.FORM_USERNAME));
@@ -303,6 +314,9 @@ public class TokenService {
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username);
audit.user(user);
return oauth.processAccessCode(scopeParam, state, redirect, client, user, username, remember, "form", audit);
+ case ACCOUNT_TEMPORARILY_DISABLED:
+ audit.error(Errors.USER_TEMPORARILY_DISABLED);
+ return Flows.forms(realm, uriInfo).setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).setFormData(formData).createLogin();
case ACCOUNT_DISABLED:
audit.error(Errors.USER_DISABLED);
return Flows.forms(realm, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin();
diff --git a/testsuite/integration/src/main/resources/META-INF/persistence.xml b/testsuite/integration/src/main/resources/META-INF/persistence.xml
index 56a5ed4517..1d2f21a522 100755
--- a/testsuite/integration/src/main/resources/META-INF/persistence.xml
+++ b/testsuite/integration/src/main/resources/META-INF/persistence.xml
@@ -16,6 +16,7 @@
org.keycloak.models.jpa.entities.SocialLinkEntity
org.keycloak.models.jpa.entities.AuthenticationLinkEntity
org.keycloak.models.jpa.entities.UserEntity
+ org.keycloak.models.jpa.entities.UsernameLoginFailureEntity
org.keycloak.models.jpa.entities.UserRoleMappingEntity
org.keycloak.models.jpa.entities.ScopeMappingEntity
diff --git a/testsuite/integration/src/test/resources/testcomposite.json b/testsuite/integration/src/test/resources/testcomposite.json
index 61038ea65b..dd4e378a43 100755
--- a/testsuite/integration/src/test/resources/testcomposite.json
+++ b/testsuite/integration/src/test/resources/testcomposite.json
@@ -6,6 +6,7 @@
"accessCodeLifespan": 600,
"accessCodeLifespanUserAction": 600,
"sslNotRequired": true,
+ "bruteForceProtected": true,
"registrationAllowed": true,
"resetPasswordAllowed": true,
"requiredCredentials": [ "password" ],
diff --git a/testsuite/integration/src/test/resources/testrealm.json b/testsuite/integration/src/test/resources/testrealm.json
index a7cf0d49e1..d3ea49f5a4 100755
--- a/testsuite/integration/src/test/resources/testrealm.json
+++ b/testsuite/integration/src/test/resources/testrealm.json
@@ -5,6 +5,7 @@
"sslNotRequired": true,
"registrationAllowed": true,
"resetPasswordAllowed": true,
+ "bruteForceProtected": true,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],
diff --git a/testsuite/performance/src/test/resources/META-INF/persistence.xml b/testsuite/performance/src/test/resources/META-INF/persistence.xml
index 5ca96af932..29af0389b9 100755
--- a/testsuite/performance/src/test/resources/META-INF/persistence.xml
+++ b/testsuite/performance/src/test/resources/META-INF/persistence.xml
@@ -16,6 +16,7 @@
org.keycloak.models.jpa.entities.SocialLinkEntity
org.keycloak.models.jpa.entities.AuthenticationLinkEntity
org.keycloak.models.jpa.entities.UserEntity
+ org.keycloak.models.jpa.entities.UsernameLoginFailureEntity
org.keycloak.models.jpa.entities.UserRoleMappingEntity
org.keycloak.models.jpa.entities.ScopeMappingEntity