-
Notifications
You must be signed in to change notification settings - Fork 71
THREESCALE-8916: Make strong passwords mandatory #4195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
0af38d4
a10c6fb
5088f4e
78c041f
068703f
287e177
b69695a
514377f
ae75cc5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| [allowlist] | ||
| description = "Global Allowlist" | ||
|
|
||
| # Ignore based on any subset of the file path | ||
| paths = [ | ||
| '''test/unit/authentication/by_password_test.rb''', | ||
| '''test/integration/admin/api/buyers_users_controller_test.rb''' | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,7 +56,7 @@ def account_params | |
| def user_params | ||
| { | ||
| signup_type: partner.signup_type, | ||
| password: permitted_params[:password].presence || SecureRandom.hex, | ||
| password: permitted_params[:password].presence, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The user is valid without a password when it's an SSO user. So there's no need to enforce a random password. Also, this random password is not shown to the caller anywhere, so it couldn't be used anyway. After this change, The SSO users for partners don't have a password, the same as any other SSO user in the project. |
||
| email: permitted_params[:email], | ||
| first_name: permitted_params[:first_name], | ||
| last_name: permitted_params[:last_name], | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,19 +22,19 @@ def destroy | |
| def create | ||
| @user = @account.users.build | ||
| @user.email = params[:email] | ||
| @user.password = SecureRandom.hex | ||
| @user.password = params[:password].presence | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same, no password required. |
||
| @user.first_name = params[:first_name].presence | ||
| @user.last_name = params[:last_name].presence | ||
| @user.open_id = params[:open_id].presence | ||
| @user.username = params[:username] | ||
| @user.signup_type = :partner | ||
| @user.role = :admin | ||
| @user.activate! | ||
| if @user.save | ||
| render json: {id: @user.id, success: true} | ||
| else | ||
| render json: {errors: @user.errors, success: false} | ||
| end | ||
| @user.save! | ||
|
|
||
| render json: {id: @user.id, success: true} | ||
| rescue StandardError | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why rescue instead of rely on
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because the previous call to |
||
| render json: {errors: @user.errors, success: false}, status: :unprocessable_entity | ||
| end | ||
|
|
||
| private | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -4,38 +4,31 @@ module ByPassword | |||||
| extend ActiveSupport::Concern | ||||||
|
|
||||||
| # strong passwords | ||||||
| SPECIAL_CHARACTERS = '-+=><_$#.:;!?@&*()~][}{|' | ||||||
| RE_STRONG_PASSWORD = / | ||||||
| \A | ||||||
| (?=.*\d) # number | ||||||
| (?=.*[a-z]) # lowercase | ||||||
| (?=.*[A-Z]) # uppercase | ||||||
| (?=.*[#{Regexp.escape(SPECIAL_CHARACTERS)}]) # special char | ||||||
| (?!.*\s) # does not end with space | ||||||
| .{8,} # at least 8 characters | ||||||
| \z | ||||||
| /x | ||||||
| STRONG_PASSWORD_FAIL_MSG = "Password must be at least 8 characters long, and contain both upper and lowercase letters, a digit and one special character of #{SPECIAL_CHARACTERS}.".freeze | ||||||
| STRONG_PASSWORD_MIN_SIZE = 15 | ||||||
|
|
||||||
| included do | ||||||
| # We only need length validations as they are already set in Authentication::ByPassword | ||||||
| has_secure_password validations: false | ||||||
|
|
||||||
| validates_presence_of :password, if: :password_required? | ||||||
| before_validation :normalize_password, if: :validate_password? | ||||||
mayorova marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| validates_presence_of :password, if: :validate_password? | ||||||
|
|
||||||
| validates_confirmation_of :password, allow_blank: true | ||||||
|
|
||||||
| validates :password, format: { :with => RE_STRONG_PASSWORD, :message => STRONG_PASSWORD_FAIL_MSG, | ||||||
| if: -> { password_required? && provider_requires_strong_passwords? } } | ||||||
| validates :password, length: { minimum: 6, allow_blank: true, | ||||||
| if: -> { password_required? && !provider_requires_strong_passwords? } } | ||||||
| validates :password, length: { minimum: STRONG_PASSWORD_MIN_SIZE }, if: :validate_strong_password? | ||||||
|
|
||||||
| validates :lost_password_token, :password_digest, length: { maximum: 255 } | ||||||
|
|
||||||
| attr_accessible :password, :password_confirmation | ||||||
| attr_accessible :password, :password_confirmation, as: %i[default member admin] | ||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is to fix a mass-assignment error from
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I understand... if there is no |
||||||
|
|
||||||
| scope :with_valid_password_token, -> { where { lost_password_token_generated_at >= 24.hours.ago } } | ||||||
|
|
||||||
| alias_method :authenticate_without_normalization, :authenticate | ||||||
|
|
||||||
| def authenticate(password) | ||||||
| authenticate_without_normalization(password&.unicode_normalize(:nfc)) | ||||||
| end | ||||||
| alias_method :authenticated?, :authenticate | ||||||
| end | ||||||
|
|
||||||
|
|
@@ -45,8 +38,16 @@ def find_with_valid_password_token(token) | |||||
| end | ||||||
| end | ||||||
|
|
||||||
| def password_required? | ||||||
| signup.by_user? && (password_digest.blank? || password_digest_changed?) | ||||||
| def validate_password? | ||||||
| # The password changed or it's a new record that must provide a password | ||||||
| will_save_change_to_password_digest? || (new_record? && signup.by_user?) | ||||||
| end | ||||||
|
|
||||||
| def validate_strong_password? | ||||||
| return false if Rails.configuration.three_scale.strong_passwords_disabled | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it will be much better to set the Because you said in a comment:
But this setting is essentially doing the same - UI will still want strong passwords but backend will allow weaker.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You're right on this
What do you mean by "overriding here"? overriding the setting? Also, if we set
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I meant that this specific line overrides Agreed that we still need the sample data check. Hopefully later we get rid of it by also generating strong password for John Doe.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. then we would need to get rid of john doe default password and signup_types before implementing your suggestion
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can get rid of this specific line regardless of John Doe.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've been trying but I don't think it's worth it right now. Having the setting checked in the method is pretty convenient for tests, so I can stub the value and easily see the different behavior. However, if we move the logic to the macro, then the setting is only read once when loading the code, so stubbing the setting has no effect, which leads to a few tests failing. I prefer to not spend the time redefining tests right now, when we actually get rid of sample data, I'll remove the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, sure. Even if it stays, it will not be a big issue. |
||||||
| return false if signup.sample_data? | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Follow up on previous question about sample data ending up in production, I would imagine the logic to something like this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of this, is we agreed to not allow weak passwords for sample dat ain production, I'd prefer to completely remove the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would be happy not having
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it'd be good to remove the sample data signup type, but that would imply all the additional logic: Creating Jonh Doe with a random password, send it via internal message to admin, and updating the places where that password is visible, like the CMS toolbar. @mayorova WDYT?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I vote for keeping it like it is now - John Doe with
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can also generate default passwords derived from e.g., buyer id and
I think this will be a good UX while keeping things way more secure. |
||||||
|
|
||||||
| validate_password? | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to call this again? It means that most for when strong password is used (most of the time)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will be called not twice, but three times, because it's also called by:
But I think that's fine because it's only called when updating a user, I don't know how common is that but doesn't seem supper common. Also, it makes the code more clear, otherwise, it would be confusing, e.g. a scenario where This way |
||||||
| end | ||||||
|
Comment on lines
+41
to
51
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This replaces
The new methods are tested and return proper values for all known scenarios. |
||||||
|
|
||||||
| def just_changed_password? | ||||||
|
|
@@ -59,9 +60,10 @@ def expire_password_token | |||||
|
|
||||||
| def generate_lost_password_token | ||||||
| token = SecureRandom.hex(32) | ||||||
| return unless update_columns(lost_password_token: token, lost_password_token_generated_at: Time.current) | ||||||
| self.lost_password_token = token | ||||||
| self.lost_password_token_generated_at = Time.current | ||||||
|
|
||||||
| token | ||||||
| token if save | ||||||
| end | ||||||
|
|
||||||
| def generate_lost_password_token! | ||||||
|
|
@@ -81,12 +83,12 @@ def update_password(new_password, new_password_confirmation) | |||||
| save | ||||||
| end | ||||||
|
|
||||||
| def using_password? | ||||||
| password_digest.present? | ||||||
| def already_using_password? | ||||||
| password_digest_in_database.present? | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wait, if
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was causing issues in the user edit form. If I recall correctly, on some scenarios the form required the user to provide their current password in situations where that password was still not persisted, so it couldn't work. I think it's correct this way. We only consider a user is using a password when it indeed has a password in DB.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, fine, if not too annoying, I'd suggest renaming the method to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I renamed it everywhere except in the |
||||||
| end | ||||||
|
|
||||||
| def can_set_password? | ||||||
| account.password_login_allowed? && !using_password? | ||||||
| account.password_login_allowed? && !already_using_password? | ||||||
| end | ||||||
|
|
||||||
| def special_fields | ||||||
|
|
@@ -96,5 +98,22 @@ def special_fields | |||||
| def reset_lost_password_token | ||||||
| self.lost_password_token = nil | ||||||
| end | ||||||
|
|
||||||
| raise "FIXME" if Gem::Version.new(Rails.version) >= Gem::Version.new("8.1") | ||||||
| # To avoid all this logic, from Rails 8.1+ we can use | ||||||
akostadinov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| # `ActiveModel::Attributes::Normalization`, so `normalizes` method | ||||||
| # works with virtual attributes like `password` and `password_validation` | ||||||
| # and normalizes on assignment rather than before_validation | ||||||
| def normalize_password | ||||||
| if password.present? | ||||||
| normalized = password.unicode_normalize(:nfc) | ||||||
| self.password = normalized unless password == normalized | ||||||
| end | ||||||
|
|
||||||
| if password_confirmation.present? | ||||||
| normalized_confirmation = password_confirmation.unicode_normalize(:nfc) | ||||||
| self.password_confirmation = normalized_confirmation unless password_confirmation == normalized_confirmation | ||||||
| end | ||||||
| end | ||||||
| end | ||||||
| end | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,7 +71,7 @@ def ensure_users(count) | |
| def signup_user | ||
| email_part = email.split('@') | ||
| user_attributes = { email: "#{email_part[0]}+test@#{email_part[1]}", username: 'john', first_name: 'John', | ||
| last_name: 'Doe', password: '123456', password_confirmation: '123456', signup_type: :minimal} | ||
| last_name: 'Doe', password: '123456', password_confirmation: '123456', signup_type: :sample_data} | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In order to exclude "John Doe" from strong password requirement, I added a new signup type
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The question is, shouldn't we prevent sample weak passwords in production environment? I think we discussed this somewhere, about how to notify provider of the sample buyer user account. e.g. email/internal messaging. But I'm not sure we came to an agreement.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We discussed this via slack, I even suggested to get rid of John Doe, we agreed on keeping the sample data. About the sample data having a weak password, that's another story. I remember you mentioned about sending the password to admin via internal messaging, but nothing was agreed at the end. |
||
| signup_params = ::Signup::SignupParams.new(plans: [], user_attributes: user_attributes, account_attributes: { org_name: 'Developer' }) | ||
| ::Signup::DeveloperAccountManager.new(@provider).create(signup_params) | ||
| end | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -344,14 +344,6 @@ def provider_id_for_audits | |
| account.try!(:provider_id_for_audits) || provider_account.try!(:provider_id_for_audits) | ||
| end | ||
|
|
||
| def provider_requires_strong_passwords? | ||
| # use fields definitons source (instance variable) as backup when creating new record | ||
| # and there is no provider account (its still new record and not set through association.build) | ||
| if source = fields_definitions_source_root | ||
| source.settings.strong_passwords_enabled? | ||
| end | ||
| end | ||
|
Comment on lines
-347
to
-353
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dead code AFAIK |
||
|
|
||
| protected | ||
|
|
||
| def account_for_sphinx | ||
|
|
@@ -436,6 +428,11 @@ def minimal? | |
| signup_type == :minimal | ||
| end | ||
|
|
||
| def sample_data? | ||
| # This is true only for John Doe | ||
| signup_type == :sample_data | ||
| end | ||
|
|
||
| def api? | ||
| signup_type == :api | ||
| end | ||
|
|
@@ -457,7 +454,7 @@ def cas? | |
| end | ||
|
|
||
| def machine? | ||
| minimal? || api? || created_by_provider? || open_id? || cas? || oauth2? | ||
| minimal? || sample_data? || api? || created_by_provider? || open_id? || cas? || oauth2? | ||
| end | ||
|
|
||
| def by_user? | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,8 +75,8 @@ def make_activation_code | |
| self.activation_code = self.class.make_token | ||
| end | ||
|
|
||
| def activate_on_minimal_signup? | ||
| minimal_signup? && password.present? && !account.try!(:bought_account_plan).try!(:approval_required?) | ||
| def activate_on_minimal_or_sample_data? | ||
| (minimal_signup? || signup.sample_data?) && password.present? && !account.try!(:bought_account_plan).try!(:approval_required?) | ||
|
Comment on lines
+78
to
+79
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method determines whether the new user must be automatically activated or not. I added the |
||
| end | ||
|
|
||
| def generate_email_verification_token | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,7 +21,7 @@ | |
| </div> | ||
| <%= form.fields_for [:user, @user ] do |user| %> | ||
| <%= user.user_defined_form %> | ||
| <%= user.input :password, as: :patternfly_input, required: true %> | ||
| <%= user.input :password, as: :patternfly_input, input_html: { type: 'password' }, required: true %> | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Password input was in clear text |
||
| <% end %> | ||
| </section> | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,8 +13,8 @@ | |
| <% end %> | ||
|
|
||
| <%= form.inputs :name => "Change Password" do %> | ||
| <%= form.input :password, required: true, input_html: { autocomplete: 'off' } %> | ||
| <%= form.input :password_confirmation, required: true, input_html: { autocomplete: 'off' } %> | ||
| <%= form.input :password, required: false, input_html: { autocomplete: 'off' } %> | ||
| <%= form.input :password_confirmation, required: false, input_html: { autocomplete: 'off' } %> | ||
|
Comment on lines
+16
to
+17
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was wrong, there's no scenario when it's required to change a password in this form.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In fact, it was a "fake" requirement, the form submit was working without provided values. |
||
| <% end -%> | ||
|
|
||
| <% if can? :update_role, @user %> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had to add this so our password leak system allows me to commit.