From 5ea3becef57fa239961bd52f3d89235e86c238aa Mon Sep 17 00:00:00 2001 From: rmartinc Date: Wed, 17 Jul 2024 13:46:57 +0200 Subject: [PATCH] Wait for the brute force off-thread processing in AbstractAdvancedBrokerTest Closes #30188 Closes #30641 Signed-off-by: rmartinc --- .../org/keycloak/testsuite/AssertEvents.java | 35 +++++++++++++++++-- .../broker/AbstractAdvancedBrokerTest.java | 22 ++++++++++-- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java index 05a6169512..94e7617a03 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java @@ -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 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; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractAdvancedBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractAdvancedBrokerTest.java index ec38b1dd97..e6b645aee3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractAdvancedBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractAdvancedBrokerTest.java @@ -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);