keycloak-aplcache

brute force protection

4/2/2014 9:09:14 PM

Changes

Details

diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index e07bdb9..308dd7c 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -33,6 +33,10 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
 
     void setRememberMe(boolean rememberMe);
 
+    boolean isBruteForceProtected();
+
+    void setBruteForceProtected(boolean value);
+
     boolean isVerifyEmail();
 
     void setVerifyEmail(boolean verifyEmail);
@@ -148,6 +152,9 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
 
     public void setUpdateProfileOnInitialSocialLogin(boolean updateProfileOnInitialSocialLogin);
 
+    public UsernameLoginFailureModel getUserLoginFailure(String username);
+    UsernameLoginFailureModel addUserLoginFailure(String username);
+
     List<UserModel> getUsers();
 
     List<UserModel> searchForUser(String search);
diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java
index 95d6613..762acc8 100755
--- a/model/api/src/main/java/org/keycloak/models/UserModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserModel.java
@@ -58,6 +58,7 @@ public interface UserModel {
     int getNotBefore();
     void setNotBefore(int notBefore);
 
+
     public static enum RequiredAction {
         VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD
     }
diff --git a/model/api/src/main/java/org/keycloak/models/UsernameLoginFailureModel.java b/model/api/src/main/java/org/keycloak/models/UsernameLoginFailureModel.java
new file mode 100755
index 0000000..59316ae
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/UsernameLoginFailureModel.java
@@ -0,0 +1,21 @@
+package org.keycloak.models;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface UsernameLoginFailureModel
+{
+   String getUsername();
+   int getFailedLoginNotBefore();
+   void setFailedLoginNotBefore(int notBefore);
+   int getNumFailures();
+   void incrementFailures();
+   void clearFailures();
+   long getLastFailure();
+   void setLastFailure(long lastFailure);
+   String getLastIPFailure();
+   void setLastIPFailure(String ip);
+
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index 04051ed..255b737 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -39,6 +39,7 @@ public class RealmEntity {
     protected boolean resetPasswordAllowed;
     protected boolean social;
     protected boolean rememberMe;
+    protected boolean bruteForceProtected;
 
     @Column(name="updateProfileOnInitSocLogin")
     protected boolean updateProfileOnInitialSocialLogin;
@@ -333,5 +334,13 @@ public class RealmEntity {
     public void setNotBefore(int notBefore) {
         this.notBefore = notBefore;
     }
+
+    public boolean isBruteForceProtected() {
+        return bruteForceProtected;
+    }
+
+    public void setBruteForceProtected(boolean bruteForceProtected) {
+        this.bruteForceProtected = bruteForceProtected;
+    }
 }
 
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
index ab25a64..156aaaa 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
@@ -48,6 +48,7 @@ public class UserEntity {
     protected boolean emailVerified;
     protected int notBefore;
 
+
     @ManyToOne
     protected RealmEntity realm;
 
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UsernameLoginFailureEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UsernameLoginFailureEntity.java
new file mode 100755
index 0000000..6011ccc
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UsernameLoginFailureEntity.java
@@ -0,0 +1,82 @@
+package org.keycloak.models.jpa.entities;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.ManyToOne;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+@Entity
+public class UsernameLoginFailureEntity {
+    // we manually set the id to be username-realmid
+    // we may have a concurrent creation of the same login failure entry that we want to avoid
+    @Id
+    protected String id;
+    protected String username;
+    protected int failedLoginNotBefore;
+    protected int numFailures;
+    protected long lastFailure;
+    protected String lastIPFailure;
+
+
+    @ManyToOne
+    protected RealmEntity realm;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public int getFailedLoginNotBefore() {
+        return failedLoginNotBefore;
+    }
+
+    public void setFailedLoginNotBefore(int failedLoginNotBefore) {
+        this.failedLoginNotBefore = failedLoginNotBefore;
+    }
+
+    public int getNumFailures() {
+        return numFailures;
+    }
+
+    public void setNumFailures(int numFailures) {
+        this.numFailures = numFailures;
+    }
+
+    public long getLastFailure() {
+        return lastFailure;
+    }
+
+    public void setLastFailure(long lastFailure) {
+        this.lastFailure = lastFailure;
+    }
+
+    public String getLastIPFailure() {
+        return lastIPFailure;
+    }
+
+    public void setLastIPFailure(String lastIPFailure) {
+        this.lastIPFailure = lastIPFailure;
+    }
+
+    public RealmEntity getRealm() {
+        return realm;
+    }
+
+    public void setRealm(RealmEntity realm) {
+        this.realm = realm;
+    }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 17c5a1a..0513650 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -4,6 +4,7 @@ import org.keycloak.models.AuthenticationLinkModel;
 import org.keycloak.models.AuthenticationProviderModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.RoleContainerModel;
+import org.keycloak.models.UsernameLoginFailureModel;
 import org.keycloak.models.jpa.entities.ApplicationEntity;
 import org.keycloak.models.jpa.entities.ApplicationRoleEntity;
 import org.keycloak.models.jpa.entities.AuthenticationLinkEntity;
@@ -18,6 +19,7 @@ import org.keycloak.models.jpa.entities.ScopeMappingEntity;
 import org.keycloak.models.jpa.entities.SocialLinkEntity;
 import org.keycloak.models.jpa.entities.UserEntity;
 import org.keycloak.models.jpa.entities.UserRoleMappingEntity;
+import org.keycloak.models.jpa.entities.UsernameLoginFailureEntity;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
 import org.keycloak.models.ApplicationModel;
@@ -123,6 +125,16 @@ public class RealmAdapter implements RealmModel {
     }
 
     @Override
+    public boolean isBruteForceProtected() {
+        return realm.isBruteForceProtected();
+    }
+
+    @Override
+    public void setBruteForceProtected(boolean value) {
+        realm.setBruteForceProtected(value);
+    }
+
+    @Override
     public boolean isVerifyEmail() {
         return realm.isVerifyEmail();
     }
@@ -340,6 +352,27 @@ public class RealmAdapter implements RealmModel {
     }
 
     @Override
+    public UsernameLoginFailureModel getUserLoginFailure(String username) {
+        String id = username + "-" + realm.getId();
+        UsernameLoginFailureEntity entity = em.find(UsernameLoginFailureEntity.class, id);
+        if (entity == null) return null;
+        return new UsernameLoginFailureAdapter(entity);
+    }
+
+    @Override
+    public UsernameLoginFailureModel addUserLoginFailure(String username) {
+        UsernameLoginFailureModel model = getUserLoginFailure(username);
+        if (model != null) return model;
+        String id = username + "-" + realm.getId();
+        UsernameLoginFailureEntity entity = new UsernameLoginFailureEntity();
+        entity.setId(id);
+        entity.setUsername(username);
+        entity.setRealm(realm);
+        em.persist(entity);
+        return new UsernameLoginFailureAdapter(entity);
+    }
+
+    @Override
     public UserModel getUserByEmail(String email) {
         TypedQuery<UserEntity> query = em.createNamedQuery("getRealmUserByEmail", UserEntity.class);
         query.setParameter("email", email);
@@ -359,6 +392,9 @@ public class RealmAdapter implements RealmModel {
 
     @Override
     public UserModel addUser(String username) {
+        if (getUser(username) != null) {
+            throw new RuntimeException("Username already exists: " + username);
+        }
         UserEntity entity = new UserEntity();
         entity.setLoginName(username);
         entity.setRealm(realm);
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
index 4eec0ca..50daa11 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
@@ -154,4 +154,7 @@ public class UserAdapter implements UserModel {
     public void setNotBefore(int notBefore) {
         user.setNotBefore(notBefore);
     }
+
+
+
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UsernameLoginFailureAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UsernameLoginFailureAdapter.java
new file mode 100755
index 0000000..ba43c82
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UsernameLoginFailureAdapter.java
@@ -0,0 +1,70 @@
+package org.keycloak.models.jpa;
+
+import org.keycloak.models.UsernameLoginFailureModel;
+import org.keycloak.models.jpa.entities.UsernameLoginFailureEntity;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class UsernameLoginFailureAdapter implements UsernameLoginFailureModel
+{
+   protected UsernameLoginFailureEntity user;
+
+   public UsernameLoginFailureAdapter(UsernameLoginFailureEntity user)
+   {
+      this.user = user;
+   }
+
+   @Override
+   public String getUsername()
+   {
+      return user.getUsername();
+   }
+
+   @Override
+   public int getFailedLoginNotBefore() {
+      return user.getFailedLoginNotBefore();
+   }
+
+   @Override
+   public void setFailedLoginNotBefore(int notBefore) {
+      user.setFailedLoginNotBefore(notBefore);
+   }
+
+   @Override
+   public int getNumFailures() {
+      return user.getNumFailures();
+   }
+
+   @Override
+   public void incrementFailures() {
+      user.setNumFailures(getNumFailures() + 1);
+   }
+
+   @Override
+   public void clearFailures() {
+      user.setNumFailures(0);
+   }
+
+   @Override
+   public long getLastFailure() {
+      return user.getLastFailure();
+   }
+
+   @Override
+   public void setLastFailure(long lastFailure) {
+      user.setLastFailure(lastFailure);
+   }
+
+   @Override
+   public String getLastIPFailure() {
+      return user.getLastIPFailure();
+   }
+
+   @Override
+   public void setLastIPFailure(String ip) {
+      user.setLastIPFailure(ip);
+   }
+
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index 68a604f..6f07885 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -26,6 +26,7 @@ import org.keycloak.models.mongo.keycloak.entities.RequiredCredentialEntity;
 import org.keycloak.models.mongo.keycloak.entities.RoleEntity;
 import org.keycloak.models.mongo.keycloak.entities.SocialLinkEntity;
 import org.keycloak.models.mongo.keycloak.entities.UserEntity;
+import org.keycloak.models.mongo.keycloak.entities.UsernameLoginFailureEntity;
 import org.keycloak.models.mongo.utils.MongoModelUtils;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
@@ -122,6 +123,15 @@ public class RealmAdapter extends AbstractMongoAdapter<RealmEntity> implements R
         updateRealm();
     }
 
+    @Override
+    public boolean isBruteForceProtected() {
+        return realm.isBruteForceProtected();
+    }
+
+    @Override
+    public void setBruteForceProtected(boolean value) {
+        realm.setBruteForceProtected(value);
+    }
 
     @Override
     public boolean isVerifyEmail() {
@@ -340,6 +350,38 @@ public class RealmAdapter extends AbstractMongoAdapter<RealmEntity> implements R
     }
 
     @Override
+    public UsernameLoginFailureAdapter getUserLoginFailure(String name) {
+        DBObject query = new QueryBuilder()
+                .and("username").is(name)
+                .and("realmId").is(getId())
+                .get();
+        UsernameLoginFailureEntity user = getMongoStore().loadSingleEntity(UsernameLoginFailureEntity.class, query, invocationContext);
+
+        if (user == null) {
+            return null;
+        } else {
+            return new UsernameLoginFailureAdapter(invocationContext, user);
+        }
+    }
+
+    @Override
+    public UsernameLoginFailureAdapter addUserLoginFailure(String username) {
+        UsernameLoginFailureAdapter userLoginFailure = getUserLoginFailure(username);
+        if (userLoginFailure != null) {
+            return userLoginFailure;
+        }
+
+        UsernameLoginFailureEntity userEntity = new UsernameLoginFailureEntity();
+        userEntity.setUsername(username);
+        // Compatibility with JPA model, which has user disabled by default
+        // userEntity.setEnabled(true);
+        userEntity.setRealmId(getId());
+
+        getMongoStore().insertEntity(userEntity, invocationContext);
+        return new UsernameLoginFailureAdapter(invocationContext, userEntity);
+    }
+
+    @Override
     public UserModel getUserByEmail(String email) {
         DBObject query = new QueryBuilder()
                 .and("email").is(email)
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
index 5d3ed09..8958e32 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
@@ -170,4 +170,7 @@ public class UserAdapter extends AbstractMongoAdapter<UserEntity> implements Use
     public UserEntity getMongoEntity() {
         return user;
     }
+
+
+
 }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UsernameLoginFailureAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UsernameLoginFailureAdapter.java
new file mode 100755
index 0000000..0722945
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UsernameLoginFailureAdapter.java
@@ -0,0 +1,73 @@
+package org.keycloak.models.mongo.keycloak.adapters;
+
+import org.keycloak.models.UsernameLoginFailureModel;
+import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.mongo.keycloak.entities.UserEntity;
+import org.keycloak.models.mongo.keycloak.entities.UsernameLoginFailureEntity;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class UsernameLoginFailureAdapter  extends AbstractMongoAdapter<UsernameLoginFailureEntity> implements UsernameLoginFailureModel {
+    protected UsernameLoginFailureEntity user;
+
+    public UsernameLoginFailureAdapter(MongoStoreInvocationContext invocationContext, UsernameLoginFailureEntity user) {
+        super(invocationContext);
+        this.user = user;
+    }
+
+    @Override
+    protected UsernameLoginFailureEntity getMongoEntity() {
+        return user;
+    }
+
+    @Override
+    public String getUsername() {
+        return user.getUsername();
+    }
+
+    @Override
+    public int getFailedLoginNotBefore() {
+        return user.getFailedLoginNotBefore();
+    }
+
+    @Override
+    public void setFailedLoginNotBefore(int notBefore) {
+        user.setFailedLoginNotBefore(notBefore);
+    }
+
+    @Override
+    public int getNumFailures() {
+        return user.getNumFailures();
+    }
+
+    @Override
+    public void incrementFailures() {
+        user.setNumFailures(getNumFailures() + 1);
+    }
+
+    @Override
+    public void clearFailures() {
+        user.setNumFailures(0);
+    }
+
+    @Override
+    public long getLastFailure() {
+        return user.getLastFailure();
+    }
+
+    @Override
+    public void setLastFailure(long lastFailure) {
+        user.setLastFailure(lastFailure);
+    }
+
+    @Override
+    public String getLastIPFailure() {
+        return user.getLastIPFailure();
+    }
+
+    @Override
+    public void setLastIPFailure(String ip) {
+        user.setLastIPFailure(ip);
+    }}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java
index cf37018..991bd07 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java
@@ -29,6 +29,7 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong
     private boolean social;
     private boolean updateProfileOnInitialSocialLogin;
     private String passwordPolicy;
+    private boolean bruteForceProtected;
 
     private int centralLoginLifespan;
     private int accessTokenLifespan;
@@ -135,6 +136,15 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong
     }
 
     @MongoField
+    public boolean isBruteForceProtected() {
+        return bruteForceProtected;
+    }
+
+    public void setBruteForceProtected(boolean bruteForceProtected) {
+        this.bruteForceProtected = bruteForceProtected;
+    }
+
+    @MongoField
     public String getPasswordPolicy() {
         return passwordPolicy;
     }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java
index f0539df..4468fd9 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java
@@ -24,6 +24,11 @@ public class UserEntity extends AbstractMongoIdentifiableEntity implements Mongo
     private boolean totp;
     private boolean enabled;
     private int notBefore;
+    private int failedLoginNotBefore;
+    private int numFailures;
+    private long lastFailure;
+    private String lastIPFailure;
+
 
     private String realmId;
 
@@ -170,4 +175,41 @@ public class UserEntity extends AbstractMongoIdentifiableEntity implements Mongo
     public void setAuthenticationLinks(List<AuthenticationLinkEntity> authenticationLinks) {
         this.authenticationLinks = authenticationLinks;
     }
+
+    @MongoField
+    public int getFailedLoginNotBefore() {
+        return failedLoginNotBefore;
+    }
+
+    public void setFailedLoginNotBefore(int failedLoginNotBefore) {
+        this.failedLoginNotBefore = failedLoginNotBefore;
+    }
+
+    @MongoField
+    public int getNumFailures() {
+        return numFailures;
+    }
+
+    public void setNumFailures(int numFailures) {
+        this.numFailures = numFailures;
+    }
+
+    @MongoField
+    public long getLastFailure() {
+        return lastFailure;
+    }
+
+    public void setLastFailure(long lastFailure) {
+        this.lastFailure = lastFailure;
+    }
+
+    @MongoField
+    public String getLastIPFailure() {
+        return lastIPFailure;
+    }
+
+    public void setLastIPFailure(String lastIPFailure) {
+        this.lastIPFailure = lastIPFailure;
+    }
+
 }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UsernameLoginFailureEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UsernameLoginFailureEntity.java
new file mode 100755
index 0000000..4852626
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UsernameLoginFailureEntity.java
@@ -0,0 +1,76 @@
+package org.keycloak.models.mongo.keycloak.entities;
+
+import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity;
+import org.keycloak.models.mongo.api.MongoCollection;
+import org.keycloak.models.mongo.api.MongoEntity;
+import org.keycloak.models.mongo.api.MongoField;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+@MongoCollection(collectionName = "userFailures")
+public class UsernameLoginFailureEntity extends AbstractMongoIdentifiableEntity implements MongoEntity {
+    private String username;
+    private int failedLoginNotBefore;
+    private int numFailures;
+    private long lastFailure;
+    private String lastIPFailure;
+
+
+    private String realmId;
+
+    @MongoField
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    @MongoField
+    public int getFailedLoginNotBefore() {
+        return failedLoginNotBefore;
+    }
+
+    public void setFailedLoginNotBefore(int failedLoginNotBefore) {
+        this.failedLoginNotBefore = failedLoginNotBefore;
+    }
+
+    @MongoField
+    public int getNumFailures() {
+        return numFailures;
+    }
+
+    public void setNumFailures(int numFailures) {
+        this.numFailures = numFailures;
+    }
+
+    @MongoField
+    public long getLastFailure() {
+        return lastFailure;
+    }
+
+    public void setLastFailure(long lastFailure) {
+        this.lastFailure = lastFailure;
+    }
+
+    @MongoField
+    public String getLastIPFailure() {
+        return lastIPFailure;
+    }
+
+    public void setLastIPFailure(String lastIPFailure) {
+        this.lastIPFailure = lastIPFailure;
+    }
+
+    @MongoField
+    public String getRealmId() {
+        return realmId;
+    }
+
+    public void setRealmId(String realmId) {
+        this.realmId = realmId;
+    }
+}
diff --git a/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java
index 513c119..1abcf5f 100755
--- a/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java
+++ b/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java
@@ -26,7 +26,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
 
     @Test
     public void authForm() {
-        AuthenticationStatus status = am.authenticateForm(realm, formData);
+        AuthenticationStatus status = am.authenticateForm(null, realm, formData);
         Assert.assertEquals(AuthenticationStatus.SUCCESS, status);
     }
 
@@ -35,7 +35,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
         formData.remove(CredentialRepresentation.PASSWORD);
         formData.add(CredentialRepresentation.PASSWORD, "invalid");
 
-        AuthenticationStatus status = am.authenticateForm(realm, formData);
+        AuthenticationStatus status = am.authenticateForm(null, realm, formData);
         Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
     }
 
@@ -43,7 +43,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
     public void authFormMissingUsername() {
         formData.remove("username");
 
-        AuthenticationStatus status = am.authenticateForm(realm, formData);
+        AuthenticationStatus status = am.authenticateForm(null, realm, formData);
         Assert.assertEquals(AuthenticationStatus.INVALID_USER, status);
     }
 
@@ -51,7 +51,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
     public void authFormMissingPassword() {
         formData.remove(CredentialRepresentation.PASSWORD);
 
-        AuthenticationStatus status = am.authenticateForm(realm, formData);
+        AuthenticationStatus status = am.authenticateForm(null, realm, formData);
         Assert.assertEquals(AuthenticationStatus.MISSING_PASSWORD, status);
     }
 
@@ -60,7 +60,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
         realm.addRequiredCredential(CredentialRepresentation.TOTP);
         user.addRequiredAction(RequiredAction.CONFIGURE_TOTP);
         
-        AuthenticationStatus status = am.authenticateForm(realm, formData);
+        AuthenticationStatus status = am.authenticateForm(null, realm, formData);
         Assert.assertEquals(AuthenticationStatus.ACTIONS_REQUIRED, status);
     }
 
@@ -68,7 +68,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
     public void authFormUserDisabled() {
         user.setEnabled(false);
 
-        AuthenticationStatus status = am.authenticateForm(realm, formData);
+        AuthenticationStatus status = am.authenticateForm(null, realm, formData);
         Assert.assertEquals(AuthenticationStatus.ACCOUNT_DISABLED, status);
     }
 
@@ -90,7 +90,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
 
         formData.add(CredentialRepresentation.TOTP, token);
 
-        AuthenticationStatus status = am.authenticateForm(realm, formData);
+        AuthenticationStatus status = am.authenticateForm(null, realm, formData);
         Assert.assertEquals(AuthenticationStatus.SUCCESS, status);
     }
 
@@ -101,7 +101,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
         formData.remove(CredentialRepresentation.PASSWORD);
         formData.add(CredentialRepresentation.PASSWORD, "invalid");
 
-        AuthenticationStatus status = am.authenticateForm(realm, formData);
+        AuthenticationStatus status = am.authenticateForm(null, realm, formData);
         Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
     }
 
@@ -112,7 +112,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
         formData.remove(CredentialRepresentation.TOTP);
         formData.add(CredentialRepresentation.TOTP, "invalid");
 
-        AuthenticationStatus status = am.authenticateForm(realm, formData);
+        AuthenticationStatus status = am.authenticateForm(null, realm, formData);
         Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
     }
 
@@ -122,7 +122,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
 
         formData.remove(CredentialRepresentation.TOTP);
 
-        AuthenticationStatus status = am.authenticateForm(realm, formData);
+        AuthenticationStatus status = am.authenticateForm(null, realm, formData);
         Assert.assertEquals(AuthenticationStatus.MISSING_TOTP, status);
     }
 
diff --git a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java
old mode 100644
new mode 100755
index f8ce23b..c0c531c
--- a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java
+++ b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java
@@ -70,10 +70,10 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest {
         MultivaluedMap<String, String> formData = createFormData("john", "password");
 
         // Authenticate user with realm1
-        Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm1, formData));
+        Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm1, formData));
 
         // Verify that user doesn't exists in realm2 and can't authenticate here
