Skip to content

Automated Test: feature-recovery-keys-implementation #318

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,12 @@
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.credential.RecoveryAuthnCodesCredentialModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.util.JsonSerialization;

import java.io.IOException;
import java.util.List;
import java.util.Objects;

/**
Expand Down Expand Up @@ -69,15 +73,15 @@ public static void setOrReplaceAuthenticationRequirement(KeycloakSession session
}));
}

public static ConfigurableAuthenticatorFactory getConfigurableAuthenticatorFactory(KeycloakSession session, String providerId) {
ConfigurableAuthenticatorFactory factory = (AuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, providerId);
if (factory == null) {
factory = (FormActionFactory)session.getKeycloakSessionFactory().getProviderFactory(FormAction.class, providerId);
}
if (factory == null) {
factory = (ClientAuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, providerId);
}
return factory;
public static ConfigurableAuthenticatorFactory getConfigurableAuthenticatorFactory(KeycloakSession session, String providerId) {
ConfigurableAuthenticatorFactory factory = (AuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, providerId);
if (factory == null) {
factory = (FormActionFactory)session.getKeycloakSessionFactory().getProviderFactory(FormAction.class, providerId);
}
if (factory == null) {
factory = (ClientAuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, providerId);
}
return factory;
}

/**
Expand Down Expand Up @@ -105,6 +109,27 @@ public static boolean createOTPCredential(KeycloakSession session, RealmModel re
return user.credentialManager().isValid(credential);
}

/**
* Create RecoveryCodes credential either in userStorage or local storage (Keycloak DB)
*/
public static void createRecoveryCodesCredential(KeycloakSession session, RealmModel realm, UserModel user, RecoveryAuthnCodesCredentialModel credentialModel, List<String> generatedCodes) {
var recoveryCodeCredentialProvider = session.getProvider(CredentialProvider.class, "keycloak-recovery-authn-codes");
String recoveryCodesJson;
try {
recoveryCodesJson = JsonSerialization.writeValueAsString(generatedCodes);
} catch (IOException e) {
throw new RuntimeException(e);
}
UserCredentialModel recoveryCodesCredential = new UserCredentialModel("", credentialModel.getType(), recoveryCodesJson);

boolean userStorageCreated = user.credentialManager().updateCredential(recoveryCodesCredential);
if (userStorageCreated) {
logger.debugf("Created RecoveryCodes credential for user '%s' in the user storage", user.getUsername());
} else {
recoveryCodeCredentialProvider.createCredential(realm, user, credentialModel);
}
}

/**
* Create "dummy" representation of the credential. Typically used when credential is provided by userStorage and we don't know further
* details about the credential besides the type
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package org.keycloak.models.utils;

import java.util.Optional;
import java.util.function.Supplier;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.SecretGenerator;
import org.keycloak.credential.CredentialModel;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.JavaAlgorithm;
import org.keycloak.jose.jws.crypto.HashUtils;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.RecoveryAuthnCodesCredentialModel;

import java.nio.charset.StandardCharsets;
import java.util.List;
Expand Down Expand Up @@ -43,4 +47,17 @@ public static List<String> generateRawCodes() {
return Stream.generate(code).limit(QUANTITY_OF_CODES_TO_GENERATE).collect(Collectors.toList());
}

/**
* Checks the user storage for the credential. If not found it will look for the credential in the local storage
*
* @param user - User model
* @return - a optional credential model
*/
public static Optional<CredentialModel> getCredential(UserModel user) {
return user.credentialManager()
.getFederatedCredentialsStream()
.filter(c -> RecoveryAuthnCodesCredentialModel.TYPE.equals(c.getType()))
.findFirst()
.or(() -> user.credentialManager().getStoredCredentialsByTypeStream(RecoveryAuthnCodesCredentialModel.TYPE).findFirst());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ private boolean isRecoveryAuthnCodeInputValid(AuthenticationFlowContext authnFlo
authnFlowContext.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, responseChallenge);
} else {
result = true;
Optional<CredentialModel> optUserCredentialFound = authenticatedUser.credentialManager().getStoredCredentialsByTypeStream(
RecoveryAuthnCodesCredentialModel.TYPE).findFirst();
Optional<CredentialModel> optUserCredentialFound = RecoveryAuthnCodesUtils.getCredential(authenticatedUser);
RecoveryAuthnCodesCredentialModel recoveryCodeCredentialModel = null;
if (optUserCredentialFound.isPresent()) {
recoveryCodeCredentialModel = RecoveryAuthnCodesCredentialModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Arrays;
import java.util.List;

import org.keycloak.Config;
import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.authentication.CredentialRegistrator;
Expand All @@ -11,8 +12,6 @@
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.authenticators.browser.RecoveryAuthnCodesFormAuthenticator;
import org.keycloak.common.Profile;
import org.keycloak.credential.RecoveryAuthnCodesCredentialProviderFactory;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
Expand All @@ -26,6 +25,8 @@
import jakarta.ws.rs.core.Response;
import org.keycloak.sessions.AuthenticationSessionModel;

import static org.keycloak.utils.CredentialHelper.createRecoveryCodesCredential;

public class RecoveryAuthnCodesAction implements RequiredActionProvider, RequiredActionFactory, EnvironmentDependentProviderFactory, CredentialRegistrator {

private static final String FIELD_GENERATED_RECOVERY_AUTHN_CODES_HIDDEN = "generatedRecoveryAuthnCodes";
Expand Down Expand Up @@ -86,13 +87,8 @@ public void requiredActionChallenge(RequiredActionContext context) {
public void processAction(RequiredActionContext reqActionContext) {
EventBuilder event = reqActionContext.getEvent();
event.event(EventType.UPDATE_CREDENTIAL);

CredentialProvider recoveryCodeCredentialProvider;
MultivaluedMap<String, String> httpReqParamsMap;

recoveryCodeCredentialProvider = reqActionContext.getSession().getProvider(CredentialProvider.class,
RecoveryAuthnCodesCredentialProviderFactory.PROVIDER_ID);

event.detail(Details.CREDENTIAL_TYPE, RecoveryAuthnCodesCredentialModel.TYPE);

httpReqParamsMap = reqActionContext.getHttpRequest().getDecodedFormParameters();
Expand All @@ -117,8 +113,7 @@ public void processAction(RequiredActionContext reqActionContext) {
AuthenticatorUtil.logoutOtherSessions(reqActionContext);
}

recoveryCodeCredentialProvider.createCredential(reqActionContext.getRealm(), reqActionContext.getUser(),
credentialModel);
createRecoveryCodesCredential(reqActionContext.getSession(), reqActionContext.getRealm(), reqActionContext.getUser(), credentialModel, generatedCodes);

reqActionContext.success();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.RecoveryAuthnCodesCredentialModel;
import org.keycloak.models.utils.RecoveryAuthnCodesUtils;

import java.util.Optional;

public class RecoveryAuthnCodeInputLoginBean {

private final int codeNumber;

public RecoveryAuthnCodeInputLoginBean(KeycloakSession session, RealmModel realm, UserModel user) {
CredentialModel credentialModel = user.credentialManager().getStoredCredentialsByTypeStream(RecoveryAuthnCodesCredentialModel.TYPE)
.findFirst().get();
Optional<CredentialModel> credentialModelOpt = RecoveryAuthnCodesUtils.getCredential(user);

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

this.codeNumber = recoveryCodeCredentialModel.getNextRecoveryAuthnCode().get().getNumber();
Comment on lines +17 to 21

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard the Optional to avoid runtime failures.
If getCredential(user) returns empty, credentialModelOpt.get() will throw and break the login page. Add an explicit guard (or fail fast with a clear message) before accessing the credential.

🛠️ Proposed fix
-        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 codes credential missing for user " + user.getUsername()));
+
+        this.codeNumber = recoveryCodeCredentialModel.getNextRecoveryAuthnCode()
+                .orElseThrow(() -> new IllegalStateException("No recovery codes available for user " + user.getUsername()))
+                .getNumber();
📝 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
Optional<CredentialModel> credentialModelOpt = RecoveryAuthnCodesUtils.getCredential(user);
RecoveryAuthnCodesCredentialModel recoveryCodeCredentialModel = RecoveryAuthnCodesCredentialModel.createFromCredentialModel(credentialModel);
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 codes credential missing for user " + user.getUsername()));
this.codeNumber = recoveryCodeCredentialModel.getNextRecoveryAuthnCode()
.orElseThrow(() -> new IllegalStateException("No recovery codes available for user " + user.getUsername()))
.getNumber();
🤖 Prompt for AI Agents
In
`@services/src/main/java/org/keycloak/forms/login/freemarker/model/RecoveryAuthnCodeInputLoginBean.java`
around lines 17 - 21, The code currently calls credentialModelOpt.get() without
checking the Optional returned by RecoveryAuthnCodesUtils.getCredential(user),
which can throw; update the logic in RecoveryAuthnCodeInputLoginBean to guard
the Optional (using isPresent()/ifPresent(), orElseThrow() with a clear message,
or provide a fallback) before calling
RecoveryAuthnCodesCredentialModel.createFromCredentialModel and accessing
getNextRecoveryAuthnCode().get().getNumber(), and ensure codeNumber is set or a
clear exception is raised when no credential exists.

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@

package org.keycloak.testsuite.federation;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jboss.logging.Logger;
Expand All @@ -33,7 +34,6 @@
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.hash.PasswordHashProvider;
import org.keycloak.credential.hash.Pbkdf2Sha512PasswordHashProviderFactory;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OTPPolicy;
Expand All @@ -43,6 +43,8 @@
import org.keycloak.models.UserModel;
import org.keycloak.models.cache.UserCache;
import org.keycloak.models.credential.PasswordUserCredentialModel;
import org.keycloak.models.credential.RecoveryAuthnCodesCredentialModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
Expand All @@ -51,11 +53,12 @@
import org.keycloak.storage.user.UserLookupProvider;
import org.keycloak.storage.user.UserQueryProvider;
import org.keycloak.storage.user.UserRegistrationProvider;
import org.keycloak.util.JsonSerialization;

/**
* UserStorage implementation created in Keycloak 4.8.3. It is used for backwards compatibility testing. Future Keycloak versions
* should work fine without a need to change the code of this provider.
*
* <p>
* TODO: Have some good mechanims to make sure that source code of this provider is really compatible with Keycloak 4.8.3
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
Expand Down Expand Up @@ -89,7 +92,7 @@ public UserModel getUserById(RealmModel realm, String id) {
}

private UserModel createUser(RealmModel realm, String username) {
return new AbstractUserAdapterFederatedStorage(session, realm, model) {
return new AbstractUserAdapterFederatedStorage(session, realm, model) {
@Override
public String getUsername() {
return username;
Expand All @@ -107,7 +110,8 @@ public void setUsername(String username1) {
@Override
public boolean supportsCredentialType(String credentialType) {
if (CredentialModel.PASSWORD.equals(credentialType)
|| isOTPType(credentialType)) {
|| isOTPType(credentialType)
|| credentialType.equals(RecoveryAuthnCodesCredentialModel.TYPE)) {
return true;
} else {
log.infof("Unsupported credential type: %s", credentialType);
Expand Down Expand Up @@ -172,6 +176,7 @@ public boolean updateCredential(RealmModel realm, UserModel user, CredentialInpu
OTPPolicy otpPolicy = session.getContext().getRealm().getOTPPolicy();

CredentialModel newOTP = new CredentialModel();
newOTP.setId(KeycloakModelUtils.generateId());
newOTP.setType(input.getType());
long createdDate = Time.currentTimeMillis();
newOTP.setCreatedDate(createdDate);
Expand All @@ -184,6 +189,15 @@ public boolean updateCredential(RealmModel realm, UserModel user, CredentialInpu

users.get(translateUserName(user.getUsername())).otp = newOTP;

return true;
} else if (input.getType().equals(RecoveryAuthnCodesCredentialModel.TYPE)) {
CredentialModel recoveryCodesModel = new CredentialModel();
recoveryCodesModel.setId(KeycloakModelUtils.generateId());
recoveryCodesModel.setType(input.getType());
recoveryCodesModel.setCredentialData(input.getChallengeResponse());
long createdDate = Time.currentTimeMillis();
recoveryCodesModel.setCreatedDate(createdDate);
users.get(translateUserName(user.getUsername())).recoveryCodes = recoveryCodesModel;
return true;
} else {
log.infof("Attempt to update unsupported credential of type: %s", input.getType());
Expand Down Expand Up @@ -213,6 +227,30 @@ private MyUser getMyUser(UserModel user) {
return users.get(translateUserName(user.getUsername()));
}

@Override
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(
JsonSerialization.readValue(myUser.recoveryCodes.getCredentialData(), List.class),
myUser.recoveryCodes.getCreatedDate(),
myUser.recoveryCodes.getUserLabel()
);
credentialModels.add(model);
} catch (IOException e) {
log.error("Could not deserialize credential of type: recovery-codes");
}
}
Comment on lines +236 to +246

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Raw List type and silently swallowed exception.

  1. Line 238 uses raw List.class, causing an unchecked conversion when passed to createFromValues(List<String>, ...). This loses type safety.
  2. The caught IOException only logs and continues, meaning recovery codes will silently be missing from the returned stream. Callers won't know deserialization failed.
  3. Line 244 has a double space typo: "deserialize credential".
Suggested improvements
         if (myUser.recoveryCodes != null) {
             try {
+                `@SuppressWarnings`("unchecked")
+                List<String> codes = JsonSerialization.readValue(myUser.recoveryCodes.getCredentialData(), List.class);
                 model = RecoveryAuthnCodesCredentialModel.createFromValues(
-                        JsonSerialization.readValue(myUser.recoveryCodes.getCredentialData(), List.class),
+                        codes,
                         myUser.recoveryCodes.getCreatedDate(),
                         myUser.recoveryCodes.getUserLabel()
                 );
                 credentialModels.add(model);
             } catch (IOException e) {
-                log.error("Could not deserialize  credential of type: recovery-codes");
+                log.error("Could not deserialize credential of type: recovery-codes", e);
+                throw new RuntimeException("Failed to deserialize recovery codes credential", e);
             }
         }
📝 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
try {
model = RecoveryAuthnCodesCredentialModel.createFromValues(
JsonSerialization.readValue(myUser.recoveryCodes.getCredentialData(), List.class),
myUser.recoveryCodes.getCreatedDate(),
myUser.recoveryCodes.getUserLabel()
);
credentialModels.add(model);
} catch (IOException e) {
log.error("Could not deserialize credential of type: recovery-codes");
}
}
try {
`@SuppressWarnings`("unchecked")
List<String> codes = JsonSerialization.readValue(myUser.recoveryCodes.getCredentialData(), List.class);
model = RecoveryAuthnCodesCredentialModel.createFromValues(
codes,
myUser.recoveryCodes.getCreatedDate(),
myUser.recoveryCodes.getUserLabel()
);
credentialModels.add(model);
} catch (IOException e) {
log.error("Could not deserialize credential of type: recovery-codes", e);
throw new RuntimeException("Failed to deserialize recovery codes credential", e);
}
}
🤖 Prompt for AI Agents
In
`@testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java`
around lines 236 - 246, The deserialization uses a raw List.class and swallows
exceptions: change the JsonSerialization.readValue call to use a typed
TypeReference<List<String>> so
RecoveryAuthnCodesCredentialModel.createFromValues receives a List<String>, and
update the catch(IOException e) in the block around
RecoveryAuthnCodesCredentialModel.createFromValues to not silently ignore
failures—either rethrow a runtime exception or propagate the IOException (or at
minimum log the error with e as parameter) so callers know deserialization
failed; also fix the double-space typo in the log message ("deserialize 
credential" -> "deserialize credential"). Reference:
RecoveryAuthnCodesCredentialModel.createFromValues, JsonSerialization.readValue,
and the catch(IOException e) logging line.

if (myUser.otp != null) {
credentialModels.add(myUser.getOtp());
}

return credentialModels.stream();
}

@Override
public Stream<String> getDisableableCredentialTypesStream(RealmModel realm, UserModel user) {
Set<String> types = new HashSet<>();
Expand All @@ -234,6 +272,8 @@ public boolean isConfiguredFor(RealmModel realm, UserModel user, String credenti

if (isOTPType(credentialType) && myUser.otp != null) {
return true;
} else if (credentialType.equals(RecoveryAuthnCodesCredentialModel.TYPE) && myUser.recoveryCodes != null) {
return true;
} else {
log.infof("Not supported credentialType '%s' for user '%s'", credentialType, user.getUsername());
return false;
Expand Down Expand Up @@ -283,7 +323,22 @@ public boolean isValid(RealmModel realm, UserModel user, CredentialInput input)
TimeBasedOTP validator = new TimeBasedOTP(storedOTPCredential.getAlgorithm(), storedOTPCredential.getDigits(),
storedOTPCredential.getPeriod(), realm.getOTPPolicy().getLookAheadWindow());
return validator.validateTOTP(otpCredential.getValue(), storedOTPCredential.getValue().getBytes());
} else {
} else if (input.getType().equals(RecoveryAuthnCodesCredentialModel.TYPE)) {
CredentialModel storedRecoveryKeys = myUser.recoveryCodes;
if (storedRecoveryKeys == null) {
log.warnf("Not found credential for the user %s", user.getUsername());
return false;
}
List generatedKeys;
try {
generatedKeys = JsonSerialization.readValue(storedRecoveryKeys.getCredentialData(), List.class);
} catch (IOException e) {
log.warnf("Cannot deserialize recovery keys credential for the user %s", user.getUsername());
return false;
}

return generatedKeys.stream().anyMatch(key -> key.equals(input.getChallengeResponse()));
} else {
Comment on lines +326 to +341

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Raw type usage and potential NPE in recovery code validation.

  1. Line 332/334: Raw List type loses type safety; same issue as in getCredentials.
  2. Line 340: If the deserialized list contains null elements (malformed data), key.equals(...) will throw NullPointerException. Consider using Objects.equals() or reversing the comparison.
  3. Line 341: Minor formatting issue - double space before else.
Suggested fix
-            List generatedKeys;
+            `@SuppressWarnings`("unchecked")
+            List<String> generatedKeys;
             try {
                 generatedKeys = JsonSerialization.readValue(storedRecoveryKeys.getCredentialData(), List.class);
             } catch (IOException e) {
                 log.warnf("Cannot deserialize recovery keys credential for the user %s", user.getUsername());
                 return false;
             }

-            return generatedKeys.stream().anyMatch(key -> key.equals(input.getChallengeResponse()));
-        }  else {
+            String challengeResponse = input.getChallengeResponse();
+            return generatedKeys.stream().anyMatch(key -> Objects.equals(key, challengeResponse));
+        } else {

Note: Add import java.util.Objects; at the top of the file.

📝 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
} else if (input.getType().equals(RecoveryAuthnCodesCredentialModel.TYPE)) {
CredentialModel storedRecoveryKeys = myUser.recoveryCodes;
if (storedRecoveryKeys == null) {
log.warnf("Not found credential for the user %s", user.getUsername());
return false;
}
List generatedKeys;
try {
generatedKeys = JsonSerialization.readValue(storedRecoveryKeys.getCredentialData(), List.class);
} catch (IOException e) {
log.warnf("Cannot deserialize recovery keys credential for the user %s", user.getUsername());
return false;
}
return generatedKeys.stream().anyMatch(key -> key.equals(input.getChallengeResponse()));
} else {
} else if (input.getType().equals(RecoveryAuthnCodesCredentialModel.TYPE)) {
CredentialModel storedRecoveryKeys = myUser.recoveryCodes;
if (storedRecoveryKeys == null) {
log.warnf("Not found credential for the user %s", user.getUsername());
return false;
}
`@SuppressWarnings`("unchecked")
List<String> generatedKeys;
try {
generatedKeys = JsonSerialization.readValue(storedRecoveryKeys.getCredentialData(), List.class);
} catch (IOException e) {
log.warnf("Cannot deserialize recovery keys credential for the user %s", user.getUsername());
return false;
}
String challengeResponse = input.getChallengeResponse();
return generatedKeys.stream().anyMatch(key -> Objects.equals(key, challengeResponse));
} else {
🤖 Prompt for AI Agents
In
`@testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java`
around lines 326 - 341, In BackwardsCompatibilityUserStorage, replace the raw
List used for deserializing recovery codes (generatedKeys) with a typed
List<String> when calling JsonSerialization.readValue to restore type safety,
handle potential null elements by using Objects.equals(key,
input.getChallengeResponse()) or by comparing
input.getChallengeResponse().equals(key) to avoid NPEs, and fix the extra space
before the else; also add import java.util.Objects if you use Objects.equals.

log.infof("Not supported to validate credential of type '%s' for user '%s'", input.getType(), user.getUsername());
return false;
}
Expand Down Expand Up @@ -369,6 +424,7 @@ static class MyUser {
private String username;
private CredentialModel hashedPassword;
private CredentialModel otp;
private CredentialModel recoveryCodes;

private MyUser(String username) {
this.username = username;
Expand All @@ -377,6 +433,10 @@ private MyUser(String username) {
public CredentialModel getOtp() {
return otp;
}

public CredentialModel getRecoveryCodes() {
return recoveryCodes;
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,9 @@ public boolean hasUserOTP(String username) {
return user.getOtp() != null;
}

public boolean hasRecoveryCodes(String username) {
BackwardsCompatibilityUserStorage.MyUser user = userPasswords.get(username);
if (user == null) return false;
return user.getRecoveryCodes() != null;
}
}
Loading