diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPStorageMapperSpi.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPStorageMapperSpi.java
index 54fe747fd5..321e0cb873 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPStorageMapperSpi.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPStorageMapperSpi.java
@@ -17,8 +17,6 @@
package org.keycloak.storage.ldap.mappers;
-import org.keycloak.credential.CredentialProvider;
-import org.keycloak.credential.CredentialProviderFactory;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
diff --git a/server-spi-private/src/test/java/org/keycloak/models/CredentialModelBackwardsCompatibilityTest.java b/server-spi-private/src/test/java/org/keycloak/models/CredentialModelBackwardsCompatibilityTest.java
new file mode 100644
index 0000000000..ba86b70d30
--- /dev/null
+++ b/server-spi-private/src/test/java/org/keycloak/models/CredentialModelBackwardsCompatibilityTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2019 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.models;
+
+import org.bouncycastle.util.Arrays;
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.credential.CredentialModel;
+import org.keycloak.models.credential.OTPCredentialModel;
+import org.keycloak.models.credential.PasswordCredentialModel;
+
+/**
+ * @author Marek Posolda
+ */
+public class CredentialModelBackwardsCompatibilityTest {
+
+ @Test
+ public void testCredentialModelLegacyGetterSetters() {
+ CredentialModel credential = new CredentialModel();
+
+ // Assert null can be read
+ Assert.assertNull(credential.getValue());
+ Assert.assertNull(credential.getDevice());
+ Assert.assertNull(credential.getAlgorithm());
+ Assert.assertNull(credential.getSalt());
+ Assert.assertEquals(0, credential.getCounter());
+ Assert.assertEquals(0, credential.getHashIterations());
+ Assert.assertEquals(0, credential.getDigits());
+ Assert.assertEquals(0, credential.getPeriod());
+
+ credential.setValue("foo");
+ credential.setDevice("foo-device");
+ credential.setAlgorithm("foo-algorithm");
+ credential.setSalt(new byte[] { 1, 2, 3});
+ credential.setCounter(15);
+ credential.setHashIterations(20);
+ credential.setDigits(25);
+ credential.setPeriod(30);
+
+ Assert.assertEquals("foo", credential.getValue());
+ Assert.assertEquals("foo-device", credential.getDevice());
+ Assert.assertTrue(Arrays.areEqual(new byte[] { 1, 2, 3 }, credential.getSalt()));
+ Assert.assertEquals(15, credential.getCounter());
+ Assert.assertEquals(20, credential.getHashIterations());
+ Assert.assertEquals(25, credential.getDigits());
+ Assert.assertEquals(30, credential.getPeriod());
+
+ // Set null to some values
+ credential.setValue(null);
+ credential.setSalt(null);
+ credential.setAlgorithm(null);
+
+ Assert.assertNull(credential.getValue());
+ Assert.assertNull(credential.getAlgorithm());
+ Assert.assertNull(credential.getSalt());
+ Assert.assertEquals("foo-device", credential.getDevice());
+ }
+
+ @Test
+ public void testCredentialModelConfigMap() {
+ MultivaluedHashMap map = new MultivaluedHashMap<>();
+ map.add("key1", "val11");
+ map.add("key1", "val12");
+ map.add("key2", "val21");
+
+ CredentialModel credential = new CredentialModel();
+ Assert.assertNull(credential.getConfig());
+ credential.setConfig(map);
+
+ MultivaluedHashMap loadedMap = credential.getConfig();
+ Assert.assertEquals(map, loadedMap);
+ }
+
+ @Test
+ public void testCredentialModelOTP() {
+ CredentialModel otp = OTPCredentialModel.createTOTP("456123", 6, 30, "someAlg");
+
+ Assert.assertEquals("456123", otp.getValue());
+ Assert.assertEquals(6, otp.getDigits());
+ Assert.assertEquals(30, otp.getPeriod());
+ Assert.assertEquals("someAlg", otp.getAlgorithm());
+
+ // Change something and assert it is changed
+ otp.setValue("789789");
+ Assert.assertEquals("789789", otp.getValue());
+
+ // Test clone
+ OTPCredentialModel cloned = OTPCredentialModel.createFromCredentialModel(otp);
+ Assert.assertEquals("789789", cloned.getOTPSecretData().getValue());
+ Assert.assertEquals(6, cloned.getOTPCredentialData().getDigits());
+ Assert.assertEquals("someAlg", cloned.getOTPCredentialData().getAlgorithm());
+ }
+
+
+ @Test
+ public void testCredentialModelPassword() {
+ byte[] salt = { 1, 2, 3 };
+ CredentialModel password = PasswordCredentialModel.createFromValues("foo", salt, 1000, "pass");
+
+ Assert.assertEquals("pass", password.getValue());
+ Assert.assertTrue(Arrays.areEqual(salt, password.getSalt()));
+ Assert.assertEquals(1000, password.getHashIterations());
+ Assert.assertEquals("foo", password.getAlgorithm());
+
+ // Change something and assert it is changed
+ password.setValue("789789");
+ Assert.assertEquals("789789", password.getValue());
+
+ // Test clone
+ PasswordCredentialModel cloned = PasswordCredentialModel.createFromCredentialModel(password);
+ Assert.assertEquals("789789", cloned.getPasswordSecretData().getValue());
+ Assert.assertEquals(1000, cloned.getPasswordCredentialData().getHashIterations());
+ Assert.assertEquals(1000, cloned.getPasswordCredentialData().getHashIterations());
+ Assert.assertEquals("foo", cloned.getPasswordCredentialData().getAlgorithm());
+
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/credential/CredentialModel.java b/server-spi/src/main/java/org/keycloak/credential/CredentialModel.java
index b62125f303..16b0481bd6 100755
--- a/server-spi/src/main/java/org/keycloak/credential/CredentialModel.java
+++ b/server-spi/src/main/java/org/keycloak/credential/CredentialModel.java
@@ -17,8 +17,16 @@
package org.keycloak.credential;
+import java.io.IOException;
import java.io.Serializable;
import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.keycloak.common.util.Base64;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.models.credential.PasswordCredentialModel;
+import org.keycloak.util.JsonSerialization;
/**
* Used just in cases when we want to "directly" update or retrieve the hash or salt of user credential (For example during export/import)
@@ -33,6 +41,9 @@ public class CredentialModel implements Serializable {
@Deprecated /** Use PasswordCredentialModel.PASSWORD_HISTORY instead **/
public static final String PASSWORD_HISTORY = "password-history";
+ @Deprecated /** Legacy stuff. Not used in Keycloak anymore **/
+ public static final String PASSWORD_TOKEN = "password-token";
+
@Deprecated /** Use OTPCredentialModel.TYPE instead **/
public static final String OTP = "otp";
@@ -116,4 +127,209 @@ public class CredentialModel implements Serializable {
return (-o1Date.compareTo(o2Date));
};
}
+
+ // DEPRECATED - the methods below exists for the backwards compatibility
+
+ /**
+ * @deprecated Recommended to use PasswordCredentialModel.getSecretData().getValue() or OTPCredentialModel.getSecretData().getValue()
+ */
+ @Deprecated
+ public String getValue() {
+ return readString("value", true);
+ }
+
+ /**
+ * @deprecated See {@link #getValue()}
+ */
+ @Deprecated
+ public void setValue(String value) {
+ writeProperty("value", value, true);
+ }
+
+ /**
+ * @deprecated Recommended to use OTPCredentialModel.getCredentialData().getDevice()
+ */
+ @Deprecated
+ public String getDevice() {
+ return readString("device", false);
+ }
+
+ /**
+ * @deprecated See {@link #getDevice()}
+ */
+ @Deprecated
+ public void setDevice(String device) {
+ writeProperty("device", device, false);
+ }
+
+ /**
+ * @deprecated Recommended to use PasswordCredentialModel.getSecretData().getSalt()
+ */
+ @Deprecated
+ public byte[] getSalt() {
+ try {
+ String saltStr = readString("salt", true);
+ return saltStr == null ? null : Base64.decode(saltStr);
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
+ }
+ }
+
+ /**
+ * @deprecated See {@link #getSalt()}
+ */
+ @Deprecated
+ public void setSalt(byte[] salt) {
+ String saltStr = salt == null ? null : Base64.encodeBytes(salt);
+ writeProperty("salt", saltStr, true);
+ }
+
+ /**
+ * @deprecated Recommended to use PasswordCredentialModel.getCredentialData().getHashIterations()
+ */
+ @Deprecated
+ public int getHashIterations() {
+ return readInt("hashIterations", false);
+ }
+
+ /**
+ * @deprecated See {@link #getHashIterations()}
+ */
+ @Deprecated
+ public void setHashIterations(int iterations) {
+ writeProperty("hashIterations", iterations, false);
+ }
+
+ /**
+ * @deprecated Recommended to use OTPCredentialModel.getCredentialData().getCounter()
+ */
+ @Deprecated
+ public int getCounter() {
+ return readInt("counter", false);
+ }
+
+ /**
+ * @deprecated See {@link #getCounter()}
+ */
+ @Deprecated
+ public void setCounter(int counter) {
+ writeProperty("counter", counter, false);
+ }
+
+ /**
+ * @deprecated Recommended to use PasswordCredentialModel.getCredentialData().getAlgorithm() or OTPCredentialModel.getCredentialData().getAlgorithm()
+ */
+ @Deprecated
+ public String getAlgorithm() {
+ return readString("algorithm", false);
+ }
+
+ /**
+ * @deprecated See {@link #getAlgorithm()}
+ */
+ @Deprecated
+ public void setAlgorithm(String algorithm) {
+ writeProperty("algorithm", algorithm, false);
+ }
+
+ /**
+ * @deprecated Recommended to use OTPCredentialModel.getCredentialData().getDigits()
+ */
+ @Deprecated
+ public int getDigits() {
+ return readInt("digits", false);
+ }
+
+ /**
+ * @deprecated See {@link #setDigits(int)}
+ */
+ @Deprecated
+ public void setDigits(int digits) {
+ writeProperty("digits", digits, false);
+ }
+
+ /**
+ * @deprecated Recommended to use OTPCredentialModel.getCredentialData().getPeriod()
+ */
+ @Deprecated
+ public int getPeriod() {
+ return readInt("period", false);
+ }
+
+ /**
+ * @deprecated See {@link #setPeriod(int)}
+ */
+ @Deprecated
+ public void setPeriod(int period) {
+ writeProperty("period", period, false);
+ }
+
+ /**
+ * @deprecated Recommended to use {@link #getCredentialData()} instead and use the subtype of CredentialData specific to your credential
+ */
+ @Deprecated
+ public MultivaluedHashMap getConfig() {
+ Map credentialData = readMapFromJson(false);
+ if (credentialData == null) {
+ return null;
+ }
+
+ Object obj = credentialData.get("config");
+ return obj == null ? null : new MultivaluedHashMap<>((Map)obj);
+ }
+
+ /**
+ * @deprecated Recommended to use {@link #setCredentialData(String)} instead and use the subtype of CredentialData specific to your credential
+ */
+ @Deprecated
+ public void setConfig(MultivaluedHashMap config) {
+ writeProperty("config", config, false);
+ }
+
+ private Map readMapFromJson(boolean secret) {
+ String jsonStr = secret ? secretData : credentialData;
+ if (jsonStr == null) {
+ return new HashMap<>();
+ }
+
+ try {
+ return JsonSerialization.readValue(jsonStr, Map.class);
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
+ }
+ }
+
+ private void writeMapAsJson(Map map, boolean secret) {
+ try {
+ String jsonStr = JsonSerialization.writeValueAsString(map);
+ if (secret) {
+ this.secretData = jsonStr;
+ } else {
+ this.credentialData = jsonStr;
+ }
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
+ }
+ }
+
+ private String readString(String key, boolean secret) {
+ Map credentialDataMap = readMapFromJson(secret);
+ return (String) credentialDataMap.get(key);
+ }
+
+ private int readInt(String key, boolean secret) {
+ Map credentialDataMap = readMapFromJson(secret);
+ Object obj = credentialDataMap.get(key);
+ return obj == null ? 0 : (Integer) obj;
+ }
+
+ private void writeProperty(String key, Object value, boolean secret) {
+ Map credentialDataMap = readMapFromJson(secret);
+ if (value == null) {
+ credentialDataMap.remove(key);
+ } else {
+ credentialDataMap.put(key, value);
+ }
+ writeMapAsJson(credentialDataMap, secret);
+ }
}
diff --git a/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashProvider.java b/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashProvider.java
index 69db2c63eb..2e44777ce4 100644
--- a/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashProvider.java
+++ b/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashProvider.java
@@ -17,6 +17,7 @@
package org.keycloak.credential.hash;
+import org.keycloak.credential.CredentialModel;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.provider.Provider;
@@ -35,4 +36,32 @@ public interface PasswordHashProvider extends Provider {
}
boolean verify(String rawPassword, PasswordCredentialModel credential);
+
+ /**
+ * @deprecated Exists due the backwards compatibility. It is recommended to use {@link #policyCheck(PasswordPolicy, PasswordCredentialModel)}
+ */
+ @Deprecated
+ default boolean policyCheck(PasswordPolicy policy, CredentialModel credential) {
+ return policyCheck(policy, PasswordCredentialModel.createFromCredentialModel(credential));
+ }
+
+ /**
+ * @deprecated Exists due the backwards compatibility. It is recommended to use {@link #encodedCredential(String, int)}}
+ */
+ @Deprecated
+ default void encode(String rawPassword, int iterations, CredentialModel credential) {
+ PasswordCredentialModel passwordCred = encodedCredential(rawPassword, iterations);
+
+ credential.setCredentialData(passwordCred.getCredentialData());
+ credential.setSecretData(passwordCred.getSecretData());
+ }
+
+ /**
+ * @deprecated Exists due the backwards compatibility. It is recommended to use {@link #verify(String, PasswordCredentialModel)}
+ */
+ @Deprecated
+ default boolean verify(String rawPassword, CredentialModel credential) {
+ PasswordCredentialModel password = PasswordCredentialModel.createFromCredentialModel(credential);
+ return verify(rawPassword, password);
+ }
}
diff --git a/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java b/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java
index 917af7c2a7..4a0346688d 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java
@@ -21,7 +21,10 @@ import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.credential.PasswordCredentialModel;
+import org.keycloak.models.credential.PasswordUserCredentialModel;
+import java.util.HashMap;
+import java.util.Map;
import java.util.UUID;
/**
@@ -42,15 +45,23 @@ public class UserCredentialModel implements CredentialInput {
@Deprecated /** Use OTPCredentialModel.TOTP instead **/
public static final String HOTP = OTPCredentialModel.HOTP;
+ @Deprecated /** Legacy stuff. Not used in Keycloak anymore **/
+ public static final String PASSWORD_TOKEN = CredentialModel.PASSWORD_TOKEN;
+
public static final String SECRET = CredentialModel.SECRET;
public static final String KERBEROS = CredentialModel.KERBEROS;
public static final String CLIENT_CERT = CredentialModel.CLIENT_CERT;
private final String credentialId;
- private final String type;
- private final String challengeResponse;
+ private String type;
+ private String challengeResponse;
+ private String device;
+ private String algorithm;
private final boolean adminRequest;
+ // Additional context informations
+ protected Map notes = new HashMap<>();
+
public UserCredentialModel(String credentialId, String type, String challengeResponse) {
this.credentialId = credentialId;
this.type = type;
@@ -65,12 +76,39 @@ public class UserCredentialModel implements CredentialInput {
this.adminRequest = adminRequest;
}
- public static UserCredentialModel password(String password) {
+ public static PasswordUserCredentialModel password(String password) {
return password(password, false);
}
- public static UserCredentialModel password(String password, boolean adminRequest) {
- return new UserCredentialModel("", PasswordCredentialModel.TYPE, password, adminRequest);
+ public static PasswordUserCredentialModel password(String password, boolean adminRequest) {
+ // It uses PasswordUserCredentialModel for backwards compatibility. Some UserStorage providers can check for that type
+ return new PasswordUserCredentialModel("", PasswordCredentialModel.TYPE, password, adminRequest);
+ }
+
+ @Deprecated /** passwordToken is legacy stuff. Not used in Keycloak anymore **/
+ public static UserCredentialModel passwordToken(String passwordToken) {
+ return new UserCredentialModel("", PASSWORD_TOKEN, passwordToken);
+ }
+
+ /**
+ * @param type must be "totp" or "hotp"
+ * @param key
+ * @return
+ */
+ public static UserCredentialModel otp(String type, String key) {
+ if (type.equals(HOTP)) return hotp(key);
+ if (type.equals(TOTP)) return totp(key);
+ throw new RuntimeException("Unknown OTP type");
+ }
+
+
+ public static UserCredentialModel totp(String key) {
+ return new UserCredentialModel("", TOTP, key);
+ }
+
+
+ public static UserCredentialModel hotp(String key) {
+ return new UserCredentialModel("", HOTP, key);
}
public static UserCredentialModel secret(String password) {
@@ -95,6 +133,10 @@ public class UserCredentialModel implements CredentialInput {
return type;
}
+ public void setType(String type) {
+ this.type = type;
+ }
+
@Override
public String getChallengeResponse() {
return challengeResponse;
@@ -103,6 +145,54 @@ public class UserCredentialModel implements CredentialInput {
public boolean isAdminRequest() {
return adminRequest;
}
+
+ /**
+ * This method exists only because of the backwards compatibility
+ */
+ @Deprecated
+ public static boolean isOtp(String type) {
+ return TOTP.equals(type) || HOTP.equals(type);
+ }
+
+ /**
+ * This method exists only because of the backwards compatibility. It is recommended to use {@link #getChallengeResponse()} instead
+ */
+ public String getValue() {
+ return getChallengeResponse();
+ }
+
+ public void setValue(String value) {
+ this.challengeResponse = value;
+ }
+
+ public String getDevice() {
+ return device;
+ }
+
+ public void setDevice(String device) {
+ this.device = device;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ public void setAlgorithm(String algorithm) {
+ this.algorithm = algorithm;
+ }
+
+ public void setNote(String key, String value) {
+ this.notes.put(key, value);
+ }
+
+ public void removeNote(String key) {
+ this.notes.remove(key);
+ }
+
+ public Object getNote(String key) {
+ return this.notes.get(key);
+ }
+
}
diff --git a/server-spi/src/main/java/org/keycloak/models/credential/PasswordUserCredentialModel.java b/server-spi/src/main/java/org/keycloak/models/credential/PasswordUserCredentialModel.java
new file mode 100644
index 0000000000..8f03deeb44
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/credential/PasswordUserCredentialModel.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.credential;
+
+import org.keycloak.models.UserCredentialModel;
+
+/**
+ * @author Marek Posolda
+ *
+ * @deprecated Recommended to use {@link UserCredentialModel} as it contains all the functionality required by this class
+ */
+public class PasswordUserCredentialModel extends UserCredentialModel {
+
+ public PasswordUserCredentialModel(String credentialId, String type, String challengeResponse, boolean adminRequest) {
+ super(credentialId, type, challengeResponse, adminRequest);
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java
new file mode 100644
index 0000000000..647f62b529
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2019 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.testsuite.federation;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import org.jboss.logging.Logger;
+import org.keycloak.common.util.Time;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.credential.CredentialInput;
+import org.keycloak.credential.CredentialInputUpdater;
+import org.keycloak.credential.CredentialInputValidator;
+import org.keycloak.credential.CredentialModel;
+import org.keycloak.credential.hash.PasswordHashProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.PasswordPolicy;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.cache.UserCache;
+import org.keycloak.models.credential.PasswordUserCredentialModel;
+import org.keycloak.storage.StorageId;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
+import org.keycloak.storage.user.UserLookupProvider;
+import org.keycloak.storage.user.UserRegistrationProvider;
+
+/**
+ * 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.
+ *
+ * TODO: Have some good mechanims to make sure that source code of this provider is really compatible with Keycloak 4.8.3
+ *
+ * @author Marek Posolda
+ */
+public class BackwardsCompatibilityUserStorage implements UserLookupProvider, UserStorageProvider, UserRegistrationProvider, CredentialInputUpdater, CredentialInputValidator {
+
+ private static final Logger log = Logger.getLogger(BackwardsCompatibilityUserStorage.class);
+
+ protected final Map users;
+ protected final ComponentModel model;
+ protected final KeycloakSession session;
+
+ public BackwardsCompatibilityUserStorage(KeycloakSession session, ComponentModel model, Map users) {
+ this.session = session;
+ this.model = model;
+ this.users = users;
+ }
+
+
+ @Override
+ public UserModel getUserById(String id, RealmModel realm) {
+ StorageId storageId = new StorageId(id);
+ final String username = storageId.getExternalId();
+ if (!users.containsKey(username)) return null;
+
+ return createUser(realm, username);
+ }
+
+ private UserModel createUser(RealmModel realm, String username) {
+ return new AbstractUserAdapterFederatedStorage(session, realm, model) {
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ @Override
+ public void setUsername(String username1) {
+ if (!username1.equals(username)) {
+ throw new RuntimeException("Unsupported to change username");
+ }
+ }
+
+ };
+ }
+
+ @Override
+ public boolean supportsCredentialType(String credentialType) {
+ return CredentialModel.PASSWORD.equals(credentialType);
+ }
+
+ @Override
+ public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
+ if (!(input instanceof UserCredentialModel)) return false;
+
+ if (input.getType().equals(UserCredentialModel.PASSWORD)) {
+
+ // Compatibility with 4.8.3 - Using "legacy" type PasswordUserCredentialModel
+ if (!(input instanceof PasswordUserCredentialModel)) {
+ log.warn("Input is not PasswordUserCredentialModel");
+ return false;
+ }
+
+ PasswordUserCredentialModel userCredentialModel = (PasswordUserCredentialModel) input;
+
+ // Those are not supposed to be set when calling this method in Keycloak 4.8.3 for password credential
+ assertNull(userCredentialModel.getDevice());
+ assertNull(userCredentialModel.getAlgorithm());
+
+ PasswordPolicy policy = session.getContext().getRealm().getPasswordPolicy();
+ PasswordHashProvider hashProvider = getHashProvider(policy);
+
+ CredentialModel newPassword = new CredentialModel();
+ newPassword.setType(CredentialModel.PASSWORD);
+ long createdDate = Time.currentTimeMillis();
+ newPassword.setCreatedDate(createdDate);
+
+ // Compatibility with 4.8.3 - Using "legacy" signature of the method on hashProvider
+ hashProvider.encode(userCredentialModel.getValue(), policy.getHashIterations(), newPassword);
+
+ // Test expected values of credentialModel
+ assertEquals(newPassword.getAlgorithm(), policy.getHashAlgorithm());
+ assertNotNull(newPassword.getValue());
+ assertNotNull(newPassword.getSalt());
+
+ users.get(user.getUsername()).hashedPassword = newPassword;
+
+ UserCache userCache = session.userCache();
+ if (userCache != null) {
+ userCache.evict(realm, user);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected PasswordHashProvider getHashProvider(PasswordPolicy policy) {
+ PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, policy.getHashAlgorithm());
+ if (hash == null) {
+ log.warnv("Realm PasswordPolicy PasswordHashProvider {0} not found", policy.getHashAlgorithm());
+ return session.getProvider(PasswordHashProvider.class, PasswordPolicy.HASH_ALGORITHM_DEFAULT);
+ }
+ return hash;
+ }
+
+ @Override
+ public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
+
+ }
+
+ @Override
+ public Set getDisableableCredentialTypes(RealmModel realm, UserModel user) {
+ return Collections.EMPTY_SET;
+ }
+
+ @Override
+ public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
+ return CredentialModel.PASSWORD.equals(credentialType);
+ }
+
+ @Override
+ public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
+ if (!(input instanceof PasswordUserCredentialModel)) return false;
+ if (input.getType().equals(UserCredentialModel.PASSWORD)) {
+ CredentialModel hashedPassword = users.get(user.getUsername()).hashedPassword;
+ if (hashedPassword == null) {
+ log.warnf("Password not set for user %s", user.getUsername());
+ return false;
+ }
+
+ PasswordUserCredentialModel userCredentialModel = (PasswordUserCredentialModel) input;
+
+ // Those are not supposed to be set when calling this method in Keycloak 4.8.3 for password credential
+ assertNull(userCredentialModel.getDevice());
+ assertNull(userCredentialModel.getAlgorithm());
+
+ PasswordPolicy policy = session.getContext().getRealm().getPasswordPolicy();
+ PasswordHashProvider hashProvider = getHashProvider(policy);
+
+ String rawPassword = userCredentialModel.getValue();
+
+ // Compatibility with 4.8.3 - using "legacy" signature of this method
+ return hashProvider.verify(rawPassword, hashedPassword);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public UserModel getUserByUsername(String username, RealmModel realm) {
+ if (!users.containsKey(username)) return null;
+
+ return createUser(realm, username);
+ }
+
+ @Override
+ public UserModel getUserByEmail(String email, RealmModel realm) {
+ return null;
+ }
+
+ @Override
+ public UserModel addUser(RealmModel realm, String username) {
+ users.put(username, new MyUser(username));
+ return createUser(realm, username);
+ }
+
+ @Override
+ public boolean removeUser(RealmModel realm, UserModel user) {
+ return users.remove(user.getUsername()) != null;
+ }
+
+ @Override
+ public void close() {
+ }
+
+
+ class MyUser {
+
+ private String username;
+ private CredentialModel hashedPassword;
+
+ private MyUser(String username) {
+ this.username = username;
+ }
+
+ }
+
+
+ private void assertNull(Object obj) {
+ if (obj != null) {
+ throw new AssertionError("Object wasn't null");
+ }
+ }
+
+ private void assertNotNull(Object obj) {
+ if (obj == null) {
+ throw new AssertionError("Object was null");
+ }
+ }
+
+ private void assertEquals(Object obj1, Object obj2) {
+ if (!(obj1.equals(obj2))) {
+ throw new AssertionError("Objects not equals");
+ }
+ }
+
+}
+
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorageFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorageFactory.java
new file mode 100644
index 0000000000..84095067f2
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorageFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2019 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.testsuite.federation;
+
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.storage.UserStorageProviderFactory;
+
+/**
+ * @author Marek Posolda
+ */
+public class BackwardsCompatibilityUserStorageFactory implements UserStorageProviderFactory {
+
+ public static final String PROVIDER_ID = "backwards-compatibility-storage";
+
+ protected Map userPasswords = new Hashtable<>();
+
+ @Override
+ public BackwardsCompatibilityUserStorage create(KeycloakSession session, ComponentModel model) {
+ return new BackwardsCompatibilityUserStorage(session, model, userPasswords);
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserMapStorage.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserMapStorage.java
index 52a98c6cf7..f21fe28a7e 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserMapStorage.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserMapStorage.java
@@ -27,6 +27,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.credential.PasswordUserCredentialModel;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.storage.StorageId;
@@ -186,12 +187,15 @@ public class UserMapStorage implements UserLookupProvider, UserStorageProvider,
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
- if (!(input instanceof UserCredentialModel)) {
+ // Test "instanceof PasswordUserCredentialModel" on purpose. We want to test that the backwards compatibility
+ if (!(input instanceof PasswordUserCredentialModel)) {
return false;
}
if (input.getType().equals(PasswordCredentialModel.TYPE)) {
String pw = userPasswords.get(user.getUsername());
- return pw != null && pw.equals(input.getChallengeResponse());
+
+ // Using "getValue" on purpose here, to test that backwards compatibility works as expected
+ return pw != null && pw.equals(((UserCredentialModel) input).getValue());
} else {
return false;
}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory
index 9d5abba8bc..fd6a4d0f6b 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory
@@ -1,3 +1,4 @@
+org.keycloak.testsuite.federation.BackwardsCompatibilityUserStorageFactory
org.keycloak.testsuite.federation.DummyUserFederationProviderFactory
org.keycloak.testsuite.federation.FailableHardcodedStorageProviderFactory
org.keycloak.testsuite.federation.UserMapStorageFactory
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java
new file mode 100644
index 0000000000..3f218bf532
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/BackwardsCompatibilityUserStorageTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2019 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.testsuite.federation.storage;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+import javax.ws.rs.core.Response;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.credential.CredentialModel;
+import org.keycloak.representations.idm.ComponentRepresentation;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.storage.StorageId;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.testsuite.AbstractAuthTest;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.federation.BackwardsCompatibilityUserStorageFactory;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.LoginPage;
+
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
+
+/**
+ * Test that userStorage implementation created in previous version is still compatible with latest Keycloak version
+ *
+ * @author Marek Posolda
+ */
+public class BackwardsCompatibilityUserStorageTest extends AbstractAuthTest {
+
+ private String backwardsCompProviderId;
+
+ @Before
+ public void addProvidersBeforeTest() throws URISyntaxException, IOException {
+ ComponentRepresentation memProvider = new ComponentRepresentation();
+ memProvider.setName("backwards-compatibility");
+ memProvider.setProviderId(BackwardsCompatibilityUserStorageFactory.PROVIDER_ID);
+ memProvider.setProviderType(UserStorageProvider.class.getName());
+ memProvider.setConfig(new MultivaluedHashMap<>());
+ memProvider.getConfig().putSingle("priority", Integer.toString(0));
+
+ backwardsCompProviderId = addComponent(memProvider);
+
+ }
+
+ protected String addComponent(ComponentRepresentation component) {
+ Response resp = testRealmResource().components().add(component);
+ String id = ApiUtil.getCreatedId(resp);
+ getCleanup().addComponentId(id);
+ return id;
+ }
+
+
+ @Page
+ protected AppPage appPage;
+
+ @Page
+ protected LoginPage loginPage;
+
+ private void loginSuccessAndLogout(String username, String password) {
+ testRealmAccountPage.navigateTo();
+ loginPage.login(username, password);
+ assertCurrentUrlStartsWith(testRealmAccountPage);
+ testRealmAccountPage.logOut();
+ }
+
+ public void loginBadPassword(String username) {
+ testRealmAccountPage.navigateTo();
+ testRealmLoginPage.form().login(username, "badpassword");
+ assertCurrentUrlDoesntStartWith(testRealmAccountPage);
+ }
+
+ @Test
+ public void testLoginSuccess() {
+ addUserAndResetPassword("tbrady", "goat");
+ addUserAndResetPassword("tbrady2", "goat2");
+
+ loginSuccessAndLogout("tbrady", "goat");
+ loginSuccessAndLogout("tbrady2", "goat2");
+ loginBadPassword("tbrady");
+ }
+
+ private void addUserAndResetPassword(String username, String password) {
+ // Save user and assert he is saved in the new storage
+ UserRepresentation user = new UserRepresentation();
+ user.setEnabled(true);
+ user.setUsername(username);
+ Response response = testRealmResource().users().create(user);
+ String userId = ApiUtil.getCreatedId(response);
+
+ Assert.assertEquals(backwardsCompProviderId, new StorageId(userId).getProviderId());
+
+ // Update his password
+ CredentialRepresentation passwordRep = new CredentialRepresentation();
+ passwordRep.setType(CredentialModel.PASSWORD);
+ passwordRep.setValue(password);
+ passwordRep.setTemporary(false);
+
+ testRealmResource().users().get(userId).resetPassword(passwordRep);
+ }
+}
\ No newline at end of file