Skip to content

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

Release Notes

  • Improvements

    • Enhanced recovery code credential management with centralized handling for better consistency across authentication flows.
    • Added support for recovery codes in backwards-compatible user storage systems.
  • Tests

    • Expanded test coverage for recovery code setup and login scenarios, including validation of storage integrity and multi-step authentication flows.

closes #38445

Signed-off-by: rtufisi <rtufisi@phasetwo.io>
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This pull request introduces centralized support for recovery authentication codes across the Keycloak authentication system. New utility methods and helper functions provide consistent credential creation and retrieval for recovery codes. The authentication flows, required actions, and login forms are updated to use these utilities. Test infrastructure extends backwards-compatibility user storage with recovery code support and adds verification scenarios.

Changes

Cohort / File(s) Summary
Core Utilities & Credential Helpers
server-spi/src/main/java/org/keycloak/models/utils/RecoveryAuthnCodesUtils.java, server-spi-private/src/main/java/org/keycloak/utils/CredentialHelper.java
New utility method getCredential(UserModel) to retrieve recovery code credentials from federated or stored credentials; new helper createRecoveryCodesCredential(...) to centralize recovery code credential creation with JSON serialization and provider-based storage handling.
Authentication & Login Components
services/src/main/java/org/keycloak/authentication/authenticators/browser/RecoveryAuthnCodesFormAuthenticator.java, services/src/main/java/org/keycloak/authentication/requiredactions/RecoveryAuthnCodesAction.java, services/src/main/java/org/keycloak/forms/login/freemarker/model/RecoveryAuthnCodeInputLoginBean.java
Updated to use new utility methods for credential retrieval and creation; refactored to leverage centralized helpers instead of direct credential provider access; introduced Optional-based credential handling and new getter method.
Test Infrastructure
testsuite/.../federation/BackwardsCompatibilityUserStorage.java, testsuite/.../federation/BackwardsCompatibilityUserStorageFactory.java
Extended backwards-compatibility user storage with recovery code credential support including deserialization, validation, and storage; added helper methods for credential aggregation and recovery code presence checks.
Test Scenario & Verification
testsuite/.../federation/storage/BackwardsCompatibilityUserStorageTest.java
Added feature flag enablement, new browser flow configuration with recovery code subflow, test page objects, helper methods for setup and assertion, and comprehensive test scenario covering recovery code setup, storage verification, and login flow.

Sequence Diagram

sequenceDiagram
    participant User as User/Browser
    participant App as Login Application
    participant RecoveryAction as RecoveryAuthnCodesAction
    participant Helper as CredentialHelper
    participant Provider as CredentialProvider
    participant Storage as User Storage

    rect rgba(200, 150, 255, 0.5)
    Note over User,Storage: Recovery Code Setup Flow
    User->>App: Initiate recovery code setup
    App->>RecoveryAction: Handle required action
    RecoveryAction->>RecoveryAction: Generate recovery codes
    RecoveryAction->>Helper: createRecoveryCodesCredential(...)
    Helper->>Helper: Serialize codes to JSON
    alt Code in User Storage
        Helper->>Storage: Save credential to user storage
        Storage-->>Helper: Confirmed
    else Code via Provider
        Helper->>Provider: Create credential via provider
        Provider-->>Helper: Credential created
    end
    Helper-->>RecoveryAction: Credential stored
    RecoveryAction-->>User: Display recovery codes
    end

    rect rgba(150, 200, 255, 0.5)
    Note over User,Storage: Recovery Code Login Flow
    User->>App: Authenticate with recovery code
    App->>App: Retrieve user
    App->>Storage: Get credential via RecoveryAuthnCodesUtils
    Storage-->>App: Optional<CredentialModel>
    alt Credential Present
        App->>App: Deserialize & validate code
        App-->>User: Authentication successful
    else Credential Missing
        App-->>User: Authentication failed
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit hops through the code so bright,
Recovery codes stored safe and tight!
From helper hands to credential's keep,
Now authentication runs both deep and steep.
Login flows dance in the moonlit night,
With tested paths, all configured right! 🔐

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The description indicates this is an automatically-generated batch PR with no meaningful detail about the actual changes made (helper functions, test scenarios, credential handling, or recovery code implementation). Add descriptive information about the key changes: new helper functions in CredentialHelper and RecoveryAuthnCodesUtils, updated authenticators and required actions, test infrastructure additions, and storage integration for recovery codes.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title refers to an automated test for recovery keys implementation, which is accurate given the substantial test additions and helper methods in BackwardsCompatibilityUserStorageTest, but lacks specificity about the primary functional changes across multiple authentication and credential management modules.

