Class: Philiprehberger::Password::Policy

Inherits:
Object
  • Object
show all
Defined in:
lib/philiprehberger/password/policy.rb

Defined Under Namespace

Classes: Result

Instance Method Summary collapse

Constructor Details

#initialize(min_length: 8, max_length: 128, require_uppercase: false, require_lowercase: false, require_digit: false, require_symbol: false, reject_common: true, custom_passwords: []) ⇒ Policy

Returns a new instance of Policy.



8
9
10
11
12
13
14
15
16
17
18
# File 'lib/philiprehberger/password/policy.rb', line 8

def initialize(min_length: 8, max_length: 128, require_uppercase: false, require_lowercase: false,
               require_digit: false, require_symbol: false, reject_common: true, custom_passwords: [])
  @min_length = min_length
  @max_length = max_length
  @require_uppercase = require_uppercase
  @require_lowercase = require_lowercase
  @require_digit = require_digit
  @require_symbol = require_symbol
  @reject_common = reject_common
  @custom_passwords = Set.new(custom_passwords.map { |p| p.to_s.downcase }).freeze
end

Instance Method Details

#validate(password, context: {}) ⇒ Result

Validate a password against the policy. Accepts an optional context hash for context-aware validation.

Parameters:

  • password (String)

    the password to validate

  • context (Hash) (defaults to: {})

    optional context with :username, :email, :app_name keys

Returns:

  • (Result)

    validation result with valid?, errors, and score



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/philiprehberger/password/policy.rb', line 26

def validate(password, context: {})
  errors = []

  return Result.new(valid?: false, errors: ['Password is required'], score: 0) if password.nil?

  pwd = password.to_s

  errors << "must be at least #{@min_length} characters" if pwd.length < @min_length
  errors << "must be at most #{@max_length} characters" if pwd.length > @max_length
  errors << 'must contain at least one uppercase letter' if @require_uppercase && !pwd.match?(/[A-Z]/)
  errors << 'must contain at least one lowercase letter' if @require_lowercase && !pwd.match?(/[a-z]/)
  errors << 'must contain at least one digit' if @require_digit && !pwd.match?(/\d/)
  errors << 'must contain at least one symbol' if @require_symbol && !pwd.match?(/[^a-zA-Z\d]/)
  errors << 'password is too common' if @reject_common && CommonPasswords.include?(pwd)
  if @custom_passwords.include?(pwd.downcase) && !errors.include?('password is too common')
    errors << 'password is too common'
  end

  # Context-aware validation
  errors.concat(validate_context(pwd, context)) unless context.nil? || context.empty?

  score = Strength.compute(pwd)[:score]

  Result.new(valid?: errors.empty?, errors: errors, score: score)
end