-        Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(realm2, formData));
+        Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(null, realm2, formData));
         Assert.assertNull(realm2.getUser("john"));
 
         // Add externalModel authenticationProvider into realm2 and point to realm1
@@ -84,7 +84,7 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest {
             ResteasyProviderFactory.pushContext(KeycloakSession.class, identitySession);
 
             // Authenticate john in realm2 and verify that now he exists here.
-            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm2, formData));
+            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm2, formData));
             UserModel john2 = realm2.getUser("john");
             Assert.assertNotNull(john2);
             Assert.assertEquals("john", john2.getLoginName());
@@ -121,8 +121,8 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest {
                 Assert.fail("Error not expected");
             }
             MultivaluedMap<String, String> formData = createFormData("john", "password-updated");
-            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm1, formData));
-            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm2, formData));
+            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm1, formData));
+            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm2, formData));
 
 
             // Switch to disallow password update propagation to realm1
@@ -136,8 +136,8 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest {
                 Assert.fail("Error not expected");
             }
             formData = createFormData("john", "password-updated2");
-            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(realm1, formData));
-            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm2, formData));
+            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm1, formData));
+            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm2, formData));
 
 
             // Allow passwordUpdate propagation again
diff --git a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java
old mode 100644
new mode 100755
index 96157fe..07526c2
--- a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java
+++ b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java
@@ -79,14 +79,14 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
             LdapTestUtils.setLdapPassword(realm, "john", "password");
 
             // Verify that user doesn't exists in realm2 and can't authenticate here
