add password complexity check before submitting during reset password flow
Project: http://git-wip-us.apache.org/repos/asf/usergrid/repo Commit: http://git-wip-us.apache.org/repos/asf/usergrid/commit/ceba1fa4 Tree: http://git-wip-us.apache.org/repos/asf/usergrid/tree/ceba1fa4 Diff: http://git-wip-us.apache.org/repos/asf/usergrid/diff/ceba1fa4 Branch: refs/heads/hotfix-20170728 Commit: ceba1fa44455a6c8f962c5e7badd615a382dddae Parents: c0c489e Author: Mike Dunker <[email protected]> Authored: Fri Jul 28 15:09:09 2017 -0700 Committer: Mike Dunker <[email protected]> Committed: Fri Jul 28 15:09:09 2017 -0700 ---------------------------------------------------------------------- .../rest/applications/users/UserResource.java | 8 + .../rest/management/users/UserResource.java | 9 ++ .../usergrid/management/ManagementService.java | 10 +- .../cassandra/ManagementServiceImpl.java | 14 ++ .../usergrid/security/PasswordPolicy.java | 53 +++++++ .../usergrid/security/PasswordPolicyFig.java | 79 ++++++++++ .../usergrid/security/PasswordPolicyImpl.java | 156 +++++++++++++++++++ .../services/guice/ServiceModuleImpl.java | 7 + 8 files changed, 331 insertions(+), 5 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/usergrid/blob/ceba1fa4/stack/rest/src/main/java/org/apache/usergrid/rest/applications/users/UserResource.java ---------------------------------------------------------------------- diff --git a/stack/rest/src/main/java/org/apache/usergrid/rest/applications/users/UserResource.java b/stack/rest/src/main/java/org/apache/usergrid/rest/applications/users/UserResource.java index 5435f7e..f0dbc14 100644 --- a/stack/rest/src/main/java/org/apache/usergrid/rest/applications/users/UserResource.java +++ b/stack/rest/src/main/java/org/apache/usergrid/rest/applications/users/UserResource.java @@ -17,6 +17,7 @@ package org.apache.usergrid.rest.applications.users; +import java.util.Collection; import java.util.Map; import java.util.UUID; @@ -465,6 +466,13 @@ public class UserResource extends ServiceResource { if ( ( password1 != null ) || ( password2 != null ) ) { if ( management.checkPasswordResetTokenForAppUser( getApplicationId(), getUserUuid(), token ) ) { if ( ( password1 != null ) && password1.equals( password2 ) ) { + // validate password + Collection<String> violations = management.passwordPolicyCheck(password1, false); + if (violations.size() > 0) { + // password not valid + errorMsg = management.getPasswordDescription(false); + return handleViewable( "resetpw_set_form", this, getOrganizationName() ); + } management.setAppUserPassword( getApplicationId(), getUser().getUuid(), password1 ); management.revokeAccessTokenForAppUser( token ); return handleViewable( "resetpw_set_success", this, getOrganizationName() ); http://git-wip-us.apache.org/repos/asf/usergrid/blob/ceba1fa4/stack/rest/src/main/java/org/apache/usergrid/rest/management/users/UserResource.java ---------------------------------------------------------------------- diff --git a/stack/rest/src/main/java/org/apache/usergrid/rest/management/users/UserResource.java b/stack/rest/src/main/java/org/apache/usergrid/rest/management/users/UserResource.java index 95f607b..cac5f2b 100644 --- a/stack/rest/src/main/java/org/apache/usergrid/rest/management/users/UserResource.java +++ b/stack/rest/src/main/java/org/apache/usergrid/rest/management/users/UserResource.java @@ -43,6 +43,7 @@ import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriInfo; +import java.util.Collection; import java.util.Map; import java.util.UUID; @@ -297,6 +298,14 @@ public class UserResource extends AbstractContextResource { if ( ( password1 != null ) || ( password2 != null ) ) { if ( management.checkPasswordResetTokenForAdminUser( user.getUuid(), tokenInfo ) ) { if ( ( password1 != null ) && password1.equals( password2 ) ) { + // validate password + Collection<String> violations = management.passwordPolicyCheck(password1, true); + if (violations.size() > 0) { + // password not valid + errorMsg = management.getPasswordDescription(true); + return handleViewable( "resetpw_set_form", this, organizationId ); + } + management.setAdminUserPassword( user.getUuid(), password1 ); management.revokeAccessTokenForAdminUser( user.getUuid(), token ); loginEndpoint = properties.getProperty("usergrid.viewable.loginEndpoint"); http://git-wip-us.apache.org/repos/asf/usergrid/blob/ceba1fa4/stack/services/src/main/java/org/apache/usergrid/management/ManagementService.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/management/ManagementService.java b/stack/services/src/main/java/org/apache/usergrid/management/ManagementService.java index 2b88b07..df42d6a 100644 --- a/stack/services/src/main/java/org/apache/usergrid/management/ManagementService.java +++ b/stack/services/src/main/java/org/apache/usergrid/management/ManagementService.java @@ -17,11 +17,7 @@ package org.apache.usergrid.management; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.UUID; +import java.util.*; import org.apache.usergrid.persistence.CredentialsInfo; import org.apache.usergrid.persistence.Entity; @@ -372,6 +368,10 @@ public interface ManagementService { Observable<Id> deleteAllEntities(final UUID applicationId,final int limit); + Collection<String> passwordPolicyCheck(String password, boolean isAdminUser); + + String getPasswordDescription(boolean isAdminUser); + // DO NOT REMOVE BELOW METHODS, THEY ARE HERE TO ALLOW EXTERNAL CLASSES TO OVERRIDE AND HOOK INTO POST PROCESSING void createOrganizationPostProcessing( final OrganizationInfo orgInfo, http://git-wip-us.apache.org/repos/asf/usergrid/blob/ceba1fa4/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java b/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java index 876cd5b..2d60441 100644 --- a/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java +++ b/stack/services/src/main/java/org/apache/usergrid/management/cassandra/ManagementServiceImpl.java @@ -54,6 +54,7 @@ import org.apache.usergrid.persistence.model.entity.Id; import org.apache.usergrid.persistence.model.entity.SimpleId; import org.apache.usergrid.security.AuthPrincipalInfo; import org.apache.usergrid.security.AuthPrincipalType; +import org.apache.usergrid.security.PasswordPolicy; import org.apache.usergrid.security.crypto.EncryptionService; import org.apache.usergrid.security.oauth.AccessInfo; import org.apache.usergrid.security.oauth.ClientCredentialsInfo; @@ -172,6 +173,8 @@ public class ManagementServiceImpl implements ManagementService { protected LocalShiroCache localShiroCache; + protected PasswordPolicy passwordPolicy; + private LoadingCache<UUID, OrganizationConfig> orgConfigByAppCache = CacheBuilder.newBuilder().maximumSize( 1000 ) .expireAfterWrite( Long.valueOf( System.getProperty(ORG_CONFIG_CACHE_PROP, "30000") ) , TimeUnit.MILLISECONDS) @@ -215,6 +218,7 @@ public class ManagementServiceImpl implements ManagementService { this.service = injector.getInstance(ApplicationService.class); this.localShiroCache = injector.getInstance(LocalShiroCache.class); + this.passwordPolicy = injector.getInstance(PasswordPolicy.class); } @Autowired @@ -3497,6 +3501,16 @@ public class ManagementServiceImpl implements ManagementService { } @Override + public Collection<String> passwordPolicyCheck(String password, boolean isAdminUser) { + return passwordPolicy.policyCheck(password, isAdminUser); + } + + @Override + public String getPasswordDescription(boolean isAdminUser) { + return passwordPolicy.getDescription(isAdminUser); + } + + @Override public void createOrganizationPostProcessing( final OrganizationInfo orgInfo, final Map<String,String> properties ){ // do nothing, this is a hook for any classes extending the ManagementServiceInterface http://git-wip-us.apache.org/repos/asf/usergrid/blob/ceba1fa4/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicy.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicy.java b/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicy.java new file mode 100644 index 0000000..cc29b20 --- /dev/null +++ b/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicy.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.usergrid.security; + + +import java.util.Collection; + + +/** + * Interface to password policy. + */ +public interface PasswordPolicy { + + String ERROR_POLICY_VIOLIATION = "error_password_policy_violation"; + + String ERROR_UPPERCASE_POLICY = "error_uppercase_policy"; + + String ERROR_DIGITS_POLICY = "error_digits_policy"; + + String ERROR_SPECIAL_CHARS_POLICY = "error_special_chars_policy"; + + String ERROR_LENGTH_POLICY = "error_length_policy"; + + + /** + * Check to see if password conforms to policy. + * + * @param password Password to check. + * @return Collection of error strings, one for each policy violated or empty if password conforms. + */ + Collection<String> policyCheck( String password, boolean isAdminUser ); + + + /** + * Get description of password policy for error messages. + */ + String getDescription( boolean isAdminUser ); +} http://git-wip-us.apache.org/repos/asf/usergrid/blob/ceba1fa4/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyFig.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyFig.java b/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyFig.java new file mode 100644 index 0000000..e93f8e4 --- /dev/null +++ b/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyFig.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.usergrid.security; + +import org.safehaus.guicyfig.Default; +import org.safehaus.guicyfig.FigSingleton; +import org.safehaus.guicyfig.GuicyFig; +import org.safehaus.guicyfig.Key; + + +@FigSingleton +public interface PasswordPolicyFig extends GuicyFig { + + String ALLOWED_SPECIAL_CHARS = "usergrid.password-policy.allowed-special-chars"; + + String MIN_UPPERCASE_ADMIN = "usergrid.password-policy.min-uppercase-admin"; + String MIN_UPPERCASE = "usergrid.password-policy.min-uppercase"; + + String MIN_DIGITS_ADMIN = "usergrid.password-policy.min-digits-admin"; + String MIN_DIGITS = "usergrid.password-policy.min-digits"; + + String MIN_SPECIAL_CHARS_ADMIN = "usergrid.password-policy.min-special-chars-admin"; + String MIN_SPECIAL_CHARS = "usergrid.password-policy.min-special-chars"; + + String MIN_LENGTH_ADMIN = "usergrid.password-policy.min-length-admin"; + String MIN_LENGTH = "usergrid.password-policy.min-length"; + + + @Key(MIN_UPPERCASE_ADMIN) + @Default("0") + int getMinUppercaseAdmin(); + + @Key(MIN_UPPERCASE) + @Default("0") + int getMinUppercase(); + + @Key(MIN_DIGITS_ADMIN) + @Default("0") + int getMinDigitsAdmin(); + + @Key(MIN_DIGITS) + @Default("0") + int getMinDigits(); + + @Key(MIN_SPECIAL_CHARS_ADMIN) + @Default("0") + int getMinSpecialCharsAdmin(); + + @Key(MIN_SPECIAL_CHARS) + @Default("0") + int getMinSpecialChars(); + + @Key(MIN_LENGTH_ADMIN) + @Default("4") + int getMinLengthAdmin(); + + @Key(MIN_LENGTH) + @Default("4") + int getMinLength(); + + @Key(ALLOWED_SPECIAL_CHARS) + @Default("`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?") + String getAllowedSpecialChars(); +} http://git-wip-us.apache.org/repos/asf/usergrid/blob/ceba1fa4/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyImpl.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyImpl.java b/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyImpl.java new file mode 100644 index 0000000..500592a --- /dev/null +++ b/stack/services/src/main/java/org/apache/usergrid/security/PasswordPolicyImpl.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.usergrid.security; + +import com.google.inject.Inject; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + + +public class PasswordPolicyImpl implements PasswordPolicy { + + private final PasswordPolicyFig passwordPolicyFig; + + + @Inject + PasswordPolicyImpl( PasswordPolicyFig passwordPolicyFig ) { + this.passwordPolicyFig = passwordPolicyFig; + } + + + @Override + public String getDescription( boolean isAdminUser ) { + + final int minLength; + final int minUppercase; + final int minDigits; + final int minSpecialChars; + + if ( isAdminUser ) { + minLength = passwordPolicyFig.getMinLengthAdmin(); + minUppercase = passwordPolicyFig.getMinUppercaseAdmin(); + minDigits = passwordPolicyFig.getMinDigitsAdmin(); + minSpecialChars = passwordPolicyFig.getMinSpecialCharsAdmin(); + } else { + minLength = passwordPolicyFig.getMinLength(); + minUppercase = passwordPolicyFig.getMinUppercase(); + minDigits = passwordPolicyFig.getMinDigits(); + minSpecialChars = passwordPolicyFig.getMinSpecialChars(); + } + + StringBuilder sb = new StringBuilder(); + sb.append( "Password must be at least " ).append( minLength ).append(" characters. "); + if ( minUppercase > 0 ) { + sb.append( "Must include " ).append( minUppercase ).append(" uppercase characters. "); + } + if ( minDigits > 0 ) { + sb.append( "Must include " ).append( minDigits ).append(" numbers. "); + } + if ( minSpecialChars > 0 ) { + sb.append( "Must include " ).append( minUppercase ).append(" special characters. "); + } + return sb.toString(); + } + + + @Override + public Collection<String> policyCheck( String password, boolean isAdminUser ) { + + final int minLength; + final int minUppercase; + final int minDigits; + final int minSpecialChars; + + if ( isAdminUser ) { + minLength = passwordPolicyFig.getMinLengthAdmin(); + minUppercase = passwordPolicyFig.getMinUppercaseAdmin(); + minDigits = passwordPolicyFig.getMinDigitsAdmin(); + minSpecialChars = passwordPolicyFig.getMinSpecialCharsAdmin(); + } else { + minLength = passwordPolicyFig.getMinLength(); + minUppercase = passwordPolicyFig.getMinUppercase(); + minDigits = passwordPolicyFig.getMinDigits(); + minSpecialChars = passwordPolicyFig.getMinSpecialChars(); + } + + return policyCheck( password, minLength, minUppercase, minDigits, minSpecialChars ); + } + + + public Collection<String> policyCheck( + String password, int minLength, int minUppercase, int minDigits, int minSpecialChars ) { + + + List<String> violations = new ArrayList<>(3); + + // check length + if ( password == null || password.length() < minLength ) { + violations.add( PasswordPolicy.ERROR_LENGTH_POLICY + + ": must be at least " + minLength + " characters" ); + } + + // count upper case + if ( minUppercase > 0 ) { + int upperCaseCount = 0; + for (char c : password.toCharArray()) { + if (StringUtils.isAllUpperCase( String.valueOf( c ) )) { + upperCaseCount++; + } + } + if (upperCaseCount < minUppercase) { + violations.add( PasswordPolicy.ERROR_UPPERCASE_POLICY + + ": requires " + minUppercase + " uppercase characters" ); + } + } + + // count digits case + if ( minDigits > 0 ) { + int digitCount = 0; + for (char c : password.toCharArray()) { + if (StringUtils.isNumeric( String.valueOf( c ) )) { + digitCount++; + } + } + if (digitCount < minDigits) { + violations.add( PasswordPolicy.ERROR_DIGITS_POLICY + + ": requires " + minDigits + " digits" ); + } + } + + // count special characters + if ( minSpecialChars > 0 ) { + int specialCharCount = 0; + for (char c : password.toCharArray()) { + if (passwordPolicyFig.getAllowedSpecialChars().contains( String.valueOf( c ) )) { + specialCharCount++; + } + } + if (specialCharCount < minSpecialChars) { + violations.add( PasswordPolicy.ERROR_SPECIAL_CHARS_POLICY + + ": requires " + minSpecialChars + " special characters" ); + } + } + + return violations; + } + + +} http://git-wip-us.apache.org/repos/asf/usergrid/blob/ceba1fa4/stack/services/src/main/java/org/apache/usergrid/services/guice/ServiceModuleImpl.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/services/guice/ServiceModuleImpl.java b/stack/services/src/main/java/org/apache/usergrid/services/guice/ServiceModuleImpl.java index 58b301a..9e5485b 100644 --- a/stack/services/src/main/java/org/apache/usergrid/services/guice/ServiceModuleImpl.java +++ b/stack/services/src/main/java/org/apache/usergrid/services/guice/ServiceModuleImpl.java @@ -31,8 +31,12 @@ import org.apache.usergrid.persistence.cache.impl.CacheFactoryImpl; import org.apache.usergrid.persistence.cache.impl.ScopedCacheSerialization; import org.apache.usergrid.persistence.cache.impl.ScopedCacheSerializationImpl; import org.apache.usergrid.persistence.core.migration.data.MigrationPlugin; +import org.apache.usergrid.security.PasswordPolicy; +import org.apache.usergrid.security.PasswordPolicyFig; +import org.apache.usergrid.security.PasswordPolicyImpl; import org.apache.usergrid.security.shiro.UsergridAuthenticationInfo; import org.apache.usergrid.security.shiro.UsergridAuthorizationInfo; +import org.safehaus.guicyfig.GuicyFigModule; // <bean id="notificationsQueueListener" class="org.apache.usergrid.services.notifications.QueueListener" @@ -70,5 +74,8 @@ public class ServiceModuleImpl extends AbstractModule implements ServiceModule { bind( new TypeLiteral<ScopedCacheSerialization<String, UsergridAuthenticationInfo>>() {}) .to( new TypeLiteral<ScopedCacheSerializationImpl<String, UsergridAuthenticationInfo>>() {}); + bind( PasswordPolicy.class ).to( PasswordPolicyImpl.class ); + + install( new GuicyFigModule( PasswordPolicyFig.class ) ); } }
