brute force protection

This commit is contained in:
Bill Burke 2014-04-02 20:09:14 -04:00
parent d8a3025ea1
commit d58870545f
27 changed files with 987 additions and 173 deletions

View file

@ -33,6 +33,10 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
void setRememberMe(boolean rememberMe); void setRememberMe(boolean rememberMe);
boolean isBruteForceProtected();
void setBruteForceProtected(boolean value);
boolean isVerifyEmail(); boolean isVerifyEmail();
void setVerifyEmail(boolean verifyEmail); void setVerifyEmail(boolean verifyEmail);
@ -148,6 +152,9 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
public void setUpdateProfileOnInitialSocialLogin(boolean updateProfileOnInitialSocialLogin); public void setUpdateProfileOnInitialSocialLogin(boolean updateProfileOnInitialSocialLogin);
public UsernameLoginFailureModel getUserLoginFailure(String username);
UsernameLoginFailureModel addUserLoginFailure(String username);
List<UserModel> getUsers(); List<UserModel> getUsers();
List<UserModel> searchForUser(String search); List<UserModel> searchForUser(String search);

View file

@ -58,6 +58,7 @@ public interface UserModel {
int getNotBefore(); int getNotBefore();
void setNotBefore(int notBefore); void setNotBefore(int notBefore);
public static enum RequiredAction { public static enum RequiredAction {
VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD
} }

View file

@ -0,0 +1,21 @@
package org.keycloak.models;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UsernameLoginFailureModel
{
String getUsername();
int getFailedLoginNotBefore();
void setFailedLoginNotBefore(int notBefore);
int getNumFailures();
void incrementFailures();
void clearFailures();
long getLastFailure();
void setLastFailure(long lastFailure);
String getLastIPFailure();
void setLastIPFailure(String ip);
}

View file

@ -4,6 +4,7 @@ import org.keycloak.models.AuthenticationLinkModel;
import org.keycloak.models.AuthenticationProviderModel; import org.keycloak.models.AuthenticationProviderModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.UsernameLoginFailureModel;
import org.keycloak.models.jpa.entities.ApplicationEntity; import org.keycloak.models.jpa.entities.ApplicationEntity;
import org.keycloak.models.jpa.entities.ApplicationRoleEntity; import org.keycloak.models.jpa.entities.ApplicationRoleEntity;
import org.keycloak.models.jpa.entities.AuthenticationLinkEntity; 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.SocialLinkEntity;
import org.keycloak.models.jpa.entities.UserEntity; import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.jpa.entities.UserRoleMappingEntity; 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.KeycloakModelUtils;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder; import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import org.keycloak.models.ApplicationModel; import org.keycloak.models.ApplicationModel;
@ -122,6 +124,16 @@ public class RealmAdapter implements RealmModel {
em.flush(); em.flush();
} }
@Override
public boolean isBruteForceProtected() {
return realm.isBruteForceProtected();
}
@Override
public void setBruteForceProtected(boolean value) {
realm.setBruteForceProtected(value);
}
@Override @Override
public boolean isVerifyEmail() { public boolean isVerifyEmail() {
return realm.isVerifyEmail(); return realm.isVerifyEmail();
@ -339,6 +351,27 @@ public class RealmAdapter implements RealmModel {
return new UserAdapter(results.get(0)); 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 @Override
public UserModel getUserByEmail(String email) { public UserModel getUserByEmail(String email) {
TypedQuery<UserEntity> query = em.createNamedQuery("getRealmUserByEmail", UserEntity.class); TypedQuery<UserEntity> query = em.createNamedQuery("getRealmUserByEmail", UserEntity.class);
@ -359,6 +392,9 @@ public class RealmAdapter implements RealmModel {
@Override @Override
public UserModel addUser(String username) { public UserModel addUser(String username) {
if (getUser(username) != null) {
throw new RuntimeException("Username already exists: " + username);
}
UserEntity entity = new UserEntity(); UserEntity entity = new UserEntity();
entity.setLoginName(username); entity.setLoginName(username);
entity.setRealm(realm); entity.setRealm(realm);

View file

@ -154,4 +154,7 @@ public class UserAdapter implements UserModel {
public void setNotBefore(int notBefore) { public void setNotBefore(int notBefore) {
user.setNotBefore(notBefore); user.setNotBefore(notBefore);
} }
} }

View file

@ -0,0 +1,70 @@
package org.keycloak.models.jpa;
import org.keycloak.models.UsernameLoginFailureModel;
import org.keycloak.models.jpa.entities.UsernameLoginFailureEntity;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UsernameLoginFailureAdapter implements UsernameLoginFailureModel
{
protected UsernameLoginFailureEntity user;
public UsernameLoginFailureAdapter(UsernameLoginFailureEntity user)
{
this.user = user;
}
@Override
public String getUsername()
{
return user.getUsername();
}
@Override
public int getFailedLoginNotBefore() {
return user.getFailedLoginNotBefore();
}
@Override
public void setFailedLoginNotBefore(int notBefore) {
user.setFailedLoginNotBefore(notBefore);
}
@Override
public int getNumFailures() {
return user.getNumFailures();
}
@Override
public void incrementFailures() {
user.setNumFailures(getNumFailures() + 1);
}
@Override
public void clearFailures() {
user.setNumFailures(0);
}
@Override
public long getLastFailure() {
return user.getLastFailure();
}
@Override
public void setLastFailure(long lastFailure) {
user.setLastFailure(lastFailure);
}
@Override
public String getLastIPFailure() {
return user.getLastIPFailure();
}
@Override
public void setLastIPFailure(String ip) {
user.setLastIPFailure(ip);
}
}

View file

@ -39,6 +39,7 @@ public class RealmEntity {
protected boolean resetPasswordAllowed; protected boolean resetPasswordAllowed;
protected boolean social; protected boolean social;
protected boolean rememberMe; protected boolean rememberMe;
protected boolean bruteForceProtected;
@Column(name="updateProfileOnInitSocLogin") @Column(name="updateProfileOnInitSocLogin")
protected boolean updateProfileOnInitialSocialLogin; protected boolean updateProfileOnInitialSocialLogin;
@ -333,5 +334,13 @@ public class RealmEntity {
public void setNotBefore(int notBefore) { public void setNotBefore(int notBefore) {
this.notBefore = notBefore; this.notBefore = notBefore;
} }
public boolean isBruteForceProtected() {
return bruteForceProtected;
}
public void setBruteForceProtected(boolean bruteForceProtected) {
this.bruteForceProtected = bruteForceProtected;
}
} }

View file

@ -48,6 +48,7 @@ public class UserEntity {
protected boolean emailVerified; protected boolean emailVerified;
protected int notBefore; protected int notBefore;
@ManyToOne @ManyToOne
protected RealmEntity realm; protected RealmEntity realm;

View file

@ -0,0 +1,82 @@
package org.keycloak.models.jpa.entities;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@Entity
public class UsernameLoginFailureEntity {
// we manually set the id to be username-realmid
// we may have a concurrent creation of the same login failure entry that we want to avoid
@Id
protected String id;
protected String username;
protected int failedLoginNotBefore;
protected int numFailures;
protected long lastFailure;
protected String lastIPFailure;
@ManyToOne
protected RealmEntity realm;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getFailedLoginNotBefore() {
return failedLoginNotBefore;
}
public void setFailedLoginNotBefore(int failedLoginNotBefore) {
this.failedLoginNotBefore = failedLoginNotBefore;
}
public int getNumFailures() {
return numFailures;
}
public void setNumFailures(int numFailures) {
this.numFailures = numFailures;
}
public long getLastFailure() {
return lastFailure;
}
public void setLastFailure(long lastFailure) {
this.lastFailure = lastFailure;
}
public String getLastIPFailure() {
return lastIPFailure;
}
public void setLastIPFailure(String lastIPFailure) {
this.lastIPFailure = lastIPFailure;
}
public RealmEntity getRealm() {
return realm;
}
public void setRealm(RealmEntity realm) {
this.realm = realm;
}
}

View file

@ -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.RoleEntity;
import org.keycloak.models.mongo.keycloak.entities.SocialLinkEntity; import org.keycloak.models.mongo.keycloak.entities.SocialLinkEntity;
import org.keycloak.models.mongo.keycloak.entities.UserEntity; 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.mongo.utils.MongoModelUtils;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder; import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
@ -122,6 +123,15 @@ public class RealmAdapter extends AbstractMongoAdapter<RealmEntity> implements R
updateRealm(); updateRealm();
} }
@Override
public boolean isBruteForceProtected() {
return realm.isBruteForceProtected();
}
@Override
public void setBruteForceProtected(boolean value) {
realm.setBruteForceProtected(value);
}
@Override @Override
public boolean isVerifyEmail() { public boolean isVerifyEmail() {
@ -339,6 +349,38 @@ public class RealmAdapter extends AbstractMongoAdapter<RealmEntity> implements R
} }
} }
@Override
public UsernameLoginFailureAdapter getUserLoginFailure(String name) {
DBObject query = new QueryBuilder()
.and("username").is(name)
.and("realmId").is(getId())
.get();
UsernameLoginFailureEntity user = getMongoStore().loadSingleEntity(UsernameLoginFailureEntity.class, query, invocationContext);
if (user == null) {
return null;
} else {
return new UsernameLoginFailureAdapter(invocationContext, user);
}
}
@Override
public UsernameLoginFailureAdapter addUserLoginFailure(String username) {
UsernameLoginFailureAdapter userLoginFailure = getUserLoginFailure(username);
if (userLoginFailure != null) {
return userLoginFailure;
}
UsernameLoginFailureEntity userEntity = new UsernameLoginFailureEntity();
userEntity.setUsername(username);
// Compatibility with JPA model, which has user disabled by default
// userEntity.setEnabled(true);
userEntity.setRealmId(getId());
getMongoStore().insertEntity(userEntity, invocationContext);
return new UsernameLoginFailureAdapter(invocationContext, userEntity);
}
@Override @Override
public UserModel getUserByEmail(String email) { public UserModel getUserByEmail(String email) {
DBObject query = new QueryBuilder() DBObject query = new QueryBuilder()

View file

@ -170,4 +170,7 @@ public class UserAdapter extends AbstractMongoAdapter<UserEntity> implements Use
public UserEntity getMongoEntity() { public UserEntity getMongoEntity() {
return user; return user;
} }
} }

View file

@ -0,0 +1,73 @@
package org.keycloak.models.mongo.keycloak.adapters;
import org.keycloak.models.UsernameLoginFailureModel;
import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.mongo.keycloak.entities.UserEntity;
import org.keycloak.models.mongo.keycloak.entities.UsernameLoginFailureEntity;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UsernameLoginFailureAdapter extends AbstractMongoAdapter<UsernameLoginFailureEntity> implements UsernameLoginFailureModel {
protected UsernameLoginFailureEntity user;
public UsernameLoginFailureAdapter(MongoStoreInvocationContext invocationContext, UsernameLoginFailureEntity user) {
super(invocationContext);
this.user = user;
}
@Override
protected UsernameLoginFailureEntity getMongoEntity() {
return user;
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public int getFailedLoginNotBefore() {
return user.getFailedLoginNotBefore();
}
@Override
public void setFailedLoginNotBefore(int notBefore) {
user.setFailedLoginNotBefore(notBefore);
}
@Override
public int getNumFailures() {
return user.getNumFailures();
}
@Override
public void incrementFailures() {
user.setNumFailures(getNumFailures() + 1);
}
@Override
public void clearFailures() {
user.setNumFailures(0);
}
@Override
public long getLastFailure() {
return user.getLastFailure();
}
@Override
public void setLastFailure(long lastFailure) {
user.setLastFailure(lastFailure);
}
@Override
public String getLastIPFailure() {
return user.getLastIPFailure();
}
@Override
public void setLastIPFailure(String ip) {
user.setLastIPFailure(ip);
}}

View file

@ -29,6 +29,7 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong
private boolean social; private boolean social;
private boolean updateProfileOnInitialSocialLogin; private boolean updateProfileOnInitialSocialLogin;
private String passwordPolicy; private String passwordPolicy;
private boolean bruteForceProtected;
private int centralLoginLifespan; private int centralLoginLifespan;
private int accessTokenLifespan; private int accessTokenLifespan;
@ -134,6 +135,15 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong
this.updateProfileOnInitialSocialLogin = updateProfileOnInitialSocialLogin; this.updateProfileOnInitialSocialLogin = updateProfileOnInitialSocialLogin;
} }
@MongoField
public boolean isBruteForceProtected() {
return bruteForceProtected;
}
public void setBruteForceProtected(boolean bruteForceProtected) {
this.bruteForceProtected = bruteForceProtected;
}
@MongoField @MongoField
public String getPasswordPolicy() { public String getPasswordPolicy() {
return passwordPolicy; return passwordPolicy;

View file

@ -24,6 +24,11 @@ public class UserEntity extends AbstractMongoIdentifiableEntity implements Mongo
private boolean totp; private boolean totp;
private boolean enabled; private boolean enabled;
private int notBefore; private int notBefore;
private int failedLoginNotBefore;
private int numFailures;
private long lastFailure;
private String lastIPFailure;
private String realmId; private String realmId;
@ -170,4 +175,41 @@ public class UserEntity extends AbstractMongoIdentifiableEntity implements Mongo
public void setAuthenticationLinks(List<AuthenticationLinkEntity> authenticationLinks) { public void setAuthenticationLinks(List<AuthenticationLinkEntity> authenticationLinks) {
this.authenticationLinks = 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;
}
} }

View file

@ -0,0 +1,76 @@
package org.keycloak.models.mongo.keycloak.entities;
import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity;
import org.keycloak.models.mongo.api.MongoCollection;
import org.keycloak.models.mongo.api.MongoEntity;
import org.keycloak.models.mongo.api.MongoField;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@MongoCollection(collectionName = "userFailures")
public class UsernameLoginFailureEntity extends AbstractMongoIdentifiableEntity implements MongoEntity {
private String username;
private int failedLoginNotBefore;
private int numFailures;
private long lastFailure;
private String lastIPFailure;
private String realmId;
@MongoField
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@MongoField
public int getFailedLoginNotBefore() {
return failedLoginNotBefore;
}
public void setFailedLoginNotBefore(int failedLoginNotBefore) {
this.failedLoginNotBefore = failedLoginNotBefore;
}
@MongoField
public int getNumFailures() {
return numFailures;
}
public void setNumFailures(int numFailures) {
this.numFailures = numFailures;
}
@MongoField
public long getLastFailure() {
return lastFailure;
}
public void setLastFailure(long lastFailure) {
this.lastFailure = lastFailure;
}
@MongoField
public String getLastIPFailure() {
return lastIPFailure;
}
public void setLastIPFailure(String lastIPFailure) {
this.lastIPFailure = lastIPFailure;
}
@MongoField
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.realmId = realmId;
}
}

View file

@ -70,10 +70,10 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest {
MultivaluedMap<String, String> formData = createFormData("john", "password"); MultivaluedMap<String, String> formData = createFormData("john", "password");
// Authenticate user with realm1 // 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 // 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")); Assert.assertNull(realm2.getUser("john"));
// Add externalModel authenticationProvider into realm2 and point to realm1 // Add externalModel authenticationProvider into realm2 and point to realm1
@ -84,7 +84,7 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest {
ResteasyProviderFactory.pushContext(KeycloakSession.class, identitySession); ResteasyProviderFactory.pushContext(KeycloakSession.class, identitySession);
// Authenticate john in realm2 and verify that now he exists here. // 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"); UserModel john2 = realm2.getUser("john");
Assert.assertNotNull(john2); Assert.assertNotNull(john2);
Assert.assertEquals("john", john2.getLoginName()); Assert.assertEquals("john", john2.getLoginName());
@ -121,8 +121,8 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest {
Assert.fail("Error not expected"); Assert.fail("Error not expected");
} }
MultivaluedMap<String, String> formData = createFormData("john", "password-updated"); MultivaluedMap<String, String> formData = createFormData("john", "password-updated");
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm1, formData)); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm1, formData));
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm2, formData)); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm2, formData));
// Switch to disallow password update propagation to realm1 // Switch to disallow password update propagation to realm1
@ -136,8 +136,8 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest {
Assert.fail("Error not expected"); Assert.fail("Error not expected");
} }
formData = createFormData("john", "password-updated2"); formData = createFormData("john", "password-updated2");
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(realm1, formData)); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm1, formData));
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm2, formData)); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm2, formData));
// Allow passwordUpdate propagation again // Allow passwordUpdate propagation again