-            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(realm, formData));
+            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(null, realm, formData));
             Assert.assertNull(realm.getUser("john"));
 
             // Add ldap authenticationProvider
             setupAuthenticationProviders();
 
             // Authenticate john and verify that now he exists in realm
-            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm, formData));
+            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData));
             UserModel john = realm.getUser("john");
             Assert.assertNotNull(john);
             Assert.assertEquals("john", john.getLoginName());
@@ -121,20 +121,20 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
 
             // User doesn't exists
             MultivaluedMap<String, String> formData = AuthProvidersExternalModelTest.createFormData("invalid", "invalid");
-            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(realm, formData));
+            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(null, realm, formData));
 
             // User exists in ldap
             formData = AuthProvidersExternalModelTest.createFormData("john", "invalid");
-            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(realm, formData));
+            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm, formData));
 
             // User exists in realm
             formData = AuthProvidersExternalModelTest.createFormData("realmUser", "invalid");
-            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(realm, formData));
+            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm, formData));
 
             // User disabled
             realmUser.setEnabled(false);
             formData = AuthProvidersExternalModelTest.createFormData("realmUser", "pass");
-            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.ACCOUNT_DISABLED, am.authenticateForm(realm, formData));
+            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.ACCOUNT_DISABLED, am.authenticateForm(null, realm, formData));
         } finally {
             ResteasyProviderFactory.clearContextData();
         }
@@ -158,7 +158,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
                 Assert.fail("Error not expected");
             }
             MultivaluedMap<String, String> formData = AuthProvidersExternalModelTest.createFormData("john", "password-updated");
-            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm, formData));
+            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData));
 
             // Password updated just in LDAP, so validating directly in realm should fail
             Assert.assertFalse(realm.validatePassword(realm.getUser("john"), "password-updated"));
@@ -174,7 +174,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
                 Assert.fail("Error not expected");
             }
             formData = AuthProvidersExternalModelTest.createFormData("john", "password-updated2");
-            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(realm, formData));
+            Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm, formData));
         } finally {
             ResteasyProviderFactory.clearContextData();
         }
diff --git a/server/src/main/webapp/WEB-INF/web.xml b/server/src/main/webapp/WEB-INF/web.xml
index 45d0d04..626ec53 100755
--- a/server/src/main/webapp/WEB-INF/web.xml
+++ b/server/src/main/webapp/WEB-INF/web.xml
@@ -35,6 +35,11 @@
     </welcome-file-list>
 
     <filter>
+        <filter-name>Keycloak Client Connection Filter</filter-name>
+        <filter-class>org.keycloak.services.filters.ClientConnectionFilter</filter-class>
+    </filter>
+
+    <filter>
         <filter-name>Keycloak Session Management</filter-name>
         <filter-class>org.keycloak.services.filters.KeycloakSessionServletFilter</filter-class>
     </filter>
