Fixing Re-authentication with passkeys
keycloak/keycloak#41249
Golden recall: —
Extra findings: 5
Golden Comments (0/0 valid, 2 N/A)
| # | Comment | Severity | Status | FriendlyReviewer Detail |
|---|---|---|---|---|
| #1 | ConditionalPasskeysEnabled() called without UserModel parameter |
N/A | N/A | Golden bug does not apply — the method isConditionalPasskeysEnabled() in the actual PR code takes no UserModel parameter. It is a simple feature flag check. |
| #2 | With UserModel parameter requiring user != null, authenticate(...) would not call webauthnAuth.fillContextForm(context) when context.getUser() is null |
N/A | N/A | Golden bug does not apply — in the actual PR code, isConditionalPasskeysEnabled() has no parameter and fillContextForm() is called regardless of context.getUser() being null or not. |
Supplementary Findings (8 findings)
| Finding | File | Severity | Legitimate? |
|---|---|---|---|
Import of AbstractUsernameFormAuthenticator.USER_SET_BEFORE_USERNAME_PASSWORD_AUTH in AuthenticatorUtils creates a bidirectional package dependency between util and browser |
AuthenticatorUtils.java:35 | MEDIUM | ✓ Real architectural concern — layer mixing |
Return value of webauthnAuth.fillContextForm(context) ignored in passkeys-enabled block |
UsernamePasswordForm.java:115 | MEDIUM | ✓ Return value potentially null, silently discarded |
Missing events.clear() after passkey login assertion — may cause interference with intermediate events |
PasskeysUsernamePasswordFormTest.java:396 | MEDIUM | ✓ Real test reliability issue |
Missing events.clear() before re-authentication with prompt=login — residual events in queue |
PasskeysOrganizationAuthenticationTest.java:263 | MEDIUM | ✓ Same pattern, real fragility |
Missing events.clear() after first login assertion before re-authentication |
PasskeysUsernameFormTest.java:313 | MEDIUM | ✓ Test fragility, same pattern |
USER_SET_BEFORE_USERNAME_PASSWORD_AUTH changed from protected to public without documentation — API surface widened |
AbstractUsernameFormAuthenticator.java:58 | LOW | ✓ Should be documented |
Unused local variable user in WebAuthnConditionalUIAuthenticator path after refactoring |
WebAuthnAuthenticator.java:99 | LOW | ✓ Cleanup justified |
Try-catch for NoSuchElementException fragile — assertThrows would be more readable |
PasskeysUsernamePasswordFormTest.java:278 | LOW | ✓ Readability improvement |
Honest Analysis
FriendlyReviewer's full review
As posted on the PR — the complete output from FriendlyReviewer.
Hi there 👋
| ⛅ Tech |
|---|
| 5 medium, 3 low |
This merge request implements passkey re-authentication support across the username/password authentication flow. The changes are functionally correct and maintain existing security measures (brute force protection, timing attack mitigation, user enumeration prevention). Key modifications include:
- Extracting re-authentication form setup into a shared utility method (
AuthenticatorUtils.setupReauthenticationInUsernamePasswordFormError()). - Making the
USER_SET_BEFORE_USERNAME_PASSWORD_AUTHconstant publicly accessible. - Moving WebAuthn conditional UI setup outside the user-set check to enable passkeys during both initial login and re-authentication.
- Updating
UsernameFormto no longer skip the form during re-authentication when passkeys are enabled.
While the implementation is solid, there are several areas for improvement, particularly around code cleanliness, test robustness, and an architectural concern about a new bidirectional package dependency.
Worth checking
- 🟡 services/src/main/java/org/keycloak/authentication/authenticators/util/AuthenticatorUtils.java (L35)
Architectural concern: importingAbstractUsernameFormAuthenticator.USER_SET_BEFORE_USERNAME_PASSWORD_AUTHcreates a bidirectional dependency between theutilandbrowserpackages. Consider moving the constant to a shared neutral location (e.g., an authentication session note key constants class) to avoid future refactoring difficulties. - 🟡 services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java (L115)
The return value ofwebauthnAuth.fillContextForm(context)is ignored. Although currently safe (the method never returns null in the conditional UI path), this could silently discard a null value if a future subclass changes behavior. Consider handling the return value or adding logging. - 🟡 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysUsernamePasswordFormTest.java (L396)
Missingevents.clear()after the initial passkey login assertion. This could cause interference from unexpected intermediate events when re-authentication begins. Addevents.clear()immediately after the initial login assertion to ensure a clean event queue. - 🟡 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysOrganizationAuthenticationTest.java (L263)
Missingevents.clear()before re-authentication withprompt=login. Residual events from the initial login could be in the queue. Addevents.clear()before the re-authentication attempt. - 🟡 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysUsernameFormTest.java (L313)
Missingevents.clear()after the first login assertion before re-authentication. Adding it would make the test more robust against unexpected intermediate events.
Small things (take or leave)
- 🔵 services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java (L58)
Access modifier changed fromprotectedtopublicforUSER_SET_BEFORE_USERNAME_PASSWORD_AUTH. While necessary for cross-package access, this widens the public API surface and should be documented. - 🔵 services/src/main/java/org/keycloak/authentication/authenticators/browser/WebAuthnAuthenticator.java (L99)
Theuserlocal variable is now only used inside theif (shouldShowWebAuthnAuthenticators(context))block, leaving it unused in theWebAuthnConditionalUIAuthenticatorsubclass path. Consider moving the variable declaration inside theifblock for clarity. - 🔵 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysUsernamePasswordFormTest.java (L278)
The try-catch pattern to assert absence of the webauthn form is fragile and hard to read. Consider usingAssert.assertThrows(NoSuchElementException.class, ...)for clearer, more maintainable assertions.
FriendlyReviewer detected 0 of 2 golden bugs in this PR, but both golden comments are not applicable. They describe a hypothetical bug where
isConditionalPasskeysEnabled()takes aUserModelparameter — but in the actual PR code, the method takes no parameter and is called regardless of the user state. The golden data does not match the actual commit.Despite the non-applicable golden set, FriendlyReviewer produced 8 supplementary findings. The most significant is the bidirectional package dependency: importing from
AbstractUsernameFormAuthenticatorintoAuthenticatorUtilscreates a circular dependency between theutilandbrowserpackages — a genuine architectural concern.The three
events.clear()findings (affecting three different test files) follow the same pattern: missing event queue cleanup between login and re-authentication assertions. All three are legitimate test reliability issues.The ignored return value of
webauthnAuth.fillContextForm(context)is a valid defensive coding concern, though it would only become a bug if a future subclass returns null. The three low-severity findings (widened API surface, unused variable, fragile try-catch) are minor but genuine.Overall, FriendlyReviewer correctly identified the real problems in this PR — architecture, test reliability, and defensive coding — even though the golden benchmark could not score it.