View file

@ -79,14 +79,14 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
LdapTestUtils.setLdapPassword(realm, "john", "password"); LdapTestUtils.setLdapPassword(realm, "john", "password");
// Verify that user doesn't exists in realm2 and can't authenticate here // 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")); Assert.assertNull(realm.getUser("john"));
// Add ldap authenticationProvider // Add ldap authenticationProvider
setupAuthenticationProviders(); setupAuthenticationProviders();
// Authenticate john and verify that now he exists in realm // 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"); UserModel john = realm.getUser("john");
Assert.assertNotNull(john); Assert.assertNotNull(john);
Assert.assertEquals("john", john.getLoginName()); Assert.assertEquals("john", john.getLoginName());
@ -121,20 +121,20 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
// User doesn't exists // User doesn't exists
MultivaluedMap<String, String> formData = AuthProvidersExternalModelTest.createFormData("invalid", "invalid"); MultivaluedMap<String, String> formData = AuthProvidersExternalModelTest.createFormData("invalid", "invalid");
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(realm, formData)); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(null, realm, formData));
// User exists in ldap // User exists in ldap
formData = AuthProvidersExternalModelTest.createFormData("john", "invalid"); 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 // User exists in realm
formData = AuthProvidersExternalModelTest.createFormData("realmUser", "invalid"); 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 // User disabled
realmUser.setEnabled(false); realmUser.setEnabled(false);
formData = AuthProvidersExternalModelTest.createFormData("realmUser", "pass"); 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 { } finally {
ResteasyProviderFactory.clearContextData(); ResteasyProviderFactory.clearContextData();
} }
@ -158,7 +158,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
Assert.fail("Error not expected"); Assert.fail("Error not expected");
} }
MultivaluedMap<String, String> formData = AuthProvidersExternalModelTest.createFormData("john", "password-updated"); MultivaluedMap<String, String> formData = AuthProvidersExternalModelTest.createFormData("john", "password-updated");
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(realm, formData)); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData));
// Password updated just in LDAP, so validating directly in realm should fail // Password updated just in LDAP, so validating directly in realm should fail
Assert.assertFalse(realm.validatePassword(realm.getUser("john"), "password-updated")); Assert.assertFalse(realm.validatePassword(realm.getUser("john"), "password-updated"));
@ -174,7 +174,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest {
Assert.fail("Error not expected"); Assert.fail("Error not expected");
} }
formData = AuthProvidersExternalModelTest.createFormData("john", "password-updated2"); 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 { } finally {
ResteasyProviderFactory.clearContextData(); ResteasyProviderFactory.clearContextData();
} }

View file

@ -26,7 +26,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
@Test @Test
public void authForm() { public void authForm() {
AuthenticationStatus status = am.authenticateForm(realm, formData); AuthenticationStatus status = am.authenticateForm(null, realm, formData);
Assert.assertEquals(AuthenticationStatus.SUCCESS, status); Assert.assertEquals(AuthenticationStatus.SUCCESS, status);
} }
@ -35,7 +35,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
formData.remove(CredentialRepresentation.PASSWORD); formData.remove(CredentialRepresentation.PASSWORD);
formData.add(CredentialRepresentation.PASSWORD, "invalid"); formData.add(CredentialRepresentation.PASSWORD, "invalid");
AuthenticationStatus status = am.authenticateForm(realm, formData); AuthenticationStatus status = am.authenticateForm(null, realm, formData);
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status); Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
} }
@ -43,7 +43,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
public void authFormMissingUsername() { public void authFormMissingUsername() {
formData.remove("username"); formData.remove("username");
AuthenticationStatus status = am.authenticateForm(realm, formData); AuthenticationStatus status = am.authenticateForm(null, realm, formData);
Assert.assertEquals(AuthenticationStatus.INVALID_USER, status); Assert.assertEquals(AuthenticationStatus.INVALID_USER, status);
} }
@ -51,7 +51,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
public void authFormMissingPassword() { public void authFormMissingPassword() {
formData.remove(CredentialRepresentation.PASSWORD); formData.remove(CredentialRepresentation.PASSWORD);
AuthenticationStatus status = am.authenticateForm(realm, formData); AuthenticationStatus status = am.authenticateForm(null, realm, formData);
Assert.assertEquals(AuthenticationStatus.MISSING_PASSWORD, status); Assert.assertEquals(AuthenticationStatus.MISSING_PASSWORD, status);
} }
@ -60,7 +60,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
realm.addRequiredCredential(CredentialRepresentation.TOTP); realm.addRequiredCredential(CredentialRepresentation.TOTP);
user.addRequiredAction(RequiredAction.CONFIGURE_TOTP); user.addRequiredAction(RequiredAction.CONFIGURE_TOTP);
AuthenticationStatus status = am.authenticateForm(realm, formData); AuthenticationStatus status = am.authenticateForm(null, realm, formData);
Assert.assertEquals(AuthenticationStatus.ACTIONS_REQUIRED, status); Assert.assertEquals(AuthenticationStatus.ACTIONS_REQUIRED, status);
} }
@ -68,7 +68,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
public void authFormUserDisabled() { public void authFormUserDisabled() {
user.setEnabled(false); user.setEnabled(false);
AuthenticationStatus status = am.authenticateForm(realm, formData); AuthenticationStatus status = am.authenticateForm(null, realm, formData);
Assert.assertEquals(AuthenticationStatus.ACCOUNT_DISABLED, status); Assert.assertEquals(AuthenticationStatus.ACCOUNT_DISABLED, status);
} }
@ -90,7 +90,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
formData.add(CredentialRepresentation.TOTP, token); formData.add(CredentialRepresentation.TOTP, token);
AuthenticationStatus status = am.authenticateForm(realm, formData); AuthenticationStatus status = am.authenticateForm(null, realm, formData);
Assert.assertEquals(AuthenticationStatus.SUCCESS, status); Assert.assertEquals(AuthenticationStatus.SUCCESS, status);
} }
@ -101,7 +101,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
formData.remove(CredentialRepresentation.PASSWORD); formData.remove(CredentialRepresentation.PASSWORD);
formData.add(CredentialRepresentation.PASSWORD, "invalid"); formData.add(CredentialRepresentation.PASSWORD, "invalid");
AuthenticationStatus status = am.authenticateForm(realm, formData); AuthenticationStatus status = am.authenticateForm(null, realm, formData);
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status); Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
} }
@ -112,7 +112,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
formData.remove(CredentialRepresentation.TOTP); formData.remove(CredentialRepresentation.TOTP);
formData.add(CredentialRepresentation.TOTP, "invalid"); formData.add(CredentialRepresentation.TOTP, "invalid");
AuthenticationStatus status = am.authenticateForm(realm, formData); AuthenticationStatus status = am.authenticateForm(null, realm, formData);
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status); Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
} }
@ -122,7 +122,7 @@ public class AuthenticationManagerTest extends AbstractModelTest {
formData.remove(CredentialRepresentation.TOTP); formData.remove(CredentialRepresentation.TOTP);
AuthenticationStatus status = am.authenticateForm(realm, formData); AuthenticationStatus status = am.authenticateForm(null, realm, formData);
Assert.assertEquals(AuthenticationStatus.MISSING_TOTP, status); Assert.assertEquals(AuthenticationStatus.MISSING_TOTP, status);
} }

