Skip to content

Implement recovery key support for user storage providers#2

Open
akshayutture-augment wants to merge 1 commit into
feature-recovery-keys-foundationfrom
feature-recovery-keys-implementation
Open

Implement recovery key support for user storage providers#2
akshayutture-augment wants to merge 1 commit into
feature-recovery-keys-foundationfrom
feature-recovery-keys-implementation

Conversation

@akshayutture-augment

@akshayutture-augment akshayutture-augment commented Nov 14, 2025

Copy link
Copy Markdown

Test 9

Summary by CodeRabbit

Release Notes

New Features

  • Users can now generate and set up recovery codes as a backup authentication method during login.
  • Recovery codes are now fully integrated into authentication flows and credential management systems.

Tests

  • Added comprehensive test coverage for recovery codes setup, configuration, and authentication workflows, including backwards compatibility scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.


Replicated from ai-code-review-evaluation/keycloak-coderabbit#9

closes #38445

Signed-off-by: rtufisi <rtufisi@phasetwo.io>
@akshayutture-augment

Copy link
Copy Markdown
Author

@coderabbitai review

@akshayutture-augment

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 22, 2025

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Nov 22, 2025

Copy link
Copy Markdown

Walkthrough

The changes introduce recovery codes authentication support by adding utility helpers for credential management (creation and retrieval), refactoring existing code to use these utilities, extending backwards-compatibility storage with recovery codes handling, and adding comprehensive test coverage for recovery code setup and authentication flows.

Changes

Cohort / File(s) Summary
Utility Helpers
server-spi-private/src/main/java/org/keycloak/utils/CredentialHelper.java
Added createRecoveryCodesCredential() method to serialize and persist recovery codes credentials; adjusted authenticator factory lookup order (FormActionFactory before ClientAuthenticatorFactory).
Utility Helpers
server-spi/src/main/java/org/keycloak/models/utils/RecoveryAuthnCodesUtils.java
Added getCredential() method to retrieve recovery codes credentials with federated credential fallback.
Client Code Refactoring
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 RecoveryAuthnCodesUtils.getCredential() and CredentialHelper.createRecoveryCodesCredential() instead of direct credential manager queries.
Test Support
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java
Extended to support recovery codes: added recoveryCodes field to internal MyUser class, implemented getCredentials() method, added recovery codes handling in credential operations (create, validate, list).
Test Support
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorageFactory.java
Added hasRecoveryCodes() method to check recovery codes presence for a user.
Integration Tests
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java
Added testRecoveryKeysSetupAndLogin() test with flow configuration, page objects, and helper methods to verify recovery codes setup and authentication.

Sequence Diagrams

sequenceDiagram
    participant User
    participant RecoveryAction as RecoveryAuthnCodesAction
    participant CredHelper as CredentialHelper
    participant Provider as RecoveryAuthnCodes Provider
    participant UserStorage as User Storage

    User->>RecoveryAction: Complete recovery codes setup
    activate RecoveryAction
    RecoveryAction->>CredHelper: createRecoveryCodesCredential(...)
    activate CredHelper
    CredHelper->>CredHelper: Serialize codes to JSON
    CredHelper->>UserStorage: Try update credential
    alt Update succeeds
        UserStorage-->>CredHelper: Success
        CredHelper-->>RecoveryAction: Credential created
    else Update fails
        CredHelper->>Provider: Create via provider
        Provider-->>CredHelper: Credential created
        CredHelper-->>RecoveryAction: Credential created
    end
    deactivate CredHelper
    RecoveryAction->>User: Setup complete
    deactivate RecoveryAction
