KEYCLOAK-12876 KEYCLOAK-13148 KEYCLOAK-13149 KEYCLOAK-13151 Re-introduce some changes to preserve UserStorage SPI backwards compatibility. Added test for backwards compatibility of user storage

This commit is contained in:
mposolda 2020-02-17 22:13:39 +01:00 committed by Stian Thorgersen
parent cd51ff3474
commit 803f398dba
11 changed files with 940 additions and 9 deletions

View file

@ -17,8 +17,6 @@
package org.keycloak.storage.ldap.mappers; 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.Provider;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi; import org.keycloak.provider.Spi;

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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<String, String> 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<String, String> 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());
}
}

View file

@ -17,8 +17,16 @@
package org.keycloak.credential; package org.keycloak.credential;
import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import java.util.Comparator; 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) * 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 **/ @Deprecated /** Use PasswordCredentialModel.PASSWORD_HISTORY instead **/
public static final String PASSWORD_HISTORY = "password-history"; 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 **/ @Deprecated /** Use OTPCredentialModel.TYPE instead **/
public static final String OTP = "otp"; public static final String OTP = "otp";
@ -116,4 +127,209 @@ public class CredentialModel implements Serializable {
return (-o1Date.compareTo(o2Date)); 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<String, String> getConfig() {
Map<String, Object> 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<String, String> config) {
writeProperty("config", config, false);
}
private Map<String, Object> 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<String, Object> 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<String, Object> credentialDataMap = readMapFromJson(secret);
return (String) credentialDataMap.get(key);
}
private int readInt(String key, boolean secret) {
Map<String, Object> credentialDataMap = readMapFromJson(secret);
Object obj = credentialDataMap.get(key);
return obj == null ? 0 : (Integer) obj;
}
private void writeProperty(String key, Object value, boolean secret) {
Map<String, Object> credentialDataMap = readMapFromJson(secret);
if (value == null) {
credentialDataMap.remove(key);
} else {
credentialDataMap.put(key, value);
}
writeMapAsJson(credentialDataMap, secret);
}
} }

View file

@ -17,6 +17,7 @@
package org.keycloak.credential.hash; package org.keycloak.credential.hash;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.PasswordPolicy; import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
@ -35,4 +36,32 @@ public interface PasswordHashProvider extends Provider {
} }
boolean verify(String rawPassword, PasswordCredentialModel credential); 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);
}
} }

View file

@ -21,7 +21,10 @@ import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialModel;
import org.keycloak.models.credential.OTPCredentialModel; import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.credential.PasswordUserCredentialModel;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
@ -42,15 +45,23 @@ public class UserCredentialModel implements CredentialInput {
@Deprecated /** Use OTPCredentialModel.TOTP instead **/ @Deprecated /** Use OTPCredentialModel.TOTP instead **/
public static final String HOTP = OTPCredentialModel.HOTP; 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 SECRET = CredentialModel.SECRET;
public static final String KERBEROS = CredentialModel.KERBEROS; public static final String KERBEROS = CredentialModel.KERBEROS;
public static final String CLIENT_CERT = CredentialModel.CLIENT_CERT; public static final String CLIENT_CERT = CredentialModel.CLIENT_CERT;
private final String credentialId; private final String credentialId;
private final String type; private String type;
private final String challengeResponse; private String challengeResponse;
private String device;
private String algorithm;
private final boolean adminRequest; private final boolean adminRequest;
// Additional context informations
protected Map<String, Object> notes = new HashMap<>();
public UserCredentialModel(String credentialId, String type, String challengeResponse) { public UserCredentialModel(String credentialId, String type, String challengeResponse) {
this.credentialId = credentialId; this.credentialId = credentialId;
this.type = type; this.type = type;
@ -65,12 +76,39 @@ public class UserCredentialModel implements CredentialInput {
this.adminRequest = adminRequest; this.adminRequest = adminRequest;
} }
public static UserCredentialModel password(String password) { public static PasswordUserCredentialModel password(String password) {
return password(password, false); return password(password, false);
} }
public static UserCredentialModel password(String password, boolean adminRequest) { public static PasswordUserCredentialModel password(String password, boolean adminRequest) {
return new UserCredentialModel("", PasswordCredentialModel.TYPE, password, 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) { public static UserCredentialModel secret(String password) {
@ -95,6 +133,10 @@ public class UserCredentialModel implements CredentialInput {
return type; return type;
} }
public void setType(String type) {
this.type = type;
}
@Override @Override
public String getChallengeResponse() { public String getChallengeResponse() {
return challengeResponse; return challengeResponse;
@ -103,6 +145,54 @@ public class UserCredentialModel implements CredentialInput {
public boolean isAdminRequest() { public boolean isAdminRequest() {
return adminRequest; 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);
}
} }

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*
* @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);
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class BackwardsCompatibilityUserStorage implements UserLookupProvider, UserStorageProvider, UserRegistrationProvider, CredentialInputUpdater, CredentialInputValidator {
private static final Logger log = Logger.getLogger(BackwardsCompatibilityUserStorage.class);
protected final Map<String, MyUser> users;
protected final ComponentModel model;
protected final KeycloakSession session;
public BackwardsCompatibilityUserStorage(KeycloakSession session, ComponentModel model, Map<String, MyUser> 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<String> 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");
}
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class BackwardsCompatibilityUserStorageFactory implements UserStorageProviderFactory<BackwardsCompatibilityUserStorage> {
public static final String PROVIDER_ID = "backwards-compatibility-storage";
protected Map<String, BackwardsCompatibilityUserStorage.MyUser> userPasswords = new Hashtable<>();
@Override
public BackwardsCompatibilityUserStorage create(KeycloakSession session, ComponentModel model) {
return new BackwardsCompatibilityUserStorage(session, model, userPasswords);
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -27,6 +27,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordUserCredentialModel;
import org.keycloak.storage.ReadOnlyException; import org.keycloak.storage.ReadOnlyException;
import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.storage.StorageId; import org.keycloak.storage.StorageId;
@ -186,12 +187,15 @@ public class UserMapStorage implements UserLookupProvider, UserStorageProvider,
@Override @Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { 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; return false;
} }
if (input.getType().equals(PasswordCredentialModel.TYPE)) { if (input.getType().equals(PasswordCredentialModel.TYPE)) {
String pw = userPasswords.get(user.getUsername()); 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 { } else {
return false; return false;
} }

View file

@ -1,3 +1,4 @@
org.keycloak.testsuite.federation.BackwardsCompatibilityUserStorageFactory
org.keycloak.testsuite.federation.DummyUserFederationProviderFactory org.keycloak.testsuite.federation.DummyUserFederationProviderFactory
org.keycloak.testsuite.federation.FailableHardcodedStorageProviderFactory org.keycloak.testsuite.federation.FailableHardcodedStorageProviderFactory
org.keycloak.testsuite.federation.UserMapStorageFactory org.keycloak.testsuite.federation.UserMapStorageFactory

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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);
}
}