@@ -44,6 +49,11 @@
         <url-pattern>/rest/*</url-pattern>
     </filter-mapping>
 
+    <filter-mapping>
+        <filter-name>Keycloak Client Connection Filter</filter-name>
+        <url-pattern>/rest/*</url-pattern>
+    </filter-mapping>
+
     <servlet-mapping>
         <servlet-name>Resteasy</servlet-name>
         <url-pattern>/rest/*</url-pattern>
diff --git a/services/src/main/java/org/keycloak/services/ClientConnection.java b/services/src/main/java/org/keycloak/services/ClientConnection.java
new file mode 100755
index 0000000..61491a4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/ClientConnection.java
@@ -0,0 +1,13 @@
+package org.keycloak.services;
+
+/**
+ * Information about the client connection
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ClientConnection {
+    String getRemoteAddr();
+    String getRemoteHost();
+    int getReportPort();
+}
diff --git a/services/src/main/java/org/keycloak/services/filters/ClientConnectionFilter.java b/services/src/main/java/org/keycloak/services/filters/ClientConnectionFilter.java
new file mode 100755
index 0000000..d74a150
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/filters/ClientConnectionFilter.java
@@ -0,0 +1,49 @@
+package org.keycloak.services.filters;
+
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.services.ClientConnection;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ClientConnectionFilter implements Filter {
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+        //To change body of implemented methods use File | Settings | File Templates.
+    }
+
+    @Override
+    public void doFilter(final ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        ResteasyProviderFactory.pushContext(ClientConnection.class, new ClientConnection() {
+            @Override
+            public String getRemoteAddr() {
+                return request.getRemoteAddr();
+            }
+
+            @Override
+            public String getRemoteHost() {
+                return request.getRemoteHost();
+            }
+
+            @Override
+            public int getReportPort() {
+                return request.getRemotePort();
+            }
+        });
+        chain.doFilter(request, response);
+    }
+
+    @Override
+    public void destroy() {
+        //To change body of implemented methods use File | Settings | File Templates.
+    }
+}
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index fe86e5b..b24de1b 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -14,6 +14,7 @@ import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.ClientConnection;
 import org.keycloak.services.resources.RealmsResource;
 import org.keycloak.spi.authentication.AuthProviderStatus;
 import org.keycloak.spi.authentication.AuthResult;
@@ -42,6 +43,15 @@ public class AuthenticationManager {
     public static final String KEYCLOAK_IDENTITY_COOKIE = "KEYCLOAK_IDENTITY";
     public static final String KEYCLOAK_REMEMBER_ME = "KEYCLOAK_REMEMBER_ME";
 
+    protected BruteForceProtector protector;
+
+    public AuthenticationManager() {
+    }
+
+    public AuthenticationManager(BruteForceProtector protector) {
+        this.protector = protector;
+    }
+
     public AccessToken createIdentityToken(RealmModel realm, UserModel user) {
         logger.info("createIdentityToken");
         AccessToken token = new AccessToken();
@@ -180,13 +190,37 @@ public class AuthenticationManager {
         return null;
     }
 
-    public AuthenticationStatus authenticateForm(RealmModel realm, MultivaluedMap<String, String> formData) {
+    public AuthenticationStatus authenticateForm(ClientConnection clientConnection, RealmModel realm, MultivaluedMap<String, String> formData) {
         String username = formData.getFirst(FORM_USERNAME);
         if (username == null) {
             logger.warn("Username not provided");
             return AuthenticationStatus.INVALID_USER;
         }
 
+        AuthenticationStatus status = authenticateInternal(realm, formData, username);
+        if (realm.isBruteForceProtected()) {
+            switch (status) {
+                case SUCCESS:
+                    protector.successfulLogin(realm, username, clientConnection);
+                    break;
+                case FAILED:
+                case MISSING_TOTP:
+                case MISSING_PASSWORD:
+                case INVALID_CREDENTIALS:
+                    protector.failedLogin(realm, username, clientConnection);
+                    break;
+                case INVALID_USER:
+                    protector.invalidUser(realm, username, clientConnection);
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        return status;
+    }
+
+    protected AuthenticationStatus authenticateInternal(RealmModel realm, MultivaluedMap<String, String> formData, String username) {
         UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username);
 
         Set<String> types = new HashSet<String>();
diff --git a/services/src/main/java/org/keycloak/services/managers/BruteForceProtector.java b/services/src/main/java/org/keycloak/services/managers/BruteForceProtector.java
new file mode 100755
index 0000000..c8190f4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/managers/BruteForceProtector.java
@@ -0,0 +1,224 @@
+package org.keycloak.services.managers;
+
+
+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;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A single thread will log failures.  This is so that we can avoid concurrent writes as we want an accurate failure count
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class BruteForceProtector implements Runnable {
+    protected static Logger logger = Logger.getLogger(BruteForceProtector.class);
+
+    protected int maxFailureWaitSeconds = 900;
+    protected int minimumQuickLoginWaitSeconds = 60;
+    protected int waitIncrementSeconds = 60;
+    protected long quickLoginCheckMilliSeconds = 1000;
+    protected int maxDeltaTime = 60 * 60 * 24 * 1000;
+    protected int failureFactor = 10;
+    protected volatile boolean run = true;
+    protected KeycloakSessionFactory factory;
+    protected CountDownLatch shutdownLatch = new CountDownLatch(1);
+
+    protected volatile long failures;
+    protected volatile long lastFailure;
+    protected volatile long totalTime;
+
+    protected LinkedBlockingQueue<LoginEvent> queue = new LinkedBlockingQueue<LoginEvent>();
+    public static final int TRANSACTION_SIZE = 20;
+
+
+    protected abstract class LoginEvent implements Comparable<LoginEvent> {
+        protected final String realmId;
+        protected final String username;
+        protected final String ip;
+
+        protected LoginEvent(String realmId, String username, String ip) {
+            this.realmId = realmId;
+            this.username = username;
+            this.ip = ip;
+        }
+
+        @Override
+        public int compareTo(LoginEvent o) {
+            return username.compareTo(o.username);
+        }
+    }
+
+    protected class SuccessfulLogin extends LoginEvent {
+        public SuccessfulLogin(String realmId, String userId, String ip) {
+            super(realmId, userId, ip);
+        }
+    }
+
+    protected class FailedLogin extends LoginEvent {
+        protected final CountDownLatch latch = new CountDownLatch(1);
+
+        public FailedLogin(String realmId, String username, String ip) {
+            super(realmId, username, ip);
+        }
+    }
+
+    public BruteForceProtector(KeycloakSessionFactory factory) {
+        this.factory = factory;
+    }
+
+    public void failure(KeycloakSession session, LoginEvent event) {
+        UsernameLoginFailureModel user = getUserModel(session, event);
+        if (user == null) return;
+        user.setLastIPFailure(event.ip);
+        long currentTime = System.currentTimeMillis();
+        long last = user.getLastFailure();
+        long deltaTime = 0;
+        if (last > 0) {
+            deltaTime = currentTime - last;
+        }
+        user.setLastFailure(currentTime);
+        if (deltaTime > 0) {
+            // if last failure was more than MAX_DELTA clear failures
+            if (deltaTime > maxDeltaTime) {
+                user.clearFailures();
+            }
+        }
+        user.incrementFailures();
+
+        int waitSeconds = waitIncrementSeconds * (user.getNumFailures() / failureFactor);
+        if (waitSeconds == 0) {
+            if (deltaTime > quickLoginCheckMilliSeconds) {
+                waitSeconds = minimumQuickLoginWaitSeconds;
+            }
+        }
+        waitSeconds = Math.min(maxFailureWaitSeconds, waitSeconds);
+        if (waitSeconds > 0) {
+            user.setFailedLoginNotBefore((int) (currentTime / 1000) + waitSeconds);
+        }
+    }
+
+    protected UsernameLoginFailureModel getUserModel(KeycloakSession session, LoginEvent event) {
+        RealmModel realm = session.getRealm(event.realmId);
+        if (realm == null) return null;
+        UsernameLoginFailureModel user = realm.getUserLoginFailure(event.username);
+        if (user == null) return null;
+        return user;
+    }
+
+    public void start() {
+        new Thread(this).start();
+    }
+
+    public void shutdown() {
+        run = false;
+        try {
+            shutdownLatch.await(5, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+
+    public void run() {
+        final ArrayList<LoginEvent> events = new ArrayList<LoginEvent>(TRANSACTION_SIZE + 1);
+        while (run) {
+            try {
+                LoginEvent take = queue.poll(2, TimeUnit.SECONDS);
+                if (take == null) {
+                    continue;
+                }
+                try {
+                    events.add(take);
+                    queue.drainTo(events, TRANSACTION_SIZE);
+                    for (LoginEvent event : events) {
+                        if (event instanceof FailedLogin) {
+                            logFailure(event);
+                        } else {
+                            logSuccess(event);
+                        }
+                    }
+
+                    Collections.sort(events); // we sort to avoid deadlock due to ordered updates.  Maybe I'm overthinking this.
+                    KeycloakSession session = factory.createSession();
+                    try {
+                        for (LoginEvent event : events) {
+                            if (event instanceof FailedLogin) {
+                                failure(session, event);
+                            }
+                        }
+                        session.getTransaction().commit();
+                    } catch (Exception e) {
+                        session.getTransaction().rollback();
+                        throw e;
+                    } finally {
+                        for (LoginEvent event : events) {
+                            if (event instanceof FailedLogin) {
+                                ((FailedLogin) event).latch.countDown();
+                            }
+                        }
+                        events.clear();
+                        session.close();
+                    }
+                } catch (Exception e) {
+                    logger.error("Failed processing event", e);
+                }
+            } catch (InterruptedException e) {
+                break;
+            } finally {
+                shutdownLatch.countDown();
+            }
+        }
+    }
+
+    protected void logSuccess(LoginEvent event) {
+        logger.warn("login success for user " + event.username + " from ip " + event.ip);
+    }
+
+    protected void logFailure(LoginEvent event) {
+        logger.warn("login failure for user " + event.username + " from ip " + event.ip);
+        failures++;
+        long delta = 0;
+        if (lastFailure > 0) {
+            delta = System.currentTimeMillis() - lastFailure;
+            if (delta > maxDeltaTime) {
+                totalTime = 0;
+
+            } else {
+                totalTime += delta;
+            }
+        }
+    }
+
+    public void successfulLogin(RealmModel realm, String username, ClientConnection clientConnection) {
+        logger.info("successful login user: " + username + " from ip " + clientConnection.getRemoteAddr());
+    }
+
+    public void invalidUser(RealmModel realm, String username, ClientConnection clientConnection) {
+        logger.warn("invalid user: " + username + " from ip " + clientConnection.getRemoteAddr());
+        // todo more?
+    }
+
+    public void failedLogin(RealmModel realm, String username, ClientConnection clientConnection) {
+        try {
+            FailedLogin event = new FailedLogin(realm.getId(), username, clientConnection.getRemoteAddr());
+            queue.offer(event);
+            // wait a minimum of seconds for event to process so that a hacker
+            // cannot flood with failed logins and overwhelm the queue and not have notBefore updated to block next requests
+            // todo failure HTTP responses should be queued via async HTTP
+            event.latch.await(5, TimeUnit.SECONDS);
+
+        } catch (InterruptedException e) {
+        }
+    }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java
index 1506a60..63f8d98 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -20,6 +20,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.ClientConnection;
 import org.keycloak.services.managers.AccessCodeEntry;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
@@ -91,6 +92,8 @@ public class TokenService {
     protected KeycloakSession session;
     @Context
     protected KeycloakTransaction transaction;
+    @Context
+    protected ClientConnection clientConnection;
 
     @Context
     protected ResourceContext resourceContext;
@@ -158,7 +161,7 @@ public class TokenService {
             throw new NotAuthorizedException("Disabled realm");
         }
 
-        if (authManager.authenticateForm(realm, form) != AuthenticationStatus.SUCCESS) {
+        if (authManager.authenticateForm(clientConnection, realm, form) != AuthenticationStatus.SUCCESS) {
             throw new NotAuthorizedException("Auth failed");
         }
 
@@ -234,7 +237,7 @@ public class TokenService {
             return oauth.redirectError(client, "access_denied", state, redirect);
         }
 
-        AuthenticationStatus status = authManager.authenticateForm(realm, formData);
+        AuthenticationStatus status = authManager.authenticateForm(clientConnection, realm, formData);
 
         String rememberMe = formData.getFirst("rememberMe");
         boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on");
diff --git a/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java b/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java
old mode 100644
new mode 100755
index 9d87e76..3a4da3e
--- a/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java
+++ b/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java
@@ -1,101 +1,101 @@
-package org.keycloak.spi.authentication.picketlink;
-
-import java.util.Map;
-
-import org.jboss.logging.Logger;
-import org.jboss.resteasy.spi.ResteasyProviderFactory;
-import org.keycloak.models.RealmModel;
-import org.keycloak.spi.authentication.AuthProviderStatus;
-import org.keycloak.spi.authentication.AuthResult;
-import org.keycloak.spi.authentication.AuthProviderConstants;
-import org.keycloak.spi.authentication.AuthenticatedUser;
-import org.keycloak.spi.authentication.AuthenticationProvider;
-import org.keycloak.spi.authentication.AuthenticationProviderException;
-import org.keycloak.spi.picketlink.PartitionManagerProvider;
-import org.keycloak.util.ProviderLoader;
-import org.picketlink.idm.IdentityManager;
-import org.picketlink.idm.PartitionManager;
-import org.picketlink.idm.credential.Credentials;
-import org.picketlink.idm.credential.Password;
-import org.picketlink.idm.credential.UsernamePasswordCredentials;
-import org.picketlink.idm.model.basic.BasicModel;
-import org.picketlink.idm.model.basic.User;
-
-/**
- * AuthenticationProvider, which delegates authentication to picketlink
- *
- * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
- */
-public class PicketlinkAuthenticationProvider implements AuthenticationProvider {
-
-    private static final Logger logger = Logger.getLogger(PicketlinkAuthenticationProvider.class);
-
-    @Override
-    public String getName() {
-        return AuthProviderConstants.PROVIDER_NAME_PICKETLINK;
-    }
-
-    @Override
-    public AuthResult validatePassword(RealmModel realm, Map<String, String> configuration, String username, String password) throws AuthenticationProviderException {
-        IdentityManager identityManager = getIdentityManager(realm);
-
-        User picketlinkUser = BasicModel.getUser(identityManager, username);
-        if (picketlinkUser == null) {
-            return new AuthResult(AuthProviderStatus.USER_NOT_FOUND);
-        }
-
-        UsernamePasswordCredentials credential = new UsernamePasswordCredentials();
-        credential.setUsername(username);
-        credential.setPassword(new Password(password.toCharArray()));
-        identityManager.validateCredentials(credential);
-        if (credential.getStatus() == Credentials.Status.VALID) {
-            AuthResult result = new AuthResult(AuthProviderStatus.SUCCESS);
-
-            AuthenticatedUser authenticatedUser = new AuthenticatedUser(picketlinkUser.getId(), picketlinkUser.getLoginName())
-                    .setName(picketlinkUser.getFirstName(), picketlinkUser.getLastName())
-                    .setEmail(picketlinkUser.getEmail());
-            result.setUser(authenticatedUser).setProviderName(getName());
-            return result;
-        } else {
-            return new AuthResult(AuthProviderStatus.INVALID_CREDENTIALS);
-        }
-    }
-
-    @Override
-    public boolean updateCredential(RealmModel realm, Map<String, String> configuration, String username, String password) throws AuthenticationProviderException {
-        IdentityManager identityManager = getIdentityManager(realm);
-
-        User picketlinkUser = BasicModel.getUser(identityManager, username);
-        if (picketlinkUser == null) {
-            logger.debugf("User '%s' doesn't exists. Skip password update", username);
-            return false;
-        }
-
-        identityManager.updateCredential(picketlinkUser, new Password(password.toCharArray()));
-        return true;
-    }
-
-    public IdentityManager getIdentityManager(RealmModel realm) throws AuthenticationProviderException {
-        IdentityManager identityManager = ResteasyProviderFactory.getContextData(IdentityManager.class);
-        if (identityManager == null) {
-            Iterable<PartitionManagerProvider> providers = ProviderLoader.load(PartitionManagerProvider.class);
-
-            // TODO: Priority?
-            PartitionManager partitionManager = null;
-            for (PartitionManagerProvider provider : providers) {
-                partitionManager = provider.getPartitionManager(realm);
-                if (partitionManager != null) {
-                    break;
-                }
-            }
-
-            if (partitionManager == null) {
-                throw new AuthenticationProviderException("Not able to locate PartitionManager with any PartitionManagerProvider");
-            }
-
-            identityManager = partitionManager.createIdentityManager();
-            ResteasyProviderFactory.pushContext(IdentityManager.class, identityManager);
-        }
-        return identityManager;
-    }
-}
+package org.keycloak.spi.authentication.picketlink;
+
+import java.util.Map;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.spi.authentication.AuthProviderStatus;
+import org.keycloak.spi.authentication.AuthResult;
+import org.keycloak.spi.authentication.AuthProviderConstants;
+import org.keycloak.spi.authentication.AuthenticatedUser;
+import org.keycloak.spi.authentication.AuthenticationProvider;
+import org.keycloak.spi.authentication.AuthenticationProviderException;
+import org.keycloak.spi.picketlink.PartitionManagerProvider;
+import org.keycloak.util.ProviderLoader;
+import org.picketlink.idm.IdentityManager;
+import org.picketlink.idm.PartitionManager;
+import org.picketlink.idm.credential.Credentials;
+import org.picketlink.idm.credential.Password;
+import org.picketlink.idm.credential.UsernamePasswordCredentials;
+import org.picketlink.idm.model.basic.BasicModel;
+import org.picketlink.idm.model.basic.User;
+
+/**
+ * AuthenticationProvider, which delegates authentication to picketlink
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class PicketlinkAuthenticationProvider implements AuthenticationProvider {
+
+    private static final Logger logger = Logger.getLogger(PicketlinkAuthenticationProvider.class);
+
+    @Override
+    public String getName() {
+        return AuthProviderConstants.PROVIDER_NAME_PICKETLINK;
+    }
+
+    @Override
+    public AuthResult validatePassword(RealmModel realm, Map<String, String> configuration, String username, String password) throws AuthenticationProviderException {
+        IdentityManager identityManager = getIdentityManager(realm);
+
+        User picketlinkUser = BasicModel.getUser(identityManager, username);
+        if (picketlinkUser == null) {
+            return new AuthResult(AuthProviderStatus.USER_NOT_FOUND);
+        }
+
+        UsernamePasswordCredentials credential = new UsernamePasswordCredentials();
+        credential.setUsername(username);
+        credential.setPassword(new Password(password.toCharArray()));
+        identityManager.validateCredentials(credential);
+        if (credential.getStatus() == Credentials.Status.VALID) {
+            AuthResult result = new AuthResult(AuthProviderStatus.SUCCESS);
+
+            AuthenticatedUser authenticatedUser = new AuthenticatedUser(picketlinkUser.getId(), picketlinkUser.getLoginName())
+                    .setName(picketlinkUser.getFirstName(), picketlinkUser.getLastName())
+                    .setEmail(picketlinkUser.getEmail());
+            result.setUser(authenticatedUser).setProviderName(getName());
+            return result;
+        } else {
+            return new AuthResult(AuthProviderStatus.INVALID_CREDENTIALS);
+        }
+    }
+
+    @Override
+    public boolean updateCredential(RealmModel realm, Map<String, String> configuration, String username, String password) throws AuthenticationProviderException {
+        IdentityManager identityManager = getIdentityManager(realm);
+
+        User picketlinkUser = BasicModel.getUser(identityManager, username);
+        if (picketlinkUser == null) {
+            logger.debugf("User '%s' doesn't exists. Skip password update", username);
+            return false;
+        }
+
+        identityManager.updateCredential(picketlinkUser, new Password(password.toCharArray()));
+        return true;
+    }
+
+    public IdentityManager getIdentityManager(RealmModel realm) throws AuthenticationProviderException {
+        IdentityManager identityManager = ResteasyProviderFactory.getContextData(IdentityManager.class);
+        if (identityManager == null) {
+            Iterable<PartitionManagerProvider> providers = ProviderLoader.load(PartitionManagerProvider.class);
+
+            // TODO: Priority?
+            PartitionManager partitionManager = null;
+            for (PartitionManagerProvider provider : providers) {
+                partitionManager = provider.getPartitionManager(realm);
+                if (partitionManager != null) {
+                    break;
+                }
+            }
+
+            if (partitionManager == null) {
+                throw new AuthenticationProviderException("Not able to locate PartitionManager with any PartitionManagerProvider");
+            }
+
+            identityManager = partitionManager.createIdentityManager();
+            ResteasyProviderFactory.pushContext(IdentityManager.class, identityManager);
+        }
+        return identityManager;
+    }
+}
diff --git a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthResult.java b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthResult.java
old mode 100644
new mode 100755
index fd52d67..297e5fe
--- a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthResult.java
+++ b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthResult.java
@@ -1,44 +1,44 @@
-package org.keycloak.spi.authentication;
-
-/**
- * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
- */
-public class AuthResult {
-
-    // Status of authentication
-    private final AuthProviderStatus authProviderStatus;
-
-    // Provider, which authenticated user
-    private String providerName;
-
-    // filled usually only in case of successful authentication and just with some Authentication providers
-    private AuthenticatedUser authenticatedUser;
-
-    public AuthResult(AuthProviderStatus authProviderStatus) {
-        this.authProviderStatus = authProviderStatus;
-    }
-
-    public AuthResult setProviderName(String providerName) {
-        this.providerName = providerName;
-        return this;
-    }
-
-    public AuthResult setUser(AuthenticatedUser user) {
-        this.authenticatedUser = user;
-        return this;
-    }
-
-    public AuthProviderStatus getAuthProviderStatus() {
-        return authProviderStatus;
-    }
-
-    public String getProviderName() {
-        return providerName;
-    }
-
-    public AuthenticatedUser getAuthenticatedUser() {
-        return authenticatedUser;
-    }
-
-
-}
+package org.keycloak.spi.authentication;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AuthResult {
+
+    // Status of authentication
+    private final AuthProviderStatus authProviderStatus;
+
+    // Provider, which authenticated user
+    private String providerName;
+
+    // filled usually only in case of successful authentication and just with some Authentication providers
+    private AuthenticatedUser authenticatedUser;
+
+    public AuthResult(AuthProviderStatus authProviderStatus) {
+        this.authProviderStatus = authProviderStatus;
+    }
+
+    public AuthResult setProviderName(String providerName) {
+        this.providerName = providerName;
+        return this;
+    }
+
+    public AuthResult setUser(AuthenticatedUser user) {
+        this.authenticatedUser = user;
+        return this;
+    }
+
+    public AuthProviderStatus getAuthProviderStatus() {
+        return authProviderStatus;
+    }
+
+    public String getProviderName() {
+        return providerName;
+    }
+
+    public AuthenticatedUser getAuthenticatedUser() {
+        return authenticatedUser;
+    }
+
+
+}
diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java
index 0a0e463..6848513 100755
--- a/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java
+++ b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java
@@ -38,6 +38,7 @@ import org.jboss.resteasy.logging.Logger;
 import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer;
 import org.jboss.resteasy.spi.ResteasyDeployment;
 import org.keycloak.models.Config;
+import org.keycloak.services.filters.ClientConnectionFilter;
 import org.keycloak.theme.DefaultLoginThemeProvider;
 import org.keycloak.services.tmp.TmpAdminRedirectServlet;
 import org.keycloak.util.JsonSerialization;
@@ -263,6 +264,10 @@ public class KeycloakServer {
         di.addFilter(filter);
         di.addFilterUrlMapping("SessionFilter", "/rest/*", DispatcherType.REQUEST);
 
+        FilterInfo connectionFilter = Servlets.filter("ClientConnectionFilter", ClientConnectionFilter.class);
+        di.addFilter(connectionFilter);
+        di.addFilterUrlMapping("ClientConnectionFilter", "/rest/*", DispatcherType.REQUEST);
+
         ServletInfo tmpAdminRedirectServlet = Servlets.servlet("TmpAdminRedirectServlet", TmpAdminRedirectServlet.class);
         tmpAdminRedirectServlet.addMappings("/admin", "/admin/");
         di.addServlet(tmpAdminRedirectServlet);