✏️ 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 feature-recovery-keys-implementation

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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/src/main/java/org/keycloak/authentication/authenticators/browser/RecoveryAuthnCodesFormAuthenticator.java (1)

80-92: ⚠️ Potential issue | 🟠 Major

Federated recovery credential removal will fail silently.

RecoveryAuthnCodesUtils.getCredential() checks federated credentials first via getFederatedCredentialsStream(). However, removeStoredCredentialById() (lines 86-87) operates only on local storage and cannot remove federated credentials. If a federated credential exists and allCodesUsed() returns true, the removal attempt silently fails and the stale federated credential persists. The fallback at lines 90-92 (adding CONFIGURE_RECOVERY_AUTHN_CODES as a required action) mitigates this partially but does not clean up the orphaned credential.

🤖 Fix all issues with AI agents
In
`@services/src/main/java/org/keycloak/forms/login/freemarker/model/RecoveryAuthnCodeInputLoginBean.java`:
- Around line 17-21: The code calls credentialModelOpt.get() and then
getNextRecoveryAuthnCode().get() without presence checks, which can throw
NoSuchElementException; update RecoveryAuthnCodeInputLoginBean to first check
credentialModelOpt.isPresent() (or use
credentialModelOpt.map(...).ifPresent/else) before calling
RecoveryAuthnCodesCredentialModel.createFromCredentialModel and
getNextRecoveryAuthnCode(), and if absent set this.codeNumber to null (or an
appropriate default) so the form renders gracefully, mirroring the presence
check logic used in RecoveryAuthnCodesFormAuthenticator.

In
`@testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java`:
- Around line 123-143: In configureBrowserFlowWithRecoveryAuthnCodes, fix the
typo in the alias passed to config.setAlias for the delayed authenticator:
change "delayed-suthenticator-config" to "delayed-authenticator-config" so the
alias matches the authenticator ID "delayed-authenticator" and config lookup;
locate the block that adds the "delayed-authenticator" execution and update the
string in config.setAlias accordingly.
- Around line 240-269: The testRecoveryKeysSetupAndLogin test calls
testAppHelper.logout() without asserting its boolean result; update the teardown
to assert the logout succeeded by replacing the bare call with
assertTrue(testAppHelper.logout(), "logout should succeed") (or equivalent
assertTrue usage already used elsewhere in the class) so the test fails if
logout returns false; ensure you reference testAppHelper.logout() and use the
existing assertTrue assertion style from other tests.
- Around line 258-262: After submitting the recovery code form the test never
completes the OAuth flow; add a call to testAppHelper.completeLogin()
immediately after enterRecoveryAuthnCodePage.clickSignInButton() and before
appPage.assertCurrent() so the authorization code is exchanged for tokens
(mirrors the pattern used in testOTPUpdateAndLogin where startLogin(...), submit
2FA, completeLogin(), then appPage.assertCurrent()).
🧹 Nitpick comments (3)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java (2)

230-252: Local variable model shadows the class field this.model (ComponentModel).

Line 233 declares RecoveryAuthnCodesCredentialModel model which shadows the inherited ComponentModel model field (Line 72). Consider renaming to recoveryModel or recoveryCredentialModel to avoid confusion.

