more brute force detection

This commit is contained in:
Bill Burke 2014-04-14 18:58:45 -04:00
parent 4c8471ef43
commit 2b8d2288fb
30 changed files with 279 additions and 28 deletions

View file

@ -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 : {

View file

@ -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;
};
});

View file

@ -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">

View file

@ -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>

View file

@ -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">

View file

@ -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
View 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";

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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",

View 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

View file

@ -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

View file

@ -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>

View file

@ -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));

View file

@ -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);

View file

@ -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);
}
@ -63,7 +85,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
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();
}
}

View file

@ -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>

View 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();
}
}
}

View file

@ -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
}
}

View file

@ -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;
}
}

View file

@ -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());

View file

@ -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());

View 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";

View file

@ -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);

View file

@ -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);

View file

@ -179,7 +179,18 @@ public class TokenService {
throw new UnauthorizedException("Disabled realm");
}
if (authManager.authenticateForm(clientConnection, realm, form) != AuthenticationStatus.SUCCESS) {
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");
}
@ -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();

View file

@ -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>

View file

@ -6,6 +6,7 @@
"accessCodeLifespan": 600,
"accessCodeLifespanUserAction": 600,
"sslNotRequired": true,
"bruteForceProtected": true,
"registrationAllowed": true,
"resetPasswordAllowed": true,
"requiredCredentials": [ "password" ],

View file

@ -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" ],

View file

@ -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>