Loading
sequenceDiagram
    participant Browser
    participant Authenticator as RecoveryAuthnCodesFormAuthenticator
    participant Utils as RecoveryAuthnCodesUtils
    participant CredManager as Credential Manager
    participant FedCreds as Federated Credentials

    Browser->>Authenticator: Check for recovery codes
    activate Authenticator
    Authenticator->>Utils: getCredential(user)
    activate Utils
    Utils->>FedCreds: Check federated credentials first
    alt Federated credential found
        FedCreds-->>Utils: CredentialModel
    else Federated not found
        Utils->>CredManager: Fall back to stored credentials
        CredManager-->>Utils: CredentialModel
    end
    Utils-->>Authenticator: Optional<CredentialModel>
    deactivate Utils
    Authenticator->>Authenticator: Process credential
    Authenticator-->>Browser: Response
    deactivate Authenticator
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Areas requiring extra attention:
    • BackwardsCompatibilityUserStorage.java — extensive credential handling logic additions for recovery codes; verify serialization/deserialization correctness and consistency with RecoveryAuthnCodesCredentialModel structure.
    • BackwardsCompatibilityUserStorageTest.java — new test flow configuration and helper methods; ensure recovery code setup and login flows exercise all critical paths.
    • Null-safety in RecoveryAuthnCodeInputLoginBean.java — uses .get() on Optional without explicit checks; verify no unchecked exception risk in current call paths.
    • Authenticator factory lookup order change in CredentialHelper.java — confirm the reordering (FormActionFactory before ClientAuthenticatorFactory) does not break existing fallback logic.

Poem

🔐 Recovery codes now hop with grace,
Utility helpers find their place,
From federated trails to storage deep,
The rabbit's secrets safe to keep,
New tests ensure the flow runs true,
Authentication codes, tried and true! 🐰

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.82% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive The PR description is vague and minimal ('Test 9' with a replication note), lacking any substantive detail about changes, objectives, testing, or impact compared to the required template. Provide a meaningful description including what was changed, why, how it was tested, and any relevant context for reviewers to understand the implementation.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Implement recovery key support for user storage providers' accurately reflects the main changes across multiple files that add recovery code/key functionality to user storage and related authentication components.
✨ 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 @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (8)
server-spi-private/src/main/java/org/keycloak/utils/CredentialHelper.java (2)

76-85: Confirm intentional change to factory lookup ordering

getConfigurableAuthenticatorFactory now tries FormActionFactory before ClientAuthenticatorFactory. This can change behavior if a providerId is implemented as both a form action and a client authenticator, as the resolved factory will differ from previous ordering. If that scenario is possible in your deployment or tests, it’s worth double-checking this change is deliberate and, if so, adding a short Javadoc note explaining the intended priority to avoid future regressions.


35-42: Recovery‑codes helper is consistent with OTP pattern; consider tightening error handling

The new createRecoveryCodesCredential helper cleanly centralizes recovery‑code creation and mirrors the existing OTP helper: try user‑storage via updateCredential, log when that path is used, otherwise delegate to the CredentialProvider. That looks correct and aligns with the PR’s goal of user‑storage support.

Two minor polish suggestions:

  • The IOException from JsonSerialization.writeValueAsString is wrapped in a bare RuntimeException, which makes debugging harder. Consider adding context, e.g. new RuntimeException("Failed to serialize recovery authn codes", e).
  • If you expect generatedCodes to be small, this is fine; if not, a brief comment noting that the JSON is the canonical storage format for user‑storage providers would help future readers understand why the plain list is serialized here rather than reusing data from credentialModel.

These don’t block the change, but would improve debuggability and readability.

Also applies to: 112-131

services/src/main/java/org/keycloak/forms/login/freemarker/model/RecoveryAuthnCodeInputLoginBean.java (1)

16-22: Avoid raw Optional.get() to make failures clearer when credential is missing or exhausted

The new use of RecoveryAuthnCodesUtils.getCredential(user) is good, but calling credentialModelOpt.get() (and later getNextRecoveryAuthnCode().get()) assumes the credential and a next code are always present. If either assumption is violated (misconfiguration, race with credential removal, all codes already used, etc.), you’ll get a NoSuchElementException with little context.

You can keep the same semantics while making the failure mode more explicit and diagnosable:

-        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 =
+                RecoveryAuthnCodesCredentialModel.createFromCredentialModel(
+                        credentialModelOpt.orElseThrow(
+                                () -> new IllegalStateException("Missing recovery-authn-codes credential for user " + user.getId())
+                        )
+                );
+
+        this.codeNumber = recoveryCodeCredentialModel.getNextRecoveryAuthnCode()
+                .map(code -> code.getNumber())
+                .orElseThrow(
+                        () -> new IllegalStateException("No remaining recovery authn codes for user " + user.getId())
+                );

This preserves the invariant (“we should only construct this bean when a valid credential with remaining codes exists”) but produces clearer diagnostics if that invariant is ever broken.

