devise-secure-password 1.1.1

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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +74 -0
  3. data/Changelog.md +98 -0
  4. data/Dockerfile +44 -0
  5. data/Dockerfile.prev +44 -0
  6. data/Gemfile +13 -0
  7. data/Gemfile.lock +272 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +382 -0
  10. data/Rakefile +11 -0
  11. data/app/controllers/devise/passwords_with_policy_controller.rb +93 -0
  12. data/app/views/devise/passwords_with_policy/edit.html.erb +20 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +6 -0
  15. data/config/locales/en.yml +84 -0
  16. data/devise-secure-password.gemspec +57 -0
  17. data/docker-entrypoint.sh +6 -0
  18. data/gemfiles/rails_5_1.gemfile +21 -0
  19. data/gemfiles/rails_5_2.gemfile +22 -0
  20. data/lib/devise/secure_password.rb +71 -0
  21. data/lib/devise/secure_password/controllers/devise_helpers.rb +23 -0
  22. data/lib/devise/secure_password/controllers/helpers.rb +58 -0
  23. data/lib/devise/secure_password/grammar.rb +13 -0
  24. data/lib/devise/secure_password/models/password_disallows_frequent_changes.rb +62 -0
  25. data/lib/devise/secure_password/models/password_disallows_frequent_reuse.rb +73 -0
  26. data/lib/devise/secure_password/models/password_has_required_content.rb +170 -0
  27. data/lib/devise/secure_password/models/password_requires_regular_updates.rb +54 -0
  28. data/lib/devise/secure_password/models/previous_password.rb +20 -0
  29. data/lib/devise/secure_password/routes.rb +11 -0
  30. data/lib/devise/secure_password/version.rb +5 -0
  31. data/lib/generators/devise/secure_password/install_generator.rb +30 -0
  32. data/lib/generators/devise/templates/README.txt +21 -0
  33. data/lib/generators/devise/templates/secure_password.rb +43 -0
  34. data/lib/support/string/character_counter.rb +55 -0
  35. data/pkg/devise-secure-password-1.1.1.gem +0 -0
  36. metadata +429 -0
