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 1fd0843adf..db271f2555 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); @@ -146,6 +150,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 c0483afdab..0275f40b15 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; @@ -124,6 +126,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(); @@ -341,6 +353,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); @@ -361,6 +394,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 1863cb7cc1..1e3a9d723d 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 @@ -42,6 +42,7 @@ public class RealmEntity { protected boolean resetPasswordAllowed; protected boolean social; protected boolean rememberMe; + protected boolean bruteForceProtected; @Column(name="updateProfileOnInitSocLogin") protected boolean updateProfileOnInitialSocialLogin; @@ -340,6 +341,14 @@ public class RealmEntity { this.notBefore = notBefore; } + public boolean isBruteForceProtected() { + return bruteForceProtected; + } + + public void setBruteForceProtected(boolean bruteForceProtected) { + this.bruteForceProtected = bruteForceProtected; + } + public Set getAuditListeners() { return auditListeners; } 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 9d8435d61d..1ffba3ba80 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 @@ -50,6 +50,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 e5f66966b4..8d3943b257 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; @@ -123,6 +124,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() { @@ -340,6 +350,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 69d4b2a743..58b9ba295d 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 @@ -32,6 +32,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; @@ -139,6 +140,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 96b87919eb..40aeae5cb2 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 setAuthenticationLink(AuthenticationLinkEntity authenticationLink) { this.authenticationLink = authenticationLink; } + + @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 e365d1e134..05374a5765 --- a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java @@ -72,10 +72,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 @@ -86,7 +86,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()); @@ -128,8 +128,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 @@ -143,8 +143,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.INVALID_CREDENTIALS, am.authenticateForm(realm2, formData)); + Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm1, formData)); + Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, 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 1711d22d49..91f99dde7a --- 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,25 +121,25 @@ 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)); // Successful authentication realmUser.setEnabled(true); formData = AuthProvidersExternalModelTest.createFormData("realmUser", "pass"); - Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm, formData)); + Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData)); } finally { ResteasyProviderFactory.clearContextData(); } @@ -158,7 +158,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest { // First authenticate successfully to sync john into realm MultivaluedMap formData = AuthProvidersExternalModelTest.createFormData("john", "password"); - Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm, formData)); + Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData)); // Change credential and validate that user can authenticate AuthenticationProviderManager authProviderManager = AuthenticationProviderManager.getManager(realm); @@ -171,7 +171,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest { Assert.fail("Error not expected"); } 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(john, "password-updated")); @@ -187,7 +187,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 8e9b7c3bb9..6c1cece290 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 @@ -29,7 +29,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); } @@ -38,7 +38,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); } @@ -46,7 +46,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); } @@ -54,7 +54,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); } @@ -63,7 +63,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); } @@ -71,7 +71,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); } @@ -93,7 +93,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); } @@ -104,7 +104,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); } @@ -115,7 +115,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); } @@ -125,7 +125,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 ff6c61207c..8bb9e53be4 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.AuthUser; @@ -41,6 +42,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(); @@ -179,13 +189,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); if (user == null) { AuthUser authUser = AuthenticationProviderManager.getManager(realm).getUser(username); 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 7a91471153..b326216889 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -24,6 +24,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; @@ -96,6 +97,8 @@ public class TokenService { protected KeycloakSession session; @Context protected KeycloakTransaction transaction; + @Context + protected ClientConnection clientConnection; @Context protected ResourceContext resourceContext; @@ -170,7 +173,7 @@ public class TokenService { throw new NotAuthorizedException("Disabled realm"); } - if (authManager.authenticateForm(realm, form) != AuthenticationStatus.SUCCESS) { + if (authManager.authenticateForm(clientConnection, realm, form) != AuthenticationStatus.SUCCESS) { audit.error(Errors.INVALID_USER_CREDENTIALS); throw new NotAuthorizedException("Auth failed"); } @@ -279,7 +282,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); if (remember) { NewCookie cookie = authManager.createRememberMeCookie(realm, uriInfo); 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 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);