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:
parent
cd51ff3474
commit
803f398dba
11 changed files with 940 additions and 9 deletions
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
||||
}
|
||||
}
|
|
@ -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<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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, Object> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
org.keycloak.testsuite.federation.BackwardsCompatibilityUserStorageFactory
|
||||
org.keycloak.testsuite.federation.DummyUserFederationProviderFactory
|
||||
org.keycloak.testsuite.federation.FailableHardcodedStorageProviderFactory
|
||||
org.keycloak.testsuite.federation.UserMapStorageFactory
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue