bitzesty/devise_zxcvbn

View on GitHub
lib/devise_zxcvbn/model.rb

Summary

Maintainability
A
1 hr
Test Coverage
require "devise_zxcvbn/email_tokeniser"
require "devise_zxcvbn/errors/devise_zxcvbn_error"
require "ostruct"

module Devise
  module Models
    module Zxcvbnable
      extend ActiveSupport::Concern

      delegate :min_password_score, to: "self.class"
      delegate :zxcvbn_tester, to: "self.class"

      included do
        validate :strong_password, unless: :skip_password_complexity?
      end

      def password_score
        @password_score = self.class.password_score(self)
      end

      def password_weak?
        password_score.score < min_password_score
      end

      protected

      def skip_password_complexity?
        !password_required?
      end

      private

      def strong_password
        if errors.messages.blank? && password_weak?
          errors.add :password, :weak_password, **i18n_variables
        end
      end

      def i18n_variables
        {
          feedback: zxcvbn_feedback,
          crack_time_display: time_to_crack,
          score: password_score.score,
          min_password_score: min_password_score
        }
      end

      def zxcvbn_feedback
        feedback = password_score.feedback.values.flatten.reject(&:empty?)
        return "Add another word or two. Uncommon words are better." if feedback.empty?

        feedback.join(". ").gsub(/\.\s*\./, ".")
      end

      def time_to_crack
        password_score.crack_times_display["offline_fast_hashing_1e10_per_second"]
      end

      class_methods do
        Devise::Models.config(self, :min_password_score)
        Devise::Models.config(self, :zxcvbn_tester)

        def password_score(user, arg_email = nil)
          return raise DeviseZxcvbnError, "the object must respond to password" unless user.respond_to?(:password)

          password = user.password.to_s

          zxcvbn_weak_words = []

          if arg_email
            zxcvbn_weak_words += [arg_email, *DeviseZxcvbn::EmailTokeniser.split(arg_email)]
          end

          # User method results are saved locally to prevent repeat calls that might be expensive
          if user.respond_to?(:email)
            local_email = user.email
            zxcvbn_weak_words += [local_email, *DeviseZxcvbn::EmailTokeniser.split(local_email)]
          end

          if user.respond_to?(:weak_words)
            return raise DeviseZxcvbnError, "weak_words must return an Array" unless user.weak_words.is_a?(Array)

            local_weak_words = user.weak_words
            zxcvbn_weak_words += local_weak_words
          end

          zxcvbn_tester.test(password, zxcvbn_weak_words)
        end
      end
    end
  end
end