keycloak-uncached

Merge pull request #1481 from patriot1burke/master brute

7/22/2015 3:46:29 PM

Changes

testsuite/integration/src/test/java/org/keycloak/testsuite/model/AuthenticationManagerTest.java 288(+0 -288)

Details

diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index f7968a0..f3fe77f 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -216,7 +216,7 @@ module.controller('UserConsentsCtrl', function($scope, realm, user, userConsents
 });
 
 
-module.controller('UserListCtrl', function($scope, realm, User, UserImpersonation) {
+module.controller('UserListCtrl', function($scope, realm, User, UserImpersonation, BruteForce, Notifications) {
     $scope.realm = realm;
     $scope.page = 0;
 
@@ -236,6 +236,13 @@ module.controller('UserListCtrl', function($scope, realm, User, UserImpersonatio
         });
     };
 
+    $scope.unlockUsers = function() {
+        BruteForce.delete({realm: realm.realm}, function(data) {
+            Notifications.success("Any temporarily locked users are now unlocked.");
+        });
+    }
+
+
     $scope.firstPage = function() {
         $scope.query.first = 0;
         $scope.searchQuery();
@@ -282,7 +289,7 @@ module.controller('UserTabCtrl', function($scope, $location, Dialog, Notificatio
     };
 });
 
-module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFederationInstances, UserImpersonation, RequiredActions, $location, Dialog, Notifications) {
+module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser, User, UserFederationInstances, UserImpersonation, RequiredActions, $location, Dialog, Notifications) {
     $scope.realm = realm;
     $scope.create = !user.id;
     $scope.editUsername = $scope.create || $scope.realm.editUsernameAllowed;
@@ -315,6 +322,23 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede
         } else {
             console.log("federationLink is null");
         }
+        console.log('realm brute force? ' + realm.bruteForceProtected)
+        $scope.temporarilyDisabled = false;
+        var isDisabled = function () {
+            BruteForceUser.get({realm: realm.realm, username: user.username}, function(data) {
+                console.log('here in isDisabled ' + data.disabled);
+                $scope.temporarilyDisabled = data.disabled;
+            });
+        };
+
+        console.log("check if disabled");
+        isDisabled();
+
+        $scope.unlockUser = function() {
+            BruteForceUser.delete({realm: realm.realm, username: user.username}, function(data) {
+                isDisabled();
+            });
+        }
     }
 
     $scope.changed = false; // $scope.create;
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
index 5e9fefd..1d6a6be 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -186,6 +186,20 @@ module.factory('RealmAdminEvents', function($resource) {
     });
 });
 
