class NotPwnedValidator
An ActiveModel
validator to check passwords against the Pwned
Passwords API.
@example Validate a password on a User
model with the default options.
class User < ApplicationRecord validates :password, not_pwned: true end
@example Validate a password on a User
model with a custom error message.
class User < ApplicationRecord validates :password, not_pwned: { message: "has been pwned %{count} times" } end
@example Validate a password on a User
model that allows the password to have been breached once.
class User < ApplicationRecord validates :password, not_pwned: { threshold: 1 } end
@example Validate a password on a User
model, handling API errors in various ways
class User < ApplicationRecord # The record is marked as invalid on network errors # (error message "could not be verified against the past data breaches".) validates :password, not_pwned: { on_error: :invalid } # The record is marked as invalid on network errors with custom error. validates :password, not_pwned: { on_error: :invalid, error_message: "might be pwned" } # An error is raised on network errors. # This means that `record.valid?` will raise `Pwned::Error`. # Not recommended to use in production. validates :password, not_pwned: { on_error: :raise_error } # Call custom proc on error. For example, capture errors in Sentry, # but do not mark the record as invalid. validates :password, not_pwned: { on_error: ->(record, error) { Raven.capture_exception(error) } } end
@since 1.2.0
Constants
- DEFAULT_ON_ERROR
The default behaviour of this validator in the case of an API failure. The default will mean that if the API fails the object will not be marked invalid.
- DEFAULT_THRESHOLD
The default threshold for whether a breach is considered pwned. The default is 0, so any password that appears in a breach will mark the record as invalid.
Public Instance Methods
Validates the value
against the Pwned
Passwords API. If the pwned_count
is higher than the optional threshold
then the record is marked as invalid.
In the case of an API error the validator will either mark the record as valid or invalid. Alternatively it will run an associated proc or re-raise the original error.
The validation will short circuit and return with no errors added if the password is blank. The Pwned::Password
initializer expects the password to be a string and will throw a TypeError
if it is nil
. Also, technically the empty string is not a password that is reported to be found in data breaches, so returns false
, short circuiting that using value.blank?
saves us a trip to the API.
@param record [ActiveModel::Validations] The object being validated @param attribute [Symbol] The attribute on the record that is currently
being validated.
@param value [String] The value of the attribute on the record that is the
subject of the validation
# File lib/pwned/not_pwned_validator.rb, line 77 def validate_each(record, attribute, value) return if value.blank? begin pwned_check = Pwned::Password.new(value, request_options) if pwned_check.pwned_count > threshold record.errors.add(attribute, :not_pwned, **options.merge(count: pwned_check.pwned_count)) end rescue Pwned::Error => error case on_error when :invalid record.errors.add(attribute, :pwned_error, **options.merge(message: options[:error_message])) when :valid # Do nothing, consider the record valid when Proc on_error.call(record, error) else raise end end end
Private Instance Methods
# File lib/pwned/not_pwned_validator.rb, line 100 def on_error options[:on_error] || DEFAULT_ON_ERROR end
# File lib/pwned/not_pwned_validator.rb, line 104 def request_options options[:request_options] || {} end
# File lib/pwned/not_pwned_validator.rb, line 108 def threshold threshold = options[:threshold] || DEFAULT_THRESHOLD raise TypeError, "#{self.class.to_s} option 'threshold' must be of type Integer" unless threshold.is_a? Integer threshold end