server-spi/src/main/java/org/keycloak/models/utils/RecoveryAuthnCodesUtils.java (1)

50-62: getCredential helper looks good; tiny Javadoc nit

Implementation (federated-first with local fallback) is clear and consistent with the intended lookup behavior. Only minor suggestion is to fix the Javadoc grammar:

-     * @return - a optional  credential model
+     * @return - an optional credential model
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java (1)

110-120: Recovery-codes wiring in the backwards-compat provider is consistent; consider documenting non-consuming semantics

The additions for RecoveryAuthnCodesCredentialModel.TYPE in supportsCredentialType, updateCredential, getCredentials, isConfiguredFor, isValid, and MyUser are internally consistent:

  • updateCredential stores recovery codes as a CredentialModel with generated ID, type, credentialData, and createdDate.
  • getCredentials deserializes myUser.recoveryCodes.getCredentialData() into a RecoveryAuthnCodesCredentialModel and returns it alongside OTP when present.
  • isValid deserializes the same list and checks whether input.getChallengeResponse() matches one of the stored values.

Given this is a backwards-compatibility test provider, it’s reasonable that validation does not remove the used code. If you want this provider to more closely mirror production semantics (one-time use of a recovery code) or to make intent explicit, consider adding a short comment near the recovery-codes branch in isValid explaining that codes are intentionally non-consuming here for simplicity.

If you’d like to tighten types a bit, you could also switch the raw List deserialization to a typed variant, but that’s non-essential for tests. For example:

