Enhancements to has_secure_password

has_secure_password is great and was largely easy to implement in favour or heavier auth gems. It does have some quirks:

  • No minimum password length enforcement
  • No support for a ‘set password only’ form where the user is just entering (or resetting) their password without other details present. If the user doesn’t enter a password (i.e. an empty string is set), the password setter doesn’t set the empty string, it simply doesn’t assign a password at all. This skips any manual minimum password length validations (since these need to be allow_nil: true).

A more complete description of the problem is here: Updating password defined by `has_secured_password` with empty string does not trigger validation error · Issue #34348 · rails/rails · GitHub

I wonder if we could use the attributes api to back this virtual password column instead?

https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html

In case any of the above was unclear, I just struggled to implement a fairly common pattern (don’t set a password on user creation (if created by an admin), or set a random password), then email the user to ask them to activate their account by setting a password. This is difficult out of the box with has_secure_password.

The allow_nil true bit is an interesting gotcha here.

From the discussion on that issue, it looks like there was some interest in fixing it but that it stalled out because people couldn’t figure out a good upgrade path for applications that might currently depend on this behavior.

What do folks think that upgrade path might look like?

In my case the simple fix was to patch the password= method so that it catered for blank string password assignment. This is the method that would need to be fixed:

https://github.com/rails/rails/blob/master/activemodel/lib/active_model/secure_password.rb#L95-L103

As you can see there is a logic hole where if the password isn’t nil and isn’t !empty (excuse the double negative) then nothing happens.

  # Deal with blank passwords not being actually set for validation
  def password=(password)
    @password = password if !password.nil? && password.blank?
    super
  end

I guess overall though, the use case is maybe a bit too opinionated. has_secure_password should perhaps just deal with the mechanics of setting and verifying a password securely. Perhaps the validation could be moved off into a seperate validator that can cope with the two common cases of:

  • user makes their own account and sets the password immediately,
  • or admin makes the user’s account and and invites them to activate and set a password at a later date.