Skip to content

Commit

Permalink
KEYCLOAK-4888
Browse files Browse the repository at this point in the history
Change default hashing provider for realm
  • Loading branch information
stianst committed May 30, 2017
1 parent 684689d commit 8c53c5a
Show file tree
Hide file tree
Showing 16 changed files with 489 additions and 251 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
private final String providerId;

private final String pbkdf2Algorithm;
private int defaultIterations;

public static final int DERIVED_KEY_SIZE = 512;

public Pbkdf2PasswordHashProvider(String providerId, String pbkdf2Algorithm) {
public Pbkdf2PasswordHashProvider(String providerId, String pbkdf2Algorithm, int defaultIterations) {
this.providerId = providerId;
this.pbkdf2Algorithm = pbkdf2Algorithm;
this.defaultIterations = defaultIterations;
}

@Override
Expand All @@ -52,6 +54,10 @@ public boolean policyCheck(PasswordPolicy policy, CredentialModel credential) {

@Override
public void encode(String rawPassword, int iterations, CredentialModel credential) {
if (iterations == -1) {
iterations = defaultIterations;
}

byte[] salt = getSalt();
String encodedPassword = encode(rawPassword, iterations, salt);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ public class Pbkdf2PasswordHashProviderFactory implements PasswordHashProviderFa

public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";

public static final int DEFAULT_ITERATIONS = 20000;

@Override
public PasswordHashProvider create(KeycloakSession session) {
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM);
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM, 20000);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ public class Pbkdf2Sha256PasswordHashProviderFactory implements PasswordHashProv

public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA256";

public static final int DEFAULT_ITERATIONS = 27500;

@Override
public PasswordHashProvider create(KeycloakSession session) {
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM);
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM, DEFAULT_ITERATIONS);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ public class Pbkdf2Sha512PasswordHashProviderFactory implements PasswordHashProv

public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA512";

public static final int DEFAULT_ITERATIONS = 30000;

@Override
public PasswordHashProvider create(KeycloakSession session) {
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM);
return new Pbkdf2PasswordHashProvider(ID, PBKDF2_ALGORITHM, DEFAULT_ITERATIONS);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.keycloak.migration.migrators.MigrateTo2_5_0;
import org.keycloak.migration.migrators.MigrateTo3_0_0;
import org.keycloak.migration.migrators.MigrateTo3_1_0;
import org.keycloak.migration.migrators.MigrateTo3_2_0;
import org.keycloak.migration.migrators.Migration;
import org.keycloak.models.KeycloakSession;

Expand All @@ -60,7 +61,8 @@ public class MigrationModelManager {
new MigrateTo2_3_0(),
new MigrateTo2_5_0(),
new MigrateTo3_0_0(),
new MigrateTo3_1_0()
new MigrateTo3_1_0(),
new MigrateTo3_2_0()
};

public static void migrate(KeycloakSession session) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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.migration.migrators;


import org.keycloak.migration.ModelVersion;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;

public class MigrateTo3_2_0 implements Migration {

public static final ModelVersion VERSION = new ModelVersion("3.1.0");

@Override
public void migrate(KeycloakSession session) {
for (RealmModel realm : session.realms().getRealms()) {
PasswordPolicy.Builder builder = realm.getPasswordPolicy().toBuilder();
if (!builder.contains(PasswordPolicy.HASH_ALGORITHM_ID) && "20000".equals(builder.get(PasswordPolicy.HASH_ITERATIONS_ID))) {
realm.setPasswordPolicy(builder.remove(PasswordPolicy.HASH_ITERATIONS_ID).build(session));
}
}
}

@Override
public ModelVersion getVersion() {
return VERSION;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public PolicyError validate(String user, String password) {

@Override
public Object parseConfig(String value) {
return value != null ? Integer.parseInt(value) : PasswordPolicy.HASH_ITERATIONS_DEFAULT;
return parseInteger(value, -1);
}

@Override
Expand Down
164 changes: 123 additions & 41 deletions server-spi/src/main/java/org/keycloak/models/PasswordPolicy.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import java.io.Serializable;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

Expand All @@ -32,62 +33,33 @@ public class PasswordPolicy implements Serializable {

public static final String HASH_ALGORITHM_ID = "hashAlgorithm";

public static final String HASH_ALGORITHM_DEFAULT = "pbkdf2";
public static final String HASH_ALGORITHM_DEFAULT = "pbkdf2-sha256";

public static final String HASH_ITERATIONS_ID = "hashIterations";

public static final int HASH_ITERATIONS_DEFAULT = 20000;
public static final int HASH_ITERATIONS_DEFAULT = 27500;

public static final String PASSWORD_HISTORY_ID = "passwordHistory";

public static final String FORCE_EXPIRED_ID = "forceExpiredPasswordChange";

private String policyString;
private Map<String, Object> policyConfig;
private Builder builder;

public static PasswordPolicy empty() {
return new PasswordPolicy(null, new HashMap<>());
}

public static PasswordPolicy parse(KeycloakSession session, String policyString) {
Map<String, Object> policyConfig = new HashMap<>();

if (policyString != null && !policyString.trim().isEmpty()) {
for (String policy : policyString.split(" and ")) {
policy = policy.trim();

String key;
String config = null;

int i = policy.indexOf('(');
if (i == -1) {
key = policy.trim();
} else {
key = policy.substring(0, i).trim();
config = policy.substring(i + 1, policy.length() - 1);
}

PasswordPolicyProvider provider = session.getProvider(PasswordPolicyProvider.class, key);
if (provider == null) {
throw new PasswordPolicyConfigException("Password policy not found");
}

Object o;
try {
o = provider.parseConfig(config);
} catch (PasswordPolicyConfigException e) {
throw new ModelException("Invalid config for " + key + ": " + e.getMessage());
}

policyConfig.put(key, o);
}
}
public static Builder build() {
return new Builder();
}

return new PasswordPolicy(policyString, policyConfig);
public static PasswordPolicy parse(KeycloakSession session, String policyString) {
return new Builder(policyString).build(session);
}

private PasswordPolicy(String policyString, Map<String, Object> policyConfig) {
this.policyString = policyString;
private PasswordPolicy(Builder builder, Map<String, Object> policyConfig) {
this.builder = builder;
this.policyConfig = policyConfig;
}

Expand All @@ -111,7 +83,7 @@ public int getHashIterations() {
if (policyConfig.containsKey(HASH_ITERATIONS_ID)) {
return getPolicyConfig(HASH_ITERATIONS_ID);
} else {
return HASH_ITERATIONS_DEFAULT;
return -1;
}
}

Expand All @@ -133,7 +105,117 @@ public int getDaysToExpirePassword() {

@Override
public String toString() {
return policyString;
return builder.asString();
}

public Builder toBuilder() {
return builder.clone();
}

public static class Builder {

private LinkedHashMap<String, String> map;

private Builder() {
this.map = new LinkedHashMap<>();
}

private Builder(LinkedHashMap<String, String> map) {
this.map = map;
}

private Builder(String policyString) {
map = new LinkedHashMap<>();

if (policyString != null && !policyString.trim().isEmpty()) {
for (String policy : policyString.split(" and ")) {
policy = policy.trim();

String key;
String config = null;

int i = policy.indexOf('(');
if (i == -1) {
key = policy.trim();
} else {
key = policy.substring(0, i).trim();
config = policy.substring(i + 1, policy.length() - 1);
}

map.put(key, config);
}
}
}

public boolean contains(String key) {
return map.containsKey(key);
}

public String get(String key) {
return map.get(key);
}

public Builder put(String key, String value) {
map.put(key, value);
return this;
}

public Builder remove(String key) {
map.remove(key);
return this;
}

public PasswordPolicy build(KeycloakSession session) {
Map<String, Object> config = new HashMap<>();
for (Map.Entry<String, String> e : map.entrySet()) {

PasswordPolicyProvider provider = session.getProvider(PasswordPolicyProvider.class, e.getKey());
if (provider == null) {
throw new PasswordPolicyConfigException("Password policy not found");
}

Object o;
try {
o = provider.parseConfig(e.getValue());
} catch (PasswordPolicyConfigException ex) {
throw new ModelException("Invalid config for " + e.getKey() + ": " + ex.getMessage());
}

config.put(e.getKey(), o);
}
return new PasswordPolicy(this, config);
}

public String asString() {
if (map.isEmpty()) {
return null;
}

StringBuilder sb = new StringBuilder();
boolean first = true;
for (Map.Entry<String, String> e : map.entrySet()) {
if (first) {
first = false;
} else {
sb.append(" and ");
}

sb.append(e.getKey());

String c = e.getValue();
if (c != null && !c.trim().isEmpty()) {
sb.append("(");
sb.append(c);
sb.append(")");
}
}
return sb.toString();
}

public Builder clone() {
return new Builder((LinkedHashMap<String, String>) map.clone());
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,6 @@ protected void setupRealmDefaults(RealmModel realm) {
realm.setLoginWithEmailAllowed(true);

realm.setEventsListeners(Collections.singleton("jboss-logging"));

realm.setPasswordPolicy(PasswordPolicy.parse(session, "hashIterations(20000)"));
}

public boolean removeRealm(RealmModel realm) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class LogChecker {

private static final Logger log = Logger.getLogger(LogChecker.class);

private static final String[] IGNORED = new String[] { ".*Jetty ALPN support not found.*" };
private static final String[] IGNORED = new String[] { ".*Jetty ALPN support not found.*", ".*org.keycloak.events.*" };

public static void checkServerLog(File logFile) throws IOException {
log.info(String.format("Checking server log: '%s'", logFile.getAbsolutePath()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public void createRealmCheckDefaultPasswordPolicy() {

adminClient.realms().create(rep);

assertEquals("hashIterations(20000)", adminClient.realm("new-realm").toRepresentation().getPasswordPolicy());
assertEquals(null, adminClient.realm("new-realm").toRepresentation().getPasswordPolicy());

adminClient.realms().realm("new-realm").remove();

Expand Down
Loading

0 comments on commit 8c53c5a

Please sign in to comment.