Wait for the brute force off-thread processing in AbstractAdvancedBrokerTest

Closes #30188
Closes #30641

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2024-07-17 13:46:57 +02:00 committed by Marek Posolda
parent 3e134f714c
commit 5ea3becef5
2 changed files with 51 additions and 6 deletions

View file

@ -27,6 +27,7 @@ import org.junit.rules.TestRule;
import org.junit.runners.model.Statement;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.representations.idm.ClientRepresentation;
@ -41,6 +42,7 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.emptyOrNullString;
import static org.hamcrest.Matchers.is;
@ -369,16 +371,21 @@ public class AssertEvents implements TestRule {
}
public EventRepresentation assertEvent() {
return assertEvent(false);
return assertEvent(false, 0);
}
public EventRepresentation assertEvent(boolean ignorePreviousEvents) {
return assertEvent(ignorePreviousEvents, 0);
}
/**
* Assert the expected event was sent to the listener by Keycloak server. Returns this event.
*
* @param ignorePreviousEvents if true, test will ignore all the events, which were already present. Test will poll the events from the queue until it finds the event of expected type
* @param seconds The seconds to wait for the next event to come
* @return the expected event
*/
public EventRepresentation assertEvent(boolean ignorePreviousEvents) {
public EventRepresentation assertEvent(boolean ignorePreviousEvents, int seconds) {
if (expected.getError() != null && ! expected.getType().endsWith("_ERROR")) {
expected.setType(expected.getType() + "_ERROR");
}
@ -387,7 +394,7 @@ public class AssertEvents implements TestRule {
// Consider 25 as a "limit" for maximum number of events in the queue for now
List<String> presentedEventTypes = new LinkedList<>();
for (int i = 0 ; i < 25 ; i++) {
EventRepresentation event = fetchNextEvent();
EventRepresentation event = fetchNextEvent(seconds);
if (event == null) {
Assert.fail("Did not find the event of expected type " + expected.getType() +". Events present: " + presentedEventTypes);
}
@ -523,4 +530,26 @@ public class AssertEvents implements TestRule {
private EventRepresentation fetchNextEvent() {
return context.testingClient.testing().pollEvent();
}
private EventRepresentation fetchNextEvent(int seconds) {
if (seconds <= 0) {
return fetchNextEvent();
}
final long millis = TimeUnit.SECONDS.toMillis(seconds);
final long start = Time.currentTimeMillis();
do {
try {
EventRepresentation event = fetchNextEvent();
if (event != null) {
return event;
}
// wait a bit to receive the event
TimeUnit.MILLISECONDS.sleep(millis / 10L);
} catch (InterruptedException e) {
// no-op
}
} while (Time.currentTimeMillis() - start < millis);
return null;
}
}

View file

@ -1,10 +1,13 @@
package org.keycloak.testsuite.broker;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.models.IdentityProviderMapperSyncMode;
import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.models.utils.TimeBasedOTP;
@ -18,6 +21,7 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.Urls;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory;
import org.keycloak.testsuite.util.AccountHelper;
@ -70,6 +74,8 @@ import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
*/
public abstract class AbstractAdvancedBrokerTest extends AbstractBrokerTest {
@Rule
public AssertEvents events = new AssertEvents(this);
protected void createRoleMappersForConsumerRealm() {
createRoleMappersForConsumerRealm(IdentityProviderMapperSyncMode.FORCE);
@ -546,7 +552,8 @@ public abstract class AbstractAdvancedBrokerTest extends AbstractBrokerTest {
totpPage.assertCurrent();
String totpSecret = totpPage.getTotpSecret();
totpPage.configure(totp.generateTOTP(totpSecret));
assertNumFederatedIdentities(realm.users().search(bc.getUserLogin()).get(0).getId(), 1);
final UserRepresentation user = realm.users().search(bc.getUserLogin()).get(0);
assertNumFederatedIdentities(user.getId(), 1);
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
@ -563,16 +570,25 @@ public abstract class AbstractAdvancedBrokerTest extends AbstractBrokerTest {
loginTotpPage.login("bad-totp");
Assert.assertEquals("Invalid authenticator code.", loginTotpPage.getInputError());
events.clear();
loginTotpPage.login("bad-totp");
Assert.assertEquals("Invalid authenticator code.", loginTotpPage.getInputError());
// wait for the disabled to come
events.expect(EventType.USER_DISABLED_BY_TEMPORARY_LOCKOUT)
.realm(consumerRealmRep.getId())
.user(user.getId())
.client((String) null)
.detail(Details.REASON, "brute_force_attack detected")
.assertEvent(true, 5);
// Login with valid TOTP. I should not be able to login
loginTotpPage.login(totp.generateTOTP(totpSecret));
Assert.assertEquals("Invalid authenticator code.", loginTotpPage.getInputError());
// Clear login failures
String userId = ApiUtil.findUserByUsername(realm, bc.getUserLogin()).getId();
realm.attackDetection().clearBruteForceForUser(userId);
realm.attackDetection().clearBruteForceForUser(user.getId());
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp);