module NaClPassword::Concern::ClassMethods

Public Instance Methods

nacl_password(attribute = :password, digest_attribute: nil, **opts) click to toggle source

Adds methods to set and authenticate against an Argon2 password. This mechanism requires you to have a XXX_digest attribute. Where XXX is the attribute name of your desired password. the digest attribute to use can be set by passing `digest_attribute: non_standard_attribute` to `nacl_password`

The following validations are added automatically:

  • Password must be present on creation

  • Password length should be less than or equal to 1024 bytes

  • Confirmation of password (using a XXX_confirmation attribute)

If confirmation validation is not needed, simply leave out the value for XXX_confirmation (i.e. don't provide a form field for it). When this attribute has a nil value, the validation will not be triggered.

It is also possible to suppress the default validations completely by passing skip_validations: true as an argument.

Add rbnacl (~> 7.1) to Gemfile to use nacl_password:

gem "rbnacl", "~> 7.1"

Example:

# Schema: User(name:string, password_digest:string, recovery_password_digest:string)
class User < ActiveRecord::Base
  include NaClPassword::Concern
  nacl_password
  nacl_password :recovery_password, validations: false
end

user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
user.save                                                  # => false, password required
user.password = 'mUc3m00RsqyRe'
user.save                                                  # => false, confirmation doesn't match
user.password_confirmation = 'mUc3m00RsqyRe'
user.save                                                  # => true
user.recovery_password = "42password"
user.recovery_password_digest                              # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
user.save                                                  # => true
user.authenticate('notright')                              # => false
user.authenticate('mUc3m00RsqyRe')                         # => user
user.authenticate_recovery_password('42password')          # => user
User.find_by(name: 'david')&.authenticate('notright')      # => false
User.find_by(name: 'david')&.authenticate('mUc3m00RsqyRe') # => user
# File lib/nacl_password/concern.rb, line 57
def nacl_password(attribute = :password, digest_attribute: nil, **opts)
  NaClPassword.setup

  digest_attribute ||= "#{attribute}_digest"

  attribute = attribute.to_sym
  digest_attribute = digest_attribute.to_sym

  if digest_attribute.to_s == attribute.to_s
    raise ArgumentError, "Digest Attribute Name can't be the same as Password Attribute Name"
  end

  skip_validations =
    CoerceBoolean.from(opts[:skip_validations]) &&
    (opts[:skip_validations] != :blank)

  length_options =
    skip_validations ? {} :
      { maximum: NaClPassword::MAX_PASSWORD_LENGTH }.
      merge(
        opts[:min_length] == :none \
          ? {} \
          : { minimum: opts[:min_length].presence&.to_i || 8 }
      )


  include InstanceMethodsOnActivation.new(attribute.to_sym, digest_attribute, **length_options)

  unless skip_validations
    include ActiveModel::Validations

    # This ensures the model has a password by checking whether the password_digest
    # is present, so that this works with both new and existing records. However,
    # when there is an error, the message is added to the password attribute instead
    # so that the error message will make sense to the end-user.
    unless opts[:skip_validations] == :blank
      validate do |record|
        unless record.__send__(digest_attribute).present?
          record.errors.add(attribute, :blank)
        end
      end
    end

    validates_length_of attribute, **length_options, allow_blank: true

    validates_confirmation_of attribute, allow_blank: true
  end
end