devise-secure-password 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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