Skip to content

THREESCALE-8916: Make strong passwords mandatory#4195

Open
jlledom wants to merge 9 commits into3scale:masterfrom
jlledom:THREESCALE-8916-admin-strong-paswords
Open

THREESCALE-8916: Make strong passwords mandatory#4195
jlledom wants to merge 9 commits into3scale:masterfrom
jlledom:THREESCALE-8916-admin-strong-paswords

Conversation

@jlledom
Copy link
Contributor

@jlledom jlledom commented Jan 9, 2026

Note
This PR includes all changes from #4194. To make it easier to review, jlledom#3 includes only the actual diff to make strong passwords mandatory.

What this PR does / why we need it:

Currently, a provider can enforce strong password for the developer portal, and weak passwords are accepted by default. About admin portal, there's no option to enable strong passwords, weak passwords are always accepted.

I think both situations make no sense. I don't think it's acceptable to allow users decide whether they enforce strong passwords or not, as long as strong passwords are possible, that should be the default. And same thing about admin portal.

After a discussion via slack, we agreed on this terms:

  1. Enforce NIST policies
  2. Enforce for all users with password, not matter how did they sign up
    • Currently, users created automatically are not affected by the strong passwords setting. This is problematic because master, providers and buyers admin users are created this way, so those are never enforced to have strong passwords no matter the setting.
  3. Unify UI and API behavior
    • API endpoints don't consistently check for strong passwords, after this PR, all endpoints ask for strong passwords, also all UI forms.
  4. Remove the strong passwords setting
    • It will continue in db but completely ignored, since its mandatory now.
  5. Add a new setting on settings.yml to allow weak passwords for us
    • For develop and test purposes, we can add strong_passwords_disabled: true to settings.yml. If not set at all, strong passwords are enabled, so clients don't have to change anything on their side.
  6. Fix javascript password validation in forms, it's very inconsistent now
  7. db:seed should accept weak passwords
    • This is possible without changes in the code, just ensure to set strong_passwords_disabled to true before running the task
  8. John Doe: allow 123456
    • I added a new signup type called :smaple_data. Sample data accepts weak passwords, no matter the setting.

This affects multiple screens, but also API endpoints, this is the complete list:

  • All scenarios with a password
    • Admin portal
      • UI
        • User invitation signup form
        • Personal details
        • Edit user
        • Forgot password
        • Create new buyer
        • Edit buyer user
      • API
        • POST /master/api/providers.xml
        • POST /admin/api/users.xml
        • PUT /admin/api/users/{id}.xml
        • POST /partners/providers
        • POST /partners/providers/{id}/users
    • Developer portal
      • UI
        • New buyer signup form
        • Existing buyer user invitation signup form
        • Personal details
        • Edit user
        • Forgot password
      • API
        • POST /admin/api/signup.xml
        • POST /admin/api/accounts/{id}/users.xml
        • PUT /admin/api/accounts/{id}/users/{id}.xml

Additionally, I also added some behavioral changes to the UI. To solve bugs or behaviors I think are incorrect. This is the summarty Claude generated:

Screens/Forms Modified

Screen/File Location Changes
React Password Validation (app/javascript/src/Login/utils/validations.ts) Frontend JS Added strong password validation to React forms. Changed from length: { minimum: 6 } to regex pattern matching backend rules
(16+ chars, ASCII printable only). Added STRONG_PASSWORD_* constants mirroring backend.
Create Buyer Account (app/views/buyers/accounts/new.html.erb) Admin Portal → Audience → Accounts → Create Added type: 'password' to password field. Previously the field was showing password as clear text.
Edit Buyer User (app/views/buyers/users/edit.html.erb) Admin Portal → Audience → Accounts → [Account] → Users → [User] → Edit Changed password and password_confirmation fields from required: true to `required:
false`. Password is now optional when editing a buyer user.
Edit Provider User (app/views/provider/admin/account/users/_form.html.erb) Admin Portal → Account Settings → Users → [User] → Edit Changed password and password_confirmation fields from required: true to
required: false. Password is now optional when editing a provider user.
Edit Personal Details (app/views/provider/admin/user/personal_details/edit.html.slim) Admin Portal → Account Settings → Personal Details Multiple changes:
1. Removed the toast/alert prompting SSO users to set password via reset form
2. Password field now always visible (not conditional on using_password)
3. Password field changed to required: false
4. Added type: 'password' to hide password input
5. "Current password" section only shown when using_password? is true (users with existing password must confirm it)
Locales (config/locales/en.yml) N/A Fixed YAML syntax for error message. Removed set_password_html translation (toast was removed).

Summary of behavioral changes

  1. Strong password validation in React - Frontend now enforces the same 16+ character ASCII-printable password rules as the backend.

  2. Password fields show as masked - Fixed forms that were accidentally displaying passwords as plain text.

  3. Password fields are optional on edit - When editing existing users (buyer or provider), password is no longer required. You can update other fields without changing the password.

  4. Password field always visible - On personal details page, the password field is now always shown regardless of how the user signed up (SSO, password, etc.). Any user can set/change their password.

  5. Current password only required when applicable - The "Current password" confirmation field only appears when the user already has a password set (using_password?). New SSO users setting a password for the first time
    don't need to provide a current password.

  6. Removed SSO password reset toast - SSO users no longer see the banner telling them to use the password reset form. They can now set their password directly from the personal details form.

  7. Works with enforce SSO - Passwords can still be set/changed even when enforce SSO is enabled.

I know this is a though one. I recommend to review the commits one by one. Also, I'll add some comments to provider further explanations.

Which issue(s) this PR fixes

https://issues.redhat.com/browse/THREESCALE-8916
https://issues.redhat.com/browse/THREESCALE-11548

Verification steps

You can go through any (ideally all) screens above an try to set a weak password. Also tests should pass.

List of added tests

This is the complete list of tests added in this PR. To cover all the new behaviour explained above:

File Description
test/unit/authentication/by_password_test.rb Added tests for validate_password?, validate_strong_password?, and using_password? methods. Covers scenarios for different signup types (new_signup, minimal, sample_data, machine), password change detection, and strong password validation bypass for sample_data users.
test/unit/liquid/drops/user_drop_test.rb Added tests for using_password? and password_required? Liquid drop methods, verifying correct behavior for SSO users, by_user signups, and sample_data signups.
test/unit/user/signup_type_test.rb Added tests for sample_data?, machine?, and by_user? methods to verify sample_data signup type is correctly identified and classified as a machine signup.
test/unit/user/states_test.rb Added ActivateOnMinimalOrSampleDataTest class with tests for auto-activation logic, covering minimal and sample_data signups with/without password and approval requirements.
test/unit/logic/provider_signup_test.rb New test file verifying John Doe (sample data user) is created with sample_data signup type, bypasses strong password validation, and is auto-activated.
test/integration/developer_portal/signup_test.rb Added StrongPasswordsTest class testing developer portal signup with strong password validation enabled/disabled.
test/integration/provider/signups_controller_integration_test.rb Added SignupsControllerStrongPasswordsTest class testing admin portal provider signup with strong password validation enabled/disabled.
test/integration/provider/admin/account/users_controller_test.rb Added tests verifying password fields are always visible on provider user edit page, including for SSO users without password.
test/functional/buyers/users_controller_test.rb Added EditPagePasswordFieldsTest class verifying password fields are always visible on buyer user edit page, including for SSO users.
test/functional/provider/admin/user/personal_details_controller_test.rb Added SSOUserWithoutPasswordTest, UserWithPasswordTest, and EnforceSSOEnabledTest classes testing personal details page behavior for SSO users, current password requirements, and enforce SSO scenarios.
test/functional/partners/providers_controller_test.rb Added tests for creating providers without password and strong password validation on Partners API.
test/functional/partners/users_controller_test.rb Added tests for creating users with/without password, error handling (422 status), and strong password validation on Partners API.
test/integration/admin/api/buyers_users_controller_test.rb Added tests for updating buyer users with password via API and strong password validation.
test/integration/user-management-api/users_test.rb Added tests for strong password validation when updating provider users via PUT /admin/api/users/{id} endpoint.

