KEYCLOAK-19773 BFD and Direct Grant - inconsistent number of failures
Do not "failure" on temporary or permanently locked users, but "forceChallenge" Failure increments number of failures, and forceChallenge doesn't Test cases cover: 1. Already disabled users 2. Temporarily disabled users by BFD 3. Permanently disabled users by BFD
This commit is contained in:
parent
e1916fbdb1
commit
c9e1e00b95
2 changed files with 100 additions and 4 deletions
|
@ -82,7 +82,7 @@ public class ValidateUsername extends AbstractDirectGrantAuthenticator {
|
||||||
context.getEvent().user(user);
|
context.getEvent().user(user);
|
||||||
context.getEvent().error(bruteForceError);
|
context.getEvent().error(bruteForceError);
|
||||||
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials");
|
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials");
|
||||||
context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
|
context.forceChallenge(challengeResponse);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ public class ValidateUsername extends AbstractDirectGrantAuthenticator {
|
||||||
context.getEvent().user(user);
|
context.getEvent().user(user);
|
||||||
context.getEvent().error(Errors.USER_DISABLED);
|
context.getEvent().error(Errors.USER_DISABLED);
|
||||||
Response challengeResponse = errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_grant", "Account disabled");
|
Response challengeResponse = errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_grant", "Account disabled");
|
||||||
context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
|
context.forceChallenge(challengeResponse);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
context.setUser(user);
|
context.setUser(user);
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.forms;
|
package org.keycloak.testsuite.forms;
|
||||||
|
|
||||||
|
import org.hamcrest.MatcherAssert;
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
@ -98,13 +99,15 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
private int lifespan;
|
private int lifespan;
|
||||||
|
|
||||||
|
private static final Integer failureFactor= 2;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
UserRepresentation user = RealmRepUtil.findUser(testRealm, "test-user@localhost");
|
UserRepresentation user = RealmRepUtil.findUser(testRealm, "test-user@localhost");
|
||||||
UserBuilder.edit(user).totpSecret("totpSecret");
|
UserBuilder.edit(user).totpSecret("totpSecret");
|
||||||
|
|
||||||
testRealm.setBruteForceProtected(true);
|
testRealm.setBruteForceProtected(true);
|
||||||
testRealm.setFailureFactor(2);
|
testRealm.setFailureFactor(failureFactor);
|
||||||
testRealm.setMaxDeltaTimeSeconds(20);
|
testRealm.setMaxDeltaTimeSeconds(20);
|
||||||
testRealm.setMaxFailureWaitSeconds(100);
|
testRealm.setMaxFailureWaitSeconds(100);
|
||||||
testRealm.setWaitIncrementSeconds(5);
|
testRealm.setWaitIncrementSeconds(5);
|
||||||
|
@ -122,7 +125,7 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest {
|
||||||
clearUserFailures();
|
clearUserFailures();
|
||||||
clearAllUserFailures();
|
clearAllUserFailures();
|
||||||
RealmRepresentation realm = adminClient.realm("test").toRepresentation();
|
RealmRepresentation realm = adminClient.realm("test").toRepresentation();
|
||||||
realm.setFailureFactor(2);
|
realm.setFailureFactor(failureFactor);
|
||||||
realm.setMaxDeltaTimeSeconds(20);
|
realm.setMaxDeltaTimeSeconds(20);
|
||||||
realm.setMaxFailureWaitSeconds(100);
|
realm.setMaxFailureWaitSeconds(100);
|
||||||
realm.setWaitIncrementSeconds(5);
|
realm.setWaitIncrementSeconds(5);
|
||||||
|
@ -316,6 +319,73 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNumberOfFailuresForDisabledUsersWithPasswordGrantType() throws Exception {
|
||||||
|
try {
|
||||||
|
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0);
|
||||||
|
assertUserNumberOfFailures(user.getId(), 0);
|
||||||
|
user.setEnabled(false);
|
||||||
|
updateUser(user);
|
||||||
|
|
||||||
|
OAuthClient.AccessTokenResponse response = getTestToken("invalid", "invalid");
|
||||||
|
Assert.assertNull(response.getAccessToken());
|
||||||
|
Assert.assertEquals(response.getError(), "invalid_grant");
|
||||||
|
Assert.assertEquals(response.getErrorDescription(), "Account disabled");
|
||||||
|
events.clear();
|
||||||
|
|
||||||
|
assertUserNumberOfFailures(user.getId(), 0);
|
||||||
|
} finally {
|
||||||
|
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0);
|
||||||
|
user.setEnabled(true);
|
||||||
|
updateUser(user);
|
||||||
|
events.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNumberOfFailuresForTemporaryDisabledUsersWithPasswordGrantType() throws Exception {
|
||||||
|
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0);
|
||||||
|
|
||||||
|
// Lock user (temporarily) and make sure the number of failures matches failure factor
|
||||||
|
lockUserWithPasswordGrant();
|
||||||
|
assertUserNumberOfFailures(user.getId(), failureFactor);
|
||||||
|
|
||||||
|
// Try to login with invalid credentials and make sure the number of failures doesn't change during temporary lockout
|
||||||
|
sendInvalidPasswordPasswordGrant();
|
||||||
|
assertUserNumberOfFailures(user.getId(), failureFactor);
|
||||||
|
|
||||||
|
events.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNumberOfFailuresForPermanentlyDisabledUsersWithPasswordGrantType() throws Exception {
|
||||||
|
RealmRepresentation realm = testRealm().toRepresentation();
|
||||||
|
try {
|
||||||
|
// Set permanent lockout for the test
|
||||||
|
realm.setPermanentLockout(true);
|
||||||
|
testRealm().update(realm);
|
||||||
|
|
||||||
|
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0);
|
||||||
|
|
||||||
|
// Lock user (permanently) and make sure the number of failures matches failure factor
|
||||||
|
lockUserWithPasswordGrant();
|
||||||
|
assertUserNumberOfFailures(user.getId(), failureFactor);
|
||||||
|
assertUserDisabledReason(BruteForceProtector.DISABLED_BY_PERMANENT_LOCKOUT);
|
||||||
|
|
||||||
|
// Try to login with invalid credentials and make sure the number of failures doesn't change during temporary lockout
|
||||||
|
sendInvalidPasswordPasswordGrant();
|
||||||
|
assertUserNumberOfFailures(user.getId(), failureFactor);
|
||||||
|
|
||||||
|
events.clear();
|
||||||
|
} finally {
|
||||||
|
realm.setPermanentLockout(false);
|
||||||
|
testRealm().update(realm);
|
||||||
|
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost", 0, 1).get(0);
|
||||||
|
user.setEnabled(true);
|
||||||
|
updateUser(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBrowserInvalidPassword() throws Exception {
|
public void testBrowserInvalidPassword() throws Exception {
|
||||||
loginSuccess();
|
loginSuccess();
|
||||||
|
@ -725,4 +795,30 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest {
|
||||||
.firstAttribute(UserModel.DISABLED_REASON);
|
.firstAttribute(UserModel.DISABLED_REASON);
|
||||||
assertEquals(expected, actual);
|
assertEquals(expected, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void assertUserNumberOfFailures(String userId, Integer numberOfFailures) {
|
||||||
|
Map<String, Object> userAttackInfo = adminClient.realm("test").attackDetection().bruteForceUserStatus(userId);
|
||||||
|
MatcherAssert.assertThat((Integer) userAttackInfo.get("numFailures"), is(numberOfFailures));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendInvalidPasswordPasswordGrant() throws Exception {
|
||||||
|
String totpSecret = totp.generateTOTP("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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void lockUserWithPasswordGrant() throws Exception {
|
||||||
|
String totpSecret = totp.generateTOTP("totpSecret");
|
||||||
|
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
||||||
|
Assert.assertNotNull(response.getAccessToken());
|
||||||
|
Assert.assertNull(response.getError());
|
||||||
|
events.clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < failureFactor; ++i) {
|
||||||
|
sendInvalidPasswordPasswordGrant();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue