brute force protection
This commit is contained in:
parent
d8a3025ea1
commit
d58870545f
27 changed files with 987 additions and 173 deletions
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
21
model/api/src/main/java/org/keycloak/models/UsernameLoginFailureModel.java
Executable file
21
model/api/src/main/java/org/keycloak/models/UsernameLoginFailureModel.java
Executable 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);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -170,4 +170,7 @@ public class UserAdapter extends AbstractMongoAdapter<UserEntity> implements Use
|
||||||
public UserEntity getMongoEntity() {
|
public UserEntity getMongoEntity() {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}}
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
14
model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java
Normal file → Executable file
14
model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java
Normal file → Executable 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
|
||||||
|
|
16
model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java
Normal file → Executable file
16
model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java
Normal file → Executable 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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
13
services/src/main/java/org/keycloak/services/ClientConnection.java
Executable file
13
services/src/main/java/org/keycloak/services/ClientConnection.java
Executable 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();
|
||||||
|
}
|
|
@ -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.
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>();
|
||||||
|
|
224
services/src/main/java/org/keycloak/services/managers/BruteForceProtector.java
Executable file
224
services/src/main/java/org/keycloak/services/managers/BruteForceProtector.java
Executable 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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
|
0
spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java
Normal file → Executable file
0
spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java
Normal file → Executable file
0
spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthResult.java
Normal file → Executable file
0
spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthResult.java
Normal file → Executable 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);
|
||||||
|
|
Loading…
Reference in a new issue