test brute force

This commit is contained in:
Bill Burke 2015-07-22 12:30:52 -04:00
parent d9b0415047
commit 48a76c2d0d
11 changed files with 702 additions and 7 deletions

View file

@ -33,6 +33,8 @@ public interface UserSessionProvider extends Provider {
UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username); UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username);
UsernameLoginFailureModel addUserLoginFailure(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 onRealmRemoved(RealmModel realm);
void onClientRemoved(RealmModel realm, ClientModel client); void onClientRemoved(RealmModel realm, ClientModel client);

View file

@ -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.FirstResultReducer;
import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer; import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer;
import org.keycloak.models.sessions.infinispan.mapreduce.SessionMapper; 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.UserSessionMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionNoteMapper; import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionNoteMapper;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
@ -293,9 +294,30 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return wrap(key, entity); return wrap(key, entity);
} }
@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 @Override
public void onRealmRemoved(RealmModel realm) { public void onRealmRemoved(RealmModel realm) {
removeUserSessions(realm); removeUserSessions(realm);
removeAllUserLoginFailures(realm);
} }
@Override @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)); tasks.put(key, new CacheTask(cache, CacheOperation.REMOVE, key, null));
} }

View file

@ -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;
}
}
}

View file

@ -92,6 +92,18 @@ public class JpaUserSessionProvider implements UserSessionProvider {
return new UsernameLoginFailureAdapter(entity); return new UsernameLoginFailureAdapter(entity);
} }
@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 @Override
public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
UserSessionEntity entity = new UserSessionEntity(); UserSessionEntity entity = new UserSessionEntity();

View file

@ -323,15 +323,25 @@ public class MemUserSessionProvider implements UserSessionProvider {
} }
@Override @Override
public void onRealmRemoved(RealmModel realm) { public void removeUserLoginFailure(RealmModel realm, String username) {
removeUserSessions(realm); loginFailures.remove(new UsernameLoginFailureKey(realm.getId(), username));
}
@Override
public void removeAllUserLoginFailures(RealmModel realm) {
Iterator<UsernameLoginFailureEntity> itr = loginFailures.values().iterator(); Iterator<UsernameLoginFailureEntity> itr = loginFailures.values().iterator();
while (itr.hasNext()) { while (itr.hasNext()) {
if (itr.next().getRealm().equals(realm.getId())) { if (itr.next().getRealm().equals(realm.getId())) {
itr.remove(); itr.remove();
} }
} }
}
@Override
public void onRealmRemoved(RealmModel realm) {
removeUserSessions(realm);
removeAllUserLoginFailures(realm);
} }
@Override @Override

View file

@ -271,9 +271,28 @@ public class MongoUserSessionProvider implements UserSessionProvider {
return new UsernameLoginFailureAdapter(invocationContext, userEntity); return new UsernameLoginFailureAdapter(invocationContext, userEntity);
} }
@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 @Override
public void onRealmRemoved(RealmModel realm) { public void onRealmRemoved(RealmModel realm) {
removeUserSessions(realm); removeUserSessions(realm);
removeAllUserLoginFailures(realm);
} }
@Override @Override

View file

@ -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();
}
}

View file

@ -108,6 +108,18 @@ public class RealmAdminResource {
return importer.createJaxrsService(realm, auth); return importer.createJaxrsService(realm, auth);
} }
/**
* 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. * Base path for managing clients under this realm.
* *

View file

@ -161,17 +161,30 @@ public class OAuthClient {
} }
public AccessTokenResponse doGrantAccessTokenRequest(String clientSecret, String username, String password) throws Exception { 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(); CloseableHttpClient client = new DefaultHttpClient();
try { try {
HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl()); HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
post.setHeader("Authorization", authorization);
List<NameValuePair> parameters = new LinkedList<NameValuePair>(); List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD)); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
parameters.add(new BasicNameValuePair("username", username)); parameters.add(new BasicNameValuePair("username", username));
parameters.add(new BasicNameValuePair("password", password)); 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) { if (clientSessionState != null) {
parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, clientSessionState)); 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 { public HttpResponse doLogout(String refreshToken, String clientSecret) throws IOException {
CloseableHttpClient client = new DefaultHttpClient(); CloseableHttpClient client = new DefaultHttpClient();
try { try {
@ -375,6 +389,11 @@ public class OAuthClient {
return b.build(realm).toString(); 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() { public String getRefreshTokenUrl() {
UriBuilder b = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(baseUrl)); UriBuilder b = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(baseUrl));
return b.build(realm).toString(); return b.build(realm).toString();

View file

@ -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();
}
}

View file

@ -22,14 +22,20 @@
package org.keycloak.testsuite.pages; 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.WebElement;
import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.FindBy;
import javax.ws.rs.core.UriBuilder;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class AppPage extends AbstractPage { 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"; public static final String baseUrl = "http://localhost:8081/app";
@FindBy(id = "account") @FindBy(id = "account")
@ -57,4 +63,11 @@ public class AppPage extends AbstractPage {
AUTH_RESPONSE, LOGOUT_REQUEST, APP_REQUEST 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);
}
} }