Skip to content

Automated Test: enhance-passkey-authentication-flow #311

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

  • New Features

    • Re-authentication workflows with WebAuthn passkeys are now properly supported, including external key scenarios.
    • Improved error handling during re-authentication processes.
  • Tests

    • Expanded test coverage for WebAuthn passkey re-authentication with discoverable and external keys.
    • Added re-authentication scenarios with password fallback options.

mposolda and others added 2 commits July 22, 2025 10:00
closes #41242
closes #41008

Signed-off-by: mposolda <mposolda@gmail.com>
This change modifies the method signature to require a UserModel parameter
for proper user context validation during conditional passkey checks.
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This pull request refactors Keycloak's authentication flow to improve reauthentication handling for passkey scenarios. Changes include exposing a constant, consolidating conditional passkey checks into helper methods, introducing reauthentication setup utilities, and adding new test coverage for passkey-based reauthentication flows.

Changes

Cohort / File(s) Summary
Reauthentication Utilities
services/src/main/java/org/keycloak/authentication/authenticators/util/AuthenticatorUtils.java
Added public static method setupReauthenticationInUsernamePasswordFormError() to configure login forms for reauthentication error scenarios by hiding username and disabling registration when prior username/password auth is detected.
AbstractUsernameFormAuthenticator
services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java
Changed USER_SET_BEFORE_USERNAME_PASSWORD_AUTH from protected to public visibility and refactored badPasswordHandler to use the new setupReauthenticationInUsernamePasswordFormError() utility method instead of inline form preparation.
Conditional Passkey Checks
services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java, services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java
Introduced isConditionalPasskeysEnabled() helper method in UsernamePasswordForm to centralize passkey enablement checks; updated UsernameForm to refine skip-logic gating on this condition, preventing form skip when conditional passkeys are enabled.
WebAuthn Authenticator Display Logic
services/src/main/java/org/keycloak/authentication/authenticators/browser/WebAuthnAuthenticator.java, services/src/main/java/org/keycloak/authentication/authenticators/browser/WebAuthnConditionalUIAuthenticator.java
Added shouldShowWebAuthnAuthenticators() helper method to WebAuthnAuthenticator to control authenticator visibility; WebAuthnConditionalUIAuthenticator overrides this to return false and adds reauthentication setup call in error response path.
Passkey Re-authentication Tests
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysUsernameFormTest.java, testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysOrganizationAuthenticationTest.java
Added new test method webauthnLoginWithDiscoverableKey_reauthentication() to verify passkey-based login during reauthentication with prompt=login; updated test assertions to expect passkeys form presence instead of absence.
Extended Passkey Scenarios
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysUsernamePasswordFormTest.java
Added comprehensive test coverage for external-key passkeys and reauthentication scenarios including webauthnLoginWithExternalKey_reauthentication(), webauthnLoginWithExternalKey_reauthenticationWithPasswordOrPasskey(), and reauthenticationOfUserWithoutPasskey(); introduced new imports and helper method setPasswordlessPolicyForExternalKey().

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant AuthForm as Username/Password Form
    participant Authenticator as AuthenticationFlowContext
    participant Utils as AuthenticatorUtils
    participant FormProvider as LoginFormsProvider

    User->>AuthForm: Submit invalid password
    AuthForm->>Authenticator: badPasswordHandler triggered
    Authenticator->>Utils: setupReauthenticationInUsernamePasswordFormError(context)
    Utils->>Authenticator: Read auth note (USER_SET_BEFORE_USERNAME_PASSWORD_AUTH)
    Authenticator->>FormProvider: Hide username field
    Authenticator->>FormProvider: Disable registration
    FormProvider->>User: Display reauthentication error form
    User->>AuthForm: Re-authenticate with credentials
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hop! The passkeys bounce with care,
Re-authentication flows everywhere!
Utils consolidate, helpers take the stage,
Conditional checks refactored on this page.
Tests verify each hopping way,
WebAuthn leaps brighter every day! 🔐

