From a8d947b1a93d1b0e2b120a71152fdb5c6a7c1809 Mon Sep 17 00:00:00 2001 From: Alexander Schwartz Date: Thu, 25 Sep 2025 06:23:20 +0200 Subject: [PATCH] Fix duplicate label when using password history (#42903) Closes #42736 Signed-off-by: Alexander Schwartz --- .../keycloak/models/jpa/JpaUserCredentialStore.java | 11 ++++++----- .../credential/PasswordCredentialProvider.java | 2 ++ .../testsuite/policy/PasswordHistoryPolicyTest.java | 2 ++ .../webauthn/account/WebAuthnSigningInTest.java | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserCredentialStore.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserCredentialStore.java index 920668cd81c..d845f677f67 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserCredentialStore.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserCredentialStore.java @@ -66,7 +66,7 @@ public class JpaUserCredentialStore implements UserCredentialStore { if (!Objects.equals(cred.getUserLabel(), entity.getUserLabel())) { // For legacy entries in the credentials, there might be a duplicate for historical reasons. // Ignore them when the credential is updated, which might happen when credentials are verified. - validateDuplicateCredential(realm, user, cred.getUserLabel(), cred.getId()); + validateDuplicateCredential(realm, user, cred.getType(), cred.getUserLabel(), cred.getId()); } entity.setCreatedDate(cred.getCreatedDate()); entity.setUserLabel(cred.getUserLabel()); @@ -135,7 +135,7 @@ public class JpaUserCredentialStore implements UserCredentialStore { @Override public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) { return getStoredCredentialsStream(realm, user).filter(credential -> - Objects.equals(type, credential.getType()) && Objects.equals(name, credential.getUserLabel())) + Objects.equals(type, credential.getType()) && Objects.equals(name, credential.getUserLabel())) .findFirst().orElse(null); } @@ -144,12 +144,13 @@ public class JpaUserCredentialStore implements UserCredentialStore { } - private void validateDuplicateCredential(RealmModel realm, UserModel user, String userLabel, String credentialId) { + private void validateDuplicateCredential(RealmModel realm, UserModel user, String credType, String userLabel, String credentialId) { if (userLabel != null) { boolean exists = getStoredCredentialEntities(realm, user) .anyMatch(existing -> existing.getUserLabel() != null && existing.getUserLabel().equalsIgnoreCase(userLabel.trim()) - && (credentialId == null || !existing.getId().equals(credentialId))); // Exclude self in update + && existing.getType().equals(credType) + && !existing.getId().equals(credentialId)); // Exclude self in update if (exists) { throw new ModelDuplicateException("Device already exists with the same name", CredentialModel.USER_LABEL); @@ -158,7 +159,7 @@ public class JpaUserCredentialStore implements UserCredentialStore { } CredentialEntity createCredentialEntity(RealmModel realm, UserModel user, CredentialModel cred) { - validateDuplicateCredential(realm, user, cred.getUserLabel(), null); + validateDuplicateCredential(realm, user, cred.getType(), cred.getUserLabel(), null); CredentialEntity entity = new CredentialEntity(); String id = cred.getId() == null ? KeycloakModelUtils.generateId() : cred.getId(); entity.setId(id); diff --git a/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java index fe31a83fdd5..baa00d831b3 100644 --- a/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java +++ b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java @@ -127,6 +127,8 @@ public class PasswordCredentialProvider implements CredentialProvider 1 || passwordAgeInDaysPolicy > 0) { oldPassword.setId(null); oldPassword.setType(PasswordCredentialModel.PASSWORD_HISTORY); + // Setting the label to "nulL" avoids duplicate label errors + oldPassword.setUserLabel(null); oldPassword = user.credentialManager().createStoredCredential(oldPassword); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/policy/PasswordHistoryPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/policy/PasswordHistoryPolicyTest.java index d402e9692bd..c643137ddd9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/policy/PasswordHistoryPolicyTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/policy/PasswordHistoryPolicyTest.java @@ -76,6 +76,8 @@ public class PasswordHistoryPolicyTest extends AbstractAuthTest { newCredential.setValue(newPassword); newCredential.setTemporary(false); userResource.resetPassword(newCredential); + CredentialRepresentation cr = userResource.credentials().stream().filter(credentialRepresentation -> credentialRepresentation.getType().equals(PASSWORD)).findFirst().get(); + userResource.setCredentialUserLabel(cr.getId(), "My Password"); } private void expectBadRequestException(Consumer f) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/account/WebAuthnSigningInTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/account/WebAuthnSigningInTest.java index c2694c60faa..62813bc6edb 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/account/WebAuthnSigningInTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/account/WebAuthnSigningInTest.java @@ -95,7 +95,7 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest { public void createWebAuthnSameUserLabel() { final String SAME_LABEL = "key123"; - SigningInPage.UserCredential webAuthn = addWebAuthnCredential(SAME_LABEL, false); + SigningInPage.UserCredential webAuthn = addWebAuthnCredential(SAME_LABEL, true); assertThat(webAuthn, notNullValue()); SigningInPage.CredentialType credentialType = webAuthnPwdlessCredentialType;