View file

@ -34,6 +34,11 @@
<welcome-file>index.html</welcome-file> <welcome-file>index.html</welcome-file>
</welcome-file-list> </welcome-file-list>
<filter>
<filter-name>Keycloak Client Connection Filter</filter-name>
<filter-class>org.keycloak.services.filters.ClientConnectionFilter</filter-class>
</filter>
<filter> <filter>
<filter-name>Keycloak Session Management</filter-name> <filter-name>Keycloak Session Management</filter-name>
<filter-class>org.keycloak.services.filters.KeycloakSessionServletFilter</filter-class> <filter-class>org.keycloak.services.filters.KeycloakSessionServletFilter</filter-class>
@ -44,6 +49,11 @@
<url-pattern>/rest/*</url-pattern> <url-pattern>/rest/*</url-pattern>
</filter-mapping> </filter-mapping>
<filter-mapping>
<filter-name>Keycloak Client Connection Filter</filter-name>
<url-pattern>/rest/*</url-pattern>
</filter-mapping>
<servlet-mapping> <servlet-mapping>
<servlet-name>Resteasy</servlet-name> <servlet-name>Resteasy</servlet-name>
<url-pattern>/rest/*</url-pattern> <url-pattern>/rest/*</url-pattern>

View file

@ -0,0 +1,13 @@
package org.keycloak.services;
/**
* Information about the client connection
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface ClientConnection {
String getRemoteAddr();
String getRemoteHost();
int getReportPort();
}

View file

@ -0,0 +1,49 @@
package org.keycloak.services.filters;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.services.ClientConnection;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ClientConnectionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
//To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void doFilter(final ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ResteasyProviderFactory.pushContext(ClientConnection.class, new ClientConnection() {
@Override
public String getRemoteAddr() {
return request.getRemoteAddr();
}
@Override
public String getRemoteHost() {
return request.getRemoteHost();
}
@Override
public int getReportPort() {
return request.getRemotePort();
}
});
chain.doFilter(request, response);
}
@Override
public void destroy() {
//To change body of implemented methods use File | Settings | File Templates.
}
}

View file

@ -14,6 +14,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.ClientConnection;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
import org.keycloak.spi.authentication.AuthProviderStatus; import org.keycloak.spi.authentication.AuthProviderStatus;
import org.keycloak.spi.authentication.AuthResult; 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_IDENTITY_COOKIE = "KEYCLOAK_IDENTITY";
public static final String KEYCLOAK_REMEMBER_ME = "KEYCLOAK_REMEMBER_ME"; 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) { public AccessToken createIdentityToken(RealmModel realm, UserModel user) {
logger.info("createIdentityToken"); logger.info("createIdentityToken");
AccessToken token = new AccessToken(); AccessToken token = new AccessToken();
@ -180,13 +190,37 @@ public class AuthenticationManager {
return null; return null;
} }
public AuthenticationStatus authenticateForm(RealmModel realm, MultivaluedMap<String, String> formData) { public AuthenticationStatus authenticateForm(ClientConnection clientConnection, RealmModel realm, MultivaluedMap<String, String> formData) {
String username = formData.getFirst(FORM_USERNAME); String username = formData.getFirst(FORM_USERNAME);
if (username == null) { if (username == null) {
logger.warn("Username not provided"); logger.warn("Username not provided");
return AuthenticationStatus.INVALID_USER; return AuthenticationStatus.INVALID_USER;
} }
AuthenticationStatus status = authenticateInternal(realm, formData, username);
if (realm.isBruteForceProtected()) {
switch (status) {
case SUCCESS:
protector.successfulLogin(realm, username, clientConnection);
break;
case FAILED:
case MISSING_TOTP:
case MISSING_PASSWORD:
case INVALID_CREDENTIALS:
protector.failedLogin(realm, username, clientConnection);
break;
case INVALID_USER:
protector.invalidUser(realm, username, clientConnection);
break;
default:
break;
}
}
return status;
}
protected AuthenticationStatus authenticateInternal(RealmModel realm, MultivaluedMap<String, String> formData, String username) {
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username); UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username);
Set<String> types = new HashSet<String>(); Set<String> types = new HashSet<String>();

View file

@ -0,0 +1,224 @@
package org.keycloak.services.managers;
import org.jboss.resteasy.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UsernameLoginFailureModel;
import org.keycloak.services.ClientConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* A single thread will log failures. This is so that we can avoid concurrent writes as we want an accurate failure count
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class BruteForceProtector implements Runnable {
protected static Logger logger = Logger.getLogger(BruteForceProtector.class);
protected int maxFailureWaitSeconds = 900;
protected int minimumQuickLoginWaitSeconds = 60;
protected int waitIncrementSeconds = 60;
protected long quickLoginCheckMilliSeconds = 1000;
protected int maxDeltaTime = 60 * 60 * 24 * 1000;
protected int failureFactor = 10;
protected volatile boolean run = true;
protected KeycloakSessionFactory factory;
protected CountDownLatch shutdownLatch = new CountDownLatch(1);
protected volatile long failures;
protected volatile long lastFailure;
protected volatile long totalTime;
protected LinkedBlockingQueue<LoginEvent> queue = new LinkedBlockingQueue<LoginEvent>();
public static final int TRANSACTION_SIZE = 20;
protected abstract class LoginEvent implements Comparable<LoginEvent> {
protected final String realmId;
protected final String username;
protected final String ip;
protected LoginEvent(String realmId, String username, String ip) {
this.realmId = realmId;
this.username = username;
this.ip = ip;
}
@Override
public int compareTo(LoginEvent o) {
return username.compareTo(o.username);
}
}
protected class SuccessfulLogin extends LoginEvent {
public SuccessfulLogin(String realmId, String userId, String ip) {
super(realmId, userId, ip);
}
}
protected class FailedLogin extends LoginEvent {
protected final CountDownLatch latch = new CountDownLatch(1);
public FailedLogin(String realmId, String username, String ip) {
super(realmId, username, ip);
}
}
public BruteForceProtector(KeycloakSessionFactory factory) {
this.factory = factory;
}
public void failure(KeycloakSession session, LoginEvent event) {
UsernameLoginFailureModel user = getUserModel(session, event);
if (user == null) return;
user.setLastIPFailure(event.ip);
long currentTime = System.currentTimeMillis();
long last = user.getLastFailure();
long deltaTime = 0;
if (last > 0) {
deltaTime = currentTime - last;
}
user.setLastFailure(currentTime);
if (deltaTime > 0) {
// if last failure was more than MAX_DELTA clear failures
if (deltaTime > maxDeltaTime) {
user.clearFailures();
}
}
user.incrementFailures();
int waitSeconds = waitIncrementSeconds * (user.getNumFailures() / failureFactor);
if (waitSeconds == 0) {
if (deltaTime > quickLoginCheckMilliSeconds) {
waitSeconds = minimumQuickLoginWaitSeconds;
}
}
waitSeconds = Math.min(maxFailureWaitSeconds, waitSeconds);
if (waitSeconds > 0) {
user.setFailedLoginNotBefore((int) (currentTime / 1000) + waitSeconds);
}
}
protected UsernameLoginFailureModel getUserModel(KeycloakSession session, LoginEvent event) {
RealmModel realm = session.getRealm(event.realmId);
if (realm == null) return null;
UsernameLoginFailureModel user = realm.getUserLoginFailure(event.username);
if (user == null) return null;
return user;
}
public void start() {
new Thread(this).start();
}
public void shutdown() {
run = false;
try {
shutdownLatch.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public void run() {
final ArrayList<LoginEvent> events = new ArrayList<LoginEvent>(TRANSACTION_SIZE + 1);
while (run) {
try {
LoginEvent take = queue.poll(2, TimeUnit.SECONDS);
if (take == null) {
continue;
}
try {
events.add(take);
queue.drainTo(events, TRANSACTION_SIZE);
for (LoginEvent event : events) {
if (event instanceof FailedLogin) {
logFailure(event);
} else {
logSuccess(event);
}
}
Collections.sort(events); // we sort to avoid deadlock due to ordered updates. Maybe I'm overthinking this.
KeycloakSession session = factory.createSession();
try {
for (LoginEvent event : events) {
if (event instanceof FailedLogin) {
failure(session, event);
}
}
session.getTransaction().commit();
} catch (Exception e) {
session.getTransaction().rollback();
throw e;
} finally {
for (LoginEvent event : events) {
if (event instanceof FailedLogin) {
((FailedLogin) event).latch.countDown();
}
}
events.clear();
session.close();
}
} catch (Exception e) {
logger.error("Failed processing event", e);
}
} catch (InterruptedException e) {
break;
} finally {
shutdownLatch.countDown();
}
}
}
protected void logSuccess(LoginEvent event) {
logger.warn("login success for user " + event.username + " from ip " + event.ip);
}
protected void logFailure(LoginEvent event) {
logger.warn("login failure for user " + event.username + " from ip " + event.ip);
failures++;
long delta = 0;
if (lastFailure > 0) {
delta = System.currentTimeMillis() - lastFailure;
if (delta > maxDeltaTime) {
totalTime = 0;
} else {
totalTime += delta;
}
}
}
public void successfulLogin(RealmModel realm, String username, ClientConnection clientConnection) {
logger.info("successful login user: " + username + " from ip " + clientConnection.getRemoteAddr());
}
public void invalidUser(RealmModel realm, String username, ClientConnection clientConnection) {
logger.warn("invalid user: " + username + " from ip " + clientConnection.getRemoteAddr());
// todo more?
}
public void failedLogin(RealmModel realm, String username, ClientConnection clientConnection) {
try {
FailedLogin event = new FailedLogin(realm.getId(), username, clientConnection.getRemoteAddr());
queue.offer(event);
// wait a minimum of seconds for event to process so that a hacker
// cannot flood with failed logins and overwhelm the queue and not have notBefore updated to block next requests
// todo failure HTTP responses should be queued via async HTTP
event.latch.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
}
}
}

View file

@ -20,6 +20,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.ClientConnection;
import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus; import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
@ -91,6 +92,8 @@ public class TokenService {
protected KeycloakSession session; protected KeycloakSession session;
@Context @Context
protected KeycloakTransaction transaction; protected KeycloakTransaction transaction;
@Context
protected ClientConnection clientConnection;
@Context @Context
protected ResourceContext resourceContext; protected ResourceContext resourceContext;
@ -158,7 +161,7 @@ public class TokenService {
throw new NotAuthorizedException("Disabled realm"); 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"); throw new NotAuthorizedException("Auth failed");
} }
@ -234,7 +237,7 @@ public class TokenService {
return oauth.redirectError(client, "access_denied", state, redirect); 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"); String rememberMe = formData.getFirst("rememberMe");
boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on"); boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on");

View file

@ -38,6 +38,7 @@ import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer;
import org.jboss.resteasy.spi.ResteasyDeployment; import org.jboss.resteasy.spi.ResteasyDeployment;
import org.keycloak.models.Config; import org.keycloak.models.Config;
import org.keycloak.services.filters.ClientConnectionFilter;
import org.keycloak.theme.DefaultLoginThemeProvider; import org.keycloak.theme.DefaultLoginThemeProvider;
import org.keycloak.services.tmp.TmpAdminRedirectServlet; import org.keycloak.services.tmp.TmpAdminRedirectServlet;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -263,6 +264,10 @@ public class KeycloakServer {
di.addFilter(filter); di.addFilter(filter);
di.addFilterUrlMapping("SessionFilter", "/rest/*", DispatcherType.REQUEST); 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); ServletInfo tmpAdminRedirectServlet = Servlets.servlet("TmpAdminRedirectServlet", TmpAdminRedirectServlet.class);
tmpAdminRedirectServlet.addMappings("/admin", "/admin/"); tmpAdminRedirectServlet.addMappings("/admin", "/admin/");
di.addServlet(tmpAdminRedirectServlet); di.addServlet(tmpAdminRedirectServlet);