Skip to content

Protect recovery authentication codes with an independent brute force budget - #4

Draft
Hockenba wants to merge 17 commits into
mainfrom
independent-recovery-brute-force-protection
Draft

Hockenba wants to merge 17 commits into
mainfrom
independent-recovery-brute-force-protection

Conversation

@Hockenba

@Hockenba Hockenba commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #3; review that one first.

Recovery authentication codes are a short static list, so an attacker who already has the password can grind them. Today those failures share the account budget, which means guessing backup codes can lock password authentication, and a user who mistypes codes can lock themselves out entirely.

A new realm option, Independent recovery code failures, gives recovery codes their own counter. Exhausting it blocks recovery code authentication only; password and other configured methods stay available. A successful recovery code clears the counter, and the existing user unlock action clears it too.

OTP is deliberately unchanged: authenticator app failures still count toward Max secondary failures and still lock the account, including under the identifier lock policy from the previous PR, so OTP guessing cannot hop between identifiers.

When contributing this upstream, add Closes #<issue> here (Keycloak requires a GitHub issue per PR).

Locking a property no longer disables the whole account: the counter for
that identifier is blocked while every other identifier keeps working, so
a user locked out by email can still authenticate by phone number.

Resolve the attempted identifier through the protected properties as well,
otherwise a login by phone number could not be matched to a user, and clear
the per-identifier and per-channel counters on success.

Signed-off-by: Hockenba <github@opalpine.com>
A property counter blocks a single identifier and deliberately leaves the
user record enabled, so neither isEnabled() nor the account-wide
isTemporarilyDisabled() check in updateUser() noticed it. The Admin Console
unlock switch therefore reported success without clearing the counter.

Also drops the per-property lookup helpers left unused by the admin API.
Temporary locks already surfaced through failedLoginNotBefore; this
asserts ui-ext reports disabled for permanent identifier lockouts.
@Hockenba Hockenba changed the title Independent recovery brute force protection Protect recovery authentication codes with an independent brute force budget Sep 24, 2026
Hockenba and others added 5 commits October 16, 2026 11:53
- Fall back to the username counter when no protected property matches the
  attempted identifier, and always protect the username under the properties policy
- Look up at most two users per protected property and support several users
  sharing a value when recording failures
- Use the identifier-aware lockout checks in IdentityBrokerService and DefaultUserProfile
- Remove property counters when a user is deleted or a protected attribute value
  changes, unless the value is still used
Unlock already cleared Attack Detection; this also checks ui-ext
reports disabled and cleared after the user-detail unlock.
…ent.

Authenticator-app failures still disable the account, including under
PROPERTIES policy, instead of only incrementing an identifier counter.
…counters

- Advance only the secondary auth counter on the user id counter for OTP
  failures when it is not already part of the attempt, so bad OTP codes no
  longer increment primary failures or trigger temporary/permanent lockout
- Apply OTP secondary lockout only to the user id counter; property and
  recovery counters, which can be shared between users, no longer disable the
  attempting user
- Send the temporary lockout event for every counter type, including OTP
@Hockenba
Hockenba force-pushed the independent-recovery-brute-force-protection branch from cda0759 to 6cda9d0 Compare October 19, 2026 18:17
# Conflicts:
#	testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java
@Hockenba
Hockenba force-pushed the independent-recovery-brute-force-protection branch from e3391e3 to 8cf8f45 Compare October 19, 2026 18:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant