class PasswordIsStrongValidator

Validates that the password is strong.

Constants

COMMON_PASSWORDS

Password that are used too often and will be easily guessed.

Public Instance Methods

validate_each(record, attribute, value) click to toggle source

Validates that `value` is a strong password. A password is strong if it meets the following rules:

  • SHOULD contain at least one letter.

  • SHOULD contain at least one digit.

  • SHOULD contain at least one special character.

  • SHOULD NOT contain `record.username` (case-insensitive).

  • SHOULD NOT be in {COMMON_PASSWORDS}.

  • SHOULD NOT repetitions.

@param record [#errors, username, ActiveRecord::Base] ActiveModel or ActiveRecord that supports username method. @param attribute [Symbol] password attribute name. @param value [String] a password. @return [void]

# File app/validators/password_is_strong_validator.rb, line 26
def validate_each(record, attribute, value)
  return if value.blank?

  if is_simple?(value)
    record.errors[attribute] << 'must contain letters, numbers, and at least one special character'
  end

  if contains_username?(record.username, value)
    record.errors[attribute] << 'must not contain the username'
  end

  if is_common_password?(value)
    record.errors[attribute] << 'must not be a common password'
  end

  if contains_repetition?(value)
    record.errors[attribute] << 'must not be a predictable sequence of characters'
  end
end

Private Instance Methods

contains_repetition?(password) click to toggle source

Password repetition (quite basic) – no “aaaaaa” or “ababab” or “abcabc” or “abcdabcd” (but note that the user can use “aaaaaab” or something).

@param password [String] @return [Boolean]

# File app/validators/password_is_strong_validator.rb, line 53
def contains_repetition?(password)
  if password.scan(/./).uniq.size < 2
    return true
  end

  if (password.size % 2 == 0) and (password.scan(/../).uniq.size < 2)
    return true
  end

  if (password.size % 3 == 0) and (password.scan(/.../).uniq.size < 2)
    return true
  end

  if (password.size % 4 == 0) and (password.scan(/..../).uniq.size < 2)
    return true
  end

  false
end
contains_username?(username, password) click to toggle source

Whether username is in password (case-insensitively).

@return [false] if `password` is blank. @return [false] if `username` is blank. @return [false] unless `username` is in `password`. @return [true] if `username` is in `password`.

# File app/validators/password_is_strong_validator.rb, line 79
def contains_username?(username, password)
  contains = false

  unless password.blank? or username.blank?
    escaped_username = Regexp.escape(username)
    username_regexp = Regexp.new(escaped_username, Regexp::IGNORECASE)

    if username_regexp.match(password)
      contains = true
    end
  end

  contains
end
is_common_password?(password) click to toggle source

Whether `password` is in {COMMON_PASSWORDS} or a simple variation of a password in {COMMON_PASSWORDS}.

@param password [String] @return [Boolean]

# File app/validators/password_is_strong_validator.rb, line 98
def is_common_password?(password)
  COMMON_PASSWORDS.each do |pw|
    common_pw = [pw, pw + "!", pw + "1", pw + "12", pw + "123", pw + "1234"]
    if common_pw.include?(password.downcase)
      return true
    end
  end
  false
end
is_simple?(password) click to toggle source

Returns whether the password is simple.

@return [false] if password contains a letter, digit and special character. @return [true] otherwise

# File app/validators/password_is_strong_validator.rb, line 112
def is_simple?(password)
  not (password =~ /[A-Za-z]/ and password =~ /[0-9]/ and password =~ /[\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x3a\x3b\x3c\x3d\x3e\x3f\x5b\x5c\x5d\x5e\x5f\x60\x7b\x7c\x7d\x7e]/)
end