Merge pull request #339 from patriot1burke/master
more brute force detection
This commit is contained in:
commit
b41bf9f231
30 changed files with 279 additions and 28 deletions
|
@ -639,6 +639,15 @@ module.config([ '$routeProvider', function($routeProvider) {
|
||||||
},
|
},
|
||||||
controller : 'RealmRevocationCtrl'
|
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', {
|
.when('/realms/:realm/sessions/realm', {
|
||||||
templateUrl : 'partials/session-realm.html',
|
templateUrl : 'partials/session-realm.html',
|
||||||
resolve : {
|
resolve : {
|
||||||
|
|
|
@ -1118,3 +1118,33 @@ module.controller('RealmAuditEventsCtrl', function($scope, RealmAuditEvents, rea
|
||||||
|
|
||||||
$scope.update();
|
$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}}/sessions/realm">Realm Sessions</a></li>
|
||||||
<li><a href="#/realms/{{realm.realm}}/token-settings">Token Settings</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 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>
|
</ul>
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<ol class="breadcrumb">
|
<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 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}}/token-settings">Token Settings</a></li>
|
||||||
<li><a href="#/realms/{{realm.realm}}/sessions/revocation">Revocation</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>
|
</ul>
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<ol class="breadcrumb">
|
<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}}/sessions/realm">Realm Sessions</a></li>
|
||||||
<li><a href="#/realms/{{realm.realm}}/token-settings">Token Settings</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 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>
|
</ul>
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<ol class="breadcrumb">
|
<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_NOT_FOUND = "user_not_found";
|
||||||
String USER_DISABLED = "user_disabled";
|
String USER_DISABLED = "user_disabled";
|
||||||
|
String USER_TEMPORARILY_DISABLED = "user_temporarily_disabled";
|
||||||
String INVALID_USER_CREDENTIALS = "invalid_user_credentials";
|
String INVALID_USER_CREDENTIALS = "invalid_user_credentials";
|
||||||
|
|
||||||
String USERNAME_MISSING = "username_missing";
|
String USERNAME_MISSING = "username_missing";
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
|
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.UserEntity</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.UserRoleMappingEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
|
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ public class RealmRepresentation {
|
||||||
protected Boolean resetPasswordAllowed;
|
protected Boolean resetPasswordAllowed;
|
||||||
protected Boolean social;
|
protected Boolean social;
|
||||||
protected Boolean updateProfileOnInitialSocialLogin;
|
protected Boolean updateProfileOnInitialSocialLogin;
|
||||||
|
protected Boolean bruteForceProtected;
|
||||||
protected String privateKey;
|
protected String privateKey;
|
||||||
protected String publicKey;
|
protected String publicKey;
|
||||||
protected RolesRepresentation roles;
|
protected RolesRepresentation roles;
|
||||||
|
@ -375,4 +376,12 @@ public class RealmRepresentation {
|
||||||
public void setNotBefore(Integer notBefore) {
|
public void setNotBefore(Integer notBefore) {
|
||||||
this.notBefore = notBefore;
|
this.notBefore = notBefore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean isBruteForceProtected() {
|
||||||
|
return bruteForceProtected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBruteForceProtected(Boolean bruteForceProtected) {
|
||||||
|
this.bruteForceProtected = bruteForceProtected;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"sslNotRequired": true,
|
"sslNotRequired": true,
|
||||||
"registrationAllowed": false,
|
"registrationAllowed": false,
|
||||||
"social": false,
|
"social": false,
|
||||||
|
"bruteForceProtected": true,
|
||||||
"updateProfileOnInitialSocialLogin": false,
|
"updateProfileOnInitialSocialLogin": false,
|
||||||
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
|
"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",
|
"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
|
socialRedirectError=Failed to redirect to social provider
|
||||||
socialProviderRemoved=Social provider removed successfully
|
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.
|
invalidUser=Invalid username or password.
|
||||||
invalidPassword=Invalid username or password.
|
invalidPassword=Invalid username or password.
|
||||||
accountDisabled=Account is disabled, contact admin
|
accountDisabled=Account is disabled, contact admin
|
||||||
|
accountTemporarilyDisabled=Account is temporarily disabled, contact admin or try again later
|
||||||
|
|
||||||
missingFirstName=Please specify first name
|
missingFirstName=Please specify first name
|
||||||
missingLastName=Please specify last name
|
missingLastName=Please specify last name
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.UserEntity</class>
|
<class>org.keycloak.models.jpa.entities.UserEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</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>
|
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
|
||||||
|
|
||||||
<exclude-unlisted-classes>true</exclude-unlisted-classes>
|
<exclude-unlisted-classes>true</exclude-unlisted-classes>
|
||||||
|
|
|
@ -45,7 +45,10 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest {
|
||||||
|
|
||||||
// Create 2 realms and user in realm1
|
// Create 2 realms and user in realm1
|
||||||
realm1 = realmManager.createRealm("realm1");
|
realm1 = realmManager.createRealm("realm1");
|
||||||
|
realm1.setBruteForceProtected(false);
|
||||||
realm2 = realmManager.createRealm("realm2");
|
realm2 = realmManager.createRealm("realm2");
|
||||||
|
realm2.setBruteForceProtected(false);
|
||||||
|
|
||||||
realm1.addRequiredCredential(CredentialRepresentation.PASSWORD);
|
realm1.addRequiredCredential(CredentialRepresentation.PASSWORD);
|
||||||
realm2.addRequiredCredential(CredentialRepresentation.PASSWORD);
|
realm2.addRequiredCredential(CredentialRepresentation.PASSWORD);
|
||||||
realm1.setAuthenticationProviders(Arrays.asList(AuthenticationProviderModel.DEFAULT_PROVIDER));
|
realm1.setAuthenticationProviders(Arrays.asList(AuthenticationProviderModel.DEFAULT_PROVIDER));
|
||||||
|
|
|
@ -48,6 +48,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
|
||||||
|
|
||||||
// Create realm and configure ldap
|
// Create realm and configure ldap
|
||||||
realm = realmManager.createRealm("realm");
|
realm = realmManager.createRealm("realm");
|
||||||
|
realm.setBruteForceProtected(false);
|
||||||
realm.addRequiredCredential(CredentialRepresentation.PASSWORD);
|
realm.addRequiredCredential(CredentialRepresentation.PASSWORD);
|
||||||
this.embeddedServer.setupLdapInRealm(realm);
|
this.embeddedServer.setupLdapInRealm(realm);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.keycloak.model.test;
|
package org.keycloak.model.test;
|
||||||
|
|
||||||
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
||||||
|
import org.junit.After;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
@ -11,8 +12,10 @@ import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserModel.RequiredAction;
|
import org.keycloak.models.UserModel.RequiredAction;
|
||||||
import org.keycloak.models.utils.TimeBasedOTP;
|
import org.keycloak.models.utils.TimeBasedOTP;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
|
import org.keycloak.services.ClientConnection;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
|
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
|
||||||
|
import org.keycloak.services.managers.BruteForceProtector;
|
||||||
|
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
|
||||||
|
@ -26,10 +29,29 @@ public class AuthenticationManagerTest extends AbstractModelTest {
|
||||||
private TimeBasedOTP otp;
|
private TimeBasedOTP otp;
|
||||||
private RealmModel realm;
|
private RealmModel realm;
|
||||||
private UserModel user;
|
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
|
@Test
|
||||||
public void authForm() {
|
public void authForm() {
|
||||||
AuthenticationStatus status = am.authenticateForm(null, realm, formData);
|
AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
|
||||||
Assert.assertEquals(AuthenticationStatus.SUCCESS, status);
|
Assert.assertEquals(AuthenticationStatus.SUCCESS, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +60,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
|
||||||
formData.remove(CredentialRepresentation.PASSWORD);
|
formData.remove(CredentialRepresentation.PASSWORD);
|
||||||
formData.add(CredentialRepresentation.PASSWORD, "invalid");
|
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);
|
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +68,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
|
||||||
public void authFormMissingUsername() {
|
public void authFormMissingUsername() {
|
||||||
formData.remove("username");
|
formData.remove("username");
|
||||||
|
|
||||||
AuthenticationStatus status = am.authenticateForm(null, realm, formData);
|
AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
|
||||||
Assert.assertEquals(AuthenticationStatus.INVALID_USER, status);
|
Assert.assertEquals(AuthenticationStatus.INVALID_USER, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +76,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
|
||||||
public void authFormMissingPassword() {
|
public void authFormMissingPassword() {
|
||||||
formData.remove(CredentialRepresentation.PASSWORD);
|
formData.remove(CredentialRepresentation.PASSWORD);
|
||||||
|
|
||||||
AuthenticationStatus status = am.authenticateForm(null, realm, formData);
|
AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
|
||||||
Assert.assertEquals(AuthenticationStatus.MISSING_PASSWORD, status);
|
Assert.assertEquals(AuthenticationStatus.MISSING_PASSWORD, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +85,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
|
||||||
realm.addRequiredCredential(CredentialRepresentation.TOTP);
|
realm.addRequiredCredential(CredentialRepresentation.TOTP);
|
||||||
user.addRequiredAction(RequiredAction.CONFIGURE_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);
|
Assert.assertEquals(AuthenticationStatus.ACTIONS_REQUIRED, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +93,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
|
||||||
public void authFormUserDisabled() {
|
public void authFormUserDisabled() {
|
||||||
user.setEnabled(false);
|
user.setEnabled(false);
|
||||||
|
|
||||||
AuthenticationStatus status = am.authenticateForm(null, realm, formData);
|
AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
|
||||||
Assert.assertEquals(AuthenticationStatus.ACCOUNT_DISABLED, status);
|
Assert.assertEquals(AuthenticationStatus.ACCOUNT_DISABLED, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +115,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
|
||||||
|
|
||||||
formData.add(CredentialRepresentation.TOTP, token);
|
formData.add(CredentialRepresentation.TOTP, token);
|
||||||
|
|
||||||
AuthenticationStatus status = am.authenticateForm(null, realm, formData);
|
AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
|
||||||
Assert.assertEquals(AuthenticationStatus.SUCCESS, status);
|
Assert.assertEquals(AuthenticationStatus.SUCCESS, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +126,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
|
||||||
formData.remove(CredentialRepresentation.PASSWORD);
|
formData.remove(CredentialRepresentation.PASSWORD);
|
||||||
formData.add(CredentialRepresentation.PASSWORD, "invalid");
|
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);
|
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +137,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
|
||||||
formData.remove(CredentialRepresentation.TOTP);
|
formData.remove(CredentialRepresentation.TOTP);
|
||||||
formData.add(CredentialRepresentation.TOTP, "invalid");
|
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);
|
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +147,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
|
||||||
|
|
||||||
formData.remove(CredentialRepresentation.TOTP);
|
formData.remove(CredentialRepresentation.TOTP);
|
||||||
|
|
||||||
AuthenticationStatus status = am.authenticateForm(null, realm, formData);
|
AuthenticationStatus status = am.authenticateForm(dummyConnection, realm, formData);
|
||||||
Assert.assertEquals(AuthenticationStatus.MISSING_TOTP, status);
|
Assert.assertEquals(AuthenticationStatus.MISSING_TOTP, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,8 +164,9 @@ public class AuthenticationManagerTest extends AbstractModelTest {
|
||||||
realm.setAccessTokenLifespan(1000);
|
realm.setAccessTokenLifespan(1000);
|
||||||
realm.addRequiredCredential(CredentialRepresentation.PASSWORD);
|
realm.addRequiredCredential(CredentialRepresentation.PASSWORD);
|
||||||
realm.setAuthenticationProviders(Arrays.asList(AuthenticationProviderModel.DEFAULT_PROVIDER));
|
realm.setAuthenticationProviders(Arrays.asList(AuthenticationProviderModel.DEFAULT_PROVIDER));
|
||||||
|
protector = new BruteForceProtector(factory);
|
||||||
am = new AuthenticationManager(providerSession);
|
protector.start();
|
||||||
|
am = new AuthenticationManager(providerSession, protector);
|
||||||
|
|
||||||
user = realm.addUser("test");
|
user = realm.addUser("test");
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
|
@ -161,4 +184,12 @@ public class AuthenticationManagerTest extends AbstractModelTest {
|
||||||
otp = new TimeBasedOTP();
|
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.SocialLinkEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.UserEntity</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.UserRoleMappingEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</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.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.provider.ProviderSessionFactory;
|
import org.keycloak.provider.ProviderSessionFactory;
|
||||||
|
import org.keycloak.services.managers.BruteForceProtector;
|
||||||
|
|
||||||
import javax.servlet.ServletContextEvent;
|
import javax.servlet.ServletContextEvent;
|
||||||
import javax.servlet.ServletContextListener;
|
import javax.servlet.ServletContextListener;
|
||||||
|
@ -17,6 +18,10 @@ public class KeycloakSessionDestroyListener implements ServletContextListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void contextDestroyed(ServletContextEvent sce) {
|
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());
|
ProviderSessionFactory providerSessionFactory = (ProviderSessionFactory) sce.getServletContext().getAttribute(ProviderSessionFactory.class.getName());
|
||||||
KeycloakSessionFactory kcSessionFactory = (KeycloakSessionFactory) sce.getServletContext().getAttribute(KeycloakSessionFactory.class.getName());
|
KeycloakSessionFactory kcSessionFactory = (KeycloakSessionFactory) sce.getServletContext().getAttribute(KeycloakSessionFactory.class.getName());
|
||||||
if (providerSessionFactory != null) {
|
if (providerSessionFactory != null) {
|
||||||
|
@ -25,6 +30,7 @@ public class KeycloakSessionDestroyListener implements ServletContextListener {
|
||||||
if (kcSessionFactory != null) {
|
if (kcSessionFactory != null) {
|
||||||
kcSessionFactory.close();
|
kcSessionFactory.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,8 @@ public class AuthenticationManager {
|
||||||
this.providerSession = providerSession;
|
this.providerSession = providerSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthenticationManager(BruteForceProtector protector) {
|
public AuthenticationManager(ProviderSession providerSession, BruteForceProtector protector) {
|
||||||
|
this.providerSession = providerSession;
|
||||||
this.protector = protector;
|
this.protector = protector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,6 +200,12 @@ public class AuthenticationManager {
|
||||||
return AuthenticationStatus.INVALID_USER;
|
return AuthenticationStatus.INVALID_USER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (realm.isBruteForceProtected()) {
|
||||||
|
if (protector.isTemporarilyDisabled(realm, username)) {
|
||||||
|
return AuthenticationStatus.ACCOUNT_TEMPORARILY_DISABLED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AuthenticationStatus status = authenticateInternal(realm, formData, username);
|
AuthenticationStatus status = authenticateInternal(realm, formData, username);
|
||||||
if (realm.isBruteForceProtected()) {
|
if (realm.isBruteForceProtected()) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
@ -313,7 +320,7 @@ public class AuthenticationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum AuthenticationStatus {
|
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.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
|
||||||
import org.keycloak.models.UsernameLoginFailureModel;
|
import org.keycloak.models.UsernameLoginFailureModel;
|
||||||
import org.keycloak.services.ClientConnection;
|
import org.keycloak.services.ClientConnection;
|
||||||
|
|
||||||
|
@ -28,8 +27,8 @@ public class BruteForceProtector implements Runnable {
|
||||||
protected int minimumQuickLoginWaitSeconds = 60;
|
protected int minimumQuickLoginWaitSeconds = 60;
|
||||||
protected int waitIncrementSeconds = 60;
|
protected int waitIncrementSeconds = 60;
|
||||||
protected long quickLoginCheckMilliSeconds = 1000;
|
protected long quickLoginCheckMilliSeconds = 1000;
|
||||||
protected int maxDeltaTime = 60 * 60 * 24 * 1000;
|
protected long maxDeltaTimeMilliSeconds = 60 * 60 * 12 * 1000; // 12 hours
|
||||||
protected int failureFactor = 10;
|
protected int failureFactor = 30;
|
||||||
protected volatile boolean run = true;
|
protected volatile boolean run = true;
|
||||||
protected KeycloakSessionFactory factory;
|
protected KeycloakSessionFactory factory;
|
||||||
protected CountDownLatch shutdownLatch = new CountDownLatch(1);
|
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 class FailedLogin extends LoginEvent {
|
||||||
protected final CountDownLatch latch = new CountDownLatch(1);
|
protected final CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
@ -90,7 +95,7 @@ public class BruteForceProtector implements Runnable {
|
||||||
user.setLastFailure(currentTime);
|
user.setLastFailure(currentTime);
|
||||||
if (deltaTime > 0) {
|
if (deltaTime > 0) {
|
||||||
// if last failure was more than MAX_DELTA clear failures
|
// if last failure was more than MAX_DELTA clear failures
|
||||||
if (deltaTime > maxDeltaTime) {
|
if (deltaTime > maxDeltaTimeMilliSeconds) {
|
||||||
user.clearFailures();
|
user.clearFailures();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,20 +114,27 @@ public class BruteForceProtector implements Runnable {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected UsernameLoginFailureModel getUserModel(KeycloakSession session, LoginEvent event) {
|
protected UsernameLoginFailureModel getUserModel(KeycloakSession session, LoginEvent event) {
|
||||||
RealmModel realm = session.getRealm(event.realmId);
|
RealmModel realm = getRealmModel(session, event);
|
||||||
if (realm == null) return null;
|
if (realm == null) return null;
|
||||||
UsernameLoginFailureModel user = realm.getUserLoginFailure(event.username);
|
UsernameLoginFailureModel user = realm.getUserLoginFailure(event.username);
|
||||||
if (user == null) return null;
|
if (user == null) return null;
|
||||||
return user;
|
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() {
|
public void start() {
|
||||||
new Thread(this).start();
|
new Thread(this, "Brute Force Protector").start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
run = false;
|
run = false;
|
||||||
try {
|
try {
|
||||||
|
queue.offer(new ShutdownEvent());
|
||||||
shutdownLatch.await(5, TimeUnit.SECONDS);
|
shutdownLatch.await(5, TimeUnit.SECONDS);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
@ -144,7 +156,7 @@ public class BruteForceProtector implements Runnable {
|
||||||
for (LoginEvent event : events) {
|
for (LoginEvent event : events) {
|
||||||
if (event instanceof FailedLogin) {
|
if (event instanceof FailedLogin) {
|
||||||
logFailure(event);
|
logFailure(event);
|
||||||
} else {
|
} else if (event instanceof SuccessfulLogin) {
|
||||||
logSuccess(event);
|
logSuccess(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -191,7 +203,7 @@ public class BruteForceProtector implements Runnable {
|
||||||
long delta = 0;
|
long delta = 0;
|
||||||
if (lastFailure > 0) {
|
if (lastFailure > 0) {
|
||||||
delta = System.currentTimeMillis() - lastFailure;
|
delta = System.currentTimeMillis() - lastFailure;
|
||||||
if (delta > maxDeltaTime) {
|
if (delta > maxDeltaTimeMilliSeconds) {
|
||||||
totalTime = 0;
|
totalTime = 0;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -221,4 +233,73 @@ public class BruteForceProtector implements Runnable {
|
||||||
} catch (InterruptedException e) {
|
} 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.setPrivateKey(realm.getPrivateKeyPem());
|
||||||
rep.setRegistrationAllowed(realm.isRegistrationAllowed());
|
rep.setRegistrationAllowed(realm.isRegistrationAllowed());
|
||||||
rep.setRememberMe(realm.isRememberMe());
|
rep.setRememberMe(realm.isRememberMe());
|
||||||
|
rep.setBruteForceProtected(realm.isBruteForceProtected());
|
||||||
rep.setVerifyEmail(realm.isVerifyEmail());
|
rep.setVerifyEmail(realm.isVerifyEmail());
|
||||||
rep.setResetPasswordAllowed(realm.isResetPasswordAllowed());
|
rep.setResetPasswordAllowed(realm.isResetPasswordAllowed());
|
||||||
rep.setAccessTokenLifespan(realm.getAccessTokenLifespan());
|
rep.setAccessTokenLifespan(realm.getAccessTokenLifespan());
|
||||||
|
|
|
@ -79,6 +79,7 @@ public class RealmManager {
|
||||||
if (id == null) id = KeycloakModelUtils.generateId();
|
if (id == null) id = KeycloakModelUtils.generateId();
|
||||||
RealmModel realm = identitySession.createRealm(id, name);
|
RealmModel realm = identitySession.createRealm(id, name);
|
||||||
realm.setName(name);
|
realm.setName(name);
|
||||||
|
realm.setBruteForceProtected(false); // default settings off for now todo set it on
|
||||||
|
|
||||||
setupAdminManagement(realm);
|
setupAdminManagement(realm);
|
||||||
setupAccountManagement(realm);
|
setupAccountManagement(realm);
|
||||||
|
@ -121,6 +122,7 @@ public class RealmManager {
|
||||||
}
|
}
|
||||||
if (rep.isEnabled() != null) realm.setEnabled(rep.isEnabled());
|
if (rep.isEnabled() != null) realm.setEnabled(rep.isEnabled());
|
||||||
if (rep.isSocial() != null) realm.setSocial(rep.isSocial());
|
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.isRegistrationAllowed() != null) realm.setRegistrationAllowed(rep.isRegistrationAllowed());
|
||||||
if (rep.isRememberMe() != null) realm.setRememberMe(rep.isRememberMe());
|
if (rep.isRememberMe() != null) realm.setRememberMe(rep.isRememberMe());
|
||||||
if (rep.isVerifyEmail() != null) realm.setVerifyEmail(rep.isVerifyEmail());
|
if (rep.isVerifyEmail() != null) realm.setVerifyEmail(rep.isVerifyEmail());
|
||||||
|
@ -227,6 +229,7 @@ public class RealmManager {
|
||||||
newRealm.setName(rep.getRealm());
|
newRealm.setName(rep.getRealm());
|
||||||
if (rep.isEnabled() != null) newRealm.setEnabled(rep.isEnabled());
|
if (rep.isEnabled() != null) newRealm.setEnabled(rep.isEnabled());
|
||||||
if (rep.isSocial() != null) newRealm.setSocial(rep.isSocial());
|
if (rep.isSocial() != null) newRealm.setSocial(rep.isSocial());
|
||||||
|
if (rep.isBruteForceProtected() != null) newRealm.setBruteForceProtected(rep.isBruteForceProtected());
|
||||||
|
|
||||||
if (rep.getNotBefore() != null) newRealm.setNotBefore(rep.getNotBefore());
|
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 class Messages {
|
||||||
|
|
||||||
public static final String ACCOUNT_DISABLED = "accountDisabled";
|
public static final String ACCOUNT_DISABLED = "accountDisabled";
|
||||||
|
public static final String ACCOUNT_TEMPORARILY_DISABLED = "accountTemporarilyDisabled";
|
||||||
|
|
||||||
public static final String INVALID_PASSWORD = "invalidPassword";
|
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.core.Dispatcher;
|
||||||
import org.jboss.resteasy.logging.Logger;
|
import org.jboss.resteasy.logging.Logger;
|
||||||
|
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||||
import org.keycloak.SkeletonKeyContextResolver;
|
import org.keycloak.SkeletonKeyContextResolver;
|
||||||
import org.keycloak.audit.AuditListener;
|
import org.keycloak.audit.AuditListener;
|
||||||
import org.keycloak.audit.AuditListenerFactory;
|
import org.keycloak.audit.AuditListenerFactory;
|
||||||
|
@ -22,6 +23,7 @@ import org.keycloak.picketlink.IdentityManagerProvider;
|
||||||
import org.keycloak.picketlink.IdentityManagerProviderFactory;
|
import org.keycloak.picketlink.IdentityManagerProviderFactory;
|
||||||
import org.keycloak.provider.ProviderSessionFactory;
|
import org.keycloak.provider.ProviderSessionFactory;
|
||||||
import org.keycloak.services.managers.ApplianceBootstrap;
|
import org.keycloak.services.managers.ApplianceBootstrap;
|
||||||
|
import org.keycloak.services.managers.BruteForceProtector;
|
||||||
import org.keycloak.services.managers.SocialRequestManager;
|
import org.keycloak.services.managers.SocialRequestManager;
|
||||||
import org.keycloak.services.managers.TokenManager;
|
import org.keycloak.services.managers.TokenManager;
|
||||||
import org.keycloak.services.resources.admin.AdminService;
|
import org.keycloak.services.resources.admin.AdminService;
|
||||||
|
@ -57,6 +59,11 @@ public class KeycloakApplication extends Application {
|
||||||
dispatcher.getDefaultContextObjects().put(KeycloakApplication.class, this);
|
dispatcher.getDefaultContextObjects().put(KeycloakApplication.class, this);
|
||||||
this.contextPath = context.getContextPath();
|
this.contextPath = context.getContextPath();
|
||||||
this.factory = createSessionFactory();
|
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();
|
this.providerSessionFactory = createProviderSessionFactory();
|
||||||
context.setAttribute(KeycloakSessionFactory.class.getName(), factory);
|
context.setAttribute(KeycloakSessionFactory.class.getName(), factory);
|
||||||
//classes.add(KeycloakSessionCleanupFilter.class);
|
//classes.add(KeycloakSessionCleanupFilter.class);
|
||||||
|
|
|
@ -12,6 +12,7 @@ import org.keycloak.services.ClientConnection;
|
||||||
import org.keycloak.provider.ProviderSession;
|
import org.keycloak.provider.ProviderSession;
|
||||||
import org.keycloak.services.managers.AuditManager;
|
import org.keycloak.services.managers.AuditManager;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
import org.keycloak.services.managers.BruteForceProtector;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.services.managers.SocialRequestManager;
|
import org.keycloak.services.managers.SocialRequestManager;
|
||||||
import org.keycloak.services.managers.TokenManager;
|
import org.keycloak.services.managers.TokenManager;
|
||||||
|
@ -51,6 +52,9 @@ public class RealmsResource {
|
||||||
@Context
|
@Context
|
||||||
protected ClientConnection clientConnection;
|
protected ClientConnection clientConnection;
|
||||||
|
|
||||||
|
@Context
|
||||||
|
protected BruteForceProtector protector;
|
||||||
|
|
||||||
protected TokenManager tokenManager;
|
protected TokenManager tokenManager;
|
||||||
protected SocialRequestManager socialRequestManager;
|
protected SocialRequestManager socialRequestManager;
|
||||||
|
|
||||||
|
@ -68,7 +72,7 @@ public class RealmsResource {
|
||||||
RealmManager realmManager = new RealmManager(session);
|
RealmManager realmManager = new RealmManager(session);
|
||||||
RealmModel realm = locateRealm(name, realmManager);
|
RealmModel realm = locateRealm(name, realmManager);
|
||||||
Audit audit = new AuditManager(realm, providers, clientConnection).createAudit();
|
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);
|
TokenService tokenService = new TokenService(realm, tokenManager, audit, authManager);
|
||||||
ResteasyProviderFactory.getInstance().injectProperties(tokenService);
|
ResteasyProviderFactory.getInstance().injectProperties(tokenService);
|
||||||
//resourceContext.initResource(tokenService);
|
//resourceContext.initResource(tokenService);
|
||||||
|
|
|
@ -179,7 +179,18 @@ public class TokenService {
|
||||||
throw new UnauthorizedException("Disabled realm");
|
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);
|
audit.error(Errors.INVALID_USER_CREDENTIALS);
|
||||||
throw new UnauthorizedException("Auth failed");
|
throw new UnauthorizedException("Auth failed");
|
||||||
}
|
}
|
||||||
|
@ -303,6 +314,9 @@ public class TokenService {
|
||||||
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username);
|
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username);
|
||||||
audit.user(user);
|
audit.user(user);
|
||||||
return oauth.processAccessCode(scopeParam, state, redirect, client, user, username, remember, "form", audit);
|
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:
|
case ACCOUNT_DISABLED:
|
||||||
audit.error(Errors.USER_DISABLED);
|
audit.error(Errors.USER_DISABLED);
|
||||||
return Flows.forms(realm, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin();
|
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.SocialLinkEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.UserEntity</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.UserRoleMappingEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
|
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
"accessCodeLifespan": 600,
|
"accessCodeLifespan": 600,
|
||||||
"accessCodeLifespanUserAction": 600,
|
"accessCodeLifespanUserAction": 600,
|
||||||
"sslNotRequired": true,
|
"sslNotRequired": true,
|
||||||
|
"bruteForceProtected": true,
|
||||||
"registrationAllowed": true,
|
"registrationAllowed": true,
|
||||||
"resetPasswordAllowed": true,
|
"resetPasswordAllowed": true,
|
||||||
"requiredCredentials": [ "password" ],
|
"requiredCredentials": [ "password" ],
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"sslNotRequired": true,
|
"sslNotRequired": true,
|
||||||
"registrationAllowed": true,
|
"registrationAllowed": true,
|
||||||
"resetPasswordAllowed": true,
|
"resetPasswordAllowed": true,
|
||||||
|
"bruteForceProtected": true,
|
||||||
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
|
"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",
|
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||||
"requiredCredentials": [ "password" ],
|
"requiredCredentials": [ "password" ],
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
|
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
<class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.UserEntity</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.UserRoleMappingEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
|
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue