KEYCLOAK-13983 Include algorithm parameters
Include suggestions made by @mposolda to enable more generic usage
This commit is contained in:
parent
396fec19a8
commit
9143bc748f
5 changed files with 60 additions and 50 deletions
|
@ -15,11 +15,15 @@ public class PasswordCredentialModel extends CredentialModel {
|
||||||
private final PasswordCredentialData credentialData;
|
private final PasswordCredentialData credentialData;
|
||||||
private final PasswordSecretData secretData;
|
private final PasswordSecretData secretData;
|
||||||
|
|
||||||
public PasswordCredentialModel(PasswordCredentialData credentialData, PasswordSecretData secretData) {
|
private PasswordCredentialModel(PasswordCredentialData credentialData, PasswordSecretData secretData) {
|
||||||
this.credentialData = credentialData;
|
this.credentialData = credentialData;
|
||||||
this.secretData = secretData;
|
this.secretData = secretData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static PasswordCredentialModel createFromValues(PasswordCredentialData credentialData, PasswordSecretData secretData) {
|
||||||
|
return new PasswordCredentialModel(credentialData, secretData);
|
||||||
|
}
|
||||||
|
|
||||||
public static PasswordCredentialModel createFromValues(String algorithm, byte[] salt, int hashIterations, String encodedPassword){
|
public static PasswordCredentialModel createFromValues(String algorithm, byte[] salt, int hashIterations, String encodedPassword){
|
||||||
PasswordCredentialData credentialData = new PasswordCredentialData(hashIterations, algorithm);
|
PasswordCredentialData credentialData = new PasswordCredentialData(hashIterations, algorithm);
|
||||||
PasswordSecretData secretData = new PasswordSecretData(encodedPassword, salt);
|
PasswordSecretData secretData = new PasswordSecretData(encodedPassword, salt);
|
||||||
|
|
|
@ -2,14 +2,16 @@ package org.keycloak.models.credential.dto;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class PasswordCredentialData {
|
public class PasswordCredentialData {
|
||||||
private final int hashIterations;
|
private final int hashIterations;
|
||||||
private final String algorithm;
|
private final String algorithm;
|
||||||
private final Map<String, String> algorithmData;
|
private final MultivaluedHashMap<String, String> additionalParameters;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creator for standard algorithms (no algorithm tuning beyond hash iterations)
|
* Creator for standard algorithms (no algorithm tuning beyond hash iterations)
|
||||||
|
@ -24,13 +26,13 @@ public class PasswordCredentialData {
|
||||||
* Creator for custom algorithms (algorithm with tuning parameters beyond simple has iterations)
|
* Creator for custom algorithms (algorithm with tuning parameters beyond simple has iterations)
|
||||||
* @param hashIterations iterations
|
* @param hashIterations iterations
|
||||||
* @param algorithm algorithm id
|
* @param algorithm algorithm id
|
||||||
* @param algorithmData additional tuning parameters
|
* @param additionalParameters additional tuning parameters
|
||||||
*/
|
*/
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public PasswordCredentialData(@JsonProperty("hashIterations") int hashIterations, @JsonProperty("algorithm") String algorithm, @JsonProperty("algorithmData") Map<String, String> algorithmData) {
|
public PasswordCredentialData(@JsonProperty("hashIterations") int hashIterations, @JsonProperty("algorithm") String algorithm, @JsonProperty("algorithmData") Map<String, List<String>> additionalParameters) {
|
||||||
this.hashIterations = hashIterations;
|
this.hashIterations = hashIterations;
|
||||||
this.algorithm = algorithm;
|
this.algorithm = algorithm;
|
||||||
this.algorithmData = algorithmData == null ? Collections.emptyMap() : Collections.unmodifiableMap(algorithmData);
|
this.additionalParameters = new MultivaluedHashMap<>(additionalParameters == null ? Collections.emptyMap() : additionalParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,11 +46,11 @@ public class PasswordCredentialData {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an immutable map of algorithm-specific settings. These settings may include additional
|
* Returns a map of algorithm-specific settings. These settings may include additional
|
||||||
* parameters such as Bcrypt memory-tuning parameters
|
* parameters such as Bcrypt memory-tuning parameters. It should be used immutably.
|
||||||
* @return algorithm data
|
* @return algorithm data
|
||||||
*/
|
*/
|
||||||
public Map<String, String> getAlgorithmData() {
|
public MultivaluedHashMap<String, String> getAdditionalParameters() {
|
||||||
return algorithmData;
|
return additionalParameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,14 @@ package org.keycloak.models.credential.dto;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.common.util.Base64;
|
import org.keycloak.common.util.Base64;
|
||||||
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
|
||||||
public class PasswordSecretData {
|
public class PasswordSecretData {
|
||||||
|
|
||||||
|
@ -15,18 +17,18 @@ public class PasswordSecretData {
|
||||||
|
|
||||||
private final String value;
|
private final String value;
|
||||||
private final byte[] salt;
|
private final byte[] salt;
|
||||||
private final Map<String, String> algorithmData;
|
private final MultivaluedHashMap<String, String> additionalParameters;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creator with the option to provide customized secret data (multiple salt values, chiefly)
|
* Creator with the option to provide customized secret data (multiple salt values, chiefly)
|
||||||
* @param value hash value
|
* @param value hash value
|
||||||
* @param salt salt value
|
* @param salt salt value
|
||||||
* @param algorithmData additional data required by the algorithm
|
* @param additionalParameters additional data required by the algorithm
|
||||||
* @throws IOException invalid base64 in salt value
|
* @throws IOException invalid base64 in salt value
|
||||||
*/
|
*/
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public PasswordSecretData(@JsonProperty("value") String value, @JsonProperty("salt") String salt, @JsonProperty("algorithmData") Map<String, String> algorithmData) throws IOException {
|
public PasswordSecretData(@JsonProperty("value") String value, @JsonProperty("salt") String salt, @JsonProperty("algorithmData") Map<String, List<String>> additionalParameters) throws IOException {
|
||||||
this.algorithmData = algorithmData == null ? Collections.emptyMap() : Collections.unmodifiableMap(algorithmData);
|
this.additionalParameters = new MultivaluedHashMap<>(additionalParameters == null ? Collections.emptyMap() : additionalParameters);
|
||||||
|
|
||||||
if (salt == null || "__SALT__".equals(salt)) {
|
if (salt == null || "__SALT__".equals(salt)) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
@ -46,7 +48,7 @@ public class PasswordSecretData {
|
||||||
public PasswordSecretData(String value, byte[] salt) {
|
public PasswordSecretData(String value, byte[] salt) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.salt = salt;
|
this.salt = salt;
|
||||||
this.algorithmData = Collections.emptyMap();
|
this.additionalParameters = new MultivaluedHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getValue() {
|
public String getValue() {
|
||||||
|
@ -57,7 +59,7 @@ public class PasswordSecretData {
|
||||||
return salt;
|
return salt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, String> getAlgorithmData() {
|
public MultivaluedHashMap<String, String> getAdditionalParameters() {
|
||||||
return algorithmData;
|
return additionalParameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,13 @@ package org.keycloak.models.credential;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.credential.CredentialModel;
|
||||||
import org.keycloak.models.credential.dto.PasswordCredentialData;
|
import org.keycloak.models.credential.dto.PasswordCredentialData;
|
||||||
import org.keycloak.models.credential.dto.PasswordSecretData;
|
import org.keycloak.models.credential.dto.PasswordSecretData;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Array;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.*;
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
|
@ -17,6 +18,28 @@ public class CredentialModelTest {
|
||||||
|
|
||||||
private ObjectMapper mapper = new ObjectMapper();
|
private ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void canDeserializeMinimalJson() {
|
||||||
|
CredentialModel model = new CredentialModel();
|
||||||
|
model.setCredentialData("{\"hashIterations\": 10000, \"algorithm\": \"custom\"}");
|
||||||
|
model.setSecretData("{\"value\": \"the value\", \"salt\": \"saltValu\"}");
|
||||||
|
|
||||||
|
PasswordCredentialModel decoded = PasswordCredentialModel.createFromCredentialModel(model);
|
||||||
|
assertThat(decoded, notNullValue());
|
||||||
|
assertThat(decoded.getPasswordCredentialData(), notNullValue());
|
||||||
|
assertThat(decoded.getPasswordCredentialData().getAlgorithm(), equalTo("custom"));
|
||||||
|
assertThat(decoded.getPasswordCredentialData().getHashIterations(), equalTo(10000));
|
||||||
|
assertThat(decoded.getPasswordCredentialData().getAdditionalParameters(), equalTo(Collections.emptyMap()));
|
||||||
|
|
||||||
|
assertThat(decoded.getPasswordSecretData(), notNullValue());
|
||||||
|
assertThat(decoded.getPasswordSecretData().getValue(), equalTo("the value"));
|
||||||
|
assertThat(decoded.getPasswordSecretData().getSalt(), notNullValue());
|
||||||
|
String base64Salt = Base64.getEncoder().encodeToString(decoded.getPasswordSecretData().getSalt());
|
||||||
|
assertThat(base64Salt, equalTo("saltValu"));
|
||||||
|
assertThat(decoded.getPasswordSecretData().getAdditionalParameters(), equalTo(Collections.emptyMap()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void canCreateDefaultCredentialModel() {
|
public void canCreateDefaultCredentialModel() {
|
||||||
PasswordCredentialModel model = PasswordCredentialModel.createFromValues("pbkdf2", new byte[32], 1000, "secretValue");
|
PasswordCredentialModel model = PasswordCredentialModel.createFromValues("pbkdf2", new byte[32], 1000, "secretValue");
|
||||||
|
@ -24,27 +47,27 @@ public class CredentialModelTest {
|
||||||
assertThat(model.getPasswordCredentialData(), notNullValue());
|
assertThat(model.getPasswordCredentialData(), notNullValue());
|
||||||
assertThat(model.getPasswordCredentialData().getAlgorithm(), equalTo("pbkdf2"));
|
assertThat(model.getPasswordCredentialData().getAlgorithm(), equalTo("pbkdf2"));
|
||||||
assertThat(model.getPasswordCredentialData().getHashIterations(), equalTo(1000));
|
assertThat(model.getPasswordCredentialData().getHashIterations(), equalTo(1000));
|
||||||
assertThat(model.getPasswordCredentialData().getAlgorithmData(), equalTo(Collections.emptyMap()));
|
assertThat(model.getPasswordCredentialData().getAdditionalParameters(), equalTo(Collections.emptyMap()));
|
||||||
|
|
||||||
assertThat(model.getPasswordSecretData(), notNullValue());
|
assertThat(model.getPasswordSecretData(), notNullValue());
|
||||||
assertThat(model.getPasswordSecretData().getAlgorithmData(), equalTo(Collections.emptyMap()));
|
assertThat(model.getPasswordSecretData().getAdditionalParameters(), equalTo(Collections.emptyMap()));
|
||||||
assertThat(model.getPasswordSecretData().getValue(), equalTo("secretValue"));
|
assertThat(model.getPasswordSecretData().getValue(), equalTo("secretValue"));
|
||||||
assertThat(Arrays.equals(model.getPasswordSecretData().getSalt(), new byte[32]), is(true));
|
assertThat(Arrays.equals(model.getPasswordSecretData().getSalt(), new byte[32]), is(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void canCreatedExtendedCredentialModel() throws IOException {
|
public void canCreatedExtendedCredentialModel() throws IOException {
|
||||||
PasswordCredentialData credentialData = new PasswordCredentialData(1000, "bcrypt", Collections.singletonMap("cost", "18"));
|
PasswordCredentialData credentialData = new PasswordCredentialData(1000, "bcrypt", Collections.singletonMap("cost", Collections.singletonList("18")));
|
||||||
PasswordSecretData secretData = new PasswordSecretData("secretValue", "AAAAAAAAAAAAAAAA", Collections.singletonMap("salt2", "BBBBBBBBBBBBBBBB"));
|
PasswordSecretData secretData = new PasswordSecretData("secretValue", "AAAAAAAAAAAAAAAA", Collections.singletonMap("salt2", Collections.singletonList("BBBBBBBBBBBBBBBB")));
|
||||||
PasswordCredentialModel model = new PasswordCredentialModel(credentialData, secretData);
|
PasswordCredentialModel model = PasswordCredentialModel.createFromValues(credentialData, secretData);
|
||||||
|
|
||||||
assertThat(model.getPasswordCredentialData(), notNullValue());
|
assertThat(model.getPasswordCredentialData(), notNullValue());
|
||||||
assertThat(model.getPasswordCredentialData().getAlgorithm(), equalTo("bcrypt"));
|
assertThat(model.getPasswordCredentialData().getAlgorithm(), equalTo("bcrypt"));
|
||||||
assertThat(model.getPasswordCredentialData().getHashIterations(), equalTo(1000));
|
assertThat(model.getPasswordCredentialData().getHashIterations(), equalTo(1000));
|
||||||
assertThat(model.getPasswordCredentialData().getAlgorithmData(), equalTo(Collections.singletonMap("cost", "18")));
|
assertThat(model.getPasswordCredentialData().getAdditionalParameters(), equalTo(Collections.singletonMap("cost", Collections.singletonList("18"))));
|
||||||
|
|
||||||
assertThat(model.getPasswordSecretData(), notNullValue());
|
assertThat(model.getPasswordSecretData(), notNullValue());
|
||||||
assertThat(model.getPasswordSecretData().getAlgorithmData(), equalTo(Collections.singletonMap("salt2", "BBBBBBBBBBBBBBBB")));
|
assertThat(model.getPasswordSecretData().getAdditionalParameters(), equalTo(Collections.singletonMap("salt2", Collections.singletonList("BBBBBBBBBBBBBBBB"))));
|
||||||
assertThat(model.getPasswordSecretData().getValue(), equalTo("secretValue"));
|
assertThat(model.getPasswordSecretData().getValue(), equalTo("secretValue"));
|
||||||
assertThat(Arrays.equals(model.getPasswordSecretData().getSalt(), new byte[12]), is(true));
|
assertThat(Arrays.equals(model.getPasswordSecretData().getSalt(), new byte[12]), is(true));
|
||||||
}
|
}
|
||||||
|
@ -60,7 +83,7 @@ public class CredentialModelTest {
|
||||||
PasswordCredentialData pcdOriginal = model.getPasswordCredentialData();
|
PasswordCredentialData pcdOriginal = model.getPasswordCredentialData();
|
||||||
PasswordCredentialData pcdRoundtrip = mapper.readValue(mapper.writeValueAsString(pcdOriginal), PasswordCredentialData.class);
|
PasswordCredentialData pcdRoundtrip = mapper.readValue(mapper.writeValueAsString(pcdOriginal), PasswordCredentialData.class);
|
||||||
|
|
||||||
assertThat(pcdRoundtrip.getAlgorithmData(), equalTo(pcdOriginal.getAlgorithmData()));
|
assertThat(pcdRoundtrip.getAdditionalParameters(), equalTo(pcdOriginal.getAdditionalParameters()));
|
||||||
assertThat(pcdRoundtrip.getAlgorithm(), equalTo(pcdOriginal.getAlgorithm()));
|
assertThat(pcdRoundtrip.getAlgorithm(), equalTo(pcdOriginal.getAlgorithm()));
|
||||||
assertThat(pcdRoundtrip.getHashIterations(), equalTo(pcdOriginal.getHashIterations()));
|
assertThat(pcdRoundtrip.getHashIterations(), equalTo(pcdOriginal.getHashIterations()));
|
||||||
|
|
||||||
|
@ -69,14 +92,14 @@ public class CredentialModelTest {
|
||||||
|
|
||||||
assertThat(psdRoundtrip.getValue(), equalTo(psdOriginal.getValue()));
|
assertThat(psdRoundtrip.getValue(), equalTo(psdOriginal.getValue()));
|
||||||
assertThat(psdRoundtrip.getSalt(), equalTo(psdOriginal.getSalt()));
|
assertThat(psdRoundtrip.getSalt(), equalTo(psdOriginal.getSalt()));
|
||||||
assertThat(psdRoundtrip.getAlgorithmData(), equalTo(psdRoundtrip.getAlgorithmData()));
|
assertThat(psdRoundtrip.getAdditionalParameters(), equalTo(psdRoundtrip.getAdditionalParameters()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void roudtripToJsonExtendedCredentialModel() throws IOException {
|
public void roudtripToJsonExtendedCredentialModel() throws IOException {
|
||||||
PasswordCredentialData credentialData = new PasswordCredentialData(1000, "bcrypt", Collections.singletonMap("cost", "18"));
|
PasswordCredentialData credentialData = new PasswordCredentialData(1000, "bcrypt", Collections.singletonMap("cost", Collections.singletonList("18")));
|
||||||
PasswordSecretData secretData = new PasswordSecretData("secretValue", "AAAAAAAAAAAAAAAA", Collections.singletonMap("salt2", "BBBBBBBBBBBBBBBB"));
|
PasswordSecretData secretData = new PasswordSecretData("secretValue", "AAAAAAAAAAAAAAAA", Collections.singletonMap("salt2", Collections.singletonList("BBBBBBBBBBBBBBBB")));
|
||||||
PasswordCredentialModel model = new PasswordCredentialModel(credentialData, secretData);
|
PasswordCredentialModel model = PasswordCredentialModel.createFromValues(credentialData, secretData);
|
||||||
|
|
||||||
roundTripAndVerify(model);
|
roundTripAndVerify(model);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
width="20px" height="20px" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
|
|
||||||
<style type="text/css">
|
|
||||||
.st0{clip-path:url(#SVGID_2_);fill:#F05133;}
|
|
||||||
</style>
|
|
||||||
<g>
|
|
||||||
<defs>
|
|
||||||
<rect id="SVGID_1_" x="0" y="0" width="20" height="20"/>
|
|
||||||
</defs>
|
|
||||||
<clipPath id="SVGID_2_">
|
|
||||||
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
|
|
||||||
</clipPath>
|
|
||||||
<path class="st0" d="M19.6,9.1l-8.7-8.7c-0.5-0.5-1.3-0.5-1.8,0L7.3,2.2l2.3,2.3c0.5-0.2,1.1-0.1,1.6,0.4c0.4,0.4,0.5,1,0.4,1.6
|
|
||||||
l2.2,2.2c0.5-0.2,1.2-0.1,1.6,0.4c0.6,0.6,0.6,1.6,0,2.2c-0.6,0.6-1.6,0.6-2.2,0c-0.5-0.5-0.6-1.1-0.3-1.7l-2.1-2.1v5.4
|
|
||||||
c0.1,0.1,0.3,0.2,0.4,0.3c0.6,0.6,0.6,1.6,0,2.2C10.5,16,9.6,16,9,15.4c-0.6-0.6-0.6-1.6,0-2.2c0.1-0.1,0.3-0.3,0.5-0.3V7.4
|
|
||||||
C9.3,7.3,9.1,7.2,9,7C8.5,6.6,8.4,5.9,8.6,5.3L6.4,3.1l-6,6c-0.5,0.5-0.5,1.3,0,1.8l8.7,8.7c0.5,0.5,1.3,0.5,1.8,0l8.7-8.7
|
|
||||||
C20.1,10.4,20.1,9.6,19.6,9.1"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
Loading…
Reference in a new issue