devise-secure_password 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|