Allow has_secure_password to configure hashing algorithm

I’d like to propose (and offer to implement) additional functionality to the has_secure_password feature, and wanted to gauge the appetite for this from the core team and the community.

The has_secure_password method currently supports bcrypt which has been the industry standard for a good while. However, some advise suggests Argon2id should be the first choice for password hashing, and inevitably eventually the best practices will change (to this or another algorithm).

NB. Cryptography and trends in security best-practices isn’t my area of expertise so please correct me on the above if it’s incorrect

Given the ongoing progression of hardware it feels prudent to firstly support newer hashing algorithms, but also allow has_secure_password to be extensible going forwards.

My proposal would be as follows:

  1. Allow has_secure_password to accept an optional algorithm keyword argument (open to alternative naming suggestions)

This would still default to bcrypt to prevent breaking changes.

For example:

class User < ApplicationRecord
  has_secure_password algorithm: :argon2id
end
  1. Refactor to choose an implementation based on the algorithm specified

Move the bcrypt version into ActiveModel::SecurePassword::BCrypt and delegate methods to that class/module.

Implement other algorithms (such as SCrypt and Argon2Id) which provides the same API as the BCrypt implementation.

Load the implementation based on the algorithm specified on the has_secure_password call (falling back to the default is none provided).

Each implementation can require the relevant underlying gem to perform the hashing.

Future proofing migrations

Considering a future scenario where an app needs to make the switch from bcrypt to say argon2id, a developer could then do the following:

class User < ApplicationRecord
  # Original implementation using BCrypt
  has_secure_password

  # New implementation
  has_secure_password :replacement_password, algorithm: :argon2id
end

The new password could then be safely migrated and stored on successful login:

class SessionsController < ApplicationController
  def create
    if (user = User.authenticate_by(user_params))
      user.replacement_password = user_params[:password]
      user.save!
      redirect_to root_path
    else
      ...
    end
  end

  private

  def user_params
    params.permit(:username, :password)
  end
end

Notes

A suggestion was made for this back in Feb 2021, but rejected without rationale. There may have been perfectly valid reasoning for not considering with hashing algorithms at the time. If this is still the case I would be interested in understanding the reasoning.

There’s a third party implement here that shows this is possible to implement

3 Likes