@jlledom jlledom changed the title Threescale 8916 admin strong paswords THREESCALE-8916: Make strong passwords mandatory Jan 9, 2026
@jlledom jlledom force-pushed the THREESCALE-8916-admin-strong-paswords branch from da18ea7 to 5b00c7f Compare January 9, 2026 14:14
self.password = unencrypted_password
ThreeScale::Analytics.user_tracking(self).track('Migrated to BCrypt')
update_columns(password_digest: password_digest, salt: nil, crypted_password: nil)
authenticate(unencrypted_password)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel uneasy about this whole file. What is it doing? Why is it wrapping? What was super?

public_search account_plans_ui_visible change_account_plan_permission service_plans_ui_visible
change_service_plan_permission enforce_sso
useraccountarea_enabled hide_service signups_enabled account_approval_required public_search
account_plans_ui_visible change_account_plan_permission service_plans_ui_visiblechange_service_plan_permission
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
account_plans_ui_visible change_account_plan_permission service_plans_ui_visiblechange_service_plan_permission
account_plans_ui_visible change_account_plan_permission service_plans_ui_visible change_service_plan_permission

@mayorova
Copy link
Contributor

To be honest, I find it quite annoying to not being able to set a "weak" password, especially it's uncomfortable in the development environment, where I'm not interested in security.
Also, customers can also have their test accounts, and it could be annoying to be obliged to use a 16-characters password... 😒

So, ideally, I'd prefer to keep the optional "strong password" setting. Whoever cares about security can enable it 😬

As for the admin portal,

About admin portal, there's no option to enable strong passwords, weak passwords are always accepted.

I think you can enable strong password on the Master portal, and this setting will be applied to the admin portal passwords. Unless this doesn't work as expected:
image

@jlledom jlledom force-pushed the THREESCALE-8916-admin-strong-paswords branch from 33fe198 to 25f4a95 Compare January 13, 2026 09:43
@jlledom jlledom self-assigned this Jan 14, 2026
@jlledom jlledom force-pushed the THREESCALE-8916-admin-strong-paswords branch 6 times, most recently from 891e8cb to 35acc2b Compare February 6, 2026 13:11
authorize! :update, user

user.update_with_flattened_attributes(flat_params)
user.update_with_flattened_attributes(flat_params, as: current_user.try(:role))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PUT /admin/api/users/{id}.xml is adding this to it's call. This endpoint is the equivalent but for buyer users: PUT /admin/api/accounts/{id}/users/{id}.xml.

This is to unify behavior.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this is relevant here. From what I can see this as: is used to prevent member permissions from being updated by member users - it's only allowed to admin users:

porta/app/models/user.rb

Lines 112 to 116 in 986e1a3

attr_accessible :title, :username, :email, :first_name, :last_name,
:conditions, :cas_identifier, :open_id, :service_conditions,
:job_role, :extra_fields, as: %i[default member admin]
attr_accessible :member_permission_service_ids, :member_permission_ids, as: %i[admin]

However, these member permissions thing is only applicable to admin users, not to buyer users, so I don't think adding as: changes anything.

It is true that on the code level there is no distinction between buyer users and admin users...

But I wouldn't add this 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I wouldn't add this 🤷

By "this" you mean the as: in buyers_users_controller.rb:40? or the as: in user.rb:116?

Do you want me to remove this change?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant as: current_user.try(:role) in this controller. Unless this breaks something, of course 🤔

Well, I don't know if we need to remove it, I just find it a bit confusing to have it here, as it is not supposed to have any function for this controller, from what I understand.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: 59aa2f7

{
signup_type: partner.signup_type,
password: permitted_params[:password].presence || SecureRandom.hex,
password: permitted_params[:password].presence,
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

@user = @account.users.build
@user.email = params[:email]
@user.password = SecureRandom.hex
@user.password = params[:password].presence
Copy link
Contributor Author

@jlledom jlledom Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, no password required.

Comment on lines +3 to +9
// IMPORTANT: These STRONG_PASSWORD_* constants are duplicated from the backend.
// The source of truth is app/lib/authentication/by_password.rb. If those constants change in Ruby,
// you must update them here as well. Do not modify these without updating the backend first.
const STRONG_PASSWORD_MIN_SIZE = 16
const RE_STRONG_PASSWORD = new RegExp(`^[ -~]{${STRONG_PASSWORD_MIN_SIZE},64}$`)
const STRONG_PASSWORD_FAIL_MSG = `Password must be at least ${STRONG_PASSWORD_MIN_SIZE} characters long, and contain only valid characters.`

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enforce the same NIST policy in frontend that we enforce in backend

validates :lost_password_token, :password_digest, length: { maximum: 255 }

attr_accessible :password, :password_confirmation
attr_accessible :password, :password_confirmation, as: %i[default member admin]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to fix a mass-assignment error from PUT /admin/api/users/{id}.xml. Since the endpoint calls the update with role: :admin. The password was excluded from mass-assignment, because it only allowed the default role.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand... if there is no as: argument here, doesn't it mean that the role set in :as when assigning attributes doesn't matter?

Comment on lines +39 to 48
def validate_password?
password_digest_changed? || (signup.by_user? && password_digest.blank?)
end

def validate_strong_password?
return false if Rails.configuration.three_scale.strong_passwords_disabled
return false if signup.sample_data?

validate_password?
end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This replaces password_required? Because it was pretty confusing:

  • Being called from views to decide whether making password inputs required. IMO that's wrong, password inputs are required or not according to their purpose, not to some computed value.
  • Being called also to decide whether validate the password or not. Which was wrong as well, since it didn't match all scenarios.

The new methods are tested and return proper values for all known scenarios.

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}
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 :sample_data to identify it.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Comment on lines +78 to +79
def activate_on_minimal_or_sample_data?
(minimal_signup? || signup.sample_data?) && password.present? && !account.try!(:bought_account_plan).try!(:approval_required?)
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 :sample_data case to it. Sample user must be activated by default.

Comment on lines -347 to -353
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code AFAIK

<%= 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 %>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Password input was in clear text

Comment on lines +16 to +17
<%= form.input :password, required: false, input_html: { autocomplete: 'off' } %>
<%= form.input :password_confirmation, required: false, input_html: { autocomplete: 'off' } %>
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Comment on lines +16 to +21
= form.input :password, as: :patternfly_input,
label: t('.new_password_label'),
required: false,
input_html: { value: '', type: 'password', required: false }

- if using_password
- if current_user.using_password?
Copy link
Contributor Author

@jlledom jlledom Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Password change inputs are now always visible. However, the current_password input is only visible if the user already has a password in the DB.

paths = [
'''test/unit/authentication/by_password_test.rb''',
'''test/integration/admin/api/buyers_users_controller_test.rb'''
]
Copy link
Contributor Author

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.

And I fill in "Current password" with "superSecret1234#"
And I press "Update Details"
Then field "New password" has inline error "is too short (minimum is 6 characters)"
Then I should see the error that the password is too weak No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no new line char at end ?!?!?!??!?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please accept my most sincere apologies: 4966a16

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope heaven forgives you because I cannot


def using_password?
password_digest.present?
password_digest_in_database.present?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait, if password_digest was set but not yet in the database, shouldn't we still return true here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 already_using_password? to make this immediately obvious.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed it everywhere except in the User liquid drop, to not break existing dev portals 8b6697b


def validate_strong_password?
return false if Rails.configuration.three_scale.strong_passwords_disabled
return false if signup.sample_data?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return false if signup.sample_data?
return false if signup.sample_data? && !Rails.env.production?

Follow up on previous question about sample data ending up in production, I would imagine the logic to something like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 :sample_data signup type. I wouldn't be needed anymore.

Copy link
Contributor

@akostadinov akostadinov Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be happy not having :sample_data login type. After all the idea behind sample data is to look like real data and being a separate type is not it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vote for keeping it like it is now - John Doe with 123456 password still being valid. I think it's good to keep the changes less disruptive for the user.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 SECRET_KEY_BASE derivative and we can present that password to the provider in the UI when viewing the user.

  • provider opens the user
  • system detects that this is the first default user John Doe
  • system generates the default password
  • checks whether it matches the user
  • warns that the user has the default password of "..." OR reports that the password has changed from the default

I think this will be a good UX while keeping things way more secure.

\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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be a good idea in case some customers want to apply higher level of security, as you wish.

Suggested change
STRONG_PASSWORD_MIN_SIZE = 15
STRONG_PASSWORD_MIN_SIZE = ENV["THREESCALE_PASSWORD_MIN_SIZE", 15]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, but we need to keep the client side in sync as well. I don't know if there's a non-dirty way to make the JS code take a value from Rails config. I'll take a look.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Server side will reject it anyway. I don't want to make things more complicated. Just a quick workaround to have available if somebody needs. But if you find a way to keep UI in sync, that's fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we use client-side rendering, the UI is mounted in an environment which is isolated from the ruby backend and has no access to it's data or config unless explicitly shared somehow. For instance, we don't use localization anymore from react components, we hardcode the strings on React components.

Due to this, the options we have to keep min size synced between client and server are:

  1. Add the password min size as a global variable in the DOM window object, we would need to edit the layout template for that.
  2. Use the data attribute from views, when mounting the React components. That would require editing all view templates for screens that include the password component and pass the pass size down through all the chain until the validator.

Both approaches seem pretty dirty to me. I prefer to just hardcode it to 15 in both client and server sides. Equally dirty, but much less complex.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we allow server to have that changed to more than 15 characters. UI stays as it is. If a customer needs to override, shorter passwords will fail on server validation. But they will still have the security settings they require although not ideal UX. That was my line of thinking. Not about allowing shorter password but only for longer where the UI will not stop then from entering a longer one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, I don't like that. I prefer UI and backend to be in sync, the opposite would be pretty confusing for client ad for future us.

assert_equal body['success'], true
end

test 'post without password creates user with no password' do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this user without a password login afterwards? I hope not before resetting its password.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, users without passwords can't login until they set a password. For this particular endpoint about partners, the endpoint requires setting either a password or an open_id. But users with open_id like the one in this test shouldn't be required to have a password in DB.

@jlledom jlledom force-pushed the THREESCALE-8916-admin-strong-paswords branch from 1138886 to bc746e2 Compare March 4, 2026 13:29
return false if Rails.configuration.three_scale.strong_passwords_disabled
return false if signup.sample_data?

validate_password?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to call this again?
As we have

      validates_presence_of :password, if: :validate_password?
      validates :password, length: { minimum: STRONG_PASSWORD_MIN_SIZE }, if: :validate_strong_password?

It means that most for when strong password is used (most of the time) validatee_password? will be called twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:

before_validation :normalize_password, if: :validate_password?

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 validate_password? is false but validate_strong_password? is true. Like for instance when you are modifying a user attribute which is not the password. What would happen then? would we validate the length of a password when that attribute is not involved in the update? Even more, password is a virtual attribute so I don't really know what would be there at the validation moment, most probably nil I guess.

This way validate_password? and validate_strong_password? always have consistent values.

akostadinov
akostadinov previously approved these changes Mar 13, 2026
Copy link
Contributor

@akostadinov akostadinov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great work, thank you for that!

I added some comments, but can be merged on your discretion.

I think that in a future PR it will be very valuable to also implement the following changes:

  • the default developer user should have a strong password and a mechanism for the provider account to know it. We may as well have a mechanism to generate default passwords based on provider id and some SECRET_KEY_BASE derived key so that we can show that in the provider portal.
  • remove the signup types, it will be less error prone if we treat everybody the same

def test_weak_password_rejected_when_strong_passwords_enabled
post signup_path, params: account_params(WEAK_PASSWORD)

assert_response :success
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn this be something unsuccessful? Seems wrong to return success when signup failed 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, because this is called from UI so one expects either :success or :redirect. In the case of error, the controller returns :success and renders the form again.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that we don't support partner accounts anymore but whatever, test can stay for the time being before we fully remove the functionality.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't support partners, that's actually better: we have one less thing to worry about. I'd be good to find out whether partners are actually not supported anymore so we can remove the logic.

It makes sense they are not supported because the way to create a partner is too cumbersome: you must create it directly form the rails console and then manually change the system_name attribute, since the generated one prevents creating tenant domains for new partner tenants.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember where but surely I read somewhere that we don't support them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert_match(/incorrect/i, flash[:danger])
end

test 'user with password can update when providing correct current password' do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should require current password for sso users but perhaps that's the safer option 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current password will be asked for any user that actually has a password in DB, no matter if they are SSO users or not. About SSO users with no password, there's no password we can require.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I didn't understand the test. I only see:

put :update, params: { user: { current_password: 'superSecret1234#', username: 'newusername' } }

So what is beingupdated? I don't see new password set. I assumed this current_password was only the new password.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test we are updating username. What the test asserts is we can't update our personal details without introducing our current password. I'm not sure how useful is this. Because I understand we should ask for the previous password if we are setting a new one, but there's no need to ask for the password to change other attributes, like the username. However, that's what we are enforcing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, got it, I read the test as intending to change the password. Fine with me.

assert @user.reload.authenticated?('new_password_123')
end

test 'SSO user without password can set password when enforce SSO is enabled' do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like a dup of SSO user can set password without providing current password above

Copy link
Contributor Author

@jlledom jlledom Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, they test the same, but the difference is the context. In one of them, SSO is enforced, in the other is not. I think both are relevant to catch regressions, in particular with passwrods tricky logic. The more tests the better.

end

def validate_strong_password?
return false if Rails.configuration.three_scale.strong_passwords_disabled
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be much better to set the STRONG_PASSWORD_MIN_SIZE based on this setting than overriding it here.

Because you said in a comment:

Nah, I don't like that. I prefer UI and backend to be in sync, the opposite would be pretty confusing for client ad for future us.

But this setting is essentially doing the same - UI will still want strong passwords but backend will allow weaker.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this setting is essentially doing the same - UI will still want strong passwords but backend will allow weaker.

You're right on this

I think it will be much better to set the STRONG_PASSWORD_MIN_SIZE based on this setting than overriding it here.

What do you mean by "overriding here"? overriding the setting? Also, if we set STRONG_PASSWORD_MIN_SIZE based on the setting, we still need this method to check for sample data.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I meant that this specific line overrides STRONG_PASSWORD_MIN_SIZE based on a configuration setting. So my suggestion was to simplify by setting STRONG_PASSWORD_MIN_SIZE based on a configuration value instead of moving the logic here.

Agreed that we still need the sample data check. Hopefully later we get rid of it by also generating strong password for John Doe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can get rid of this specific line regardless of John Doe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 validate_strong_password? method and then I'll worry about the tests.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.


def validate_strong_password?
return false if Rails.configuration.three_scale.strong_passwords_disabled
return false if signup.sample_data?
Copy link
Contributor

Choose a reason for hiding this comment

The 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 SECRET_KEY_BASE derivative and we can present that password to the provider in the UI when viewing the user.

  • provider opens the user
  • system detects that this is the first default user John Doe
  • system generates the default password
  • checks whether it matches the user
  • warns that the user has the default password of "..." OR reports that the password has changed from the default

I think this will be a good UX while keeping things way more secure.

test 'new user with password_digest' do
user = user_with_password.call('superSecret1234#')

assert user.authenticated?('superSecret1234#')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the original method represents the intention much better reads much better.

Suggested change
assert user.authenticated?('superSecret1234#')
assert user.authenticate('superSecret1234#')

I mean I can't understand the whole authenticated? alias. Maybe something like authenticates_by?("password") would be ok. Still I find using the standard method preferable.

But well that might only be me 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I understand, but the alias comes from the ancients. We could get rid of it but not in this PR I think. I'm calling authenticate from the tests I modified in this PR: d2dbd76

Copy link
Contributor Author

@jlledom jlledom Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While working on this I found there was a precedence bug in the authenticate definition. I fixed it here: 1b05dc3

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy either way. But I'm more happy now, thanks!

@jlledom jlledom force-pushed the THREESCALE-8916-admin-strong-paswords branch from 8dceb4b to 77fc304 Compare March 17, 2026 09:44
@jlledom
Copy link
Contributor Author

jlledom commented Mar 17, 2026

We can also generate default passwords derived from e.g., buyer id and SECRET_KEY_BASE derivative and we can present that password to the provider in the UI when viewing the user.

I'm fine with creating a strong password for john doe, but how do you suggest to generate the derivative exactly?

  • provider opens the user
  • system detects that this is the first default user John Doe
  • system generates the default password
  • checks whether it matches the user
  • warns that the user has the default password of "..." OR reports that the password has changed from the default

Fine for me.

@akostadinov
Copy link
Contributor

We can also generate default passwords derived from e.g., buyer id and SECRET_KEY_BASE derivative and we can present that password to the provider in the UI when viewing the user.

I'm fine with creating a strong password for john doe, but how do you suggest to generate the derivative exactly?

Something like this should be fine I think

module InitialDeveloperPassword
  # SALT used like the `purpose` in `MessageVerifier`
  SALT   = "onboarding.v1.sample-developer-password"
  KEY_BYTES    = 32   # 256-bit subkey
  LENGTH = 12 # password length

  def self.subkey
    # Uses Rails.application.secret_key_base under the hood
    @subkey = Rails.application.key_generator.generate_key(SALT, KEY_BYTES)
  end

  def self.payload_for(user)
    [user.id, user.created_at.to_i].join(":")
  end

  def self.for(user)
    mac   = OpenSSL::HMAC.digest("SHA256", subkey, payload_for(user))
    Base64.urlsafe_encode64(mac, padding: false)[0, LENGTH]
  end
end

Maybe this should be a service or idk where to put it, but in general I think this is a reasonable way to do it.

I think more work will be adjusting the UI to:

  • detect is the user is the sample Joe Doe
  • present the password to the admin
    • also warn the admin that in production password needs to be changed or user removed
    • OR tell admin that the password has been changed

@jlledom
Copy link
Contributor Author

jlledom commented Mar 17, 2026

We can also generate default passwords derived from e.g., buyer id and SECRET_KEY_BASE derivative and we can present that password to the provider in the UI when viewing the user.

@akostadinov https://redhat.atlassian.net/browse/THREESCALE-12470

akostadinov
akostadinov previously approved these changes Mar 20, 2026
Copy link
Contributor

@akostadinov akostadinov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, really nice!

I saw a couple of minor nitpicks but feel free to ignore 👼

}
end

def test_weak_password_rejected_when_strong_passwords_enabled
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use test "weak password rejected when strong passwords enabled"?

just another minor nitpick

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude did it 😞

cec3bf7

@jlledom jlledom force-pushed the THREESCALE-8916-admin-strong-paswords branch from cec3bf7 to c8c8e99 Compare March 20, 2026 12:39
jlledom and others added 8 commits March 20, 2026 13:57
- Validate strong passwords in React forms
- Some forms treat the password field as clear text.
- Make password fields required only when they are actually required.
- `Current password` field visible and required only when it should.
- Password can be set for all users, no matter how did they signup.
- SSO Users: don't see the toast to change password.
  It can be changed from the same form
  This fixes https://issues.redhat.com/browse/THREESCALE-11548
- When enforce SSO is enabled, passwords can still be set or changed.
This is to exclude John Doe from strong passwords validation

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@jlledom jlledom force-pushed the THREESCALE-8916-admin-strong-paswords branch from c8c8e99 to ae75cc5 Compare March 20, 2026 13:06
@jlledom
Copy link
Contributor Author

jlledom commented Mar 20, 2026

Rebased and fixed conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants