Separate password and OTP brute force protection to prevent OTP bypass attacks by default#46168
Separate password and OTP brute force protection to prevent OTP bypass attacks by default#46168pskopek wants to merge 1 commit intokeycloak:mainfrom
Conversation
...ispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureEntity.java
Show resolved
Hide resolved
...ak/models/sessions/infinispan/changes/remote/updater/loginfailures/LoginFailuresUpdater.java
Outdated
Show resolved
Hide resolved
3a51e50 to
c695a03
Compare
…s attacks by default Closes keycloak#46164 Signed-off-by: Peter Skopek <peter.skopek@ibm.com> Update model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/remote/updater/loginfailures/LoginFailuresUpdater.java Co-authored-by: Pedro Ruivo <pruivo@users.noreply.github.com> Signed-off-by: Peter Skopek <peter.skopek@ibm.com> Add recovery codes to the list of brute force checked authenticators. Closes keycloak#46164 Signed-off-by: Peter Skopek <peter.skopek@ibm.com>
c695a03 to
6e32ba3
Compare
|
Fixed the DCO issue (no other changes). |
| if (realm.getRequiredCredentialsStream().anyMatch(r -> Objects.equals(r.getType(), RequiredCredentialModel.PASSWORD.getType()))) { | ||
| return RequiredCredentialModel.PASSWORD.getType(); | ||
| } else if (realm.getRequiredCredentialsStream().anyMatch(r -> Objects.equals(r.getType(), RequiredCredentialModel.TOTP.getType()))) { | ||
| return RequiredCredentialModel.TOTP.getType(); |
There was a problem hiding this comment.
Is it possible the realm requires both a password and TOTP? If so, this method will never return the TOTP type.
| } | ||
|
|
||
| @Test | ||
| public void testGrantPermamentOtpAbsolut() { |
There was a problem hiding this comment.
typo
| public void testGrantPermamentOtpAbsolut() { | |
| public void testGrantPermanentOtpAbsolut() { |
| AccessTokenResponse response = getTestToken(getPassword("test-user@localhost"), totpSecret); | ||
| assertTokenNull(response); | ||
| Assert.assertNotNull(response.getError()); | ||
| Assert.assertEquals(response.getError(), "invalid_grant"); |
There was a problem hiding this comment.
parameters in the wrong order:
| Assert.assertEquals(response.getError(), "invalid_grant"); | |
| Assert.assertEquals("invalid_grant", response.getError()); |
| events.clear(); | ||
| } | ||
| } | ||
| { |
There was a problem hiding this comment.
I'm not sure why you need this block
| Assert.assertEquals(response.getError(), "invalid_grant"); | ||
| Assert.assertEquals(response.getErrorDescription(), "Invalid user credentials"); |
There was a problem hiding this comment.
| Assert.assertEquals(response.getError(), "invalid_grant"); | |
| Assert.assertEquals(response.getErrorDescription(), "Invalid user credentials"); | |
| Assert.assertEquals("invalid_grant", response.getError()); | |
| Assert.assertEquals("Invalid user credentials", response.getErrorDescription()); |
rmartinc
left a comment
There was a problem hiding this comment.
Thanks @pskopek!
I think the test is not testing the feature and, checking the test, I detected the category is not correctly detected in direct access grants. I did a quick fix in branch https://github.com/rmartinc/keycloak/tree/pr-46168. But probably something will fail. 😄
| ); | ||
|
|
||
| public static final String OTP_CATEGORY = OTPCredentialModel.TYPE; | ||
| public static final int MAX_OTP_FAILURES = 100; |
There was a problem hiding this comment.
I would have allowed this as configurable in the brute force, but we can do this later if it is requested.
| Assert.assertNotNull(response.getError()); | ||
| Assert.assertEquals(response.getError(), "invalid_grant"); | ||
| Assert.assertEquals("Invalid user credentials", response.getErrorDescription()); | ||
| assertUserDisabledEvent(Errors.USER_TEMPORARILY_DISABLED); |
There was a problem hiding this comment.
@pskopek I think that this test is not testing that the the user is permanently locked after 100 failed OTP attempts. Probably you need to set in the realm failure factor to more than 100 and quick login check millis to 0. This way the user is not temporarily locked out and you can check the OTP is really permanently locking the user.
| : AuthenticationProcessor.LAST_PROCESSED_EXECUTION | ||
| ) | ||
| ); | ||
| if (execution == null) return null; |
There was a problem hiding this comment.
Playing with your test, I think that direct access grants return null execution here so OTP is not detected here for that grant. Maybe we have to pass the authenticator on failure and use the credentials used in success. I did a quick tets in branch: https://github.com/rmartinc/keycloak/tree/pr-46168
mposolda
left a comment
There was a problem hiding this comment.
I did not checked the details in the codebase as it is already reviewed by others. Just some high level thoughts:
-
It seems to me that the behavior of automatically doing permanent lockout for OTP is not backwards compatible. Existing deployments may not expect this behavior as there is not permanent lockout as of now. I suppose we may need a switch on the brute-force protector (disabled by default) and make this behavior enabled just with the switch?
-
It will be good to document this also in the brute-force protector documentation (probably together with the new switch added from above if we add this). As upgrading guide is something, which people read just when updating to the particular release.
-
I think we should also document (in both brute-force docs and upgrading guide) the behavior around custom authenticators (x509, passkeys, IDP login etc) and about the fact that they do not count anymore to brute-force and successful login with those do not reset brute-force
|
|
||
| [NOTE] | ||
| ==== | ||
| OTP type authenticators will cause permanent user account lockout even in case of temporary lockout brute force mode is set. This will mitigate probabilistic attack. The OTP oauthenticator type threshold is hardcoded to 100. This counter is incremented only in case of using OTP type authenticator. |
There was a problem hiding this comment.
| OTP type authenticators will cause permanent user account lockout even in case of temporary lockout brute force mode is set. This will mitigate probabilistic attack. The OTP oauthenticator type threshold is hardcoded to 100. This counter is incremented only in case of using OTP type authenticator. | |
| OTP type authenticators will cause permanent user account lockout even in case of temporary lockout brute force mode is set. This will mitigate probabilistic attack. The OTP authenticator type threshold is hardcoded to 100. This counter is incremented only in case of using OTP type authenticator. |
| } | ||
|
|
||
| @Override | ||
| public int getNumOtpFailures() { |
There was a problem hiding this comment.
Is it possible to rename this method (and other related methods and constants) to something more generic, so that brute-force protector API do not reference "OTP" as concrete authentication method in those methods? Perhaps something like getNumSecondaryAuthFailures()
Separate password and OTP brute force protection to prevent OTP bypass attacks by default
Closes #46164