Merge pull request #2931 from stianst/master

KEYCLOAK-3091 Change brute force to use userId
This commit is contained in:
Stian Thorgersen 2016-06-13 18:02:32 +02:00 committed by GitHub
commit c1e202eaf9
19 changed files with 134 additions and 138 deletions

View file

@ -33,16 +33,16 @@ import java.util.Map;
public interface AttackDetectionResource { public interface AttackDetectionResource {
@GET @GET
@Path("brute-force/usernames/{username}") @Path("brute-force/users/{userId}")
@NoCache @NoCache
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
Map<String, Object> bruteForceUserStatus(@PathParam("username") String username); Map<String, Object> bruteForceUserStatus(@PathParam("userId") String userId);
@Path("brute-force/usernames/{username}") @Path("brute-force/users/{userId}")
@DELETE @DELETE
void clearBruteForceForUser(@PathParam("username") String username); void clearBruteForceForUser(@PathParam("userId") String userId);
@Path("brute-force/usernames") @Path("brute-force/users")
@DELETE @DELETE
void clearAllBruteForce(); void clearAllBruteForce();

View file

@ -30,7 +30,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UsernameLoginFailureModel; import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity; import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity;
@ -377,24 +377,24 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
} }
@Override @Override
public UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username) { public UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId) {
LoginFailureKey key = new LoginFailureKey(realm.getId(), username); LoginFailureKey key = new LoginFailureKey(realm.getId(), userId);
return wrap(key, loginFailureCache.get(key)); return wrap(key, loginFailureCache.get(key));
} }
@Override @Override
public UsernameLoginFailureModel addUserLoginFailure(RealmModel realm, String username) { public UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId) {
LoginFailureKey key = new LoginFailureKey(realm.getId(), username); LoginFailureKey key = new LoginFailureKey(realm.getId(), userId);
LoginFailureEntity entity = new LoginFailureEntity(); LoginFailureEntity entity = new LoginFailureEntity();
entity.setRealm(realm.getId()); entity.setRealm(realm.getId());
entity.setUsername(username); entity.setUserId(userId);
tx.put(loginFailureCache, key, entity); tx.put(loginFailureCache, key, entity);
return wrap(key, entity); return wrap(key, entity);
} }
@Override @Override
public void removeUserLoginFailure(RealmModel realm, String username) { public void removeUserLoginFailure(RealmModel realm, String userId) {
tx.remove(loginFailureCache, new LoginFailureKey(realm.getId(), username)); tx.remove(loginFailureCache, new LoginFailureKey(realm.getId(), userId));
} }
@Override @Override
@ -538,8 +538,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
} }
UsernameLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) { UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
return entity != null ? new UsernameLoginFailureAdapter(this, loginFailureCache, key, entity) : null; return entity != null ? new UserLoginFailureAdapter(this, loginFailureCache, key, entity) : null;
} }
List<ClientSessionModel> wrapClientSessions(RealmModel realm, Collection<ClientSessionEntity> entities, boolean offline) { List<ClientSessionModel> wrapClientSessions(RealmModel realm, Collection<ClientSessionEntity> entities, boolean offline) {

View file

@ -18,21 +18,21 @@
package org.keycloak.models.sessions.infinispan; package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache; import org.infinispan.Cache;
import org.keycloak.models.UsernameLoginFailureModel; import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class UsernameLoginFailureAdapter implements UsernameLoginFailureModel { public class UserLoginFailureAdapter implements UserLoginFailureModel {
private InfinispanUserSessionProvider provider; private InfinispanUserSessionProvider provider;
private Cache<LoginFailureKey, LoginFailureEntity> cache; private Cache<LoginFailureKey, LoginFailureEntity> cache;
private LoginFailureKey key; private LoginFailureKey key;
private LoginFailureEntity entity; private LoginFailureEntity entity;
public UsernameLoginFailureAdapter(InfinispanUserSessionProvider provider, Cache<LoginFailureKey, LoginFailureEntity> cache, LoginFailureKey key, LoginFailureEntity entity) { public UserLoginFailureAdapter(InfinispanUserSessionProvider provider, Cache<LoginFailureKey, LoginFailureEntity> cache, LoginFailureKey key, LoginFailureEntity entity) {
this.provider = provider; this.provider = provider;
this.cache = cache; this.cache = cache;
this.key = key; this.key = key;
@ -40,8 +40,8 @@ public class UsernameLoginFailureAdapter implements UsernameLoginFailureModel {
} }
@Override @Override
public String getUsername() { public String getUserId() {
return entity.getUsername(); return entity.getUserId();
} }
@Override @Override

View file

@ -24,19 +24,19 @@ import java.io.Serializable;
*/ */
public class LoginFailureEntity implements Serializable { public class LoginFailureEntity implements Serializable {
private String username; private String userId;
private String realm; private String realm;
private int failedLoginNotBefore; private int failedLoginNotBefore;
private int numFailures; private int numFailures;
private long lastFailure; private long lastFailure;
private String lastIPFailure; private String lastIPFailure;
public String getUsername() { public String getUserId() {
return username; return userId;
} }
public void setUsername(String username) { public void setUserId(String userId) {
this.username = username; this.userId = userId;
} }
public String getRealm() { public String getRealm() {

View file

@ -25,11 +25,11 @@ import java.io.Serializable;
public class LoginFailureKey implements Serializable { public class LoginFailureKey implements Serializable {
private final String realm; private final String realm;
private final String username; private final String userId;
public LoginFailureKey(String realm, String username) { public LoginFailureKey(String realm, String userId) {
this.realm = realm; this.realm = realm;
this.username = username; this.userId = userId;
} }
@Override @Override
@ -40,7 +40,7 @@ public class LoginFailureKey implements Serializable {
LoginFailureKey key = (LoginFailureKey) o; LoginFailureKey key = (LoginFailureKey) o;
if (realm != null ? !realm.equals(key.realm) : key.realm != null) return false; if (realm != null ? !realm.equals(key.realm) : key.realm != null) return false;
if (username != null ? !username.equals(key.username) : key.username != null) return false; if (userId != null ? !userId.equals(key.userId) : key.userId != null) return false;
return true; return true;
} }
@ -48,7 +48,7 @@ public class LoginFailureKey implements Serializable {
@Override @Override
public int hashCode() { public int hashCode() {
int result = realm != null ? realm.hashCode() : 0; int result = realm != null ? realm.hashCode() : 0;
result = 31 * result + (username != null ? username.hashCode() : 0); result = 31 * result + (userId != null ? userId.hashCode() : 0);
return result; return result;
} }

View file

@ -21,9 +21,9 @@ package org.keycloak.models;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface UsernameLoginFailureModel public interface UserLoginFailureModel
{ {
String getUsername(); String getUserId();
int getFailedLoginNotBefore(); int getFailedLoginNotBefore();
void setFailedLoginNotBefore(int notBefore); void setFailedLoginNotBefore(int notBefore);
int getNumFailures(); int getNumFailures();

View file

@ -48,9 +48,9 @@ public interface UserSessionProvider extends Provider {
void removeUserSessions(RealmModel realm); void removeUserSessions(RealmModel realm);
void removeClientSession(RealmModel realm, ClientSessionModel clientSession); void removeClientSession(RealmModel realm, ClientSessionModel clientSession);
UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username); UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId);
UsernameLoginFailureModel addUserLoginFailure(RealmModel realm, String username); UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId);
void removeUserLoginFailure(RealmModel realm, String username); void removeUserLoginFailure(RealmModel realm, String userId);
void removeAllUserLoginFailures(RealmModel realm); void removeAllUserLoginFailures(RealmModel realm);
void onRealmRemoved(RealmModel realm); void onRealmRemoved(RealmModel realm);

View file

@ -20,6 +20,7 @@ package org.keycloak.services.managers;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
/** /**
@ -27,7 +28,7 @@ import org.keycloak.provider.Provider;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface BruteForceProtector extends Provider { public interface BruteForceProtector extends Provider {
void failedLogin(RealmModel realm, String username, ClientConnection clientConnection); void failedLogin(RealmModel realm, UserModel user, ClientConnection clientConnection);
boolean isTemporarilyDisabled(KeycloakSession session, RealmModel realm, String username); boolean isTemporarilyDisabled(KeycloakSession session, RealmModel realm, UserModel user);
} }

View file

@ -37,6 +37,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocol.Error; import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
@ -543,8 +544,10 @@ public class AuthenticationProcessor {
if (username == null) { if (username == null) {
} else { } else {
getBruteForceProtector().failedLogin(realm, username, connection); UserModel user = KeycloakModelUtils.findUserByNameOrEmail(session, realm, username);
if (user != null) {
getBruteForceProtector().failedLogin(realm, user, connection);
}
} }
} }
} }
@ -851,7 +854,7 @@ public class AuthenticationProcessor {
if (authenticatedUser == null) return; if (authenticatedUser == null) return;
if (!authenticatedUser.isEnabled()) throw new AuthenticationFlowException(AuthenticationFlowError.USER_DISABLED); if (!authenticatedUser.isEnabled()) throw new AuthenticationFlowException(AuthenticationFlowError.USER_DISABLED);
if (realm.isBruteForceProtected()) { if (realm.isBruteForceProtected()) {
if (getBruteForceProtector().isTemporarilyDisabled(session, realm, authenticatedUser.getUsername())) { if (getBruteForceProtector().isTemporarilyDisabled(session, realm, authenticatedUser)) {
throw new AuthenticationFlowException(AuthenticationFlowError.USER_TEMPORARILY_DISABLED); throw new AuthenticationFlowException(AuthenticationFlowError.USER_TEMPORARILY_DISABLED);
} }
} }

View file

@ -100,7 +100,7 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
return false; return false;
} }
if (context.getRealm().isBruteForceProtected()) { if (context.getRealm().isBruteForceProtected()) {
if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user.getUsername())) { if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user)) {
context.getEvent().user(user); context.getEvent().user(user);
context.getEvent().error(Errors.USER_TEMPORARILY_DISABLED); context.getEvent().error(Errors.USER_TEMPORARILY_DISABLED);
Response challengeResponse = temporarilyDisabledUser(context); Response challengeResponse = temporarilyDisabledUser(context);

View file

@ -84,7 +84,7 @@ public class ValidateUsername extends AbstractDirectGrantAuthenticator {
return; return;
} }
if (context.getRealm().isBruteForceProtected()) { if (context.getRealm().isBruteForceProtected()) {
if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user.getUsername())) { if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user)) {
context.getEvent().user(user); context.getEvent().user(user);
context.getEvent().error(Errors.USER_TEMPORARILY_DISABLED); context.getEvent().error(Errors.USER_TEMPORARILY_DISABLED);
Response challengeResponse = errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_grant", "Account temporarily disabled"); Response challengeResponse = errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_grant", "Account temporarily disabled");

View file

@ -22,7 +22,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UsernameLoginFailureModel; import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import java.util.ArrayList; import java.util.ArrayList;
@ -55,18 +55,18 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
protected abstract class LoginEvent implements Comparable<LoginEvent> { protected abstract class LoginEvent implements Comparable<LoginEvent> {
protected final String realmId; protected final String realmId;
protected final String username; protected final String userId;
protected final String ip; protected final String ip;
protected LoginEvent(String realmId, String username, String ip) { protected LoginEvent(String realmId, String userId, String ip) {
this.realmId = realmId; this.realmId = realmId;
this.username = username; this.userId = userId;
this.ip = ip; this.ip = ip;
} }
@Override @Override
public int compareTo(LoginEvent o) { public int compareTo(LoginEvent o) {
return username.compareTo(o.username); return userId.compareTo(o.userId);
} }
} }
@ -79,8 +79,8 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
protected class FailedLogin extends LoginEvent { protected class FailedLogin extends LoginEvent {
protected final CountDownLatch latch = new CountDownLatch(1); protected final CountDownLatch latch = new CountDownLatch(1);
public FailedLogin(String realmId, String username, String ip) { public FailedLogin(String realmId, String userId, String ip) {
super(realmId, username, ip); super(realmId, userId, ip);
} }
} }
@ -92,11 +92,11 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
logger.debug("failure"); logger.debug("failure");
RealmModel realm = getRealmModel(session, event); RealmModel realm = getRealmModel(session, event);
logFailure(event); logFailure(event);
UserModel user = session.users().getUserByUsername(event.username.toString(), realm); UserModel user = session.users().getUserById(event.userId, realm);
UsernameLoginFailureModel userLoginFailure = getUserModel(session, event); UserLoginFailureModel userLoginFailure = getUserModel(session, event);
if (user != null) { if (user != null) {
if (userLoginFailure == null) { if (userLoginFailure == null) {
userLoginFailure = session.sessions().addUserLoginFailure(realm, event.username.toLowerCase()); userLoginFailure = session.sessions().addUserLoginFailure(realm, event.userId);
} }
userLoginFailure.setLastIPFailure(event.ip); userLoginFailure.setLastIPFailure(event.ip);
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
@ -135,10 +135,10 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
} }
protected UsernameLoginFailureModel getUserModel(KeycloakSession session, LoginEvent event) { protected UserLoginFailureModel getUserModel(KeycloakSession session, LoginEvent event) {
RealmModel realm = getRealmModel(session, event); RealmModel realm = getRealmModel(session, event);
if (realm == null) return null; if (realm == null) return null;
UsernameLoginFailureModel user = session.sessions().getUserLoginFailure(realm, event.username.toLowerCase()); UserLoginFailureModel user = session.sessions().getUserLoginFailure(realm, event.userId);
if (user == null) return null; if (user == null) return null;
return user; return user;
} }
@ -212,7 +212,7 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
} }
protected void logFailure(LoginEvent event) { protected void logFailure(LoginEvent event) {
logger.loginFailure(event.username, event.ip); logger.loginFailure(event.userId, event.ip);
failures++; failures++;
long delta = 0; long delta = 0;
if (lastFailure > 0) { if (lastFailure > 0) {
@ -227,9 +227,9 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
} }
@Override @Override
public void failedLogin(RealmModel realm, String username, ClientConnection clientConnection) { public void failedLogin(RealmModel realm, UserModel user, ClientConnection clientConnection) {
try { try {
FailedLogin event = new FailedLogin(realm.getId(), username, clientConnection.getRemoteAddr()); FailedLogin event = new FailedLogin(realm.getId(), user.getId(), clientConnection.getRemoteAddr());
queue.offer(event); queue.offer(event);
// wait a minimum of seconds for type to process so that a hacker // wait a minimum of seconds for type to process so that a hacker
// cannot flood with failed logins and overwhelm the queue and not have notBefore updated to block next requests // cannot flood with failed logins and overwhelm the queue and not have notBefore updated to block next requests
@ -241,17 +241,17 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
} }
@Override @Override
public boolean isTemporarilyDisabled(KeycloakSession session, RealmModel realm, String username) { public boolean isTemporarilyDisabled(KeycloakSession session, RealmModel realm, UserModel user) {
UsernameLoginFailureModel failure = session.sessions().getUserLoginFailure(realm, username.toLowerCase()); UserLoginFailureModel failure = session.sessions().getUserLoginFailure(realm, user.getId());
if (failure == null) {
return false; if (failure != null) {
int currTime = (int) (System.currentTimeMillis() / 1000);
if (currTime < failure.getFailedLoginNotBefore()) {
logger.debugv("Current: {0} notBefore: {1}", currTime, failure.getFailedLoginNotBefore());
return true;
}
} }
int currTime = (int)(System.currentTimeMillis()/1000);
if (currTime < failure.getFailedLoginNotBefore()) {
logger.debugv("Current: {0} notBefore: {1}", currTime , failure.getFailedLoginNotBefore());
return true;
}
return false; return false;
} }

View file

@ -21,7 +21,8 @@ import org.keycloak.common.ClientConnection;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UsernameLoginFailureModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.BruteForceProtector;
@ -72,14 +73,14 @@ public class AttackDetectionResource {
/** /**
* Get status of a username in brute force detection * Get status of a username in brute force detection
* *
* @param username * @param userId
* @return * @return
*/ */
@GET @GET
@Path("brute-force/usernames/{username}") @Path("brute-force/users/{userId}")
@NoCache @NoCache
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Map<String, Object> bruteForceUserStatus(@PathParam("username") String username) { public Map<String, Object> bruteForceUserStatus(@PathParam("userId") String userId) {
auth.requireView(); auth.requireView();
Map<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();
@ -89,9 +90,11 @@ public class AttackDetectionResource {
data.put("lastIPFailure", "n/a"); data.put("lastIPFailure", "n/a");
if (!realm.isBruteForceProtected()) return data; if (!realm.isBruteForceProtected()) return data;
UsernameLoginFailureModel model = session.sessions().getUserLoginFailure(realm, username.toLowerCase()); UserModel user = session.users().getUserById(userId, realm);
UserLoginFailureModel model = session.sessions().getUserLoginFailure(realm, userId);
if (model == null) return data; if (model == null) return data;
if (session.getProvider(BruteForceProtector.class).isTemporarilyDisabled(session, realm, username)) { if (session.getProvider(BruteForceProtector.class).isTemporarilyDisabled(session, realm, user)) {
data.put("disabled", true); data.put("disabled", true);
} }
data.put("numFailures", model.getNumFailures()); data.put("numFailures", model.getNumFailures());
@ -105,16 +108,16 @@ public class AttackDetectionResource {
* *
* This can release temporary disabled user * This can release temporary disabled user
* *
* @param username * @param userId
*/ */
@Path("brute-force/usernames/{username}") @Path("brute-force/users/{userId}")
@DELETE @DELETE
public void clearBruteForceForUser(@PathParam("username") String username) { public void clearBruteForceForUser(@PathParam("userId") String userId) {
auth.requireManage(); auth.requireManage();
UsernameLoginFailureModel model = session.sessions().getUserLoginFailure(realm, username.toLowerCase()); UserLoginFailureModel model = session.sessions().getUserLoginFailure(realm, userId);
if (model != null) { if (model != null) {
session.sessions().removeUserLoginFailure(realm, username); session.sessions().removeUserLoginFailure(realm, userId);
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success(); adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
} }
} }
@ -125,7 +128,7 @@ public class AttackDetectionResource {
* This can release temporary disabled users * This can release temporary disabled users
* *
*/ */
@Path("brute-force/usernames") @Path("brute-force/users")
@DELETE @DELETE
public void clearAllBruteForce() { public void clearAllBruteForce() {
auth.requireManage(); auth.requireManage();

View file

@ -81,7 +81,6 @@ import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import javax.ws.rs.WebApplicationException; import javax.ws.rs.WebApplicationException;
import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -94,15 +93,12 @@ import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.keycloak.models.UsernameLoginFailureModel; import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.resources.AccountService; import org.keycloak.services.resources.AccountService;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import org.keycloak.theme.Theme;
import org.keycloak.theme.Theme.Type;
import org.keycloak.theme.ThemeProvider;
/** /**
* Base resource for managing users * Base resource for managing users
@ -166,8 +162,8 @@ public class UsersResource {
attrsToRemove = Collections.emptySet(); attrsToRemove = Collections.emptySet();
} }
if (rep.isEnabled() != null && rep.isEnabled() && rep.getUsername() != null) { if (rep.isEnabled() != null && rep.isEnabled()) {
UsernameLoginFailureModel failureModel = session.sessions().getUserLoginFailure(realm, rep.getUsername().toLowerCase()); UserLoginFailureModel failureModel = session.sessions().getUserLoginFailure(realm, id);
if (failureModel != null) { if (failureModel != null) {
failureModel.clearFailures(); failureModel.clearFailures();
} }
@ -300,7 +296,7 @@ public class UsersResource {
rep.setFederatedIdentities(reps); rep.setFederatedIdentities(reps);
} }
if (session.getProvider(BruteForceProtector.class).isTemporarilyDisabled(session, realm, rep.getUsername())) { if (session.getProvider(BruteForceProtector.class).isTemporarilyDisabled(session, realm, user)) {
rep.setEnabled(false); rep.setEnabled(false);
} }

View file

@ -52,7 +52,7 @@ public class AttackDetectionResourceTest extends AbstractAdminTest {
public void test() { public void test() {
AttackDetectionResource detection = adminClient.realm("test").attackDetection(); AttackDetectionResource detection = adminClient.realm("test").attackDetection();
assertBruteForce(detection.bruteForceUserStatus("test-user@localhost"), 0, false, false); assertBruteForce(detection.bruteForceUserStatus(findUser("test-user@localhost").getId()), 0, false, false);
oauthClient.doLogin("test-user@localhost", "invalid"); oauthClient.doLogin("test-user@localhost", "invalid");
oauthClient.doLogin("test-user@localhost", "invalid"); oauthClient.doLogin("test-user@localhost", "invalid");
@ -62,21 +62,21 @@ public class AttackDetectionResourceTest extends AbstractAdminTest {
oauthClient.doLogin("test-user2", "invalid"); oauthClient.doLogin("test-user2", "invalid");
oauthClient.doLogin("nosuchuser", "invalid"); oauthClient.doLogin("nosuchuser", "invalid");
assertBruteForce(detection.bruteForceUserStatus("test-user@localhost"), 3, true, true); assertBruteForce(detection.bruteForceUserStatus(findUser("test-user@localhost").getId()), 3, true, true);
assertBruteForce(detection.bruteForceUserStatus("test-user2"), 2, true, true); assertBruteForce(detection.bruteForceUserStatus(findUser("test-user2").getId()), 2, true, true);
assertBruteForce(detection.bruteForceUserStatus("nosuchuser"), 0, false, false); assertBruteForce(detection.bruteForceUserStatus("nosuchuser"), 0, false, false);
detection.clearBruteForceForUser("test-user@localhost"); detection.clearBruteForceForUser(findUser("test-user@localhost").getId());
assertAdminEvents.assertEvent("test", OperationType.DELETE, AdminEventPaths.attackDetectionClearBruteForceForUserPath("test-user@localhost")); assertAdminEvents.assertEvent("test", OperationType.DELETE, AdminEventPaths.attackDetectionClearBruteForceForUserPath(findUser("test-user@localhost").getId()));
assertBruteForce(detection.bruteForceUserStatus("test-user@localhost"), 0, false, false); assertBruteForce(detection.bruteForceUserStatus(findUser("test-user@localhost").getId()), 0, false, false);
assertBruteForce(detection.bruteForceUserStatus("test-user2"), 2, true, true); assertBruteForce(detection.bruteForceUserStatus(findUser("test-user2").getId()), 2, true, true);
detection.clearAllBruteForce(); detection.clearAllBruteForce();
assertAdminEvents.assertEvent("test", OperationType.DELETE, AdminEventPaths.attackDetectionClearAllBruteForcePath()); assertAdminEvents.assertEvent("test", OperationType.DELETE, AdminEventPaths.attackDetectionClearAllBruteForcePath());
assertBruteForce(detection.bruteForceUserStatus("test-user@localhost"), 0, false, false); assertBruteForce(detection.bruteForceUserStatus(findUser("test-user@localhost").getId()), 0, false, false);
assertBruteForce(detection.bruteForceUserStatus("test-user2"), 0, false, false); assertBruteForce(detection.bruteForceUserStatus(findUser("test-user2").getId()), 0, false, false);
} }
private void assertBruteForce(Map<String, Object> status, Integer expectedNumFailures, Boolean expectedFailure, Boolean expectedDisabled) { private void assertBruteForce(Map<String, Object> status, Integer expectedNumFailures, Boolean expectedFailure, Boolean expectedDisabled) {

View file

@ -26,24 +26,21 @@ import org.keycloak.models.Constants;
import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.AssertEvents.ExpectedEvent;
import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginTotpPage; import org.keycloak.testsuite.pages.LoginTotpPage;
import org.keycloak.testsuite.pages.RegisterPage; import org.keycloak.testsuite.pages.RegisterPage;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.TestRealmKeycloakTest; import org.keycloak.testsuite.TestRealmKeycloakTest;
import org.keycloak.testsuite.util.GreenMailRule; import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.UserBuilder;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -64,6 +61,8 @@ public class BruteForceTest extends TestRealmKeycloakTest {
testRealm.setFailureFactor(2); testRealm.setFailureFactor(2);
findClientInRealmRep(testRealm, "test-app").setDirectAccessGrantsEnabled(true); findClientInRealmRep(testRealm, "test-app").setDirectAccessGrantsEnabled(true);
testRealm.getUsers().add(UserBuilder.create().username("user2").email("user2@localhost").password("password").build());
} }
@Before @Before
@ -110,33 +109,11 @@ public class BruteForceTest extends TestRealmKeycloakTest {
} }
protected void clearUserFailures() throws Exception { protected void clearUserFailures() throws Exception {
String token = getAdminToken(); adminClient.realm("test").attackDetection().clearBruteForceForUser(findUser("test-user@localhost").getId());
Client client = ClientBuilder.newClient();
Response response = client.target(AppPage.AUTH_SERVER_URL)
.path("admin/realms/test/attack-detection/brute-force/usernames/test-user@localhost")
.request()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.delete();
Assert.assertEquals(204, response.getStatus());
response.close();
client.close();
} }
protected void clearAllUserFailures() throws Exception { protected void clearAllUserFailures() throws Exception {
String token = getAdminToken(); adminClient.realm("test").attackDetection().clearAllBruteForce();
Client client = ClientBuilder.newClient();
Response response = client.target(AppPage.AUTH_SERVER_URL)
.path("admin/realms/test/attack-detection/brute-force/usernames")
.request()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.delete();
Assert.assertEquals(204, response.getStatus());
response.close();
client.close();
} }
@Test @Test
@ -292,6 +269,17 @@ public class BruteForceTest extends TestRealmKeycloakTest {
clearAllUserFailures(); clearAllUserFailures();
} }
@Test
public void testEmail() throws Exception {
String userId = adminClient.realm("test").users().search("user2", null, null, null, 0, 1).get(0).getId();
loginSuccess("user2@localhost");
loginInvalidPassword("user2@localhost");
loginInvalidPassword("user2@localhost");
expectTemporarilyDisabled("user2@localhost", userId);
clearAllUserFailures();
}
@Test @Test
public void testBrowserMissingPassword() throws Exception { public void testBrowserMissingPassword() throws Exception {
loginSuccess(); loginSuccess();
@ -334,20 +322,25 @@ public class BruteForceTest extends TestRealmKeycloakTest {
} }
public void expectTemporarilyDisabled() throws Exception { public void expectTemporarilyDisabled() throws Exception {
expectTemporarilyDisabled("test-user@localhost"); expectTemporarilyDisabled("test-user@localhost", null);
} }
public void expectTemporarilyDisabled(String username) throws Exception { public void expectTemporarilyDisabled(String username, String userId) throws Exception {
loginPage.open(); loginPage.open();
loginPage.login(username, "password"); loginPage.login(username, "password");
loginPage.assertCurrent(); loginPage.assertCurrent();
String src = driver.getPageSource(); String src = driver.getPageSource();
Assert.assertEquals("Invalid username or password.", loginPage.getError()); Assert.assertEquals("Invalid username or password.", loginPage.getError());
events.expectLogin().session((String) null).error(Errors.USER_TEMPORARILY_DISABLED) ExpectedEvent event = events.expectLogin()
.detail(Details.USERNAME, "test-user@localhost") .session((String) null)
.removeDetail(Details.CONSENT) .error(Errors.USER_TEMPORARILY_DISABLED)
.assertEvent(); .detail(Details.USERNAME, username)
.removeDetail(Details.CONSENT);
if(userId != null) {
event.user(userId);
}
event.assertEvent();
} }
public void loginSuccess() throws Exception { public void loginSuccess() throws Exception {

View file

@ -28,7 +28,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UsernameLoginFailureModel; import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.managers.UserManager; import org.keycloak.services.managers.UserManager;
import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule;
@ -471,10 +471,10 @@ public class UserSessionProviderTest {
@Test @Test
public void loginFailures() { public void loginFailures() {
UsernameLoginFailureModel failure1 = session.sessions().addUserLoginFailure(realm, "user1"); UserLoginFailureModel failure1 = session.sessions().addUserLoginFailure(realm, "user1");
failure1.incrementFailures(); failure1.incrementFailures();
UsernameLoginFailureModel failure2 = session.sessions().addUserLoginFailure(realm, "user2"); UserLoginFailureModel failure2 = session.sessions().addUserLoginFailure(realm, "user2");
failure2.incrementFailures(); failure2.incrementFailures();
failure2.incrementFailures(); failure2.incrementFailures();

View file

@ -365,7 +365,7 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
console.log('realm brute force? ' + realm.bruteForceProtected) console.log('realm brute force? ' + realm.bruteForceProtected)
$scope.temporarilyDisabled = false; $scope.temporarilyDisabled = false;
var isDisabled = function () { var isDisabled = function () {
BruteForceUser.get({realm: realm.realm, username: user.username}, function(data) { BruteForceUser.get({realm: realm.realm, userId: user.id}, function(data) {
console.log('here in isDisabled ' + data.disabled); console.log('here in isDisabled ' + data.disabled);
$scope.temporarilyDisabled = data.disabled; $scope.temporarilyDisabled = data.disabled;
}); });
@ -375,7 +375,7 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
isDisabled(); isDisabled();
$scope.unlockUser = function() { $scope.unlockUser = function() {
BruteForceUser.delete({realm: realm.realm, username: user.username}, function(data) { BruteForceUser.delete({realm: realm.realm, userId: user.id}, function(data) {
isDisabled(); isDisabled();
}); });
} }

View file

@ -227,15 +227,15 @@ module.factory('RealmAdminEvents', function($resource) {
}); });
module.factory('BruteForce', function($resource) { module.factory('BruteForce', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/attack-detection/brute-force/usernames', { return $resource(authUrl + '/admin/realms/:realm/attack-detection/brute-force/users', {
realm : '@realm' realm : '@realm'
}); });
}); });
module.factory('BruteForceUser', function($resource) { module.factory('BruteForceUser', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/attack-detection/brute-force/usernames/:username', { return $resource(authUrl + '/admin/realms/:realm/attack-detection/brute-force/users/:userId', {
realm : '@realm', realm : '@realm',
username : '@username' userId : '@userId'
}); });
}); });