more brute force detection
This commit is contained in:
parent
4c8471ef43
commit
2b8d2288fb
30 changed files with 279 additions and 28 deletions
|
@ -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 : {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<li><a href="#/realms/{{realm.realm}}/sessions/realm">Realm Sessions</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/token-settings">Token Settings</a></li>
|
||||
<li class="active"><a href="#/realms/{{realm.realm}}/sessions/revocation">Revocation</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/sessions/brute-force">Brute Force</a></li>
|
||||
</ul>
|
||||
<div id="content">
|
||||
<ol class="breadcrumb">
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<div class="bs-sidebar col-sm-3 " data-ng-include data-src="'partials/realm-menu.html'"></div>
|
||||
<div id="content-area" class="col-sm-9" role="main">
|
||||
<ul class="nav nav-tabs nav-tabs-pf" data-ng-show="!create">
|
||||
<li><a href="#/realms/{{realm.realm}}/sessions/realm">Realm Sessions</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/token-settings">Token Settings</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/sessions/revocation">Revocation</a></li>
|
||||
<li class="active"><a href="#/realms/{{realm.realm}}/sessions/brute-force">Brute Force</a></li>
|
||||
</ul>
|
||||
<div id="content">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}">{{realm.realm}}</a></li>
|
||||
<li class="active">Brute Force</li>
|
||||
</ol>
|
||||
<h2><span>{{realm.realm}}</span> Brute Force Protection Settings</h2>
|
||||
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
|
||||
<fieldset class="border-top">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label" for="bruteForceProtected">Enabled</label>
|
||||
<div class="col-sm-4">
|
||||
<input ng-model="realm.bruteForceProtected" name="bruteForceProtected" id="bruteForceProtected" onoffswitch />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="pull-right form-actions" data-ng-show="access.manageRealm">
|
||||
<button kc-reset data-ng-show="changed">Clear changes</button>
|
||||
<button kc-save data-ng-show="changed">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
|
@ -4,6 +4,7 @@
|
|||
<li class="active"><a href="#/realms/{{realm.realm}}/sessions/realm">Realm Sessions</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/token-settings">Token Settings</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/sessions/revocation">Revocation</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/sessions/brute-force">Brute Force</a></li>
|
||||
</ul>
|
||||
<div id="content">
|
||||
<ol class="breadcrumb">
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<li><a href="#/realms/{{realm.realm}}/sessions/realm">Realm Sessions</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/token-settings">Token Settings</a></li>
|
||||
<li class="active"><a href="#/realms/{{realm.realm}}/sessions/revocation">Revocation</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/sessions/brute-force">Brute Force</a></li>
|
||||
</ul>
|
||||
<div id="content">
|
||||
<ol class="breadcrumb">
|
||||
|
|
1
audit/api/src/main/java/org/keycloak/audit/Errors.java
Normal file → Executable file
1
audit/api/src/main/java/org/keycloak/audit/Errors.java
Normal file → Executable file
|
@ -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";
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
3
forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties
Normal file → Executable file
3
forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties
Normal file → Executable file
|
@ -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
|
||||
accountDisabled=Account is disabled, contact admin\
|
||||
accountTemporarilyDisabled=Account is temporarily disabled, contact admin or try again later
|
|
@ -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
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
|
||||
|
||||
<exclude-unlisted-classes>true</exclude-unlisted-classes>
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
|
||||
|
||||
|
|
6
services/src/main/java/org/keycloak/services/listeners/KeycloakSessionDestroyListener.java
Normal file → Executable file
6
services/src/main/java/org/keycloak/services/listeners/KeycloakSessionDestroyListener.java
Normal file → Executable file
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
1
services/src/main/java/org/keycloak/services/messages/Messages.java
Normal file → Executable file
1
services/src/main/java/org/keycloak/services/messages/Messages.java
Normal file → Executable file
|
@ -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";
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"accessCodeLifespan": 600,
|
||||
"accessCodeLifespanUserAction": 600,
|
||||
"sslNotRequired": true,
|
||||
"bruteForceProtected": true,
|
||||
"registrationAllowed": true,
|
||||
"resetPasswordAllowed": true,
|
||||
"requiredCredentials": [ "password" ],
|
||||
|
|
|
@ -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" ],
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
|
||||
|
||||
|
|
Loading…
Reference in a new issue