configurable hash iterations
This commit is contained in:
parent
4cbc423d67
commit
15d7568792
14 changed files with 165 additions and 28 deletions
|
@ -908,6 +908,7 @@ module.factory('PasswordPolicy', function() {
|
|||
var p = {};
|
||||
|
||||
p.policyMessages = {
|
||||
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.",
|
||||
lowerCase: "Minimal number (integer type) of lowercase characters in password. Default value is 1.",
|
||||
|
@ -916,6 +917,7 @@ module.factory('PasswordPolicy', function() {
|
|||
}
|
||||
|
||||
p.allPolicies = [
|
||||
{ name: 'hashIterations', value: 1 },
|
||||
{ name: 'length', value: 8 },
|
||||
{ name: 'digits', value: 1 },
|
||||
{ name: 'lowerCase', value: 1 },
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
<input class="form-control disabled" type="text" value="{{p.name|capitalize}}" readonly>
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" ng-model="p.value" type="number" placeholder="No value assigned" min="1" max="50">
|
||||
<input class="form-control" ng-model="p.value" type="number" placeholder="No value assigned" min="1">
|
||||
</td>
|
||||
<td class="actions">
|
||||
<div class="action-div"><i class="pficon pficon-delete" ng-click="removePolicy($index)" tooltip-placement="right" tooltip="Remove Policy"></i></div>
|
||||
|
|
53
model/api/src/main/java/org/keycloak/models/CredentialValidation.java
Executable file
53
model/api/src/main/java/org/keycloak/models/CredentialValidation.java
Executable file
|
@ -0,0 +1,53 @@
|
|||
package org.keycloak.models;
|
||||
|
||||
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class CredentialValidation {
|
||||
|
||||
private static int hashIterations(RealmModel realm) {
|
||||
PasswordPolicy policy = realm.getPasswordPolicy();
|
||||
if (policy != null) {
|
||||
return policy.getHashIterations();
|
||||
}
|
||||
return -1;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Will update password if hash iteration policy has changed
|
||||
*
|
||||
* @param realm
|
||||
* @param user
|
||||
* @param password
|
||||
* @return
|
||||
*/
|
||||
public static boolean validatePassword(RealmModel realm, UserModel user, String password) {
|
||||
boolean validated = false;
|
||||
UserCredentialValueModel passwordCred = null;
|
||||
for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
|
||||
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
||||
validated = new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue(), cred.getHashIterations());
|
||||
passwordCred = cred;
|
||||
}
|
||||
}
|
||||
if (validated) {
|
||||
int iterations = hashIterations(realm);
|
||||
if (iterations > -1 && iterations != passwordCred.getHashIterations()) {
|
||||
UserCredentialValueModel newCred = new UserCredentialValueModel();
|
||||
newCred.setType(passwordCred.getType());
|
||||
newCred.setDevice(passwordCred.getDevice());
|
||||
newCred.setSalt(passwordCred.getSalt());
|
||||
newCred.setHashIterations(iterations);
|
||||
newCred.setValue(new Pbkdf2PasswordEncoder(newCred.getSalt()).encode(password, iterations));
|
||||
user.updateCredentialDirectly(newCred);
|
||||
}
|
||||
|
||||
}
|
||||
return validated;
|
||||
|
||||
}
|
||||
}
|
31
model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
Normal file → Executable file
31
model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
Normal file → Executable file
|
@ -52,11 +52,28 @@ public class PasswordPolicy {
|
|||
list.add(new UpperCase(args));
|
||||
} else if (name.equals(SpecialChars.NAME)) {
|
||||
list.add(new SpecialChars(args));
|
||||
} else if (name.equals(HashIterations.NAME)) {
|
||||
list.add(new HashIterations(args));
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return -1 if no hash iterations setting
|
||||
*/
|
||||
public int getHashIterations() {
|
||||
if (policies == null) return -1;
|
||||
for (Policy p : policies) {
|
||||
if (p instanceof HashIterations) {
|
||||
return ((HashIterations)p).iterations;
|
||||
}
|
||||
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public String validate(String password) {
|
||||
for (Policy p : policies) {
|
||||
String error = p.validate(password);
|
||||
|
@ -71,6 +88,20 @@ public class PasswordPolicy {
|
|||
public String validate(String password);
|
||||
}
|
||||
|
||||
private static class HashIterations implements Policy {
|
||||
private static final String NAME = "hashIterations";
|
||||
private int iterations;
|
||||
|
||||
public HashIterations(String[] args) {
|
||||
iterations = intArg(NAME, 1, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String validate(String password) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Length implements Policy {
|
||||
private static final String NAME = "length";
|
||||
private int min;
|
||||
|
|
9
model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java
Normal file → Executable file
9
model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java
Normal file → Executable file
|
@ -11,6 +11,7 @@ public class UserCredentialValueModel {
|
|||
private String value;
|
||||
private String device;
|
||||
private byte[] salt;
|
||||
private int hashIterations;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
|
@ -43,4 +44,12 @@ public class UserCredentialValueModel {
|
|||
public void setSalt(byte[] salt) {
|
||||
this.salt = salt;
|
||||
}
|
||||
|
||||
public int getHashIterations() {
|
||||
return hashIterations;
|
||||
}
|
||||
|
||||
public void setHashIterations(int iterations) {
|
||||
this.hashIterations = iterations;
|
||||
}
|
||||
}
|
||||
|
|
9
model/api/src/main/java/org/keycloak/models/entities/CredentialEntity.java
Normal file → Executable file
9
model/api/src/main/java/org/keycloak/models/entities/CredentialEntity.java
Normal file → Executable file
|
@ -9,6 +9,7 @@ public class CredentialEntity {
|
|||
private String value;
|
||||
private String device;
|
||||
private byte[] salt;
|
||||
private int hashIterations;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
|
@ -41,4 +42,12 @@ public class CredentialEntity {
|
|||
public void setSalt(byte[] salt) {
|
||||
this.salt = salt;
|
||||
}
|
||||
|
||||
public int getHashIterations() {
|
||||
return hashIterations;
|
||||
}
|
||||
|
||||
public void setHashIterations(int hashIterations) {
|
||||
this.hashIterations = hashIterations;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ public class Pbkdf2PasswordEncoder {
|
|||
* @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) {
|
||||
public String encode(String rawPassword, int iterations) {
|
||||
|
||||
String encodedPassword;
|
||||
|
||||
|
@ -59,6 +59,10 @@ public class Pbkdf2PasswordEncoder {
|
|||
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
|
||||
|
@ -69,6 +73,16 @@ public class Pbkdf2PasswordEncoder {
|
|||
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
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.keycloak.models.cache;
|
|||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.AuthenticationProviderModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.CredentialValidation;
|
||||
import org.keycloak.models.OAuthClientModel;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -380,13 +381,7 @@ public class RealmAdapter implements RealmModel {
|
|||
|
||||
@Override
|
||||
public boolean validatePassword(UserModel user, String password) {
|
||||
for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
|
||||
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
||||
return new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue());
|
||||
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return CredentialValidation.validatePassword(this, user, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.keycloak.models.jpa;
|
|||
import org.keycloak.models.AuthenticationLinkModel;
|
||||
import org.keycloak.models.AuthenticationProviderModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.CredentialValidation;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.RoleContainerModel;
|
||||
|
@ -962,19 +963,9 @@ public class RealmAdapter implements RealmModel {
|
|||
return role.getContainer().removeRole(role);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean validatePassword(UserModel user, String password) {
|
||||
for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
|
||||
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
||||
return new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue());
|
||||
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return CredentialValidation.validatePassword(this, user, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.keycloak.models.jpa;
|
|||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.AuthenticationLinkModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleContainerModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
|
@ -197,8 +198,15 @@ public class UserAdapter implements UserModel {
|
|||
}
|
||||
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
||||
byte[] salt = getSalt();
|
||||
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue()));
|
||||
int hashIterations = 1;
|
||||
PasswordPolicy policy = realm.getPasswordPolicy();
|
||||
if (policy != null) {
|
||||
hashIterations = policy.getHashIterations();
|
||||
if (hashIterations == -1) hashIterations = 1;
|
||||
}
|
||||
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
|
||||
credentialEntity.setSalt(salt);
|
||||
credentialEntity.setHashIterations(hashIterations);
|
||||
} else {
|
||||
credentialEntity.setValue(cred.getValue());
|
||||
}
|
||||
|
@ -228,6 +236,7 @@ public class UserAdapter implements UserModel {
|
|||
credModel.setDevice(credEntity.getDevice());
|
||||
credModel.setValue(credEntity.getValue());
|
||||
credModel.setSalt(credEntity.getSalt());
|
||||
credModel.setHashIterations(credEntity.getHashIterations());
|
||||
|
||||
result.add(credModel);
|
||||
}
|
||||
|
@ -251,6 +260,7 @@ public class UserAdapter implements UserModel {
|
|||
credentialEntity.setValue(credModel.getValue());
|
||||
credentialEntity.setSalt(credModel.getSalt());
|
||||
credentialEntity.setDevice(credModel.getDevice());
|
||||
credentialEntity.setHashIterations(credModel.getHashIterations());
|
||||
|
||||
em.flush();
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ public class CredentialEntity {
|
|||
protected String value;
|
||||
protected String device;
|
||||
protected byte[] salt;
|
||||
protected int hashIterations;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
protected UserEntity user;
|
||||
|
@ -80,5 +81,11 @@ public class CredentialEntity {
|
|||
this.salt = salt;
|
||||
}
|
||||
|
||||
public int getHashIterations() {
|
||||
return hashIterations;
|
||||
}
|
||||
|
||||
public void setHashIterations(int hashIterations) {
|
||||
this.hashIterations = hashIterations;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import org.keycloak.models.ApplicationModel;
|
|||
import org.keycloak.models.AuthenticationLinkModel;
|
||||
import org.keycloak.models.AuthenticationProviderModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.CredentialValidation;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OAuthClientModel;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
|
@ -808,12 +809,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
|
||||
@Override
|
||||
public boolean validatePassword(UserModel user, String password) {
|
||||
for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
|
||||
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
||||
return new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return CredentialValidation.validatePassword(this, user, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -4,6 +4,7 @@ import org.keycloak.models.ApplicationModel;
|
|||
import org.keycloak.models.AuthenticationLinkModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
|
@ -199,8 +200,15 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
|
|||
}
|
||||
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
||||
byte[] salt = Pbkdf2PasswordEncoder.getSalt();
|
||||
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue()));
|
||||
int hashIterations = 1;
|
||||
PasswordPolicy policy = realm.getPasswordPolicy();
|
||||
if (policy != null) {
|
||||
hashIterations = policy.getHashIterations();
|
||||
if (hashIterations == -1) hashIterations = 1;
|
||||
}
|
||||
credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue(), hashIterations));
|
||||
credentialEntity.setSalt(salt);
|
||||
credentialEntity.setHashIterations(hashIterations);
|
||||
} else {
|
||||
credentialEntity.setValue(cred.getValue());
|
||||
}
|
||||
|
@ -229,6 +237,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
|
|||
credModel.setDevice(credEntity.getDevice());
|
||||
credModel.setValue(credEntity.getValue());
|
||||
credModel.setSalt(credEntity.getSalt());
|
||||
credModel.setHashIterations(credEntity.getHashIterations());
|
||||
|
||||
result.add(credModel);
|
||||
}
|
||||
|
@ -249,6 +258,8 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
|
|||
credentialEntity.setValue(credModel.getValue());
|
||||
credentialEntity.setSalt(credModel.getSalt());
|
||||
credentialEntity.setDevice(credModel.getDevice());
|
||||
credentialEntity.setHashIterations(credModel.getHashIterations());
|
||||
|
||||
|
||||
getMongoStore().updateEntity(user, invocationContext);
|
||||
}
|
||||
|
|
|
@ -8,11 +8,13 @@ import org.keycloak.models.ApplicationModel;
|
|||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.OAuthClientModel;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RequiredCredentialModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.SocialLinkModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserCredentialValueModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
|
@ -135,6 +137,13 @@ public class AdapterTest extends AbstractModelTest {
|
|||
cred.setValue("geheim");
|
||||
user.updateCredential(cred);
|
||||
Assert.assertTrue(realmModel.validatePassword(user, "geheim"));
|
||||
List<UserCredentialValueModel> creds = user.getCredentialsDirectly();
|
||||
Assert.assertEquals(creds.get(0).getHashIterations(), 1);
|
||||
realmModel.setPasswordPolicy( new PasswordPolicy("hashIterations(200)"));
|
||||
Assert.assertTrue(realmModel.validatePassword(user, "geheim"));
|
||||
creds = user.getCredentialsDirectly();
|
||||
Assert.assertEquals(creds.get(0).getHashIterations(), 200);
|
||||
realmModel.setPasswordPolicy( new PasswordPolicy("hashIterations(1)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Reference in a new issue