KEYCLOAK-1900 Refactoring

This commit is contained in:
Stian Thorgersen 2015-12-08 13:25:50 +01:00
parent 162dd4051d
commit 321cac2ab8
18 changed files with 293 additions and 339 deletions

View file

@ -1103,6 +1103,7 @@ module.factory('PasswordPolicy', function() {
var p = {};
p.policyMessages = {
hashAlgorithm: "Default hashing algorithm. Default is 'pbkdf2'.",
hashIterations: "Number of hashing iterations. Default is 1. Recommended is 50000.",
length: "Minimal password length (integer type). Default value is 8.",
digits: "Minimal number (integer type) of digits in password. Default value is 1.",
@ -1116,6 +1117,7 @@ module.factory('PasswordPolicy', function() {
}
p.allPolicies = [
{ name: 'hashAlgorithm', value: 'pbkdf2' },
{ name: 'hashIterations', value: 1 },
{ name: 'length', value: 8 },
{ name: 'digits', value: 1 },

View file

@ -1,41 +0,0 @@
package org.keycloak.hash;
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
/**
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
*/
public class DefaultPasswordHashProvider implements PasswordHashProvider {
private final String algorithm;
private final int iterations;
public DefaultPasswordHashProvider() {
this.algorithm = "pbkdf2";
this.iterations = 1;
}
public String encode(String rawPassword, byte[] salt) {
return this.encode(rawPassword, salt, this.iterations);
}
public String encode(String rawPassword, byte[] salt, int iterations) {
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder(salt, iterations);
return encoder.encode(rawPassword);
}
public boolean verify(String rawPassword, String encodedPassword, byte[] salt) {
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder(salt);
return encoder.verify(rawPassword, encodedPassword);
}
public String getAlgorithm() {
return this.algorithm;
}
public void close() {
}
}

View file

@ -1,35 +0,0 @@
package org.keycloak.hash;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
*/
public class DefaultPasswordHashProviderFactory implements PasswordHashProviderFactory {
@Override
public PasswordHashProvider create(KeycloakSession session) {
return new DefaultPasswordHashProvider();
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getId() {
return "pbkdf2";
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,30 @@
package org.keycloak.hash;
import org.keycloak.models.*;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class PasswordHashManager {
public static UserCredentialValueModel encode(KeycloakSession session, RealmModel realm, String rawPassword) {
PasswordPolicy passwordPolicy = realm.getPasswordPolicy();
String algorithm = passwordPolicy.getHashAlgorithm();
int iterations = passwordPolicy.getHashIterations();
if (iterations < 1) {
iterations = 1;
}
PasswordHashProvider provider = session.getProvider(PasswordHashProvider.class, algorithm);
if (provider == null) {
throw new RuntimeException("Password hash provider for algorithm " + algorithm + " not found");
}
return provider.encode(rawPassword, iterations);
}
public static boolean verify(KeycloakSession session, RealmModel realm, String password, UserCredentialValueModel credential) {
String algorithm = credential.getAlgorithm() != null ? credential.getAlgorithm() : realm.getPasswordPolicy().getHashAlgorithm();
PasswordHashProvider provider = session.getProvider(PasswordHashProvider.class, algorithm);
return provider.verify(password, credential);
}
}

View file

@ -1,5 +1,6 @@
package org.keycloak.hash;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.provider.Provider;
/**
@ -7,10 +8,8 @@ import org.keycloak.provider.Provider;
*/
public interface PasswordHashProvider extends Provider {
String encode(String rawPassword, byte[] salt);
UserCredentialValueModel encode(String rawPassword, int iterations);
String encode(String rawPassword, byte[] salt, int iterations);
boolean verify(String rawPassword, String encodedPassword, byte[] salt);
boolean verify(String rawPassword, UserCredentialValueModel credential);
}

View file

@ -0,0 +1,91 @@
package org.keycloak.hash;
import org.keycloak.Config;
import org.keycloak.common.util.Base64;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
/**
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
*/
public class Pbkdf2PasswordHashProvider implements PasswordHashProviderFactory, PasswordHashProvider {
public static final String ID = "pbkdf2";
private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
private static final int DERIVED_KEY_SIZE = 512;
public UserCredentialValueModel encode(String rawPassword, int iterations) {
byte[] salt = getSalt();
String encodedPassword = encode(rawPassword, iterations, salt);
UserCredentialValueModel credentials = new UserCredentialValueModel();
credentials.setAlgorithm(ID);
credentials.setType(UserCredentialModel.PASSWORD);
credentials.setSalt(salt);
credentials.setHashIterations(iterations);
credentials.setValue(encodedPassword);
return credentials;
}
public boolean verify(String rawPassword, UserCredentialValueModel credential) {
return encode(rawPassword, credential.getHashIterations(), credential.getSalt()).equals(credential.getValue());
}
@Override
public PasswordHashProvider create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
public void close() {
}
@Override
public String getId() {
return ID;
}
private String encode(String rawPassword, int iterations, byte[] salt) {
KeySpec spec = new PBEKeySpec(rawPassword.toCharArray(), salt, iterations, DERIVED_KEY_SIZE);
try {
byte[] key = getSecretKeyFactory().generateSecret(spec).getEncoded();
return Base64.encodeBytes(key);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("Credential could not be encoded");
}
}
private byte[] getSalt() {
byte[] buffer = new byte[16];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(buffer);
return buffer;
}
private SecretKeyFactory getSecretKeyFactory() {
try {
return SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("PBKDF2 algorithm not found");
}
}
}

View file

@ -1,6 +1,7 @@
package org.keycloak.models;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import org.keycloak.hash.PasswordHashManager;
import org.keycloak.hash.PasswordHashProvider;
import java.io.Serializable;
import java.util.ArrayList;
@ -67,6 +68,8 @@ public class PasswordPolicy implements Serializable {
list.add(new SpecialChars(arg));
} else if (name.equals(NotUsername.NAME)) {
list.add(new NotUsername(arg));
} else if (name.equals(HashAlgorithm.NAME)) {
list.add(new HashAlgorithm(arg));
} else if (name.equals(HashIterations.NAME)) {
list.add(new HashIterations(arg));
} else if (name.equals(RegexPatterns.NAME)) {
@ -83,6 +86,18 @@ public class PasswordPolicy implements Serializable {
return list;
}
public String getHashAlgorithm() {
if (policies == null)
return Constants.DEFAULT_HASH_ALGORITHM;
for (Policy p : policies) {
if (p instanceof HashAlgorithm) {
return ((HashAlgorithm) p).algorithm;
}
}
return Constants.DEFAULT_HASH_ALGORITHM;
}
/**
*
* @return -1 if no hash iterations setting
@ -131,9 +146,9 @@ public class PasswordPolicy implements Serializable {
return -1;
}
public Error validate(UserModel user, String password) {
public Error validate(KeycloakSession session, UserModel user, String password) {
for (Policy p : policies) {
Error error = p.validate(user, password);
Error error = p.validate(session, user, password);
if (error != null) {
return error;
}
@ -141,9 +156,9 @@ public class PasswordPolicy implements Serializable {
return null;
}
public Error validate(String user, String password) {
public Error validate(KeycloakSession session, String user, String password) {
for (Policy p : policies) {
Error error = p.validate(user, password);
Error error = p.validate(session, user, password);
if (error != null) {
return error;
}
@ -152,8 +167,8 @@ public class PasswordPolicy implements Serializable {
}
private static interface Policy extends Serializable {
public Error validate(UserModel user, String password);
public Error validate(String user, String password);
public Error validate(KeycloakSession session, UserModel user, String password);
public Error validate(KeycloakSession session, String user, String password);
}
public static class Error {
@ -174,6 +189,25 @@ public class PasswordPolicy implements Serializable {
}
}
private static class HashAlgorithm implements Policy {
private static final String NAME = "hashAlgorithm";
private String algorithm;
public HashAlgorithm(String arg) {
algorithm = stringArg(NAME, Constants.DEFAULT_HASH_ALGORITHM, arg);
}
@Override
public Error validate(KeycloakSession session, String user, String password) {
return null;
}
@Override
public Error validate(KeycloakSession session, UserModel user, String password) {
return null;
}
}
private static class HashIterations implements Policy {
private static final String NAME = "hashIterations";
private int iterations;
@ -183,12 +217,12 @@ public class PasswordPolicy implements Serializable {
}
@Override
public Error validate(String user, String password) {
public Error validate(KeycloakSession session, String user, String password) {
return null;
}
@Override
public Error validate(UserModel user, String password) {
public Error validate(KeycloakSession session, UserModel user, String password) {
return null;
}
}
@ -200,13 +234,13 @@ public class PasswordPolicy implements Serializable {
}
@Override
public Error validate(String username, String password) {
public Error validate(KeycloakSession session, String username, String password) {
return username.equals(password) ? new Error(INVALID_PASSWORD_NOT_USERNAME) : null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
public Error validate(KeycloakSession session, UserModel user, String password) {
return validate(session, user.getUsername(), password);
}
}
@ -221,13 +255,13 @@ public class PasswordPolicy implements Serializable {
@Override
public Error validate(String username, String password) {
public Error validate(KeycloakSession session, String username, String password) {
return password.length() < min ? new Error(INVALID_PASSWORD_MIN_LENGTH_MESSAGE, min) : null;
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
public Error validate(KeycloakSession session, UserModel user, String password) {
return validate(session, user.getUsername(), password);
}
}
@ -242,7 +276,7 @@ public class PasswordPolicy implements Serializable {
@Override
public Error validate(String username, String password) {
public Error validate(KeycloakSession session, String username, String password) {
int count = 0;
for (char c : password.toCharArray()) {
if (Character.isDigit(c)) {
@ -253,8 +287,8 @@ public class PasswordPolicy implements Serializable {
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
public Error validate(KeycloakSession session, UserModel user, String password) {
return validate(session, user.getUsername(), password);
}
}
@ -268,7 +302,7 @@ public class PasswordPolicy implements Serializable {
}
@Override
public Error validate(String username, String password) {
public Error validate(KeycloakSession session, String username, String password) {
int count = 0;
for (char c : password.toCharArray()) {
if (Character.isLowerCase(c)) {
@ -279,8 +313,8 @@ public class PasswordPolicy implements Serializable {
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
public Error validate(KeycloakSession session, UserModel user, String password) {
return validate(session, user.getUsername(), password);
}
}
@ -293,7 +327,7 @@ public class PasswordPolicy implements Serializable {
}
@Override
public Error validate(String username, String password) {
public Error validate(KeycloakSession session, String username, String password) {
int count = 0;
for (char c : password.toCharArray()) {
if (Character.isUpperCase(c)) {
@ -304,8 +338,8 @@ public class PasswordPolicy implements Serializable {
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
public Error validate(KeycloakSession session, UserModel user, String password) {
return validate(session, user.getUsername(), password);
}
}
@ -319,7 +353,7 @@ public class PasswordPolicy implements Serializable {
}
@Override
public Error validate(String username, String password) {
public Error validate(KeycloakSession session, String username, String password) {
int count = 0;
for (char c : password.toCharArray()) {
if (!Character.isLetterOrDigit(c)) {
@ -330,8 +364,8 @@ public class PasswordPolicy implements Serializable {
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
public Error validate(KeycloakSession session, UserModel user, String password) {
return validate(session, user.getUsername(), password);
}
}
@ -345,7 +379,7 @@ public class PasswordPolicy implements Serializable {
}
@Override
public Error validate(String username, String password) {
public Error validate(KeycloakSession session, String username, String password) {
Pattern pattern = Pattern.compile(regexPattern);
Matcher matcher = pattern.matcher(password);
if (!matcher.matches()) {
@ -355,8 +389,8 @@ public class PasswordPolicy implements Serializable {
}
@Override
public Error validate(UserModel user, String password) {
return validate(user.getUsername(), password);
public Error validate(KeycloakSession session, UserModel user, String password) {
return validate(session, user.getUsername(), password);
}
}
@ -370,18 +404,19 @@ public class PasswordPolicy implements Serializable {
}
@Override
public Error validate(String user, String password) {
public Error validate(KeycloakSession session, String user, String password) {
return null;
}
@Override
public Error validate(UserModel user, String password) {
public Error validate(KeycloakSession session, UserModel user, String password) {
if (passwordHistoryPolicyValue != -1) {
UserCredentialValueModel cred = getCredentialValueModel(user, UserCredentialModel.PASSWORD);
if (cred != null) {
if(new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue(), cred.getHashIterations())) {
PasswordHashProvider hashProvider = session.getProvider(PasswordHashProvider.class, cred.getAlgorithm());
if(hashProvider.verify(password, cred)) {
return new Error(INVALID_PASSWORD_HISTORY, passwordHistoryPolicyValue);
}
}
@ -389,7 +424,8 @@ public class PasswordPolicy implements Serializable {
List<UserCredentialValueModel> passwordExpiredCredentials = getCredentialValueModels(user, passwordHistoryPolicyValue - 1,
UserCredentialModel.PASSWORD_HISTORY);
for (UserCredentialValueModel credential : passwordExpiredCredentials) {
if (new Pbkdf2PasswordEncoder(credential.getSalt()).verify(password, credential.getValue(), credential.getHashIterations())) {
PasswordHashProvider hashProvider = session.getProvider(PasswordHashProvider.class, cred.getAlgorithm());
if (hashProvider.verify(password, credential)) {
return new Error(INVALID_PASSWORD_HISTORY, passwordHistoryPolicyValue);
}
}
@ -444,12 +480,12 @@ public class PasswordPolicy implements Serializable {
}
@Override
public Error validate(String username, String password) {
public Error validate(KeycloakSession session, String username, String password) {
return null;
}
@Override
public Error validate(UserModel user, String password) {
public Error validate(KeycloakSession session, UserModel user, String password) {
return null;
}
}
@ -462,6 +498,14 @@ public class PasswordPolicy implements Serializable {
}
}
private static String stringArg(String policy, String defaultValue, String arg) {
if (arg == null) {
return defaultValue;
} else {
return arg;
}
}
@Override
public String toString() {
return policyString;

View file

@ -395,7 +395,7 @@ public class UserFederationManager implements UserProvider {
public void updateCredential(RealmModel realm, UserModel user, UserCredentialModel credential) {
if (credential.getType().equals(UserCredentialModel.PASSWORD)) {
if (realm.getPasswordPolicy() != null) {
PasswordPolicy.Error error = realm.getPasswordPolicy().validate(user, credential.getValue());
PasswordPolicy.Error error = realm.getPasswordPolicy().validate(session, user, credential.getValue());
if (error != null) throw new ModelException(error.getMessage(), error.getParameters());
}
}

View file

@ -1,7 +1,6 @@
package org.keycloak.models.utils;
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
import org.keycloak.hash.PasswordHashManager;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.RSAProvider;
@ -17,7 +16,6 @@ import org.keycloak.representations.PasswordToken;
import org.keycloak.common.util.Time;
import org.keycloak.hash.PasswordHashProvider;
import java.io.IOException;
import java.util.List;
/**
@ -61,30 +59,14 @@ public class CredentialValidation {
if(unhashedCredValue == null){
return false;
}
String algorithm;
if (credential.getAlgorithm() == null || credential.getAlgorithm().trim().equals("")) {
algorithm = Constants.DEFAULT_HASH_ALGORITHM;
} else {
algorithm = credential.getAlgorithm();
}
byte[] salt = getSalt();
PasswordHashProvider provider = session.getProvider(PasswordHashProvider.class, algorithm);
byte[] credSalt = credential.getSalt();
boolean validated = provider.verify(unhashedCredValue, credential.getValue(), credSalt);
boolean validated = PasswordHashManager.verify(session, realm, unhashedCredValue, credential);
if (validated) {
int iterations = hashIterations(realm);
if (iterations > -1 && iterations != credential.getHashIterations()) {
UserCredentialValueModel newCred = new UserCredentialValueModel();
newCred.setType(credential.getType());
newCred.setDevice(credential.getDevice());
newCred.setSalt(credential.getSalt());
newCred.setHashIterations(iterations);
newCred.setValue(provider.encode(unhashedCredValue, salt, iterations));
UserCredentialValueModel newCred = PasswordHashManager.encode(session, realm, unhashedCredValue);
user.updateCredentialDirectly(newCred);
}

View file

@ -1,106 +0,0 @@
package org.keycloak.models.utils;
import org.keycloak.common.util.Base64;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
/**
* <p>
* Encoder that uses PBKDF2 function to cryptographically derive passwords.
* </p>
* <p>Passwords are returned with a Base64 encoding.</p>
*
* @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>
*
*/
public class Pbkdf2PasswordEncoder {
public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
public static final String RNG_ALGORITHM = "SHA1PRNG";
private static final int DERIVED_KEY_SIZE = 512;
private static final int ITERATIONS = 1;
private final int iterations;
private byte[] salt;
public Pbkdf2PasswordEncoder(byte[] salt, int iterations) {
this.salt = salt;
this.iterations = iterations;
}
public Pbkdf2PasswordEncoder(byte[] salt) {
this(salt, ITERATIONS);
}
/**
* Encode the raw password provided
* @param rawPassword The password used as a master key to derive into a session key
* @return encoded password in Base64
*/
public String encode(String rawPassword, int iterations) {
String encodedPassword;
KeySpec spec = new PBEKeySpec(rawPassword.toCharArray(), salt, iterations, DERIVED_KEY_SIZE);
try {
byte[] key = getSecretKeyFactory().generateSecret(spec).getEncoded();
encodedPassword = Base64.encodeBytes(key);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("Credential could not be encoded");
}
return encodedPassword;
}
public String encode(String rawPassword) {
return encode(rawPassword, iterations);
}
/**
* Encode the password provided and compare with the hash stored into the database
* @param rawPassword The password provided
* @param encodedPassword Encoded hash stored into the database
* @return true if the password is valid, otherwise false for invalid credentials
*/
public boolean verify(String rawPassword, String encodedPassword) {
return encode(rawPassword).equals(encodedPassword);
}
/**
* Encode the password provided and compare with the hash stored into the database
* @param rawPassword The password provided
* @param encodedPassword Encoded hash stored into the database
* @return true if the password is valid, otherwise false for invalid credentials
*/
public boolean verify(String rawPassword, String encodedPassword, int iterations) {
return encode(rawPassword, iterations).equals(encodedPassword);
}
/**
* Generate a salt for each password
* @return cryptographically strong random number
*/
public static byte[] getSalt() {
byte[] buffer = new byte[16];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(buffer);
return buffer;
}
private static SecretKeyFactory getSecretKeyFactory() {
try {
return SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("PBKDF2 algorithm not found");
}
}
}

View file

@ -1 +1 @@
org.keycloak.hash.DefaultPasswordHashProviderFactory
org.keycloak.hash.Pbkdf2PasswordHashProvider

View file

@ -15,73 +15,73 @@ public class PasswordPolicyTest {
@Test
public void testLength() {
PasswordPolicy policy = new PasswordPolicy("length");
Assert.assertEquals("invalidPasswordMinLengthMessage", policy.validate("jdoe", "1234567").getMessage());
Assert.assertArrayEquals(new Object[]{8}, policy.validate("jdoe", "1234567").getParameters());
Assert.assertNull(policy.validate("jdoe", "12345678"));
Assert.assertEquals("invalidPasswordMinLengthMessage", policy.validate(null, "jdoe", "1234567").getMessage());
Assert.assertArrayEquals(new Object[]{8}, policy.validate(null, "jdoe", "1234567").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "12345678"));
policy = new PasswordPolicy("length(4)");
Assert.assertEquals("invalidPasswordMinLengthMessage", policy.validate("jdoe", "123").getMessage());
Assert.assertArrayEquals(new Object[]{4}, policy.validate("jdoe", "123").getParameters());
Assert.assertNull(policy.validate("jdoe", "1234"));
Assert.assertEquals("invalidPasswordMinLengthMessage", policy.validate(null, "jdoe", "123").getMessage());
Assert.assertArrayEquals(new Object[]{4}, policy.validate(null, "jdoe", "123").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "1234"));
}
@Test
public void testDigits() {
PasswordPolicy policy = new PasswordPolicy("digits");
Assert.assertEquals("invalidPasswordMinDigitsMessage", policy.validate("jdoe", "abcd").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policy.validate("jdoe", "abcd").getParameters());
Assert.assertNull(policy.validate("jdoe", "abcd1"));
Assert.assertEquals("invalidPasswordMinDigitsMessage", policy.validate(null, "jdoe", "abcd").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policy.validate(null, "jdoe", "abcd").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "abcd1"));
policy = new PasswordPolicy("digits(2)");
Assert.assertEquals("invalidPasswordMinDigitsMessage", policy.validate("jdoe", "abcd1").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policy.validate("jdoe", "abcd1").getParameters());
Assert.assertNull(policy.validate("jdoe", "abcd12"));
Assert.assertEquals("invalidPasswordMinDigitsMessage", policy.validate(null, "jdoe", "abcd1").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policy.validate(null, "jdoe", "abcd1").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "abcd12"));
}
@Test
public void testLowerCase() {
PasswordPolicy policy = new PasswordPolicy("lowerCase");
Assert.assertEquals("invalidPasswordMinLowerCaseCharsMessage", policy.validate("jdoe", "ABCD1234").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policy.validate("jdoe", "ABCD1234").getParameters());
Assert.assertNull(policy.validate("jdoe", "ABcD1234"));
Assert.assertEquals("invalidPasswordMinLowerCaseCharsMessage", policy.validate(null, "jdoe", "ABCD1234").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policy.validate(null, "jdoe", "ABCD1234").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "ABcD1234"));
policy = new PasswordPolicy("lowerCase(2)");
Assert.assertEquals("invalidPasswordMinLowerCaseCharsMessage", policy.validate("jdoe", "ABcD1234").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policy.validate("jdoe", "ABcD1234").getParameters());
Assert.assertNull(policy.validate("jdoe", "aBcD1234"));
Assert.assertEquals("invalidPasswordMinLowerCaseCharsMessage", policy.validate(null, "jdoe", "ABcD1234").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policy.validate(null, "jdoe", "ABcD1234").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "aBcD1234"));
}
@Test
public void testUpperCase() {
PasswordPolicy policy = new PasswordPolicy("upperCase");
Assert.assertEquals("invalidPasswordMinUpperCaseCharsMessage", policy.validate("jdoe", "abcd1234").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policy.validate("jdoe", "abcd1234").getParameters());
Assert.assertNull(policy.validate("jdoe", "abCd1234"));
Assert.assertEquals("invalidPasswordMinUpperCaseCharsMessage", policy.validate(null, "jdoe", "abcd1234").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policy.validate(null, "jdoe", "abcd1234").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "abCd1234"));
policy = new PasswordPolicy("upperCase(2)");
Assert.assertEquals("invalidPasswordMinUpperCaseCharsMessage", policy.validate("jdoe", "abCd1234").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policy.validate("jdoe", "abCd1234").getParameters());
Assert.assertNull(policy.validate("jdoe", "AbCd1234"));
Assert.assertEquals("invalidPasswordMinUpperCaseCharsMessage", policy.validate(null, "jdoe", "abCd1234").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policy.validate(null, "jdoe", "abCd1234").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "AbCd1234"));
}
@Test
public void testSpecialChars() {
PasswordPolicy policy = new PasswordPolicy("specialChars");
Assert.assertEquals("invalidPasswordMinSpecialCharsMessage", policy.validate("jdoe", "abcd1234").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policy.validate("jdoe", "abcd1234").getParameters());
Assert.assertNull(policy.validate("jdoe", "ab&d1234"));
Assert.assertEquals("invalidPasswordMinSpecialCharsMessage", policy.validate(null, "jdoe", "abcd1234").getMessage());
Assert.assertArrayEquals(new Object[]{1}, policy.validate(null, "jdoe", "abcd1234").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "ab&d1234"));
policy = new PasswordPolicy("specialChars(2)");
Assert.assertEquals("invalidPasswordMinSpecialCharsMessage", policy.validate("jdoe", "ab&d1234").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policy.validate("jdoe", "ab&d1234").getParameters());
Assert.assertNull(policy.validate("jdoe", "ab&d-234"));
Assert.assertEquals("invalidPasswordMinSpecialCharsMessage", policy.validate(null, "jdoe", "ab&d1234").getMessage());
Assert.assertArrayEquals(new Object[]{2}, policy.validate(null, "jdoe", "ab&d1234").getParameters());
Assert.assertNull(policy.validate(null, "jdoe", "ab&d-234"));
}
@Test
public void testNotUsername() {
PasswordPolicy policy = new PasswordPolicy("notUsername");
Assert.assertEquals("invalidPasswordNotUsernameMessage", policy.validate("jdoe", "jdoe").getMessage());
Assert.assertNull(policy.validate("jdoe", "ab&d1234"));
Assert.assertEquals("invalidPasswordNotUsernameMessage", policy.validate(null, "jdoe", "jdoe").getMessage());
Assert.assertNull(policy.validate(null, "jdoe", "ab&d1234"));
}
@Test
@ -119,33 +119,33 @@ public class PasswordPolicyTest {
//Fails to match one of the regex pattern
policy = new PasswordPolicy("regexPattern(jdoe) and regexPattern(j*d)");
Assert.assertEquals("invalidPasswordRegexPatternMessage", policy.validate("jdoe", "jdoe").getMessage());
Assert.assertEquals("invalidPasswordRegexPatternMessage", policy.validate(null, "jdoe", "jdoe").getMessage());
////Fails to match all of the regex patterns
policy = new PasswordPolicy("regexPattern(j*p) and regexPattern(j*d) and regexPattern(adoe)");
Assert.assertEquals("invalidPasswordRegexPatternMessage", policy.validate("jdoe", "jdoe").getMessage());
Assert.assertEquals("invalidPasswordRegexPatternMessage", policy.validate(null, "jdoe", "jdoe").getMessage());
policy = new PasswordPolicy("regexPattern([a-z][a-z][a-z][a-z][0-9])");
Assert.assertEquals("invalidPasswordRegexPatternMessage", policy.validate("jdoe", "jdoe").getMessage());
Assert.assertEquals("invalidPasswordRegexPatternMessage", policy.validate(null, "jdoe", "jdoe").getMessage());
policy = new PasswordPolicy("regexPattern(jdoe)");
Assert.assertNull(policy.validate("jdoe", "jdoe"));
Assert.assertNull(policy.validate(null, "jdoe", "jdoe"));
policy = new PasswordPolicy("regexPattern([a-z][a-z][a-z][a-z][0-9])");
Assert.assertNull(policy.validate("jdoe", "jdoe0"));
Assert.assertNull(policy.validate(null, "jdoe", "jdoe0"));
}
@Test
public void testComplex() {
PasswordPolicy policy = new PasswordPolicy("length(8) and digits(2) and lowerCase(2) and upperCase(2) and specialChars(2) and notUsername()");
Assert.assertNotNull(policy.validate("jdoe", "12aaBB&"));
Assert.assertNotNull(policy.validate("jdoe", "aaaaBB&-"));
Assert.assertNotNull(policy.validate("jdoe", "12AABB&-"));
Assert.assertNotNull(policy.validate("jdoe", "12aabb&-"));
Assert.assertNotNull(policy.validate("jdoe", "12aaBBcc"));
Assert.assertNotNull(policy.validate("12aaBB&-", "12aaBB&-"));
Assert.assertNotNull(policy.validate(null, "jdoe", "12aaBB&"));
Assert.assertNotNull(policy.validate(null, "jdoe", "aaaaBB&-"));
Assert.assertNotNull(policy.validate(null, "jdoe", "12AABB&-"));
Assert.assertNotNull(policy.validate(null, "jdoe", "12aabb&-"));
Assert.assertNotNull(policy.validate(null, "jdoe", "12aaBBcc"));
Assert.assertNotNull(policy.validate(null, "12aaBB&-", "12aaBB&-"));
Assert.assertNull(policy.validate("jdoe", "12aaBB&-"));
Assert.assertNull(policy.validate(null, "jdoe", "12aaBB&-"));
}
}

