Skip to content

Commit

Permalink
Add score based password verification
Browse files Browse the repository at this point in the history
Signed-off-by: Andrey Pleskach <ples@aiven.io>
  • Loading branch information
willyborankin committed Mar 15, 2023
1 parent ca4d752 commit 57023ae
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 31 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ dependencies {
implementation ('org.opensaml:opensaml-saml-impl:3.4.5') {
exclude(group: 'org.apache.velocity', module: 'velocity')
}
implementation "com.nulab-inc:zxcvbn:1.7.0"
testImplementation 'org.opensaml:opensaml-messaging-impl:3.4.5'
implementation 'org.opensaml:opensaml-messaging-api:3.4.5'
runtimeOnly 'org.opensaml:opensaml-profile-api:3.4.5'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
Expand Down Expand Up @@ -73,6 +74,7 @@
import org.opensearch.cluster.metadata.IndexNameExpressionResolver;
import org.opensearch.cluster.node.DiscoveryNodes;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.Strings;
import org.opensearch.common.component.Lifecycle.State;
import org.opensearch.common.component.LifecycleComponent;
import org.opensearch.common.component.LifecycleListener;
Expand Down Expand Up @@ -162,6 +164,7 @@
import org.opensearch.security.support.ConfigConstants;
import org.opensearch.security.support.HeaderHelper;
import org.opensearch.security.support.ModuleInfo;
import org.opensearch.security.support.PasswordValidationScoreStrength;
import org.opensearch.security.support.ReflectionHelper;
import org.opensearch.security.support.SecuritySettings;
import org.opensearch.security.support.SecurityUtils;
Expand Down Expand Up @@ -1031,7 +1034,23 @@ public List<Setting<?>> getSettings() {
settings.add(Setting.simpleString(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, Property.NodeScope, Property.Filtered));
settings.add(Setting.simpleString(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, Property.NodeScope, Property.Filtered));


settings.add(Setting.boolSetting(ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION, false, Property.NodeScope));
settings.add(Setting.intSetting(ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_MIN_PASSWORD_LENGTH, 8, Property.NodeScope));
settings.add(Setting.simpleString(
ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_SCORE_STRENGTH,
PasswordValidationScoreStrength.STRONG.name().toLowerCase(Locale.ROOT),
value -> {
if (!Strings.isNullOrEmpty(value)) {
PasswordValidationScoreStrength.valueOf(value.toUpperCase(Locale.ROOT));
}
}, Property.NodeScope));
settings.add(
Setting.boolSetting(
ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_CHECK_USERNAME_SIMILARITY,
true, Property.NodeScope
)
);

// Compliance
settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_WATCHED_INDICES, Collections.emptyList(), Function.identity(), Property.NodeScope)); //not filtered here
settings.add(Setting.listSetting(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_WATCHED_FIELDS, Collections.emptyList(), Function.identity(), Property.NodeScope)); //not filtered here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.ImmutableList;
import com.nulabinc.zxcvbn.Zxcvbn;
import org.bouncycastle.crypto.generators.OpenBSDBCrypt;

import org.opensearch.action.index.IndexResponse;
Expand Down Expand Up @@ -59,6 +60,7 @@
*/
public class AccountApiAction extends AbstractApiAction {
private static final String RESOURCE_NAME = "account";

private static final List<Route> routes = addRoutesPrefix(ImmutableList.of(
new Route(Method.GET, "/account"),
new Route(Method.PUT, "/account")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.collect.ImmutableList;

import com.nulabinc.zxcvbn.Zxcvbn;
import org.opensearch.action.index.IndexResponse;
import org.opensearch.client.Client;
import org.opensearch.cluster.service.ClusterService;
Expand Down Expand Up @@ -72,7 +73,8 @@ public class InternalUsersApiAction extends PatchableResourceApiAction {
@Inject
public InternalUsersApiAction(final Settings settings, final Path configPath, final RestController controller,
final Client client, final AdminDNs adminDNs, final ConfigurationRepository cl,
final ClusterService cs, final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator,
final ClusterService cs, final PrincipalExtractor principalExtractor,
final PrivilegesEvaluator evaluator,
ThreadPool threadPool, AuditLog auditLog) {
super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool,
auditLog);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@

package org.opensearch.security.dlic.rest.api;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.opensearch.client.Client;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.settings.Settings;
Expand All @@ -30,6 +24,12 @@
import org.opensearch.security.ssl.transport.PrincipalExtractor;
import org.opensearch.threadpool.ThreadPool;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

public class SecurityRestApiActions {

public static Collection<RestHandler> getHandler(final Settings settings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

package org.opensearch.security.dlic.rest.validation;

import com.nulabinc.zxcvbn.Zxcvbn;
import org.opensearch.common.bytes.BytesReference;
import org.opensearch.common.settings.Settings;
import org.opensearch.rest.RestRequest;
Expand All @@ -19,7 +20,10 @@
* Validator for Account Api Action.
*/
public class AccountValidator extends CredentialsValidator {
public AccountValidator(RestRequest request, BytesReference ref, Settings opensearchSettings, Object... param) {
public AccountValidator(final RestRequest request,
final BytesReference ref,
final Settings opensearchSettings,
final Object... param) {
super(request, ref, opensearchSettings, param);
allowedKeys.put("current_password", DataType.STRING);
mandatoryKeys.add("current_password");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@

package org.opensearch.security.dlic.rest.validation;

import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;

import com.google.common.collect.ImmutableList;
import com.nulabinc.zxcvbn.Strength;
import com.nulabinc.zxcvbn.Zxcvbn;
import org.opensearch.common.Strings;
import org.opensearch.common.bytes.BytesReference;
import org.opensearch.common.compress.NotXContentException;
Expand All @@ -23,13 +27,22 @@
import org.opensearch.rest.RestRequest;
import org.opensearch.security.ssl.util.Utils;
import org.opensearch.security.support.ConfigConstants;
import org.opensearch.security.support.PasswordValidationScoreStrength;

import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION;
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_CHECK_USERNAME_SIMILARITY;
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_MIN_PASSWORD_LENGTH;
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_SCORE_STRENGTH;
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX;

/**
* Validator for validating password and hash present in the payload
*/
public class CredentialsValidator extends AbstractConfigurationValidator {

public CredentialsValidator(final RestRequest request, BytesReference ref, final Settings opensearchSettings,
public CredentialsValidator(final RestRequest request,
final BytesReference ref,
final Settings opensearchSettings,
Object... param) {
super(request, ref, opensearchSettings, param);
this.payloadMandatory = true;
Expand All @@ -47,8 +60,14 @@ public boolean validate() {
return false;
}

final String regex = this.opensearchSettings.get(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, null);

final String regex = this.opensearchSettings.get(SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, null);
final boolean useScoreBasedValidation = this.opensearchSettings.getAsBoolean(SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION, false);
final PasswordValidationScoreStrength scoreStrength = PasswordValidationScoreStrength.valueOf(
this.opensearchSettings.get(
SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_SCORE_STRENGTH,
PasswordValidationScoreStrength.STRONG.name()
).toUpperCase(Locale.ROOT)
);
if ((request.method() == RestRequest.Method.PUT || request.method() == RestRequest.Method.PATCH)
&& this.content != null
&& this.content.length() > 1) {
Expand All @@ -61,35 +80,28 @@ public boolean validate() {
this.errorType = ErrorType.INVALID_PASSWORD;
return false;
}

if (!Strings.isNullOrEmpty(regex)) {
// Password can be null for an existing user. Regex will validate password if present
if (!Pattern.compile("^"+regex+"$").matcher(password).matches()) {
if(log.isDebugEnabled()) {
log.debug("Regex does not match password");
}
this.errorType = ErrorType.INVALID_PASSWORD;
return false;
}

final String username = Utils.coalesce(request.param("name"), hasParams() ? (String) param[0] : null);
final boolean isDebugEnabled = log.isDebugEnabled();

final String username = Utils.coalesce(request.param("name"), hasParams() ? (String) param[0] : null);
if (!Strings.isNullOrEmpty(regex) || useScoreBasedValidation) {
if (username == null || username.isEmpty()) {
if (isDebugEnabled) {
if (log.isDebugEnabled()) {
log.debug("Unable to validate username because no user is given");
}
return false;
}

if (username.toLowerCase().equals(password.toLowerCase())) {
if (isDebugEnabled) {
if (log.isDebugEnabled()) {
log.debug("Username must not match password");
}
this.errorType = ErrorType.INVALID_PASSWORD;
return false;
}
}
if (!Strings.isNullOrEmpty(regex)) {
return regexBasedPasswordValidation(regex, password);
} else if (useScoreBasedValidation) {
return scoreBasedPasswordValidation(scoreStrength, username, password);
}
}
} catch (NotXContentException e) {
//this.content is not valid json/yaml
Expand All @@ -99,4 +111,53 @@ public boolean validate() {
}
return true;
}

private boolean regexBasedPasswordValidation(final String regex, final String password) {
// Password can be null for an existing user. Regex will validate password if present
if (!Pattern.compile("^" + regex + "$").matcher(password).matches()) {
if (log.isDebugEnabled()) {
log.debug("Regex does not match password");
}
this.errorType = ErrorType.INVALID_PASSWORD;
return false;
}
return true;
}

private boolean scoreBasedPasswordValidation(final PasswordValidationScoreStrength scoreStrength,
final String username, final String password) {
final int minPasswordLength = opensearchSettings.getAsInt(SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_MIN_PASSWORD_LENGTH, 8);
if (password.length() < minPasswordLength) {
this.errorType = ErrorType.INVALID_PASSWORD;
if (log.isDebugEnabled()) {
log.debug("Password is too small");
}
return false;
}
final Strength strength = new Zxcvbn().measure(password, ImmutableList.of(username));
if (strength.getScore() < scoreStrength.score()) {
this.errorType = ErrorType.INVALID_PASSWORD;
if (log.isDebugEnabled()) {
log.debug("Password is too weak");
}
return false;
}
final boolean checkUsernameSimilarity = this.opensearchSettings.getAsBoolean(
SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_CHECK_USERNAME_SIMILARITY, true);
if (checkUsernameSimilarity) {
final boolean similar = strength.getSequence()
.stream()
.filter(m -> m.matchedWord != null)
.anyMatch(m -> m.matchedWord.equals("similar"));
if (similar) {
this.errorType = ErrorType.INVALID_PASSWORD;
if (log.isDebugEnabled()) {
log.debug("Password is similar to username");
}
return false;
}
}
return true;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

package org.opensearch.security.dlic.rest.validation;

import com.nulabinc.zxcvbn.Zxcvbn;
import org.opensearch.common.bytes.BytesReference;
import org.opensearch.common.settings.Settings;
import org.opensearch.rest.RestRequest;
Expand All @@ -20,8 +21,11 @@
*/
public class InternalUsersValidator extends CredentialsValidator {

public InternalUsersValidator(final RestRequest request, boolean isSuperAdmin, BytesReference ref, final Settings opensearchSettings,
Object... param) {
public InternalUsersValidator(final RestRequest request,
final boolean isSuperAdmin,
final BytesReference ref,
final Settings opensearchSettings,
Object... param) {
super(request, ref, opensearchSettings, param);
allowedKeys.put("backend_roles", DataType.ARRAY);
allowedKeys.put("attributes", DataType.OBJECT);
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/org/opensearch/security/support/ConfigConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,18 @@ public enum RolesMappingResolution {
public static final String SECURITY_RESTAPI_ROLES_ENABLED = "plugins.security.restapi.roles_enabled";
public static final String SECURITY_RESTAPI_ENDPOINTS_DISABLED = "plugins.security.restapi.endpoints_disabled";
public static final String SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX = "plugins.security.restapi.password_validation_regex";

public static final String SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE = "plugins.security.restapi.password_validation_error_message";

public static final String SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION = "plugins.security.restapi.passwords.score.based.validation";

// 1 very guessable: protection from throttled online attacks.
public static final String SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_SCORE_STRENGTH = "plugins.security.restapi.passwords.score.based.validation.score_strength";

public static final String SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_MIN_PASSWORD_LENGTH = "plugins.security.restapi.passwords.score.based.validation.min_password_length";

public static final String SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_CHECK_USERNAME_SIMILARITY = "plugins.security.restapi.passwords.score.based.validation.check_username_similarity";

// Illegal Opcodes from here on
public static final String SECURITY_UNSUPPORTED_DISABLE_REST_AUTH_INITIALLY = "plugins.security.unsupported.disable_rest_auth_initially";
public static final String SECURITY_UNSUPPORTED_DISABLE_INTERTRANSPORT_AUTH_INITIALLY = "plugins.security.unsupported.disable_intertransport_auth_initially";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.opensearch.security.support;

import org.opensearch.common.Strings;
import org.opensearch.common.settings.Setting;

import java.util.Map;

import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_SCORE_STRENGTH;

public enum PasswordValidationScoreStrength {
FAIR(1),
GOOD(2),
STRONG(3),
VERY_STRONG(4);

private final int score;

private PasswordValidationScoreStrength(int score) {
this.score = score;
}

public int score() {
return this.score;
}

}
Loading

0 comments on commit 57023ae

Please sign in to comment.