keycloak-aplcache
Changes
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UsernameLoginFailureAdapter.java 73(+73 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UsernameLoginFailureEntity.java 76(+76 -0)
server/src/main/webapp/WEB-INF/web.xml 10(+10 -0)
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();
}
server/src/main/webapp/WEB-INF/web.xml 10(+10 -0)
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);