From d58870545f84d4431502425dbdbcc6e00339a199 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Wed, 2 Apr 2014 20:09:14 -0400 Subject: [PATCH] brute force protection --- .../java/org/keycloak/models/RealmModel.java | 7 + .../java/org/keycloak/models/UserModel.java | 1 + .../models/UsernameLoginFailureModel.java | 21 ++ .../org/keycloak/models/jpa/RealmAdapter.java | 36 +++ .../org/keycloak/models/jpa/UserAdapter.java | 3 + .../jpa/UsernameLoginFailureAdapter.java | 70 ++++++ .../models/jpa/entities/RealmEntity.java | 9 + .../models/jpa/entities/UserEntity.java | 1 + .../entities/UsernameLoginFailureEntity.java | 82 +++++++ .../mongo/keycloak/adapters/RealmAdapter.java | 42 ++++ .../mongo/keycloak/adapters/UserAdapter.java | 3 + .../adapters/UsernameLoginFailureAdapter.java | 73 ++++++ .../mongo/keycloak/entities/RealmEntity.java | 10 + .../mongo/keycloak/entities/UserEntity.java | 42 ++++ .../entities/UsernameLoginFailureEntity.java | 76 ++++++ .../test/AuthProvidersExternalModelTest.java | 14 +- .../model/test/AuthProvidersLDAPTest.java | 16 +- .../model/test/AuthenticationManagerTest.java | 20 +- server/src/main/webapp/WEB-INF/web.xml | 10 + .../keycloak/services/ClientConnection.java | 13 + .../filters/ClientConnectionFilter.java | 49 ++++ .../managers/AuthenticationManager.java | 36 ++- .../managers/BruteForceProtector.java | 224 ++++++++++++++++++ .../services/resources/TokenService.java | 7 +- .../PicketlinkAuthenticationProvider.java | 202 ++++++++-------- .../spi/authentication/AuthResult.java | 88 +++---- .../keycloak/testutils/KeycloakServer.java | 5 + 27 files changed, 987 insertions(+), 173 deletions(-) create mode 100755 model/api/src/main/java/org/keycloak/models/UsernameLoginFailureModel.java create mode 100755 model/jpa/src/main/java/org/keycloak/models/jpa/UsernameLoginFailureAdapter.java create mode 100755 model/jpa/src/main/java/org/keycloak/models/jpa/entities/UsernameLoginFailureEntity.java create mode 100755 model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UsernameLoginFailureAdapter.java create mode 100755 model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UsernameLoginFailureEntity.java mode change 100644 => 100755 model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java mode change 100644 => 100755 model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java create mode 100755 services/src/main/java/org/keycloak/services/ClientConnection.java create mode 100755 services/src/main/java/org/keycloak/services/filters/ClientConnectionFilter.java create mode 100755 services/src/main/java/org/keycloak/services/managers/BruteForceProtector.java mode change 100644 => 100755 spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java mode change 100644 => 100755 spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthResult.java 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 e07bdb95c5..308dd7caac 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 getUsers(); List 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 95d6613a05..762acc8f8f 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 0000000000..59316ae547 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/UsernameLoginFailureModel.java @@ -0,0 +1,21 @@ +package org.keycloak.models; + +/** + * @author Bill Burke + * @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/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 17c5a1a8f8..05136508db 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; @@ -122,6 +124,16 @@ public class RealmAdapter implements RealmModel { em.flush(); } + @Override + public boolean isBruteForceProtected() { + return realm.isBruteForceProtected(); + } + + @Override + public void setBruteForceProtected(boolean value) { + realm.setBruteForceProtected(value); + } + @Override public boolean isVerifyEmail() { return realm.isVerifyEmail(); @@ -339,6 +351,27 @@ public class RealmAdapter implements RealmModel { return new UserAdapter(results.get(0)); } + @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 query = em.createNamedQuery("getRealmUserByEmail", UserEntity.class); @@ -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 4eec0cab3e..50daa11449 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 0000000000..ba43c82388 --- /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 Bill Burke + * @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/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 04051edddd..255b73737c 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 ab25a647db..156aaaa28b 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 0000000000..6011cccfdc --- /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 Bill Burke + * @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/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 68a604f8b1..6f078855e6 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 implements R updateRealm(); } + @Override + public boolean isBruteForceProtected() { + return realm.isBruteForceProtected(); + } + + @Override + public void setBruteForceProtected(boolean value) { + realm.setBruteForceProtected(value); + } @Override public boolean isVerifyEmail() { @@ -339,6 +349,38 @@ public class RealmAdapter extends AbstractMongoAdapter 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() 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 5d3ed09068..8958e32afd 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 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 0000000000..0722945a2d --- /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 Bill Burke + * @version $Revision: 1 $ + */ +public class UsernameLoginFailureAdapter extends AbstractMongoAdapter 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 cf37018970..991bd07b86 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; @@ -134,6 +135,15 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong this.updateProfileOnInitialSocialLogin = updateProfileOnInitialSocialLogin; } + @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 f0539df323..4468fd9f7d 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 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 0000000000..4852626f4d --- /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 Bill Burke + * @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/AuthProvidersExternalModelTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java old mode 100644 new mode 100755 index f8ce23b442..c0c531c3a7 --- 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 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 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 96157fe762..07526c2eeb --- 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 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 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/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java index 513c119f97..1abcf5fbd6 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/server/src/main/webapp/WEB-INF/web.xml b/server/src/main/webapp/WEB-INF/web.xml index 45d0d0479f..626ec535d0 100755 --- a/server/src/main/webapp/WEB-INF/web.xml +++ b/server/src/main/webapp/WEB-INF/web.xml @@ -34,6 +34,11 @@ index.html + + Keycloak Client Connection Filter + org.keycloak.services.filters.ClientConnectionFilter + + Keycloak Session Management org.keycloak.services.filters.KeycloakSessionServletFilter @@ -44,6 +49,11 @@ /rest/* + + Keycloak Client Connection Filter + /rest/* + + Resteasy /rest/* 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 0000000000..61491a42a5 --- /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 Bill Burke + * @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 0000000000..d74a150bb8 --- /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 Bill Burke + * @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 fe86e5b603..b24de1bfaf 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 formData) { + public AuthenticationStatus authenticateForm(ClientConnection clientConnection, RealmModel realm, MultivaluedMap 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 formData, String username) { UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username); Set types = new HashSet(); 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 0000000000..c8190f49a0 --- /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 Bill Burke + * @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 queue = new LinkedBlockingQueue(); + public static final int TRANSACTION_SIZE = 20; + + + protected abstract class LoginEvent implements Comparable { + 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 events = new ArrayList(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 1506a60c85..63f8d981f3 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 9d87e769d7..3a4da3e70e --- 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 Marek Posolda - */ -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 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 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 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 Marek Posolda + */ +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 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 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 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 fd52d679aa..297e5fe048 --- 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 Marek Posolda - */ -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 Marek Posolda + */ +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 0a0e463066..6848513614 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);