View file

@ -1,5 +1,6 @@
package org.keycloak.models.jpa;
import org.keycloak.hash.PasswordHashManager;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
@ -42,8 +43,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@ -390,19 +389,12 @@ public class UserAdapter implements UserModel {
}
private void setValue(CredentialEntity credentialEntity, UserCredentialModel cred) {
byte[] salt = getSalt();
int hashIterations = 1;
PasswordHashProvider provider = session.getProvider(PasswordHashProvider.class);
PasswordPolicy policy = realm.getPasswordPolicy();
if (policy != null) {
hashIterations = policy.getHashIterations();
if (hashIterations == -1)
hashIterations = 1;
}
UserCredentialValueModel encoded = PasswordHashManager.encode(session, realm, cred.getValue());
credentialEntity.setCreatedDate(Time.toMillis(Time.currentTime()));
credentialEntity.setValue(provider.encode(cred.getValue(), salt, hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
credentialEntity.setAlgorithm(encoded.getAlgorithm());
credentialEntity.setValue(encoded.getValue());
credentialEntity.setSalt(encoded.getSalt());
credentialEntity.setHashIterations(encoded.getHashIterations());
}
private CredentialEntity getCredentialEntity(UserEntity userEntity, String credType) {

View file

@ -1,11 +1,10 @@
package org.keycloak.models.mongo.keycloak.adapters;
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.hash.PasswordHashManager;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.OTPPolicy;
@ -329,19 +328,12 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
}
private void setValue(CredentialEntity credentialEntity, UserCredentialModel cred) {
byte[] salt = getSalt();
int hashIterations = 1;
PasswordHashProvider provider = session.getProvider(PasswordHashProvider.class);
PasswordPolicy policy = realm.getPasswordPolicy();
if (policy != null) {
hashIterations = policy.getHashIterations();
if (hashIterations == -1)
hashIterations = 1;
}
UserCredentialValueModel encoded = PasswordHashManager.encode(session, realm, cred.getValue());
credentialEntity.setCreatedDate(Time.toMillis(Time.currentTime()));
credentialEntity.setValue(provider.encode(cred.getValue(), salt, hashIterations));
credentialEntity.setSalt(salt);
credentialEntity.setHashIterations(hashIterations);
credentialEntity.setAlgorithm(encoded.getAlgorithm());
credentialEntity.setValue(encoded.getValue());
credentialEntity.setSalt(encoded.getSalt());
credentialEntity.setHashIterations(encoded.getHashIterations());
}
private CredentialEntity getCredentialEntity(MongoUserEntity userEntity, String credType) {

View file

@ -53,7 +53,7 @@ public class RegistrationPassword implements FormAction, FormActionFactory {
errors.add(new FormMessage(RegistrationPage.FIELD_PASSWORD_CONFIRM, Messages.INVALID_PASSWORD_CONFIRM));
}
if (formData.getFirst(RegistrationPage.FIELD_PASSWORD) != null) {
PasswordPolicy.Error err = context.getRealm().getPasswordPolicy().validate(context.getRealm().isRegistrationEmailAsUsername() ? formData.getFirst(RegistrationPage.FIELD_EMAIL) : formData.getFirst(RegistrationPage.FIELD_USERNAME), formData.getFirst(RegistrationPage.FIELD_PASSWORD));
PasswordPolicy.Error err = context.getRealm().getPasswordPolicy().validate(context.getSession(), context.getRealm().isRegistrationEmailAsUsername() ? formData.getFirst(RegistrationPage.FIELD_EMAIL) : formData.getFirst(RegistrationPage.FIELD_USERNAME), formData.getFirst(RegistrationPage.FIELD_PASSWORD));
if (err != null)
errors.add(new FormMessage(RegistrationPage.FIELD_PASSWORD, err.getMessage(), err.getParameters()));
}

View file

@ -1,6 +1,7 @@
package org.keycloak.services.validation;
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.FormMessage;
@ -25,7 +26,7 @@ public class Validation {
// Actually allow same emails like angular. See ValidationTest.testEmailValidation()
private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*");
public static List<FormMessage> validateRegistrationForm(RealmModel realm, MultivaluedMap<String, String> formData, List<String> requiredCredentialTypes, PasswordPolicy policy) {
public static List<FormMessage> validateRegistrationForm(KeycloakSession session, RealmModel realm, MultivaluedMap<String, String> formData, List<String> requiredCredentialTypes, PasswordPolicy policy) {
List<FormMessage> errors = new ArrayList<>();
if (!realm.isRegistrationEmailAsUsername() && isBlank(formData.getFirst(FIELD_USERNAME))) {
@ -55,7 +56,7 @@ public class Validation {
}
if (formData.getFirst(FIELD_PASSWORD) != null) {
PasswordPolicy.Error err = policy.validate(realm.isRegistrationEmailAsUsername()?formData.getFirst(FIELD_EMAIL):formData.getFirst(FIELD_USERNAME), formData.getFirst(FIELD_PASSWORD));
PasswordPolicy.Error err = policy.validate(session, realm.isRegistrationEmailAsUsername()?formData.getFirst(FIELD_EMAIL):formData.getFirst(FIELD_USERNAME), formData.getFirst(FIELD_PASSWORD));
if (err != null)
errors.add(new FormMessage(FIELD_PASSWORD, err.getMessage(), err.getParameters()));
}

View file

@ -1,16 +1,17 @@
package org.keycloak.testsuite.adduser;
import org.codehaus.jackson.type.TypeReference;
import org.junit.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RoleMappingResource;
import org.keycloak.admin.client.resource.RoleScopeResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.Base64;
import org.keycloak.hash.Pbkdf2PasswordHashProvider;
import org.keycloak.models.Constants;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import org.keycloak.representations.idm.*;
import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.util.JsonSerialization;
@ -19,7 +20,6 @@ import org.keycloak.wildfly.adduser.AddUser;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.*;
@ -59,7 +59,11 @@ public class AddUserTest {
UserRepresentation user = realms.get(0).getUsers().get(0);
assertEquals(new Integer(100000), user.getCredentials().get(0).getHashIterations());
assertNull(user.getCredentials().get(0).getValue());
assertEquals(new Pbkdf2PasswordEncoder(Base64Url.decode(user.getCredentials().get(0).getSalt()), 100000).encode("password"), user.getCredentials().get(0).getHashedSaltedValue());
CredentialRepresentation credentials = user.getCredentials().get(0);
assertEquals(Pbkdf2PasswordHashProvider.ID, credentials.getAlgorithm());
assertEquals(new Integer(100000), credentials.getHashIterations());
KeycloakServer server = new KeycloakServer();
try {

View file

@ -12,9 +12,8 @@ import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder;
import org.jboss.aesh.console.command.registry.CommandRegistry;
import org.keycloak.common.util.Base64;
import org.keycloak.hash.DefaultPasswordHashProvider;
import org.keycloak.models.Constants;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import org.keycloak.hash.Pbkdf2PasswordHashProvider;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
@ -139,14 +138,14 @@ public class AddUser {
user.setUsername(userName);
user.setCredentials(new LinkedList<CredentialRepresentation>());
byte[] salt = Pbkdf2PasswordEncoder.getSalt();
iterations = iterations > 0 ? iterations : DEFAULT_HASH_ITERATIONS;
UserCredentialValueModel credentialValueModel = new Pbkdf2PasswordHashProvider().encode(password, iterations > 0 ? iterations : DEFAULT_HASH_ITERATIONS);
CredentialRepresentation credentials = new CredentialRepresentation();
credentials.setType(CredentialRepresentation.PASSWORD);
credentials.setHashIterations(iterations);
credentials.setSalt(Base64.encodeBytes(salt));
credentials.setHashedSaltedValue(new DefaultPasswordHashProvider().encode(password, salt, iterations));
credentials.setType(credentialValueModel.getType());
credentials.setAlgorithm(credentialValueModel.getAlgorithm());
credentials.setHashIterations(credentialValueModel.getHashIterations());
credentials.setSalt(Base64.encodeBytes(credentialValueModel.getSalt()));
credentials.setHashedSaltedValue(credentialValueModel.getValue());
user.getCredentials().add(credentials);