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