devise-secure_password 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile +44 -0
- data/Dockerfile.prev +44 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +280 -0
- data/LICENSE.txt +21 -0
- data/README.md +326 -0
- data/Rakefile +11 -0
- data/app/controllers/devise/passwords_with_policy_controller.rb +52 -0
- data/app/views/devise/passwords_with_policy/edit.html.erb +16 -0
- data/bin/console +14 -0
- data/bin/setup +6 -0
- data/config/locales/en.yml +71 -0
- data/devise-secure_password.gemspec +57 -0
- data/docker-entrypoint.sh +6 -0
- data/gemfiles/rails-5_0_6.gemfile +17 -0
- data/gemfiles/rails-5_1_4.gemfile +16 -0
- data/lib/devise/secure_password.rb +70 -0
- data/lib/devise/secure_password/controllers/active_helpers.rb +40 -0
- data/lib/devise/secure_password/controllers/devise_helpers.rb +64 -0
- data/lib/devise/secure_password/hooks/password_requires_regular_updates.rb +5 -0
- data/lib/devise/secure_password/models/password_disallows_frequent_changes.rb +60 -0
- data/lib/devise/secure_password/models/password_disallows_frequent_reuse.rb +71 -0
- data/lib/devise/secure_password/models/password_has_required_content.rb +131 -0
- data/lib/devise/secure_password/models/password_requires_regular_updates.rb +56 -0
- data/lib/devise/secure_password/models/previous_password.rb +20 -0
- data/lib/devise/secure_password/routes.rb +11 -0
- data/lib/devise/secure_password/version.rb +5 -0
- data/lib/generators/devise/secure_password/install_generator.rb +30 -0
- data/lib/generators/devise/templates/README.txt +21 -0
- data/lib/generators/devise/templates/secure_password.rb +43 -0
- data/lib/support/string/character_counter.rb +53 -0
- data/pkg/devise-secure_password-1.0.0.gem +0 -0
- metadata +471 -0
data/bin/setup
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
en:
|
2
|
+
secure_password:
|
3
|
+
password_has_required_content:
|
4
|
+
errors:
|
5
|
+
messages:
|
6
|
+
unknown_characters: "contains %{count} invalid %{subject}"
|
7
|
+
minimum_characters: "must contain at least %{count} %{type} %{subject}"
|
8
|
+
maximum_characters: "must contain less than %{count} %{type} %{subject}"
|
9
|
+
password_disallows_frequent_reuse:
|
10
|
+
errors:
|
11
|
+
messages:
|
12
|
+
password_is_recent: "Last %{count} passwords may not be reused"
|
13
|
+
password_disallows_frequent_changes:
|
14
|
+
errors:
|
15
|
+
messages:
|
16
|
+
password_is_recent: "Password cannot be changed more than once per %{timeframe}"
|
17
|
+
password_requires_regular_updates:
|
18
|
+
alerts:
|
19
|
+
messages:
|
20
|
+
password_updated: "Your password has been updated."
|
21
|
+
errors:
|
22
|
+
messages:
|
23
|
+
password_expired: "Your password has expired. Passwords must be changed every %{timeframe}"
|
24
|
+
devise:
|
25
|
+
passwords_with_policy:
|
26
|
+
edit:
|
27
|
+
titles:
|
28
|
+
section_title: "Change your password"
|
29
|
+
labels:
|
30
|
+
current_password: "Current password"
|
31
|
+
new_password: "New password"
|
32
|
+
confirm_new_password: "Confirm new password"
|
33
|
+
buttons:
|
34
|
+
change_my_password: "Change my password"
|
35
|
+
# Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words()
|
36
|
+
datetime:
|
37
|
+
distance_in_words:
|
38
|
+
half_a_minute: "half a minute"
|
39
|
+
less_than_x_seconds:
|
40
|
+
one: "1 second" # default was: "less than 1 second"
|
41
|
+
other: "%{count} seconds" # default was: "less than %{count} seconds"
|
42
|
+
x_seconds:
|
43
|
+
one: "1 second"
|
44
|
+
other: "%{count} seconds"
|
45
|
+
less_than_x_minutes:
|
46
|
+
one: "a minute" # default was: "less than a minute"
|
47
|
+
other: "%{count} minutes" # default was: "less than %{count} minutes"
|
48
|
+
x_minutes:
|
49
|
+
one: "1 minute"
|
50
|
+
other: "%{count} minutes"
|
51
|
+
about_x_hours:
|
52
|
+
one: "1 hour" # default was: "about 1 hour"
|
53
|
+
other: "%{count} hours" # default was: "about %{count} hours"
|
54
|
+
x_days:
|
55
|
+
one: "1 day"
|
56
|
+
other: "%{count} days"
|
57
|
+
about_x_months:
|
58
|
+
one: "1 month" # default was: "about 1 month"
|
59
|
+
other: "%{count} months" # default was: "about %{count} months"
|
60
|
+
x_months:
|
61
|
+
one: "1 month"
|
62
|
+
other: "%{count} months"
|
63
|
+
about_x_years:
|
64
|
+
one: "1 year" # default was: "about 1 year"
|
65
|
+
other: "%{count} years" # default was: "about %{count} years"
|
66
|
+
over_x_years:
|
67
|
+
one: "1 year" # default was: "over 1 year"
|
68
|
+
other: "%{count} years" # default was: "over %{count} years"
|
69
|
+
almost_x_years:
|
70
|
+
one: "1 year" # default was: "almost 1 year"
|
71
|
+
other: "%{count} years" # default was: "almost %{count} years"
|
@@ -0,0 +1,57 @@
|
|
1
|
+
#
|
2
|
+
# devise-secure_password.gemspec
|
3
|
+
#
|
4
|
+
lib = File.expand_path('lib', __dir__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
|
7
|
+
require 'date'
|
8
|
+
require 'devise/secure_password/version'
|
9
|
+
|
10
|
+
Gem::Specification.new do |spec|
|
11
|
+
spec.name = 'devise-secure_password'
|
12
|
+
spec.version = Devise::SecurePassword::VERSION.dup
|
13
|
+
spec.platform = Gem::Platform::RUBY
|
14
|
+
|
15
|
+
spec.authors = ['Mark Eissler']
|
16
|
+
spec.email = ['mark.eissler@valimail.com']
|
17
|
+
|
18
|
+
spec.summary = 'A devise password policy enforcement extension.'
|
19
|
+
spec.description = 'Adds configurable password policy enforcement to devise.'
|
20
|
+
|
21
|
+
spec.homepage = 'https://github.com/valimail/devise-secure_password'
|
22
|
+
spec.license = 'MIT'
|
23
|
+
|
24
|
+
spec.files = Dir['./**/*'].reject do |f|
|
25
|
+
f.match(%r{^./(test|spec|features|lib/tasks)/|Gemfile.lock.ci})
|
26
|
+
end
|
27
|
+
spec.executables = spec.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
28
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
29
|
+
spec.require_paths = ['lib']
|
30
|
+
|
31
|
+
spec.add_runtime_dependency 'devise', '>= 4.0.0', '< 5.0.0'
|
32
|
+
spec.add_runtime_dependency 'railties', '>= 5.0.0', '< 6.0.0'
|
33
|
+
|
34
|
+
spec.add_development_dependency 'bundler', '~> 1.16', '>= 1.16.1'
|
35
|
+
spec.add_development_dependency 'capybara', '~> 2.16', '>= 2.16.1'
|
36
|
+
spec.add_development_dependency 'capybara-screenshot', '~> 1.0', '>= 1.0.18'
|
37
|
+
spec.add_development_dependency 'coffee-rails', '~> 4.2'
|
38
|
+
spec.add_development_dependency 'database_cleaner', '~> 1.6', '>= 1.6.2'
|
39
|
+
spec.add_development_dependency 'devise', '~> 4.0'
|
40
|
+
spec.add_development_dependency 'flay', '~> 2.10', '>= 2.10.0'
|
41
|
+
spec.add_development_dependency 'launchy', '~> 2.4', '>= 2.4.3'
|
42
|
+
spec.add_development_dependency 'rails', '~> 5.1', '>= 5.1.4'
|
43
|
+
spec.add_development_dependency 'rake', '~> 12.3'
|
44
|
+
spec.add_development_dependency 'rspec', '~> 3.7'
|
45
|
+
spec.add_development_dependency 'rspec-rails', '~> 3.7'
|
46
|
+
spec.add_development_dependency 'rspec_junit_formatter', '~> 0.3'
|
47
|
+
spec.add_development_dependency 'rubocop', '~> 0'
|
48
|
+
spec.add_development_dependency 'ruby2ruby', '~> 2.4', '>= 2.4.0'
|
49
|
+
spec.add_development_dependency 'sass-rails', '~> 5.0'
|
50
|
+
spec.add_development_dependency 'selenium-webdriver', '~> 3.7', '>= 3.7.0'
|
51
|
+
spec.add_development_dependency 'simplecov', '~> 0.15.1'
|
52
|
+
spec.add_development_dependency 'simplecov-console', '~> 0.4.2'
|
53
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3', '>= 1.3.13'
|
54
|
+
spec.add_development_dependency 'therubyracer', '~> 0.12.3'
|
55
|
+
|
56
|
+
spec.required_ruby_version = '>= 2.4'
|
57
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
4
|
+
|
5
|
+
ENV['RAILS_TARGET'] ||= '5.0.6'
|
6
|
+
|
7
|
+
gemspec path: '../'
|
8
|
+
|
9
|
+
group :development, :test do
|
10
|
+
gem 'byebug', '>= 0'
|
11
|
+
end
|
12
|
+
|
13
|
+
group :test do
|
14
|
+
gem 'jquery-rails', '~> 4.3.1'
|
15
|
+
gem 'rails', '~> 5.0.0'
|
16
|
+
gem 'shoulda-matchers', git: 'https://github.com/thoughtbot/shoulda-matchers.git', branch: 'rails-5'
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
4
|
+
|
5
|
+
ENV['RAILS_TARGET'] ||= '5.1.4'
|
6
|
+
|
7
|
+
gemspec path: '../'
|
8
|
+
|
9
|
+
group :development, :test do
|
10
|
+
gem 'byebug', '>= 0'
|
11
|
+
end
|
12
|
+
|
13
|
+
group :test do
|
14
|
+
gem 'rails', '~> 5.1.0'
|
15
|
+
gem 'shoulda-matchers', git: 'https://github.com/thoughtbot/shoulda-matchers.git', branch: 'rails-5'
|
16
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
#
|
2
|
+
# lib/devise-secure_password.rb
|
3
|
+
#
|
4
|
+
require 'active_support/concern'
|
5
|
+
require 'devise'
|
6
|
+
require 'devise/secure_password/routes'
|
7
|
+
require 'devise/secure_password/version'
|
8
|
+
require 'devise/secure_password/models/password_has_required_content'
|
9
|
+
require 'devise/secure_password/models/password_disallows_frequent_reuse'
|
10
|
+
require 'devise/secure_password/models/password_disallows_frequent_changes'
|
11
|
+
require 'devise/secure_password/models/password_requires_regular_updates'
|
12
|
+
|
13
|
+
module Devise
|
14
|
+
# password_content_enforcement configuration parameters
|
15
|
+
@password_required_uppercase_count = 1
|
16
|
+
@password_required_lowercase_count = 1
|
17
|
+
@password_required_number_count = 1
|
18
|
+
@password_required_special_count = 1
|
19
|
+
|
20
|
+
# password_frequent_reuse_prevention configuration parameters
|
21
|
+
@password_previously_used_count = 24
|
22
|
+
|
23
|
+
# password_frequent_change_prevention configuration parameters
|
24
|
+
@password_minimum_age = 1.day
|
25
|
+
|
26
|
+
# password_regular_update_enforcement configuration parameters
|
27
|
+
@password_maximum_age = 60.days
|
28
|
+
|
29
|
+
class << self
|
30
|
+
attr_accessor :password_required_uppercase_count
|
31
|
+
attr_accessor :password_required_lowercase_count
|
32
|
+
attr_accessor :password_required_number_count
|
33
|
+
attr_accessor :password_required_special_count
|
34
|
+
attr_accessor :password_previously_used_count
|
35
|
+
attr_accessor :password_minimum_age
|
36
|
+
attr_accessor :password_maximum_age
|
37
|
+
end
|
38
|
+
|
39
|
+
module SecurePassword
|
40
|
+
module Controllers
|
41
|
+
autoload :DeviseHelpers, 'devise/secure_password/controllers/devise_helpers'
|
42
|
+
autoload :ActiveHelpers, 'devise/secure_password/controllers/active_helpers'
|
43
|
+
end
|
44
|
+
|
45
|
+
class Engine < ::Rails::Engine
|
46
|
+
ActiveSupport.on_load(:devise_controller) do
|
47
|
+
include ActionView::Helpers::DateHelper
|
48
|
+
include Devise::SecurePassword::Controllers::DeviseHelpers
|
49
|
+
end
|
50
|
+
ActiveSupport.on_load(:action_controller) do
|
51
|
+
include ActionView::Helpers::DateHelper
|
52
|
+
include Devise::SecurePassword::Controllers::ActiveHelpers
|
53
|
+
end
|
54
|
+
|
55
|
+
# add exceptions to the inflector so it doesn't get tripped up by our concerns that end in an 's'
|
56
|
+
ActiveSupport::Inflector.inflections do |inflect|
|
57
|
+
inflect.uncountable :password_disallows_frequent_changes
|
58
|
+
inflect.uncountable :password_requires_regular_updates
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# modules
|
65
|
+
Devise.add_module :password_has_required_content, model: 'devise/secure_password/models/password_has_required_content'
|
66
|
+
Devise.add_module :password_disallows_frequent_reuse, model: 'devise/secure_password/models/password_disallows_frequent_reuse'
|
67
|
+
Devise.add_module :password_disallows_frequent_changes, model: 'devise/secure_password/models/password_disallows_frequent_changes'
|
68
|
+
Devise.add_module :password_requires_regular_updates,
|
69
|
+
model: 'devise/secure_password/models/password_requires_regular_updates',
|
70
|
+
controller: :passwords_with_policy, route: :passwords_with_policy
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Devise
|
2
|
+
module SecurePassword
|
3
|
+
module Controllers
|
4
|
+
module ActiveHelpers
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_action :pending_password_expired_redirect!, except: [:destroy]
|
9
|
+
end
|
10
|
+
|
11
|
+
# Redirect to password change page if password needs to be changed.
|
12
|
+
def pending_password_expired_redirect!
|
13
|
+
return unless skip_current_controller? && redirected_in_session? && warden.session && warden.session['secure_password_expired']
|
14
|
+
redirect_to edit_user_password_with_policy_url, alert: "#{error_string_for_password_expired}."
|
15
|
+
end
|
16
|
+
|
17
|
+
def redirected_in_session?
|
18
|
+
warden.authenticated? && warden.session['secure_password_last_controller'] == 'Devise::SessionsController'
|
19
|
+
end
|
20
|
+
|
21
|
+
def skip_current_controller?
|
22
|
+
exclusion_list = [
|
23
|
+
'Devise::SessionsController',
|
24
|
+
'Devise::PasswordsWithPolicyController#edit',
|
25
|
+
'Devise::PasswordsWithPolicyController#update'
|
26
|
+
]
|
27
|
+
exclusion_list.select { |e| e == "#{self.class.name}#" + action_name || e == self.class.name.to_s }.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def error_string_for_password_expired
|
31
|
+
return 'password expired' unless warden.user.class.respond_to?(:password_maximum_age)
|
32
|
+
I18n.t(
|
33
|
+
'secure_password.password_requires_regular_updates.errors.messages.password_expired',
|
34
|
+
timeframe: distance_of_time_in_words(warden.user.class.password_maximum_age)
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Devise
|
2
|
+
module SecurePassword
|
3
|
+
module Controllers
|
4
|
+
module DeviseHelpers
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# rubocop:disable Style/ClassAndModuleChildren
|
8
|
+
class ::DeviseController
|
9
|
+
alias old_require_no_authentication require_no_authentication
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
# Override the devise require_no_authentication before callback so users
|
14
|
+
# have to prevent authenticated users with expired passwords from
|
15
|
+
# escaping to other pages without first updating their passwords.
|
16
|
+
def require_no_authentication
|
17
|
+
return if check_password_expired_and_redirect!
|
18
|
+
|
19
|
+
old_require_no_authentication
|
20
|
+
end
|
21
|
+
|
22
|
+
# Store the name of the current controller and action in the warden
|
23
|
+
# session store then redirect if signed in and password expired. The
|
24
|
+
# stored values will be used by non-devise controllers to prevent a
|
25
|
+
# user from escaping the change password process.
|
26
|
+
def check_password_expired_and_redirect!
|
27
|
+
assert_is_devise_resource!
|
28
|
+
|
29
|
+
return if skip_current_devise_controller?
|
30
|
+
|
31
|
+
if signed_in?(scope_name) && warden.session(scope_name)[:secure_password_expired]
|
32
|
+
save_controller_state
|
33
|
+
redirect_to edit_user_password_with_policy_url, alert: "#{error_string_for_password_expired}."
|
34
|
+
return true
|
35
|
+
end
|
36
|
+
|
37
|
+
false
|
38
|
+
end
|
39
|
+
|
40
|
+
def save_controller_state
|
41
|
+
warden.session(scope_name)[:secure_last_controller] = self.class.name
|
42
|
+
warden.session(scope_name)[:secure_last_action] = action_name
|
43
|
+
end
|
44
|
+
|
45
|
+
def skip_current_devise_controller?
|
46
|
+
exclusion_list = [
|
47
|
+
'Devise::SessionsController'
|
48
|
+
]
|
49
|
+
exclusion_list.select { |e| e == "#{self.class.name}#" + action_name || e == self.class.name.to_s }.empty?
|
50
|
+
end
|
51
|
+
|
52
|
+
def error_string_for_password_expired
|
53
|
+
class_obj = scope_name.to_s.camelize.constantize
|
54
|
+
I18n.t(
|
55
|
+
'secure_password.password_requires_regular_updates.errors.messages.password_expired',
|
56
|
+
timeframe: distance_of_time_in_words(class_obj.password_maximum_age)
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
# rubocop:enable Style/ClassAndModuleChildren
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Devise
|
2
|
+
module Models
|
3
|
+
module PasswordDisallowsFrequentChanges
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class ConfigurationError < RuntimeError; end
|
7
|
+
|
8
|
+
included do
|
9
|
+
include ActionView::Helpers::DateHelper
|
10
|
+
validate :validate_password_frequent_change
|
11
|
+
|
12
|
+
set_callback(:initialize, :before, :before_resource_initialized)
|
13
|
+
set_callback(:initialize, :after, :after_resource_initialized)
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate_password_frequent_change
|
17
|
+
if encrypted_password_changed? && password_recent?
|
18
|
+
error_string = I18n.t(
|
19
|
+
'secure_password.password_disallows_frequent_changes.errors.messages.password_is_recent',
|
20
|
+
timeframe: distance_of_time_in_words(self.class.password_minimum_age)
|
21
|
+
)
|
22
|
+
errors.add(:base, error_string)
|
23
|
+
end
|
24
|
+
|
25
|
+
errors.count.zero?
|
26
|
+
end
|
27
|
+
|
28
|
+
def password_recent?
|
29
|
+
last_password = previous_passwords.unscoped.last
|
30
|
+
last_password&.fresh?(self.class.password_minimum_age)
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def before_resource_initialized
|
36
|
+
return if self.class.respond_to?(:password_previously_used_count)
|
37
|
+
|
38
|
+
raise ConfigurationError, <<-ERROR.strip_heredoc
|
39
|
+
|
40
|
+
The password_disallows_frequent_changes module depends on the
|
41
|
+
password_disallows_frequent_reuse module. Verify that you have
|
42
|
+
added both modules to your model, for example:
|
43
|
+
|
44
|
+
devise :database_authenticatable, :registerable,
|
45
|
+
:password_disallows_frequent_reuse,
|
46
|
+
:password_disallows_frequent_changes
|
47
|
+
ERROR
|
48
|
+
end
|
49
|
+
|
50
|
+
def after_resource_initialized
|
51
|
+
raise ConfigurationError, 'invalid type for password_minimum_age' \
|
52
|
+
unless self.class.password_minimum_age.is_a?(::ActiveSupport::Duration)
|
53
|
+
end
|
54
|
+
|
55
|
+
module ClassMethods
|
56
|
+
::Devise::Models.config(self, :password_minimum_age)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Devise
|
2
|
+
module Models
|
3
|
+
module PasswordDisallowsFrequentReuse
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
require 'devise/secure_password/models/previous_password'
|
7
|
+
|
8
|
+
included do
|
9
|
+
# we need to specify the foreign_key here to support the use of isolated subclasses in tests
|
10
|
+
has_many :previous_passwords,
|
11
|
+
-> { limit(User.password_previously_used_count) },
|
12
|
+
class_name: 'Devise::Models::PreviousPassword',
|
13
|
+
foreign_key: 'user_id',
|
14
|
+
dependent: :destroy
|
15
|
+
validate :validate_password_frequent_reuse
|
16
|
+
|
17
|
+
set_callback(:save, :before, :before_resource_saved)
|
18
|
+
set_callback(:save, :after, :after_resource_saved, if: :dirty_password?)
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate_password_frequent_reuse
|
22
|
+
if encrypted_password_changed? && previous_password?(password)
|
23
|
+
error_string = I18n.t(
|
24
|
+
'secure_password.password_disallows_frequent_reuse.errors.messages.password_is_recent',
|
25
|
+
count: self.class.password_previously_used_count
|
26
|
+
)
|
27
|
+
errors.add(:base, error_string)
|
28
|
+
end
|
29
|
+
|
30
|
+
errors.count.zero?
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def before_resource_saved; end
|
36
|
+
|
37
|
+
def after_resource_saved
|
38
|
+
salt = ::BCrypt::Password.new(encrypted_password).salt
|
39
|
+
previous_password = previous_passwords.build(user_id: id, salt: salt, encrypted_password: encrypted_password)
|
40
|
+
previous_password.save!
|
41
|
+
end
|
42
|
+
|
43
|
+
def previous_password?(password)
|
44
|
+
salts = previous_passwords.select(:salt).map(&:salt)
|
45
|
+
pepper = self.class.pepper.presence || ''
|
46
|
+
|
47
|
+
salts.each do |salt|
|
48
|
+
candidate = ::BCrypt::Engine.hash_secret("#{password}#{pepper}", salt)
|
49
|
+
return true unless previous_passwords.find_by(encrypted_password: candidate).nil?
|
50
|
+
end
|
51
|
+
|
52
|
+
false
|
53
|
+
end
|
54
|
+
|
55
|
+
def dirty_password?
|
56
|
+
if Rails.version > '5.1'
|
57
|
+
saved_change_to_encrypted_password?
|
58
|
+
else
|
59
|
+
encrypted_password_changed?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module ClassMethods
|
64
|
+
config_params = %i(
|
65
|
+
password_previously_used_count
|
66
|
+
)
|
67
|
+
::Devise::Models.config(self, *config_params)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|