Proposed rename
     public Stream<CredentialModel> getCredentials(RealmModel realm, UserModel user) {
         var myUser = getMyUser(user);
-        RecoveryAuthnCodesCredentialModel model;
         List<CredentialModel> credentialModels = new ArrayList<>();
         if (myUser.recoveryCodes != null) {
             try {
-                model = RecoveryAuthnCodesCredentialModel.createFromValues(
+                RecoveryAuthnCodesCredentialModel recoveryModel = RecoveryAuthnCodesCredentialModel.createFromValues(
                         JsonSerialization.readValue(myUser.recoveryCodes.getCredentialData(), List.class),
                         myUser.recoveryCodes.getCreatedDate(),
                         myUser.recoveryCodes.getUserLabel()
                 );
-                credentialModels.add(model);
+                credentialModels.add(recoveryModel);
             } catch (IOException e) {
                 log.error("Could not deserialize  credential of type: recovery-codes");
             }

326-341: Raw type List — use List<String> for type safety.

Lines 332 and 334 use raw List without type parameters. Using List<String> would provide compile-time type safety and avoid an unchecked cast warning when comparing with input.getChallengeResponse() (a String) on Line 340.

Proposed fix
-            List generatedKeys;
+            List<String> generatedKeys;
             try {
-                generatedKeys = JsonSerialization.readValue(storedRecoveryKeys.getCredentialData(), List.class);
+                `@SuppressWarnings`("unchecked")
+                List<String> keys = JsonSerialization.readValue(storedRecoveryKeys.getCredentialData(), List.class);
+                generatedKeys = keys;
             } catch (IOException e) {
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java (1)

476-483: Is setDriver(driver) necessary here?

The enterRecoveryAuthnCodePage is already injected via @Page (line 104), which should configure the driver automatically. If setDriver is needed as a workaround (e.g., for Arquillian page-object lifecycle quirks), a brief comment would help future readers understand why.

Comment on lines +17 to 21
Optional<CredentialModel> credentialModelOpt = RecoveryAuthnCodesUtils.getCredential(user);

RecoveryAuthnCodesCredentialModel recoveryCodeCredentialModel = RecoveryAuthnCodesCredentialModel.createFromCredentialModel(credentialModel);
RecoveryAuthnCodesCredentialModel recoveryCodeCredentialModel = RecoveryAuthnCodesCredentialModel.createFromCredentialModel(credentialModelOpt.get());

this.codeNumber = recoveryCodeCredentialModel.getNextRecoveryAuthnCode().get().getNumber();

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Unchecked Optional.get() will throw NoSuchElementException if no credential exists.

Line 19 calls credentialModelOpt.get() without an isPresent() guard. If neither federated nor stored credentials are found, this throws a NoSuchElementException, resulting in a 500 error during form rendering.

By contrast, RecoveryAuthnCodesFormAuthenticator (Line 82) correctly checks optUserCredentialFound.isPresent() before calling .get().

🐛 Proposed fix: handle empty Optional gracefully
-        Optional<CredentialModel> credentialModelOpt = RecoveryAuthnCodesUtils.getCredential(user);
-
-        RecoveryAuthnCodesCredentialModel recoveryCodeCredentialModel = RecoveryAuthnCodesCredentialModel.createFromCredentialModel(credentialModelOpt.get());
-
-        this.codeNumber = recoveryCodeCredentialModel.getNextRecoveryAuthnCode().get().getNumber();
+        Optional<CredentialModel> credentialModelOpt = RecoveryAuthnCodesUtils.getCredential(user);
+
+        RecoveryAuthnCodesCredentialModel recoveryCodeCredentialModel = credentialModelOpt
+                .map(RecoveryAuthnCodesCredentialModel::createFromCredentialModel)
+                .orElseThrow(() -> new IllegalStateException("Recovery authentication codes credential not found for user"));
+
+        this.codeNumber = recoveryCodeCredentialModel.getNextRecoveryAuthnCode()
+                .orElseThrow(() -> new IllegalStateException("No remaining recovery authentication codes"))
+                .getNumber();
🤖 Prompt for AI Agents
In
`@services/src/main/java/org/keycloak/forms/login/freemarker/model/RecoveryAuthnCodeInputLoginBean.java`
around lines 17 - 21, The code calls credentialModelOpt.get() and then
getNextRecoveryAuthnCode().get() without presence checks, which can throw
NoSuchElementException; update RecoveryAuthnCodeInputLoginBean to first check
credentialModelOpt.isPresent() (or use
credentialModelOpt.map(...).ifPresent/else) before calling
RecoveryAuthnCodesCredentialModel.createFromCredentialModel and
getNextRecoveryAuthnCode(), and if absent set this.codeNumber to null (or an
appropriate default) so the form renders gracefully, mirroring the presence
check logic used in RecoveryAuthnCodesFormAuthenticator.

Comment on lines +123 to +143
void configureBrowserFlowWithRecoveryAuthnCodes(KeycloakTestingClient testingClient, long delay) {
final String newFlowAlias = BROWSER_FLOW_WITH_RECOVERY_AUTHN_CODES;
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session)
.selectFlow(newFlowAlias)
.inForms(forms -> forms
.clear()
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, UsernamePasswordFormFactory.PROVIDER_ID)
.addSubFlowExecution(AuthenticationExecutionModel.Requirement.REQUIRED, reqSubFlow -> reqSubFlow
.addSubFlowExecution("Recovery-Authn-Codes subflow", AuthenticationFlow.BASIC_FLOW, AuthenticationExecutionModel.Requirement.ALTERNATIVE, altSubFlow -> altSubFlow
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, RecoveryAuthnCodesFormAuthenticatorFactory.PROVIDER_ID)
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, "delayed-authenticator", config -> {
config.setAlias("delayed-suthenticator-config");
config.setConfig(Map.of("delay", Long.toString(delay)));
})
)
)
)
.defineAsBrowserFlow()
);
}

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo in config alias: "delayed-suthenticator-config""delayed-authenticator-config".

Line 135 has a typo — suthenticator instead of authenticator.

✏️ Proposed fix
-                                            config.setAlias("delayed-suthenticator-config");
+                                            config.setAlias("delayed-authenticator-config");
📝 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
void configureBrowserFlowWithRecoveryAuthnCodes(KeycloakTestingClient testingClient, long delay) {
final String newFlowAlias = BROWSER_FLOW_WITH_RECOVERY_AUTHN_CODES;
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session)
.selectFlow(newFlowAlias)
.inForms(forms -> forms
.clear()
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, UsernamePasswordFormFactory.PROVIDER_ID)
.addSubFlowExecution(AuthenticationExecutionModel.Requirement.REQUIRED, reqSubFlow -> reqSubFlow
.addSubFlowExecution("Recovery-Authn-Codes subflow", AuthenticationFlow.BASIC_FLOW, AuthenticationExecutionModel.Requirement.ALTERNATIVE, altSubFlow -> altSubFlow
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, RecoveryAuthnCodesFormAuthenticatorFactory.PROVIDER_ID)
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, "delayed-authenticator", config -> {
config.setAlias("delayed-suthenticator-config");
config.setConfig(Map.of("delay", Long.toString(delay)));
})
)
)
)
.defineAsBrowserFlow()
);
}
void configureBrowserFlowWithRecoveryAuthnCodes(KeycloakTestingClient testingClient, long delay) {
final String newFlowAlias = BROWSER_FLOW_WITH_RECOVERY_AUTHN_CODES;
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session)
.selectFlow(newFlowAlias)
.inForms(forms -> forms
.clear()
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, UsernamePasswordFormFactory.PROVIDER_ID)
.addSubFlowExecution(AuthenticationExecutionModel.Requirement.REQUIRED, reqSubFlow -> reqSubFlow
.addSubFlowExecution("Recovery-Authn-Codes subflow", AuthenticationFlow.BASIC_FLOW, AuthenticationExecutionModel.Requirement.ALTERNATIVE, altSubFlow -> altSubFlow
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, RecoveryAuthnCodesFormAuthenticatorFactory.PROVIDER_ID)
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, "delayed-authenticator", config -> {
config.setAlias("delayed-authenticator-config");
config.setConfig(Map.of("delay", Long.toString(delay)));
})
)
)
)
.defineAsBrowserFlow()
);
}
🤖 Prompt for AI Agents
In
`@testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java`
around lines 123 - 143, In configureBrowserFlowWithRecoveryAuthnCodes, fix the
typo in the alias passed to config.setAlias for the delayed authenticator:
change "delayed-suthenticator-config" to "delayed-authenticator-config" so the
alias matches the authenticator ID "delayed-authenticator" and config lookup;
locate the block that adds the "delayed-authenticator" execution and update the
string in config.setAlias accordingly.

Comment on lines +240 to +269
@Test
public void testRecoveryKeysSetupAndLogin() throws URISyntaxException, IOException {
try {
configureBrowserFlowWithRecoveryAuthnCodes(testingClient, 0);

String userId = addUserAndResetPassword("otp1", "pass");
getCleanup().addUserId(userId);

// Setup RecoveryKeys
List<String> recoveryKeys = setupRecoveryKeysForUserWithRequiredAction(userId, true);

// Assert user has RecoveryKeys in the userStorage
assertUserDontHaveDBCredentials();
assertUserHasRecoveryKeysCredentialInUserStorage(true);

TestAppHelper testAppHelper = new TestAppHelper(oauth, loginPage, appPage);

// Authenticate as the user
testAppHelper.startLogin("otp1", "pass");
enterRecoveryCodes(enterRecoveryAuthnCodePage, driver, 0, recoveryKeys);
enterRecoveryAuthnCodePage.clickSignInButton();

appPage.assertCurrent();

testAppHelper.logout();
} finally {
// Revert copy of browser flow to original to keep clean slate after this test
BrowserFlowTest.revertFlows(testRealm(), BROWSER_FLOW_WITH_RECOVERY_AUTHN_CODES);
}
}

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing assertion on logout() return value.

