diff --git a/server-spi/src/main/java/org/keycloak/models/credential/dto/PasswordSecretData.java b/server-spi/src/main/java/org/keycloak/models/credential/dto/PasswordSecretData.java index 34cdfe09e0..b3bfc51ec1 100644 --- a/server-spi/src/main/java/org/keycloak/models/credential/dto/PasswordSecretData.java +++ b/server-spi/src/main/java/org/keycloak/models/credential/dto/PasswordSecretData.java @@ -1,18 +1,40 @@ package org.keycloak.models.credential.dto; +import java.io.IOException; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import org.jboss.logging.Logger; +import org.keycloak.common.util.Base64; public class PasswordSecretData { + + public static final Logger logger = Logger.getLogger(PasswordSecretData.class); + private final String value; private final byte[] salt; @JsonCreator - public PasswordSecretData(@JsonProperty("value") String value, @JsonProperty("salt") byte[] salt) { + public PasswordSecretData(@JsonProperty("value") String value, @JsonProperty("salt") String salt) { + this(value, decodeSalt(salt)); + } + + public PasswordSecretData(String value, byte[] salt) { this.value = value; this.salt = salt; } + private static byte[] decodeSalt(String salt) { + try { + return Base64.decode(salt); + } catch (IOException ioe) { + // Could happen under some corner cases that value is still placeholder value "__SALT__" . For example when importing JSON from + // previous version and using custom hash provider without salt support. + logger.tracef("Can't base64 decode the salt %s . Fallback to null salt", salt); + return null; + } + } + public String getValue() { return value; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java index c614a18672..b6f47b393b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java @@ -28,6 +28,8 @@ import org.keycloak.admin.client.resource.UserResource; import org.keycloak.common.constants.KerberosConstants; import org.keycloak.models.Constants; import org.keycloak.models.LDAPConstants; +import org.keycloak.models.credential.PasswordCredentialModel; +import org.keycloak.models.credential.dto.PasswordCredentialData; import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; @@ -39,6 +41,7 @@ import org.keycloak.representations.idm.ClientMappingsRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; @@ -58,6 +61,7 @@ import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.client.KeycloakTestingClient; import org.keycloak.testsuite.util.RealmRepUtil; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -72,6 +76,8 @@ import java.util.stream.Collectors; import org.hamcrest.Matcher; import org.hamcrest.Matchers; +import org.keycloak.util.JsonSerialization; + import static org.junit.Assert.assertThat; /** @@ -82,7 +88,7 @@ public class ExportImportUtil { // In the old testsuite, this method exists as a public method of ImportTest from the model package. // However, model package is not ready to be migrated yet. - public static void assertDataImportedInRealm(Keycloak adminClient, KeycloakTestingClient testingClient, RealmRepresentation realm) { + public static void assertDataImportedInRealm(Keycloak adminClient, KeycloakTestingClient testingClient, RealmRepresentation realm) throws IOException { Assert.assertTrue(realm.isVerifyEmail()); Assert.assertEquals((Integer)3600000, realm.getOfflineSessionIdleTimeout()); Assert.assertEquals((Integer)1500, realm.getAccessTokenLifespanForImplicitFlow()); @@ -179,6 +185,13 @@ public class ExportImportUtil { // user with creation timestamp as string in import Assert.assertEquals(new Long(123655), loginclient.getCreatedTimestamp()); + UserRepresentation hashedPasswordUser = findByUsername(realmRsc, "hashedpassworduser"); + CredentialRepresentation password = realmRsc.users().get(hashedPasswordUser.getId()).credentials().stream() + .filter(credential -> PasswordCredentialModel.TYPE.equals(credential.getType())) + .findFirst().get(); + PasswordCredentialData credentialData = JsonSerialization.readValue(password.getCredentialData(), PasswordCredentialData.class); + Assert.assertEquals(1234, credentialData.getHashIterations()); + List realmRoles = realmRolesForUser(realmRsc, admin); Assert.assertEquals(1, realmRoles.size()); Assert.assertEquals("admin", realmRoles.iterator().next().getName()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json index 345cb7b738..4b6d48461b 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json @@ -150,6 +150,20 @@ } ] }, + { + "username": "hashedpassworduser", + "createdTimestamp" : "123655", + "enabled": true, + "credentials": [ + { + "id": "b0c22f34-f6fb-4165-8fd3-54d3cb4599d1", + "type": "password", + "createdDate": 1576751136227, + "secretData": "{\"value\":\"pass\",\"salt\":\"__SALT__\"}", + "credentialData": "{\"hashIterations\":1234,\"algorithm\":\"pbkdf2-sha256\"}" + } + ] + }, { "username": "admin", "enabled": true,