keycloak-memoizeit

test brute force

7/22/2015 1:30:52 PM

Details

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/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/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
index 2ad8e1b..4f284ba 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));
@@ -194,6 +207,7 @@ public class OAuthClient {
         }
     }
 
+
     public HttpResponse doLogout(String refreshToken, String clientSecret) throws IOException {
         CloseableHttpClient client = new DefaultHttpClient();
         try {
@@ -375,6 +389,11 @@ public class OAuthClient {
         return b.build(realm).toString();
     }
 
+    public String getResourceOwnerPasswordCredentialGrantUrl(String realmName) {
+        UriBuilder b = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(baseUrl));
+        return b.build(realmName).toString();
+    }
+
     public String getRefreshTokenUrl() {
         UriBuilder b = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(baseUrl));
         return b.build(realm).toString();
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);
+
+    }
+
 }