@@ -0,0 +1,20 @@
1
+ <h2><%= t('.titles.section_title') %></h2>
2
+
3
+ <%= form_for(resource, as: resource_name, url: [resource_name, :password_with_policy], html: { method: :put }) do |f| %>
4
+ <% if resource.errors.full_messages.count.positive? %>
5
+ <%= devise_error_messages! %>
6
+ <% end %>
7
+
8
+ <%= f.hidden_field :update_action, value: :change_password %>
9
+
10
+ <p><%= f.label :current_password, t('.labels.current_password') %><br />
11
+ <%= f.password_field :current_password %></p>
12
+
13
+ <p><%= f.label :password, t('.labels.new_password') %><br />
14
+ <%= f.password_field :password %></p>
15
+
16
+ <p><%= f.label :password_confirmation, t('.labels.confirm_new_password') %><br />
17
+ <%= f.password_field :password_confirmation %></p>
18
+
19
+ <p><%= f.submit t('.buttons.change_my_password') %></p>
20
+ <% end %>
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "devise/secure_password"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,84 @@
1
+ en:
2
+ secure_password:
3
+ character:
4
+ one: "character"
5
+ other: "characters"
6
+
7
+ types:
8
+ uppercase: "uppercase"
9
+ downcase: "downcase"
10
+ number: "number"
11
+ special: "special"
12
+
13
+ password_has_required_content:
14
+ errors:
15
+ messages:
16
+ unknown_characters: "contains %{count} invalid %{subject}"
17
+ minimum_characters: "must contain at least %{count} %{type} %{subject}"
18
+ maximum_characters: "must contain less than %{count} %{type} %{subject}"
19
+ minimum_length: "must contain at least %{count} %{subject}"
20
+ maximum_length: "must contain less than %{count} %{subject}"
21
+ password_disallows_frequent_reuse:
22
+ errors:
23
+ messages:
24
+ password_is_recent: "Last %{count} passwords may not be reused"
25
+ password_disallows_frequent_changes:
26
+ errors:
27
+ messages:
28
+ password_is_recent: "Password cannot be changed more than once per %{timeframe}"
29
+ password_requires_regular_updates:
30
+ alerts:
31
+ messages:
32
+ password_updated: "Your password has been updated."
33
+ errors:
34
+ messages:
35
+ password_expired: "Your password has expired. Passwords must be changed every %{timeframe}"
36
+ datetime:
37
+ # update distance_in_words translations to remove the determiner words:
38
+ # about, almost, over, less than, etc.
39
+ precise_distance_in_words:
40
+ half_a_minute: "half a minute"
41
+ less_than_x_seconds:
42
+ one: "1 second" # default was: "less than 1 second"
43
+ other: "%{count} seconds" # default was: "less than %{count} seconds"
44
+ x_seconds:
45
+ one: "1 second"
46
+ other: "%{count} seconds"
47
+ less_than_x_minutes:
48
+ one: "a minute" # default was: "less than a minute"
49
+ other: "%{count} minutes" # default was: "less than %{count} minutes"
50
+ x_minutes:
51
+ one: "1 minute"
52
+ other: "%{count} minutes"
53
+ about_x_hours:
54
+ one: "1 hour" # default was: "about 1 hour"
55
+ other: "%{count} hours" # default was: "about %{count} hours"
56
+ x_days:
57
+ one: "1 day"
58
+ other: "%{count} days"
59
+ about_x_months:
60
+ one: "1 month" # default was: "about 1 month"
61
+ other: "%{count} months" # default was: "about %{count} months"
62
+ x_months:
63
+ one: "1 month"
64
+ other: "%{count} months"
65
+ about_x_years:
66
+ one: "1 year" # default was: "about 1 year"
67
+ other: "%{count} years" # default was: "about %{count} years"
68
+ over_x_years:
69
+ one: "1 year" # default was: "over 1 year"
70
+ other: "%{count} years" # default was: "over %{count} years"
71
+ almost_x_years:
72
+ one: "1 year" # default was: "almost 1 year"
73
+ other: "%{count} years" # default was: "almost %{count} years"
74
+ devise:
75
+ passwords_with_policy:
76
+ edit:
77
+ titles:
78
+ section_title: "Change your password"
79
+ labels:
80
+ current_password: "Current password"
81
+ new_password: "New password"
82
+ confirm_new_password: "Confirm new password"
83
+ buttons:
84
+ change_my_password: "Change my password"
@@ -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', '< 7.0.0'
33
+
34
+ spec.add_development_dependency 'bundler', '>= 1.16.1'
35
+ spec.add_development_dependency 'capybara', '>= 2.16.1', '~> 3.11'
36
+ spec.add_development_dependency 'capybara-screenshot', '>= 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.0'
41
+ spec.add_development_dependency 'launchy', '>= 2.4.3'
42
+ spec.add_development_dependency 'rails', '~> 5.2', '>= 5.2.0'
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.74.0'
48
+ spec.add_development_dependency 'rubocop-rails', '>= 2.3.2'
49
+ spec.add_development_dependency 'rubocop-rspec', '>= 1.35.0'
50
+ spec.add_development_dependency 'ruby2ruby', '>= 2.4.0'
51
+ spec.add_development_dependency 'selenium-webdriver', '>= 3.7.0'
52
+ spec.add_development_dependency 'simplecov', '~> 0.18.2'
53
+ spec.add_development_dependency 'simplecov-console', '>= 0.4.2'
54
+ spec.add_development_dependency 'sqlite3', '>= 1.3.13'
55
+
56
+ spec.required_ruby_version = '>= 2.4'
57
+ end
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ #
3
+ # Silence errors from Xvfb because we are starting it in non-priveleged mode.
4
+ #
5
+ Xvfb :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
6
+ exec "$@"
@@ -0,0 +1,21 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ ENV['RAILS_TARGET'] ||= '5.1'
6
+
7
+ gemspec path: '../'
8
+
9
+ gem 'rails', '~> 5.1.0'
10
+ gem 'sass-rails', '~> 5.0'
11
+ gem 'sqlite3', '~> 1.3.11'
12
+ gem 'therubyracer', '~> 0.12.3', platforms: :ruby
13
+
14
+ group :development, :test do
15
+ gem 'byebug', '>= 0'
16
+ end
17
+
18
+ group :test do
19
+ gem 'codecov', require: false
20
+ gem 'shoulda-matchers', git: 'https://github.com/thoughtbot/shoulda-matchers.git', branch: 'rails-5'
21
+ end
@@ -0,0 +1,22 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ ENV['RAILS_TARGET'] ||= '5.2'
6
+
7
+ gemspec path: '../'
8
+
9
+ gem 'bootsnap', '>= 1.1.0', require: false
10
+ gem 'mini_racer', '~> 0.2.0', platforms: :ruby
11
+ gem 'rails', '~> 5.2.0'
12
+ gem 'sassc-rails', '~> 1.3.0'
13
+ gem 'sqlite3', '~> 1.3.13'
14
+
15
+ group :development, :test do
16
+ gem 'byebug', '>= 0'
17
+ end
18
+
19
+ group :test do
20
+ gem 'codecov', require: false
21
+ gem 'shoulda-matchers', git: 'https://github.com/thoughtbot/shoulda-matchers.git', branch: 'rails-5'
22
+ end
@@ -0,0 +1,71 @@
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
+ require 'devise/secure_password/grammar'
13
+
14
+ module Devise
15
+ # password_content_enforcement configuration parameters
16
+ @password_required_uppercase_count = 1
17
+ @password_required_lowercase_count = 1
18
+ @password_required_number_count = 1
19
+ @password_required_special_character_count = 1
20
+
21
+ # password_frequent_reuse_prevention configuration parameters
22
+ @password_previously_used_count = 8
23
+
24
+ # password_frequent_change_prevention configuration parameters
25
+ @password_minimum_age = 1.day
26
+
27
+ # password_regular_update_enforcement configuration parameters
28
+ @password_maximum_age = 180.days
29
+
30
+ class << self
31
+ attr_accessor :password_required_uppercase_count
32
+ attr_accessor :password_required_lowercase_count
33
+ attr_accessor :password_required_number_count
34
+ attr_accessor :password_required_special_character_count
35
+ attr_accessor :password_previously_used_count
36
+ attr_accessor :password_minimum_age
37
+ attr_accessor :password_maximum_age
38
+ end
39
+
40
+ module SecurePassword
41
+ module Controllers
42
+ autoload :Helpers, 'devise/secure_password/controllers/helpers'
43
+ autoload :DeviseHelpers, 'devise/secure_password/controllers/devise_helpers'
44
+ end
45
+
46
+ class Engine < ::Rails::Engine
47
+ ActiveSupport.on_load(:devise_controller) do
48
+ include ActionView::Helpers::DateHelper
49
+ include Devise::SecurePassword::Controllers::DeviseHelpers
50
+ end
51
+ ActiveSupport.on_load(:action_controller) do
52
+ include ActionView::Helpers::DateHelper
53
+ include Devise::SecurePassword::Controllers::Helpers
54
+ end
55
+
56
+ # add exceptions to the inflector so it doesn't get tripped up by our concerns that end in an 's'
57
+ ActiveSupport::Inflector.inflections do |inflect|
58
+ inflect.uncountable :password_disallows_frequent_changes
59
+ inflect.uncountable :password_requires_regular_updates
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ # modules
66
+ Devise.add_module :password_has_required_content, model: 'devise/secure_password/models/password_has_required_content'
67
+ Devise.add_module :password_disallows_frequent_reuse, model: 'devise/secure_password/models/password_disallows_frequent_reuse'
68
+ Devise.add_module :password_disallows_frequent_changes, model: 'devise/secure_password/models/password_disallows_frequent_changes'
69
+ Devise.add_module :password_requires_regular_updates,
70
+ model: 'devise/secure_password/models/password_requires_regular_updates',
71
+ controller: :passwords_with_policy, route: :passwords_with_policy
@@ -0,0 +1,23 @@
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 devise_sign_in sign_in
10
+
11
+ protected
12
+
13
+ def sign_in(*args)
14
+ devise_sign_in(*args).tap do
15
+ set_devise_secure_password_expired! if warden_user_has_password_expiration?
16
+ end
17
+ end
18
+ end
19
+ # rubocop:enable Style/ClassAndModuleChildren
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,58 @@
1
+ module Devise
2
+ module SecurePassword
3
+ module Controllers
4
+ module Helpers
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include Devise::SecurePassword::Grammar
9
+
10
+ before_action :authenticate_secure_password!, unless: :devise_controller?
11
+ end
12
+
13
+ def authenticate_secure_password_expired?
14
+ return false if devise_controller?
15
+
16
+ session[:devise_secure_password_expired] == true
17
+ end
18
+
19
+ def authenticate_secure_password!
20
+ return unless authenticate_secure_password_expired?
21
+
22
+ redirect_to authenticate_secure_password_path, alert: "#{error_string_for_password_expired}."
23
+ end
24
+
25
+ def authenticate_secure_password_path
26
+ return unless warden.user
27
+
28
+ :"edit_#{devise_secure_password_scope}_password_with_policy"
29
+ end
30
+
31
+ private
32
+
33
+ def devise_secure_password_scope
34
+ Devise::Mapping.find_scope!(warden.user)
35
+ end
36
+
37
+ def error_string_for_password_expired
38
+ I18n.t(
39
+ 'secure_password.password_requires_regular_updates.errors.messages.password_expired',
40
+ timeframe: precise_distance_of_time_in_words(warden.user.class.password_maximum_age)
41
+ )
42
+ end
43
+
44
+ def set_devise_secure_password_expired!
45
+ session[:devise_secure_password_expired] = warden.user.password_expired?
46
+ end
47
+
48
+ def unset_devise_secure_password_expired!
49
+ session.delete(:devise_secure_password_expired)
50
+ end
51
+
52
+ def warden_user_has_password_expiration?
53
+ warden&.user&.respond_to?(:password_expired?)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,13 @@
1
+ # lib/support/string/grammar.rb
2
+ #
3
+ module Devise
4
+ module SecurePassword
5
+ module Grammar
6
+ # distance_in_words without determiner words: about, almost, over, etc.
7
+ def precise_distance_of_time_in_words(from_time, to_time = 0, options = {})
8
+ precise_options = { scope: :'secure_password.datetime.precise_distance_in_words' }
9
+ distance_of_time_in_words(from_time, to_time, options.merge!(precise_options))
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,62 @@
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
+ include Devise::SecurePassword::Grammar
11
+
12
+ validate :validate_password_frequent_change, if: :password_required?
13
+
14
+ set_callback(:initialize, :before, :before_resource_initialized)
15
+ set_callback(:initialize, :after, :after_resource_initialized)
16
+ end
17
+
18
+ def validate_password_frequent_change
19
+ if encrypted_password_changed? && password_recent?
20
+ error_string = I18n.t(
21
+ 'secure_password.password_disallows_frequent_changes.errors.messages.password_is_recent',
22
+ timeframe: precise_distance_of_time_in_words(self.class.password_minimum_age)
23
+ )
24
+ errors.add(:base, error_string)
25
+ end
26
+
27
+ errors.count.zero?
28
+ end
29
+
30
+ def password_recent?
31
+ last_password = previous_passwords.first
32
+ last_password&.fresh?(self.class.password_minimum_age)
33
+ end
34
+
35
+ protected
36
+
37
+ def before_resource_initialized
38
+ return if self.class.respond_to?(:password_previously_used_count)
39
+
40
+ raise ConfigurationError, <<-ERROR.strip_heredoc
41
+
42
+ The password_disallows_frequent_changes module depends on the
43
+ password_disallows_frequent_reuse module. Verify that you have
44
+ added both modules to your model, for example:
45
+
46
+ devise :database_authenticatable, :registerable,
47
+ :password_disallows_frequent_reuse,
48
+ :password_disallows_frequent_changes
49
+ ERROR
50
+ end
51
+
52
+ def after_resource_initialized
53
+ raise ConfigurationError, 'invalid type for password_minimum_age' \
54
+ unless self.class.password_minimum_age.is_a?(::ActiveSupport::Duration)
55
+ end
56
+
57
+ module ClassMethods
58
+ ::Devise::Models.config(self, :password_minimum_age)
59
+ end
60
+ end
61
+ end
62
+ end