Skip to content

Create recovery keys in user storage or local - #1

Open
linxia0415 wants to merge 1 commit into
mainfrom
pr-38446
Open

Create recovery keys in user storage or local#1
linxia0415 wants to merge 1 commit into
mainfrom
pr-38446

Conversation

@linxia0415

@linxia0415 linxia0415 commented Jun 3, 2026

Copy link
Copy Markdown

Create recovery keys in user storage or local. Closes keycloak/keycloak#38445

Summary by CodeRabbit

Release Notes

  • New Features

    • Recovery authentication codes are now fully supported with enhanced credential creation and dual-storage capabilities, including seamless fallback to local storage when needed
    • Improved backward compatibility for recovery codes in federated user storage
  • Tests

    • Added comprehensive integration tests for recovery authentication codes setup, validation, and login workflows in federated storage scenarios

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

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR enables custom UserStorageProviders to manage recovery-authn-codes credentials alongside passwords and OTP. It introduces abstraction utilities for credential retrieval and creation, refactors existing components to use these centralized helpers, and extends a test user storage provider to demonstrate full recovery-codes credential lifecycle support including creation, validation, and retrieval.

Changes

Recovery Codes User Storage Delegation

Layer / File(s) Summary
Recovery codes retrieval utility
server-spi/src/main/java/org/keycloak/models/utils/RecoveryAuthnCodesUtils.java
New getCredential(UserModel user) method provides unified lookup by searching federated credentials first, then stored credentials, returning an Optional<CredentialModel>.
Recovery codes credential creation helper
server-spi-private/src/main/java/org/keycloak/utils/CredentialHelper.java
New createRecoveryCodesCredential(...) method centralizes credential creation by JSON-serializing codes, attempting user-storage persistence, and falling back to the recovery-codes provider with logging.
Required action refactored to use creation helper
services/src/main/java/org/keycloak/authentication/requiredactions/RecoveryAuthnCodesAction.java
Delegates credential creation to CredentialHelper.createRecoveryCodesCredential() instead of obtaining and calling a credential provider directly.
Authenticator and form bean refactored to use retrieval utility
services/src/main/java/org/keycloak/authentication/authenticators/browser/RecoveryAuthnCodesFormAuthenticator.java, services/src/main/java/org/keycloak/forms/login/freemarker/model/RecoveryAuthnCodeInputLoginBean.java
Both components now use RecoveryAuthnCodesUtils.getCredential() for consistent credential lookup instead of directly streaming stored credentials.
Test user storage extended with recovery codes support
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java
Implements full recovery-codes lifecycle: declares support, creates/updates codes via JSON serialization, retrieves codes as credentials, validates incoming codes against stored list, and tracks codes via internal user model.
Factory helper to query recovery codes presence
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorageFactory.java
New hasRecoveryCodes(String username) method checks whether a user has stored recovery codes in the federated storage.
Integration test for recovery codes with federated storage
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java
New testRecoveryKeysSetupAndLogin() validates complete flow: configures recovery-codes execution, generates and stores codes in federated storage, logs in by entering stored recovery codes.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 Recovery codes now dance with storage,
No longer bound to Keycloak's cage alone,
Each provider hops freely through the portal,
Credentials delegated, fully grown! 🔐🌿

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The pull request description is minimal and only references the issue without explaining the changes made to implement the fix. Expand the description to explain how the implementation delegates recovery-key creation to user storage, including details about the new helper methods and the fallback mechanism.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Create recovery keys in user storage or local' clearly and concisely summarizes the main change: adding support for creating recovery keys in either external user storage or local storage, which is the primary objective of the pull request.
Linked Issues check ✅ Passed The pull request successfully implements the requirement to delegate recovery-keys credential creation to user storage through new helper methods and refactored logic that uses CredentialInputUpdater.
Out of Scope Changes check ✅ Passed All changes are directly related to enabling recovery-keys credential delegation to user storage and include necessary helper methods, refactoring, and test coverage without introducing unrelated modifications.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-38446

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

🧹 Nitpick comments (2)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java (1)

326-341: 💤 Low value

Recovery code is not consumed after successful validation.

In production recovery-code implementations, codes are one-time use and should be removed or marked as consumed after successful validation. This implementation allows the same code to be reused indefinitely.

This is acceptable for a test provider, but if you intend this to more accurately simulate real user storage behavior, consider removing the matched code from the list after successful validation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java`
around lines 326 - 341, The validation currently reads storedRecoveryKeys
(myUser.recoveryCodes) into generatedKeys and returns true if any key equals
input.getChallengeResponse() but never removes the matched code; update the
logic in BackwardsCompatibilityUserStorage (the
RecoveryAuthnCodesCredentialModel.TYPE branch) so that after finding a matching
key you remove that key from generatedKeys, reserialize the updated list (use
JsonSerialization.readValue/ writeValueAsString on
storedRecoveryKeys.getCredentialData()), set the new credential data on
myUser.recoveryCodes, and persist the change via your user/credential update
path so the code is consumed and cannot be reused.
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java (1)

135-135: 💤 Low value

Typo in config alias: "suthenticator" should be "authenticator".

Minor typo in the configuration alias name.

Suggested fix
-                                            config.setAlias("delayed-suthenticator-config");
+                                            config.setAlias("delayed-authenticator-config");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java`
at line 135, The alias string passed to config.setAlias contains a typo; update
the alias in BackwardsCompatibilityUserStorageTest so the call
config.setAlias("delayed-suthenticator-config") is corrected to
config.setAlias("delayed-authenticator-config") to use the proper
"authenticator" spelling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@services/src/main/java/org/keycloak/forms/login/freemarker/model/RecoveryAuthnCodeInputLoginBean.java`:
- Around line 17-19: The code calls credentialModelOpt.get() without checking
the Optional returned by RecoveryAuthnCodesUtils.getCredential in
RecoveryAuthnCodeInputLoginBean, which can throw NoSuchElementException; update
the code to handle the absent case (either check credentialModelOpt.isPresent()
before calling RecoveryAuthnCodesCredentialModel.createFromCredentialModel, or
use credentialModelOpt.orElseThrow(...) to throw a clear, specific exception, or
return/handle a missing-credential state) so that
RecoveryAuthnCodesCredentialModel.createFromCredentialModel is only invoked with
a present CredentialModel.

---

Nitpick comments:
In
`@testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java`:
- Around line 326-341: The validation currently reads storedRecoveryKeys
(myUser.recoveryCodes) into generatedKeys and returns true if any key equals
input.getChallengeResponse() but never removes the matched code; update the
logic in BackwardsCompatibilityUserStorage (the
RecoveryAuthnCodesCredentialModel.TYPE branch) so that after finding a matching
key you remove that key from generatedKeys, reserialize the updated list (use
JsonSerialization.readValue/ writeValueAsString on
storedRecoveryKeys.getCredentialData()), set the new credential data on
myUser.recoveryCodes, and persist the change via your user/credential update
path so the code is consumed and cannot be reused.

In
`@testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java`:
- Line 135: The alias string passed to config.setAlias contains a typo; update
the alias in BackwardsCompatibilityUserStorageTest so the call
config.setAlias("delayed-suthenticator-config") is corrected to
config.setAlias("delayed-authenticator-config") to use the proper
"authenticator" spelling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5fd45d0c-f1bb-4732-96f2-a1d1a924e939

📥 Commits

Reviewing files that changed from the base of the PR and between 51f9079 and ce2dcc5.

📒 Files selected for processing (8)
  • server-spi-private/src/main/java/org/keycloak/utils/CredentialHelper.java
  • server-spi/src/main/java/org/keycloak/models/utils/RecoveryAuthnCodesUtils.java
  • 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
  • testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java
  • testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorageFactory.java
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unchecked Optional.get() may throw NoSuchElementException.

Calling .get() on credentialModelOpt without checking isPresent() will throw a NoSuchElementException if no credential is found. While this bean is presumably only instantiated when a credential exists, defensive handling would be safer.

🛡️ Proposed fix to handle missing credential gracefully
-        Optional<CredentialModel> credentialModelOpt = RecoveryAuthnCodesUtils.getCredential(user);
-
-        RecoveryAuthnCodesCredentialModel recoveryCodeCredentialModel = RecoveryAuthnCodesCredentialModel.createFromCredentialModel(credentialModelOpt.get());
+        CredentialModel credentialModel = RecoveryAuthnCodesUtils.getCredential(user)
+                .orElseThrow(() -> new IllegalStateException("Recovery authentication codes credential not found for user"));

+        RecoveryAuthnCodesCredentialModel recoveryCodeCredentialModel = RecoveryAuthnCodesCredentialModel.createFromCredentialModel(credentialModel);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/src/main/java/org/keycloak/forms/login/freemarker/model/RecoveryAuthnCodeInputLoginBean.java`
around lines 17 - 19, The code calls credentialModelOpt.get() without checking
the Optional returned by RecoveryAuthnCodesUtils.getCredential in
RecoveryAuthnCodeInputLoginBean, which can throw NoSuchElementException; update
the code to handle the absent case (either check credentialModelOpt.isPresent()
before calling RecoveryAuthnCodesCredentialModel.createFromCredentialModel, or
use credentialModelOpt.orElseThrow(...) to throw a clear, specific exception, or
return/handle a missing-credential state) so that
RecoveryAuthnCodesCredentialModel.createFromCredentialModel is only invoked with
a present CredentialModel.

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.

Not possible to delegate creating or deleting RecoveryKeys credential to userStorage

2 participants