At line 264, testAppHelper.logout() is called without wrapping it in assertTrue(...), unlike other tests in this class (e.g., lines 237, 405). A failed logout would silently pass.

✏️ Proposed fix
-            testAppHelper.logout();
+            assertTrue(testAppHelper.logout());
🤖 Prompt for AI Agents
In
`@testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java`
around lines 240 - 269, The testRecoveryKeysSetupAndLogin test calls
testAppHelper.logout() without asserting its boolean result; update the teardown
to assert the logout succeeded by replacing the bare call with
assertTrue(testAppHelper.logout(), "logout should succeed") (or equivalent
assertTrue usage already used elsewhere in the class) so the test fails if
logout returns false; ensure you reference testAppHelper.logout() and use the
existing assertTrue assertion style from other tests.

Comment on lines +258 to +262
testAppHelper.startLogin("otp1", "pass");
enterRecoveryCodes(enterRecoveryAuthnCodePage, driver, 0, recoveryKeys);
enterRecoveryAuthnCodePage.clickSignInButton();

appPage.assertCurrent();

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:

#!/bin/bash
# First, find and examine the EnterRecoveryAuthnCodePage class
fd "EnterRecoveryAuthnCodePage.java" --exec cat {} 2>/dev/null

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 2210


🏁 Script executed:

#!/bin/bash
# Also examine the test file around the lines in question
fd "BackwardsCompatibilityUserStorageTest.java" --exec sed -n '210,270p' {} 2>/dev/null

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 2302


🏁 Script executed:

#!/bin/bash
# Search for clickSignInButton usage patterns in test files
rg "clickSignInButton" -A 3 -B 3 --max-count 20 2>/dev/null

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 9068


🏁 Script executed:

#!/bin/bash
# Find and examine TestAppHelper.completeLogin() method
fd "TestAppHelper.java" --exec grep -A 10 "completeLogin" {} 2>/dev/null

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 1337


🏁 Script executed:

#!/bin/bash
# Search for assertCurrent implementations in AppPage and similar classes
rg "assertCurrent\(\)" -A 5 "AppPage" --max-count 10 2>/dev/null

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 69


🏁 Script executed:

#!/bin/bash
# Look for more context about how tests complete 2FA flows
rg "clickSignInButton\(\)" -A 5 "appPage" 2>/dev/null

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 69


🏁 Script executed:

#!/bin/bash
# Verify the pattern - check if there are any other tests that skip completeLogin after 2FA
rg "enterRecoveryCodes|clickSignInButton" -B 2 -A 5 "BackwardsCompatibilityUserStorageTest" 2>/dev/null

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 69


🏁 Script executed:

#!/bin/bash
# Check if there are recovery code tests in other files that DO call completeLogin
rg "enterRecoveryCodes.*recovery" -A 10 2>/dev/null | head -40

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 2100


Add testAppHelper.completeLogin() call before the assertion.

The completeLogin() method is required to exchange the OAuth authorization code for tokens. Without it, the test skips the entire token exchange phase and may pass without properly validating the OAuth flow completion. This is inconsistent with the testOTPUpdateAndLogin method in the same class, which correctly calls completeLogin() after submitting the second authentication factor.

Current code:
            testAppHelper.startLogin("otp1", "pass");
            enterRecoveryCodes(enterRecoveryAuthnCodePage, driver, 0, recoveryKeys);
            enterRecoveryAuthnCodePage.clickSignInButton();

            appPage.assertCurrent();

Add testAppHelper.completeLogin(); after clickSignInButton() and before appPage.assertCurrent().

🤖 Prompt for AI Agents
In
`@testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java`
around lines 258 - 262, After submitting the recovery code form the test never
completes the OAuth flow; add a call to testAppHelper.completeLogin()
immediately after enterRecoveryAuthnCodePage.clickSignInButton() and before
appPage.assertCurrent() so the authorization code is exchanged for tokens
(mirrors the pattern used in testOTPUpdateAndLogin where startLogin(...), submit
2FA, completeLogin(), then appPage.assertCurrent()).

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