-            List generatedKeys;
+            List<String> generatedKeys;
             try {
-                generatedKeys = JsonSerialization.readValue(storedRecoveryKeys.getCredentialData(), List.class);
+                generatedKeys = JsonSerialization.readValue(
+                        storedRecoveryKeys.getCredentialData(),
+                        new org.keycloak.util.JsonSerialization.TypeReference<List<String>>() {}
+                );

()

Also applies to: 169-205, 230-252, 273-281, 326-341, 422-440

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java (3)

74-106: Flow configuration for recovery codes is clear; fix minor alias typo

The new browser flow setup using FlowUtil and the dedicated constant for BROWSER_FLOW_WITH_RECOVERY_AUTHN_CODES is readable and nicely encapsulated in configureBrowserFlowWithRecoveryAuthnCodes.

One tiny nit: the alias for the delayed authenticator has a typo, which may make debugging harder later:

-                                        .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, "delayed-authenticator", config -> {
-                                            config.setAlias("delayed-suthenticator-config");
+                                        .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, "delayed-authenticator", config -> {
+                                            config.setAlias("delayed-authenticator-config");

Also applies to: 123-143


240-269: Avoid hard-coding the expected recovery code index in the helper

testRecoveryKeysSetupAndLogin currently calls:

enterRecoveryCodes(enterRecoveryAuthnCodePage, driver, 0, recoveryKeys);

and enterRecoveryCodes asserts that the page always asks for code index 0. This makes the test brittle if the authenticator’s selection strategy changes (for example, random or non-zero index) even though the overall behavior remains correct.

You can make the helper resilient by letting it adapt to whichever index the page requests, only asserting that the index is within bounds:

-    private void enterRecoveryCodes(EnterRecoveryAuthnCodePage enterRecoveryAuthnCodePage, WebDriver driver,
-                                    int expectedCode, List<String> generatedRecoveryAuthnCodes) {
+    private void enterRecoveryCodes(EnterRecoveryAuthnCodePage enterRecoveryAuthnCodePage, WebDriver driver,
+                                    List<String> generatedRecoveryAuthnCodes) {
         enterRecoveryAuthnCodePage.setDriver(driver);
         enterRecoveryAuthnCodePage.assertCurrent();
-        int requestedCode = enterRecoveryAuthnCodePage.getRecoveryAuthnCodeToEnterNumber();
-        org.junit.Assert.assertEquals("Incorrect code presented to login", expectedCode, requestedCode);
-        enterRecoveryAuthnCodePage.enterRecoveryAuthnCode(generatedRecoveryAuthnCodes.get(requestedCode));
+        int requestedCode = enterRecoveryAuthnCodePage.getRecoveryAuthnCodeToEnterNumber();
+        org.junit.Assert.assertTrue("Requested code index is out of range",
+                requestedCode >= 0 && requestedCode < generatedRecoveryAuthnCodes.size());
+        enterRecoveryAuthnCodePage.enterRecoveryAuthnCode(generatedRecoveryAuthnCodes.get(requestedCode));
     }

and at the call site:

-            enterRecoveryCodes(enterRecoveryAuthnCodePage, driver, 0, recoveryKeys);
+            enterRecoveryCodes(enterRecoveryAuthnCodePage, driver, recoveryKeys);

This still verifies correctness while decoupling the test from a specific index choice.

Also applies to: 476-483


410-436: Required action string for recovery keys: align with existing pattern (optional)

setupRecoveryKeysForUserWithRequiredAction uses name() for the required action, while the OTP helper above uses toString():

userRep.setRequiredActions(Arrays.asList(
        UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES.name()));

Given RequiredAction is an enum, both name() and toString() are currently equivalent, but for consistency with the OTP path and to reduce surprise if toString() were ever customized, you could align them:

-        userRep.setRequiredActions(Arrays.asList(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES.name()));
+        userRep.setRequiredActions(Arrays.asList(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES.toString()));

Purely stylistic; behavior today is the same.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0852ea and 134437a.

📒 Files selected for processing (8)
  • server-spi-private/src/main/java/org/keycloak/utils/CredentialHelper.java (3 hunks)
  • server-spi/src/main/java/org/keycloak/models/utils/RecoveryAuthnCodesUtils.java (2 hunks)
  • services/src/main/java/org/keycloak/authentication/authenticators/browser/RecoveryAuthnCodesFormAuthenticator.java (1 hunks)
  • services/src/main/java/org/keycloak/authentication/requiredactions/RecoveryAuthnCodesAction.java (3 hunks)
  • services/src/main/java/org/keycloak/forms/login/freemarker/model/RecoveryAuthnCodeInputLoginBean.java (1 hunks)
  • testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java (12 hunks)
  • testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorageFactory.java (1 hunks)
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java (8 hunks)
🔇 Additional comments (4)
services/src/main/java/org/keycloak/authentication/requiredactions/RecoveryAuthnCodesAction.java (1)

28-29: Good reuse of shared helper for recovery‑codes creation

Switching processAction to use CredentialHelper.createRecoveryCodesCredential(...) is a nice consolidation: you gain the user‑storage‑aware behavior and keep the local CredentialProvider creation path in one place. The flow (validate inputs, optionally logout sessions, create credential, then success()) remains clear and unchanged from the caller’s perspective.

Also applies to: 109-118

testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorageFactory.java (1)

53-57: Test helper for recovery codes mirrors existing OTP helper

hasRecoveryCodes is a simple, clear mirror of hasUserOTP, with the same null‑safe pattern over the shared userPasswords map. This should integrate cleanly with tests needing to assert recovery‑codes presence in the backwards‑compatibility storage.

services/src/main/java/org/keycloak/authentication/authenticators/browser/RecoveryAuthnCodesFormAuthenticator.java (1)

80-93: Utility‑based credential lookup improves coverage for different storage backends

Using RecoveryAuthnCodesUtils.getCredential(authenticatedUser) here is a solid improvement over the previous direct credentialManager stream: it centralizes the lookup logic (including federated vs. local credentials) and keeps the rest of the flow the same:

  • If a credential is found, you reconstruct RecoveryAuthnCodesCredentialModel and remove it when all codes are used.
  • If no credential is found or all codes are used, you add the CONFIGURE_RECOVERY_AUTHN_CODES required action.

This keeps the authenticator logic readable while making it more robust across different user‑storage implementations.

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java (1)

456-463: Storage assertion and account REST helper look consistent

  • assertUserHasRecoveryKeysCredentialInUserStorage mirrors the existing OTP helper and cleanly reuses BackwardsCompatibilityUserStorageFactory.hasRecoveryCodes("otp1") via testingClient.server().fetch(...). This is a good pattern for asserting against custom user storage without leaking implementation details into the test.
  • The use of new TypeReference<>() {} in getOtpCredentialFromAccountREST keeps the type signature of the local variable authoritative while simplifying the generic boilerplate.

No changes needed here.

Also applies to: 466-473

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.

2 participants