+module.factory('BruteForce', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/attack-detection/brute-force/usernames', {
+        realm : '@realm'
+    });
+});
+
+module.factory('BruteForceUser', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/attack-detection/brute-force/usernames/:username', {
+        realm : '@realm',
+        username : '@username'
+    });
+});
+
+
 module.factory('RequiredActions', function($resource) {
     return $resource(authUrl + '/admin/realms/:id/authentication/required-actions/:alias', {
         realm : '@realm',
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
index e9da897..5b4ae7f 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
@@ -66,6 +66,16 @@
                 </div>
                 <kc-tooltip>A disabled user cannot login.</kc-tooltip>
             </div>
+            <div class="form-group clearfix block" data-ng-show="realm.bruteForceProtected && !create">
+                <label class="col-md-2 control-label" for="temporarilyDisabled">User Temporarily Locked</label>
+                <div class="col-md-1">
+                    <input ng-model="temporarilyDisabled" name="temporarilyDisabled" id="temporarilyDisabled" data-ng-readonly="true" data-ng-disabled="true" onoffswitch />
+                </div>
+                <kc-tooltip>The user may have been locked due to failing to login too many times.</kc-tooltip>
+                <div class="col-sm-2">
+                    <button type="submit" data-ng-click="unlockUser()" data-ng-show="temporarilyDisabled" class="btn btn-default">Unlock User</button>
+                </div>
+            </div>
             <div class="form-group clearfix block" data-ng-show="!create && user.federationLink">
                 <label class="col-md-2 control-label" for="userEnabled">Federation Link</label>
                 <div class="col-md-6">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
index 508e07f..0d069e0 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
@@ -18,6 +18,7 @@
                     <button id="viewAllUsers" class="btn btn-default" ng-click="query.search = null; firstPage()">View all users</button>
 
                     <div class="pull-right" data-ng-show="access.manageUsers">
+                        <button data-ng-click="unlockUsers()" class="btn btn-default">Unlock Users</button>
                         <a id="createUser" class="btn btn-default" href="#/create/user/{{realm.realm}}">Add User</a>
                     </div>
                 </div>
diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
index 2a874a8..3b4d147 100755
--- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
@@ -33,6 +33,8 @@ public interface UserSessionProvider extends Provider {
 
     UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username);
     UsernameLoginFailureModel addUserLoginFailure(RealmModel realm, String username);
+    void removeUserLoginFailure(RealmModel realm, String username);
+    void removeAllUserLoginFailures(RealmModel realm);
 
     void onRealmRemoved(RealmModel realm);
     void onClientRemoved(RealmModel realm, ClientModel client);
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index f865ca3..2202009 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -20,6 +20,7 @@ import org.keycloak.models.sessions.infinispan.mapreduce.ClientSessionMapper;
 import org.keycloak.models.sessions.infinispan.mapreduce.FirstResultReducer;
 import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer;
 import org.keycloak.models.sessions.infinispan.mapreduce.SessionMapper;
+import org.keycloak.models.sessions.infinispan.mapreduce.UserLoginFailureMapper;
 import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionMapper;
 import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionNoteMapper;
 import org.keycloak.models.utils.KeycloakModelUtils;
@@ -294,8 +295,29 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
     }
 
     @Override
+    public void removeUserLoginFailure(RealmModel realm, String username) {
+        LoginFailureKey key = new LoginFailureKey(realm.getId(), username);
+        tx.remove(loginFailureCache, key);
+    }
+
+    @Override
+    public void removeAllUserLoginFailures(RealmModel realm) {
+        Map<LoginFailureKey, Object> sessions = new MapReduceTask(loginFailureCache)
+                .mappedWith(UserLoginFailureMapper.create(realm.getId()).emitKey())
+                .reducedWith(new FirstResultReducer())
+                .execute();
+
+        for (LoginFailureKey id : sessions.keySet()) {
+            tx.remove(loginFailureCache, id);
+        }
+    }
+
+
+
+    @Override
     public void onRealmRemoved(RealmModel realm) {
         removeUserSessions(realm);
+        removeAllUserLoginFailures(realm);
     }
 
     @Override
@@ -474,7 +496,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
             }
         }
 
-        public void remove(Cache cache, String key) {
+        public void remove(Cache cache, Object key) {
             tasks.put(key, new CacheTask(cache, CacheOperation.REMOVE, key, null));
         }
 
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserLoginFailureMapper.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserLoginFailureMapper.java
new file mode 100755
index 0000000..766a863
--- /dev/null
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserLoginFailureMapper.java
@@ -0,0 +1,53 @@
+package org.keycloak.models.sessions.infinispan.mapreduce;
+
+import org.infinispan.distexec.mapreduce.Collector;
+import org.infinispan.distexec.mapreduce.Mapper;
+import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
+import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+
+import java.io.Serializable;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class UserLoginFailureMapper implements Mapper<LoginFailureKey, LoginFailureEntity, LoginFailureKey, Object>, Serializable {
+
+    public UserLoginFailureMapper(String realm) {
+        this.realm = realm;
+    }
+
+    private enum EmitValue {
+        KEY, ENTITY
+    }
+
+    private String realm;
+
+    private EmitValue emit = EmitValue.ENTITY;
+
+    public static UserLoginFailureMapper create(String realm) {
+        return new UserLoginFailureMapper(realm);
+    }
+
+    public UserLoginFailureMapper emitKey() {
+        emit = EmitValue.KEY;
+        return this;
+    }
+
+    @Override
+    public void map(LoginFailureKey key, LoginFailureEntity e, Collector collector) {
+        if (!realm.equals(e.getRealm())) {
+            return;
+        }
+
+        switch (emit) {
+            case KEY:
+                collector.emit(key, key);
+                break;
+            case ENTITY:
+                collector.emit(key, e);
+                break;
+        }
+    }
+
+}
diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java
index 8614ec0..20ac967 100755
--- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java
+++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java
@@ -93,6 +93,18 @@ public class JpaUserSessionProvider implements UserSessionProvider {
     }
 
     @Override
+    public void removeUserLoginFailure(RealmModel realm, String username) {
+        UsernameLoginFailureEntity entity = em.find(UsernameLoginFailureEntity.class, new UsernameLoginFailureEntity.Key(realm.getId(), username));
+        if (entity == null) return;
+        em.remove(entity);
+    }
+
+    @Override
+    public void removeAllUserLoginFailures(RealmModel realm) {
+        em.createNamedQuery("removeLoginFailuresByRealm").setParameter("realmId", realm.getId()).executeUpdate();
+    }
+
+    @Override
     public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
         UserSessionEntity entity = new UserSessionEntity();
         entity.setId(KeycloakModelUtils.generateId());
diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
index ac6655f..c32c4db 100755
--- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
+++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
@@ -323,15 +323,25 @@ public class MemUserSessionProvider implements UserSessionProvider {
     }
 
     @Override
-    public void onRealmRemoved(RealmModel realm) {
-        removeUserSessions(realm);
+    public void removeUserLoginFailure(RealmModel realm, String username) {
+        loginFailures.remove(new UsernameLoginFailureKey(realm.getId(), username));
+    }
 
+    @Override
+    public void removeAllUserLoginFailures(RealmModel realm) {
         Iterator<UsernameLoginFailureEntity> itr = loginFailures.values().iterator();
         while (itr.hasNext()) {
             if (itr.next().getRealm().equals(realm.getId())) {
                 itr.remove();
             }
         }
+
+    }
+
+    @Override
+    public void onRealmRemoved(RealmModel realm) {
+        removeUserSessions(realm);
+        removeAllUserLoginFailures(realm);
     }
 
     @Override
diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java
index 82045fd..d75da9a 100755
--- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java
+++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java
@@ -272,8 +272,27 @@ public class MongoUserSessionProvider implements UserSessionProvider {
     }
 
     @Override
+    public void removeUserLoginFailure(RealmModel realm, String username) {
+        DBObject query = new QueryBuilder()
+                .and("username").is(username)
+                .and("realmId").is(realm.getId())
+                .get();
+        mongoStore.removeEntities(MongoUsernameLoginFailureEntity.class, query, false, invocationContext);
+    }
+
+    @Override
+    public void removeAllUserLoginFailures(RealmModel realm) {
+        DBObject query = new QueryBuilder()
+                .and("realmId").is(realm.getId())
+                .get();
+        mongoStore.removeEntities(MongoUsernameLoginFailureEntity.class, query, false, invocationContext);
+
+    }
+
+    @Override
     public void onRealmRemoved(RealmModel realm) {
         removeUserSessions(realm);
+        removeAllUserLoginFailures(realm);
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index 1508746..fbe37b4 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -596,9 +596,8 @@ public class AuthenticationProcessor {
     }
 
     public void validateUser(UserModel authenticatedUser) {
-        if (authenticatedUser != null) {
-            if (!authenticatedUser.isEnabled()) throw new AuthException(Error.USER_DISABLED);
-        }
+        if (authenticatedUser == null) return;
+        if (!authenticatedUser.isEnabled()) throw new AuthException(Error.USER_DISABLED);
         if (realm.isBruteForceProtected()) {
             if (protector.isTemporarilyDisabled(session, realm, authenticatedUser.getUsername())) {
                 throw new AuthException(Error.USER_TEMPORARILY_DISABLED);
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index c9ca6de..ea95c14 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -592,121 +592,6 @@ public class AuthenticationManager {
         return null;
     }
 
-    public AuthenticationStatus authenticateForm(KeycloakSession session, ClientConnection clientConnection, RealmModel realm, MultivaluedMap<String, String> formData) {
-        String username = formData.getFirst(FORM_USERNAME);
-        if (username == null) {
-            logger.debug("Username not provided");
-            return AuthenticationStatus.INVALID_USER;
-        }
-
-        if (realm.isBruteForceProtected()) {
-            if (protector.isTemporarilyDisabled(session, realm, username)) {
-                return AuthenticationStatus.ACCOUNT_TEMPORARILY_DISABLED;
-            }
-        }
-
-        AuthenticationStatus status = authenticateInternal(session, 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(KeycloakSession session, RealmModel realm, MultivaluedMap<String, String> formData, String username) {
-        UserModel user = KeycloakModelUtils.findUserByNameOrEmail(session, realm, username);
-
-        if (user == null) {
-            logger.debugv("User {0} not found", username);
-            return AuthenticationStatus.INVALID_USER;
-        }
-
-        Set<String> types = new HashSet<String>();
-
-        for (RequiredCredentialModel credential : realm.getRequiredCredentials()) {
-            types.add(credential.getType());
-        }
-
-        if (types.contains(CredentialRepresentation.PASSWORD)) {
-            List<UserCredentialModel> credentials = new LinkedList<UserCredentialModel>();
-
-            String password = formData.getFirst(CredentialRepresentation.PASSWORD);
-            if (password != null) {
-                credentials.add(UserCredentialModel.password(password));
-            }
-
-            String passwordToken = formData.getFirst(CredentialRepresentation.PASSWORD_TOKEN);
-            if (passwordToken != null) {
-                credentials.add(UserCredentialModel.passwordToken(passwordToken));
-            }
-
-            String totp = formData.getFirst(CredentialRepresentation.TOTP);
-            if (totp != null) {
-                credentials.add(UserCredentialModel.totp(totp));
-            }
-
-            if ((password == null || password.isEmpty()) && (passwordToken == null || passwordToken.isEmpty())) {
-                logger.debug("Password not provided");
-                return AuthenticationStatus.MISSING_PASSWORD;
-            }
-
-            logger.debugv("validating password for user: {0}", username);
-
-            if (!session.users().validCredentials(realm, user, credentials)) {
-                return AuthenticationStatus.INVALID_CREDENTIALS;
-            }
-
-            if (!user.isEnabled()) {
-                return AuthenticationStatus.ACCOUNT_DISABLED;
-            }
-
-            if (user.isTotp() && totp == null) {
-                return AuthenticationStatus.MISSING_TOTP;
-            }
-
-            if (!user.getRequiredActions().isEmpty()) {
-                return AuthenticationStatus.ACTIONS_REQUIRED;
-            } else {
-                return AuthenticationStatus.SUCCESS;
-            }
-        } else if (types.contains(CredentialRepresentation.SECRET)) {
-            String secret = formData.getFirst(CredentialRepresentation.SECRET);
-            if (secret == null) {
-                logger.debug("Secret not provided");
-                return AuthenticationStatus.MISSING_PASSWORD;
-            }
-            if (!session.users().validCredentials(realm, user, UserCredentialModel.secret(secret))) {
-                return AuthenticationStatus.INVALID_CREDENTIALS;
-            }
-            if (!user.isEnabled()) {
-                return AuthenticationStatus.ACCOUNT_DISABLED;
-            }
-            if (!user.getRequiredActions().isEmpty()) {
-                return AuthenticationStatus.ACTIONS_REQUIRED;
-            } else {
-                return AuthenticationStatus.SUCCESS;
-            }
-        } else {
-            logger.warn("Do not know how to authenticate user");
-            return AuthenticationStatus.FAILED;
-        }
-    }
-
     public enum AuthenticationStatus {
         SUCCESS, ACCOUNT_TEMPORARILY_DISABLED, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java
new file mode 100755
index 0000000..38eec9c
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java
@@ -0,0 +1,158 @@
+package org.keycloak.services.resources.admin;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.BadRequestException;
+import org.jboss.resteasy.spi.NotFoundException;
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.ClientConnection;
+import org.keycloak.events.Event;
+import org.keycloak.events.EventQuery;
+import org.keycloak.events.EventStoreProvider;
+import org.keycloak.events.EventType;
+import org.keycloak.events.admin.AdminEvent;
+import org.keycloak.events.admin.AdminEventQuery;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.exportimport.ClientImporter;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserFederationProviderModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.UsernameLoginFailureModel;
+import org.keycloak.models.cache.CacheRealmProvider;
+import org.keycloak.models.cache.CacheUserProvider;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.representations.adapters.action.GlobalRequestResult;
+import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.services.ErrorResponse;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.BruteForceProtector;
+import org.keycloak.services.managers.LDAPConnectionTestManager;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.managers.ResourceAdminManager;
+import org.keycloak.services.managers.UsersSyncManager;
+import org.keycloak.timer.TimerProvider;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * Base resource class for the admin REST api of one realm
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class AttackDetectionResource {
+    protected static final Logger logger = Logger.getLogger(AttackDetectionResource.class);
+    protected RealmAuth auth;
+    protected RealmModel realm;
+    private AdminEventBuilder adminEvent;
+
+    @Context
+    protected KeycloakSession session;
+
+    @Context
+    protected UriInfo uriInfo;
+
+    @Context
+    protected ClientConnection connection;
+
+    @Context
+    protected HttpHeaders headers;
+
+    @Context
+    protected BruteForceProtector protector;
+
+    public AttackDetectionResource(RealmAuth auth, RealmModel realm, AdminEventBuilder adminEvent) {
+        this.auth = auth;
+        this.realm = realm;
+        this.adminEvent = adminEvent.realm(realm);
+
+        auth.init(RealmAuth.Resource.REALM);
+    }
+
+    /**
+     * Get status of a username in brute force detection
+     *
+     * @param username
+     * @return
+     */
+    @GET
+    @Path("brute-force/usernames/{username}")
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public Map<String, Object> bruteForceUserStatus(@PathParam("username") String username) {
+        auth.hasView();
+        Map<String, Object> data = new HashMap<>();
+        data.put("disabled", false);
+        data.put("numFailures", 0);
+        data.put("lastFailure", 0);
+        data.put("lastIPFailure", "n/a");
+        if (!realm.isBruteForceProtected()) return data;
+
+        UsernameLoginFailureModel model = session.sessions().getUserLoginFailure(realm, username);
+        if (model == null) return data;
+        if (protector.isTemporarilyDisabled(session, realm, username)) {
+            data.put("disabled", true);
+        }
+        data.put("numFailures", model.getNumFailures());
+        data.put("lastFailure", model.getLastFailure());
+        data.put("lastIPFailure", model.getLastIPFailure());
+        return data;
+    }
+
+    /**
+     * Clear any user login failures for the user.  This can release temporary disabled user
+     *
+     * @param username
+     */
+    @Path("brute-force/usernames/{username}")
+    @DELETE
+    public void clearBruteForceForUser(@PathParam("username") String username) {
+        auth.requireManage();
+        UsernameLoginFailureModel model = session.sessions().getUserLoginFailure(realm, username);
+        if (model != null) {
+            session.sessions().removeUserLoginFailure(realm, username);
+            adminEvent.operation(OperationType.DELETE).success();
+        }
+    }
+
+    /**
+     * Clear any user login failures for all users.  This can release temporary disabled users
+     *
+     */
+    @Path("brute-force/usernames")
+    @DELETE
+    public void clearAllBruteForce() {
+        auth.requireManage();
+        session.sessions().removeAllUserLoginFailures(realm);
+        adminEvent.operation(OperationType.DELETE).success();
+    }
+
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 85779bc..40710a1 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -109,6 +109,18 @@ public class RealmAdminResource {
     }
 
     /**
+     * Base path for managing attack detection.
+     *
+     * @return
+     */
+    @Path("attack-detection")
+    public AttackDetectionResource getClientImporter() {
+        AttackDetectionResource resource = new AttackDetectionResource(auth, realm, adminEvent);
+        ResteasyProviderFactory.getInstance().injectProperties(resource);
+        return resource;
+    }
+
+    /**
      * Base path for managing clients under this realm.
      *
      * @return
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java
new file mode 100755
index 0000000..a5eaa64
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java
@@ -0,0 +1,375 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.forms;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.models.Constants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.TimeBasedOTP;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.AppPage.RequestType;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LoginTotpPage;
+import org.keycloak.testsuite.rule.GreenMailRule;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.WebDriver;
+
+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.util.Collections;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class BruteForceTest {
+
+    @ClassRule
+    public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
+            UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm);
+
+            UserCredentialModel credentials = new UserCredentialModel();
+            credentials.setType(CredentialRepresentation.TOTP);
+            credentials.setValue("totpSecret");
+            user.updateCredential(credentials);
+
+            user.setTotp(true);
+            appRealm.setEventsListeners(Collections.singleton("dummy"));
+
+            appRealm.setBruteForceProtected(true);
+            appRealm.setFailureFactor(2);
+        }
+
+    });
+
+    @Rule
+    public AssertEvents events = new AssertEvents(keycloakRule);
+
+    @Rule
+    public WebRule webRule = new WebRule(this);
+
+    @Rule
+    public GreenMailRule greenMail = new GreenMailRule();
+
+    @WebResource
+    protected WebDriver driver;
+
+    @WebResource
+    protected AppPage appPage;
+
+    @WebResource
+    protected LoginPage loginPage;
+
+    @WebResource
+    protected LoginTotpPage loginTotpPage;
+
+    @WebResource
+    protected OAuthClient oauth;
+
+
+    private TimeBasedOTP totp = new TimeBasedOTP();
+
+    private int lifespan;
+
+    @Before
+    public void before() throws MalformedURLException {
+        totp = new TimeBasedOTP();
+    }
+
+    public String getAdminToken() throws Exception {
+        String clientId = Constants.ADMIN_CONSOLE_CLIENT_ID;
+        return oauth.doGrantAccessTokenRequest("master", "admin", "admin", null, clientId, null).getAccessToken();
+    }
+
+    public OAuthClient.AccessTokenResponse getTestToken(String password, String totp) throws Exception {
+        return oauth.doGrantAccessTokenRequest("test", "test-user@localhost", password, totp, oauth.getClientId(), "password");
+
+    }
+
+    protected void clearUserFailures() throws Exception {
+        String token = getAdminToken();
+        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 {
+        String token = getAdminToken();
+        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
+    public void testGrantInvalidPassword() throws Exception {
+        {
+            String totpSecret = totp.generate("totpSecret");
+            OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
+            Assert.assertNotNull(response.getAccessToken());
+            Assert.assertNull(response.getError());
+            events.clear();
+        }
+        {
+            String totpSecret = totp.generate("totpSecret");
+            OAuthClient.AccessTokenResponse response = getTestToken("invalid", totpSecret);
+            Assert.assertNull(response.getAccessToken());
+            Assert.assertEquals(response.getError(), "invalid_grant");
+            Assert.assertEquals(response.getErrorDescription(), "Invalid user credentials");
+            events.clear();
+        }
+        {
+            String totpSecret = totp.generate("totpSecret");
+            OAuthClient.AccessTokenResponse response = getTestToken("invalid", totpSecret);
+            Assert.assertNull(response.getAccessToken());
+            Assert.assertEquals(response.getError(), "invalid_grant");
+            Assert.assertEquals(response.getErrorDescription(), "Invalid user credentials");
+            events.clear();
+        }
+        {
+            String totpSecret = totp.generate("totpSecret");
+            OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
+            Assert.assertNull(response.getAccessToken());
+            Assert.assertNotNull(response.getError());
+            Assert.assertEquals(response.getError(), "invalid_grant");
+            Assert.assertEquals(response.getErrorDescription(), "Account temporarily disabled");
+            events.clear();
+        }
+        clearUserFailures();
+        {
+            String totpSecret = totp.generate("totpSecret");
+            OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
+            Assert.assertNotNull(response.getAccessToken());
+            Assert.assertNull(response.getError());
+            events.clear();
+        }
+
+    }
+
+    @Test
+    public void testGrantInvalidOtp() throws Exception {
+        {
+            String totpSecret = totp.generate("totpSecret");
+            OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
+            Assert.assertNotNull(response.getAccessToken());
+            Assert.assertNull(response.getError());
+            events.clear();
+        }
+        {
+            OAuthClient.AccessTokenResponse response = getTestToken("password", "shite");
+            Assert.assertNull(response.getAccessToken());
+            Assert.assertEquals(response.getError(), "invalid_grant");
+            Assert.assertEquals(response.getErrorDescription(), "Invalid user credentials");
+            events.clear();
+        }
+        {
+            OAuthClient.AccessTokenResponse response = getTestToken("password", "shite");
+            Assert.assertNull(response.getAccessToken());
+            Assert.assertEquals(response.getError(), "invalid_grant");
+            Assert.assertEquals(response.getErrorDescription(), "Invalid user credentials");
+            events.clear();
+        }
+        {
+            String totpSecret = totp.generate("totpSecret");
+            OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
+            Assert.assertNull(response.getAccessToken());
+            Assert.assertNotNull(response.getError());
+            Assert.assertEquals(response.getError(), "invalid_grant");
+            Assert.assertEquals(response.getErrorDescription(), "Account temporarily disabled");
+            events.clear();
+        }
+        clearUserFailures();
+        {
+            String totpSecret = totp.generate("totpSecret");
+            OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
+            Assert.assertNotNull(response.getAccessToken());
+            Assert.assertNull(response.getError());
+            events.clear();
+        }
+
+    }
+
+
+
+
+    @Test
+    public void testBrowserInvalidPassword() throws Exception {
+        loginSuccess();
+        loginInvalidPassword();
+        loginInvalidPassword();
+        expectTemporarilyDisabled();
+        clearUserFailures();
+        loginSuccess();
+        loginInvalidPassword();
+        loginInvalidPassword();
+        expectTemporarilyDisabled();
+        clearAllUserFailures();
+        loginSuccess();
+    }
+
+    @Test
+    public void testBrowserMissingPassword() throws Exception {
+        loginSuccess();
+        loginMissingPassword();
+        loginMissingPassword();
+        expectTemporarilyDisabled();
+        clearUserFailures();
+        loginSuccess();
+    }
+
+    @Test
+    public void testBrowserInvalidTotp() throws Exception {
+        loginSuccess();
+        loginWithTotpFailure();
+        loginWithTotpFailure();
+        expectTemporarilyDisabled();
+        clearUserFailures();
+        loginSuccess();
+    }
+
+    @Test
+    public void testBrowserMissingTotp() throws Exception {
+        loginSuccess();
+        loginWithMissingTotp();
+        loginWithMissingTotp();
+        expectTemporarilyDisabled();
+        clearUserFailures();
+        loginSuccess();
+    }
+
+    public void expectTemporarilyDisabled() throws Exception {
+        loginPage.open();
+        loginPage.login("test-user@localhost", "password");
+
+        loginPage.assertCurrent();
+        String src = driver.getPageSource();
+        Assert.assertEquals("Account is temporarily disabled, contact admin or try again later.", loginPage.getError());
+        events.expectLogin().session((String) null).error(Errors.USER_TEMPORARILY_DISABLED)
+                .detail(Details.USERNAME, "test-user@localhost")
+                .removeDetail(Details.CONSENT)
+                .assertEvent();
+    }
+
+
+
+    public void loginSuccess() throws Exception {
+        loginPage.open();
+        loginPage.login("test-user@localhost", "password");
+
+        loginTotpPage.assertCurrent();
+
+        String totpSecret = totp.generate("totpSecret");
+        loginTotpPage.login(totpSecret);
+
+        Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+        events.expectLogin().assertEvent();
+
+        appPage.logout();
+        events.clear();
+
+
+    }
+
+    public void loginWithTotpFailure() throws Exception {
+        loginPage.open();
+        loginPage.login("test-user@localhost", "password");
+
+        loginTotpPage.assertCurrent();
+
+        loginTotpPage.login("123456");
+        loginTotpPage.assertCurrent();
+        Assert.assertEquals("Invalid authenticator code.", loginPage.getError());
+        events.clear();
+    }
+
+    public void loginWithMissingTotp() throws Exception {
+        loginPage.open();
+        loginPage.login("test-user@localhost", "password");
+
+        loginTotpPage.assertCurrent();
+
+        loginTotpPage.login(null);
+        loginTotpPage.assertCurrent();
+        Assert.assertEquals("Invalid authenticator code.", loginPage.getError());
+
+        events.clear();
+    }
+
+
+    public void loginInvalidPassword() throws Exception {
+        loginPage.open();
+        loginPage.login("test-user@localhost", "invalid");
+
+        loginPage.assertCurrent();
+
+        Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+        events.clear();
+    }
+
+    public void loginMissingPassword() {
+        loginPage.open();
+        loginPage.missingPassword("test-user@localhost");
+
+        loginPage.assertCurrent();
+
+        Assert.assertEquals("Invalid username or password.", loginPage.getError());
+        events.clear();
+    }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
index 1799a64..745a5a2 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
@@ -138,6 +138,21 @@ public class LoginTest {
     }
 
     @Test
+    public void loginMissingPassword() {
+        loginPage.open();
+        loginPage.missingPassword("login-test");
+
+        loginPage.assertCurrent();
+
+        Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+        events.expectLogin().user(userId).session((String) null).error("invalid_user_credentials")
+                .detail(Details.USERNAME, "login-test")
+                .removeDetail(Details.CONSENT)
+                .assertEvent();
+    }
+
+    @Test
     public void loginInvalidPasswordDisabledUser() {
         keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
             @Override
@@ -215,6 +230,20 @@ public class LoginTest {
     }
 
     @Test
+    public void loginMissingUsername() {
+        loginPage.open();
+        loginPage.missingUsername();
+
+        loginPage.assertCurrent();
+
+        Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+        events.expectLogin().user((String) null).session((String) null).error("user_not_found")
+                .removeDetail(Details.CONSENT)
+                .assertEvent();
+    }
+
+    @Test
     public void loginSuccess() {
         loginPage.open();
         loginPage.login("login-test", "password");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
index 3ea0915..0374715 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
@@ -122,6 +122,25 @@ public class LoginTotpTest {
     }
 
     @Test
+    public void loginWithMissingTotp() throws Exception {
+        loginPage.open();
+        loginPage.login("test-user@localhost", "password");
+
+        loginTotpPage.assertCurrent();
+
+        loginTotpPage.login(null);
+        loginTotpPage.assertCurrent();
+        Assert.assertEquals("Invalid authenticator code.", loginPage.getError());
+
+        //loginPage.assertCurrent();  // Invalid authenticator code.
+        //Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+        events.expectLogin().error("invalid_user_credentials").session((String) null)
+                .removeDetail(Details.CONSENT)
+                .assertEvent();
+    }
+
+    @Test
     public void loginWithTotpSuccess() throws Exception {
         loginPage.open();
         loginPage.login("test-user@localhost", "password");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
index 1bac627..abfe3c3 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
@@ -161,17 +161,30 @@ public class OAuthClient {
     }
 
     public AccessTokenResponse doGrantAccessTokenRequest(String clientSecret, String username,  String password) throws Exception {
+        return doGrantAccessTokenRequest(realm, username, password, null, clientId, clientSecret);
+    }
+
+    public AccessTokenResponse doGrantAccessTokenRequest(String realm, String username, String password, String totp,
+                                                         String clientId, String clientSecret) throws Exception {
         CloseableHttpClient client = new DefaultHttpClient();
         try {
-            HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl());
-
-            String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
-            post.setHeader("Authorization", authorization);
+            HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
 
             List<NameValuePair> parameters = new LinkedList<NameValuePair>();
             parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
             parameters.add(new BasicNameValuePair("username", username));
             parameters.add(new BasicNameValuePair("password", password));
+            if (totp != null) {
+                parameters.add(new BasicNameValuePair("totp", totp));
+
+            }
+            if (clientSecret != null) {
+                String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
+                post.setHeader("Authorization", authorization);
+            } else {
+                parameters.add(new BasicNameValuePair("client_id", clientId));
+
+            }
 
             if (clientSessionState != null) {
                 parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, clientSessionState));
@@ -219,6 +232,7 @@ public class OAuthClient {
         }
     }
 
+
     public HttpResponse doLogout(String refreshToken, String clientSecret) throws IOException {
         CloseableHttpClient client = new DefaultHttpClient();
         try {
@@ -400,6 +414,11 @@ public class OAuthClient {
         return b.build(realm).toString();
     }
 
+    public String getResourceOwnerPasswordCredentialGrantUrl(String realm) {
+        UriBuilder b = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(baseUrl));
+        return b.build(realm).toString();
+    }
+
     public String getServiceAccountUrl() {
         return getResourceOwnerPasswordCredentialGrantUrl();
     }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java
old mode 100644
new mode 100755
index 7fa6c9e..82d696e
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java
@@ -22,14 +22,20 @@
 package org.keycloak.testsuite.pages;
 
 
+import org.keycloak.OAuth2Constants;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.testsuite.OAuthClient;
 import org.openqa.selenium.WebElement;
 import org.openqa.selenium.support.FindBy;
 
+import javax.ws.rs.core.UriBuilder;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
 public class AppPage extends AbstractPage {
 
+    public static final String AUTH_SERVER_URL = "http://localhost:8081/auth";
     public static final String baseUrl = "http://localhost:8081/app";
 
     @FindBy(id = "account")
@@ -57,4 +63,11 @@ public class AppPage extends AbstractPage {
         AUTH_RESPONSE, LOGOUT_REQUEST, APP_REQUEST
     }
 
+    public void logout() {
+        String logoutUri = OIDCLoginProtocolService.logoutUrl(UriBuilder.fromUri(AUTH_SERVER_URL))
+                .queryParam(OAuth2Constants.REDIRECT_URI,baseUrl).build("test").toString();
+        driver.navigate().to(logoutUri);
+
+    }
+
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
old mode 100644
new mode 100755
index 2be207c..2ea8b62
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
@@ -91,6 +91,19 @@ public class LoginPage extends AbstractPage {
         submitButton.click();
     }
 
+    public void missingPassword(String username) {
+        usernameInput.clear();
+        usernameInput.sendKeys(username);
+        passwordInput.clear();
+        submitButton.click();
+
+    }
+    public void missingUsername() {
+        usernameInput.clear();
+        submitButton.click();
+
+    }
+
     public String getUsername() {
         return usernameInput.getAttribute("value");
     }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java
old mode 100644
new mode 100755
index e1a934a..b725a16
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java
@@ -43,7 +43,8 @@ public class LoginTotpPage extends AbstractPage {
     private WebElement loginErrorMessage;
 
     public void login(String totp) {
-        totpInput.sendKeys(totp);
+        totpInput.clear();
+        if (totp != null) totpInput.sendKeys(totp);
 
         submitButton.click();
     }