🚥 Pre-merge checks | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'Automated Test: enhance-passkey-authentication-flow' does not accurately reflect the PR content; changes include core authentication logic modifications and utility refactoring, not just automated tests. Revise title to accurately describe the main changes, such as 'Refactor passkey authentication flow with conditional UI support' to reflect the significant code changes beyond test additions.
Description check ❓ Inconclusive The description is minimal and generic, stating only that it was 'automatically created' with no details about the actual changes, objectives, or technical impact of the PR. Expand the description to explain the main changes (refactored authenticator logic, new helper utilities, conditional passkey handling) and the rationale for these modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch enhance-passkey-authentication-flow

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java`:
- Line 47: The call to isConditionalPasskeysEnabled() in UsernameForm is missing
the required UserModel parameter and causes a compilation error; update the
conditional to call isConditionalPasskeysEnabled(context.getUser()) (you can
rely on the existing null check context.getUser() != null) so it matches the
inherited method signature isConditionalPasskeysEnabled(UserModel) from
UsernamePasswordForm and compiles correctly.

In
`@testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysUsernamePasswordFormTest.java`:
- Around line 277-333: The test reauthenticationOfUserWithoutPasskey currently
uses driver.findElement(...) inside broad try-catch blocks which swallows all
exceptions; replace those blocks by checking
driver.findElements(By.xpath("//form[`@id`='webauth']")).isEmpty() and asserting
true (or using an assertion like assertTrue/isEmpty) to verify the webauthn form
is absent instead of catching Exception. Update both occurrences in
reauthenticationOfUserWithoutPasskey (the two try-catch sections around checking
"//form[`@id`='webauth']") and keep existing assertions/flow
(loginPage.assertCurrent(), fail() removal) so test logic remains the same.

@@ -44,7 +44,7 @@ public UsernameForm(KeycloakSession session) {

@Override
public void authenticate(AuthenticationFlowContext context) {
if (context.getUser() != null) {
if (context.getUser() != null && !isConditionalPasskeysEnabled()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for all isConditionalPasskeysEnabled method definitions
rg "isConditionalPasskeysEnabled" -B 2 -A 3 --type java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 3546


🏁 Script executed:

# Locate UsernameForm.java and its parent class
find . -name "UsernameForm.java" -o -name "UsernamePasswordForm.java" | head -20

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 282


🏁 Script executed:

# Check the class definition and parent class of UsernameForm
head -50 services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 2047


🏁 Script executed:

# Check all methods in UsernamePasswordForm to confirm no overloads
grep -n "isConditionalPasskeysEnabled" services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 293


Method signature mismatch — compilation error.

The call isConditionalPasskeysEnabled() is missing the required UserModel argument. The inherited method isConditionalPasskeysEnabled(UserModel user) in UsernamePasswordForm requires a parameter.

Proposed fix
-        if (context.getUser() != null && !isConditionalPasskeysEnabled()) {
+        if (context.getUser() != null && !isConditionalPasskeysEnabled(context.getUser())) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (context.getUser() != null && !isConditionalPasskeysEnabled()) {
if (context.getUser() != null && !isConditionalPasskeysEnabled(context.getUser())) {
🤖 Prompt for AI Agents
In
`@services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java`
at line 47, The call to isConditionalPasskeysEnabled() in UsernameForm is
missing the required UserModel parameter and causes a compilation error; update
the conditional to call isConditionalPasskeysEnabled(context.getUser()) (you can
rely on the existing null check context.getUser() != null) so it matches the
inherited method signature isConditionalPasskeysEnabled(UserModel) from
UsernamePasswordForm and compiles correctly.

Comment on lines +277 to +333
// Test user re-authentication with password when passkeys feature enabled, but passkeys is not enabled for the realm. Passkeys should not be shown during re-authentication
@Test
public void reauthenticationOfUserWithoutPasskey() throws Exception {
// set passwordless policy for discoverable keys
try (Closeable c = getWebAuthnRealmUpdater()
.setWebAuthnPolicyRpEntityName("localhost")
.setWebAuthnPolicyRequireResidentKey(Constants.WEBAUTHN_POLICY_OPTION_YES)
.setWebAuthnPolicyUserVerificationRequirement(Constants.WEBAUTHN_POLICY_OPTION_REQUIRED)
.setWebAuthnPolicyPasskeysEnabled(Boolean.TRUE)
.setWebAuthnPolicyPasskeysEnabled(Boolean.FALSE)
.update()) {

// Login with password
oauth.openLoginForm();
WaitUtils.waitForPageToLoad();

// WebAuthn elements not available
loginPage.assertCurrent();
try {
MatcherAssert.assertThat(driver.findElement(By.xpath("//form[@id='webauth']")), nullValue());
fail("Not expected to have webauthn button");
} catch (Exception nsee) {
// expected
}

// Login with password
loginPage.login("test-user@localhost", getPassword("test-user@localhost"));
appPage.assertCurrent();

events.clear();

// Re-authentication now with prompt=login. Passkeys login should not be available on the page as this user does not have passkey
oauth.loginForm()
.prompt(OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
.open();
WaitUtils.waitForPageToLoad();

loginPage.assertCurrent();
assertEquals("Please re-authenticate to continue", loginPage.getInfoMessage());
try {
MatcherAssert.assertThat(driver.findElement(By.xpath("//form[@id='webauth']")), nullValue());
fail("Not expected to have webauthn button");
} catch (Exception nsee) {
// expected
}

// Login with password
loginPage.login(getPassword("test-user@localhost"));
appPage.assertCurrent();

UserRepresentation testUser = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost").toRepresentation();

events.expectLogin()
.user(testUser.getId())
.detail(Details.USERNAME, testUser.getUsername())
.detail(WebAuthnConstants.USER_VERIFICATION_CHECKED, nullValue())
.assertEvent();

logout();
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "PasskeysUsernamePasswordFormTest.java" | head -5

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 229


🏁 Script executed:

cat -n testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysUsernamePasswordFormTest.java | sed -n '277,333p'

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 2901


🏁 Script executed:

# Search for similar patterns of findElement with try-catch in test files
rg "findElement.*nullValue" testsuite/ -A 3 -B 1 --max-count 10

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 2013


🏁 Script executed:

# Check WebDriver usage patterns with findElements in the codebase
rg "findElements\(" testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/ -A 2 --max-count 15

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 6857


Use findElements().isEmpty() instead of catching broad Exception with findElement().

The current try-catch blocks swallow all exceptions indiscriminately, which can mask real WebDriver failures. Catching Exception is overly broad; findElement() throws NoSuchElementException specifically when an element is not found. The safer approach is to use findElements(), which returns an empty list instead of throwing, and then assert emptiness. This pattern is already used elsewhere in the test suite and aligns with WebDriver best practices.

Suggested fix

Replace both occurrences (lines 291-296 and 312-317) with:

-            try {
-                MatcherAssert.assertThat(driver.findElement(By.xpath("//form[`@id`='webauth']")), nullValue());
-                fail("Not expected to have webauthn button");
-            } catch (Exception nsee) {
-                // expected
-            }
+            Assert.assertTrue("Not expected to have webauthn button",
+                    driver.findElements(By.xpath("//form[`@id`='webauth']")).isEmpty());
🤖 Prompt for AI Agents
In
`@testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysUsernamePasswordFormTest.java`
around lines 277 - 333, The test reauthenticationOfUserWithoutPasskey currently
uses driver.findElement(...) inside broad try-catch blocks which swallows all
exceptions; replace those blocks by checking
driver.findElements(By.xpath("//form[`@id`='webauth']")).isEmpty() and asserting
true (or using an assertion like assertTrue/isEmpty) to verify the webauthn form
is absent instead of catching Exception. Update both occurrences in
reauthenticationOfUserWithoutPasskey (the two try-catch sections around checking
"//form[`@id`='webauth']") and keep existing assertions/flow
(loginPage.assertCurrent(), fail() removal) so test logic remains the same.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant