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.
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
algorithmkeyword argument (open to alternative naming suggestions)
This would still default to bcrypt to prevent breaking changes.
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
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