devise-security 0.13.0 → 0.14.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +1 -1
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +30 -12
- data/Appraisals +21 -5
- data/Gemfile +8 -1
- data/README.md +47 -7
- data/app/views/devise/paranoid_verification_code/show.html.erb +1 -1
- data/app/views/devise/password_expired/show.html.erb +1 -1
- data/config/locales/de.yml +11 -11
- data/config/locales/fr.yml +13 -13
- data/config/locales/ja.yml +29 -0
- data/devise-security.gemspec +12 -6
- data/gemfiles/rails_4.2_stable.gemfile +9 -1
- data/gemfiles/rails_5.0_stable.gemfile +8 -1
- data/gemfiles/rails_5.1_stable.gemfile +8 -1
- data/gemfiles/{rails_5.2.0.gemfile → rails_5.2_stable.gemfile} +8 -1
- data/gemfiles/rails_6.0_beta.gemfile +15 -0
- data/lib/devise-security.rb +2 -4
- data/lib/devise-security/models/{old_password.rb → active_record/old_password.rb} +1 -2
- data/lib/devise-security/models/compatibility.rb +6 -15
- data/lib/devise-security/models/compatibility/active_record.rb +29 -0
- data/lib/devise-security/models/compatibility/mongoid.rb +21 -0
- data/lib/devise-security/models/database_authenticatable_patch.rb +1 -1
- data/lib/devise-security/models/expirable.rb +6 -2
- data/lib/devise-security/models/mongoid/old_password.rb +21 -0
- data/lib/devise-security/models/password_archivable.rb +16 -7
- data/lib/devise-security/models/password_expirable.rb +5 -0
- data/lib/devise-security/models/secure_validatable.rb +2 -2
- data/lib/devise-security/orm/mongoid.rb +7 -0
- data/lib/devise-security/patches/controller_security_question.rb +1 -1
- data/lib/devise-security/version.rb +1 -1
- data/lib/generators/devise_security/install_generator.rb +1 -1
- data/test/dummy/app/models/application_record.rb +8 -2
- data/test/dummy/app/models/application_user_record.rb +11 -0
- data/test/dummy/app/models/captcha_user.rb +5 -2
- data/test/dummy/app/models/mongoid/confirmable_fields.rb +13 -0
- data/test/dummy/app/models/mongoid/database_authenticable_fields.rb +17 -0
- data/test/dummy/app/models/mongoid/expirable_fields.rb +11 -0
- data/test/dummy/app/models/mongoid/lockable_fields.rb +13 -0
- data/test/dummy/app/models/mongoid/mappings.rb +13 -0
- data/test/dummy/app/models/mongoid/omniauthable_fields.rb +11 -0
- data/test/dummy/app/models/mongoid/paranoid_verification_fields.rb +10 -0
- data/test/dummy/app/models/mongoid/password_archivable_fields.rb +9 -0
- data/test/dummy/app/models/mongoid/password_expirable_fields.rb +10 -0
- data/test/dummy/app/models/mongoid/recoverable_fields.rb +11 -0
- data/test/dummy/app/models/mongoid/registerable_fields.rb +19 -0
- data/test/dummy/app/models/mongoid/rememberable_fields.rb +10 -0
- data/test/dummy/app/models/mongoid/secure_validatable_fields.rb +11 -0
- data/test/dummy/app/models/mongoid/security_questionable_fields.rb +13 -0
- data/test/dummy/app/models/mongoid/session_limitable_fields.rb +10 -0
- data/test/dummy/app/models/mongoid/timeoutable_fields.rb +9 -0
- data/test/dummy/app/models/mongoid/trackable_fields.rb +14 -0
- data/test/dummy/app/models/mongoid/validatable_fields.rb +7 -0
- data/test/dummy/app/models/secure_user.rb +5 -1
- data/test/dummy/app/models/security_question_user.rb +7 -4
- data/test/dummy/app/models/user.rb +5 -0
- data/test/dummy/app/models/widget.rb +4 -0
- data/test/dummy/app/mongoid/admin.rb +31 -0
- data/test/dummy/app/mongoid/one_user.rb +58 -0
- data/test/dummy/app/mongoid/shim.rb +25 -0
- data/test/dummy/app/mongoid/user_on_engine.rb +41 -0
- data/test/dummy/app/mongoid/user_on_main_app.rb +41 -0
- data/test/dummy/app/mongoid/user_with_validations.rb +37 -0
- data/test/dummy/app/mongoid/user_without_email.rb +35 -0
- data/test/dummy/config/application.rb +10 -7
- data/test/dummy/config/environments/test.rb +2 -2
- data/test/dummy/config/initializers/devise.rb +3 -4
- data/test/dummy/config/initializers/migration_class.rb +8 -6
- data/test/dummy/config/mongoid.yml +6 -0
- data/test/dummy/lib/shared_expirable_columns.rb +14 -0
- data/test/dummy/lib/shared_security_questions_fields.rb +16 -0
- data/test/dummy/lib/shared_user.rb +32 -0
- data/test/dummy/lib/shared_user_with_password_verification.rb +13 -0
- data/test/dummy/lib/shared_user_without_email.rb +28 -0
- data/test/dummy/lib/shared_user_without_omniauth.rb +15 -0
- data/test/dummy/lib/shared_verification_fields.rb +15 -0
- data/test/orm/active_record.rb +12 -0
- data/test/orm/mongoid.rb +12 -0
- data/test/support/mongoid.yml +6 -0
- data/test/test_helper.rb +16 -9
- data/test/test_install_generator.rb +1 -0
- data/test/test_password_archivable.rb +6 -7
- data/test/test_password_expirable.rb +3 -1
- data/test/test_secure_validatable.rb +11 -10
- data/test/test_security_question_controller.rb +9 -11
- metadata +125 -16
@@ -0,0 +1,15 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "rails", "~> 6.0.0.beta1"
|
6
|
+
|
7
|
+
group :active_record do
|
8
|
+
gem "sqlite3", "~> 1.3.0"
|
9
|
+
end
|
10
|
+
|
11
|
+
group :mongoid do
|
12
|
+
gem "mongoid", "~> 6.0"
|
13
|
+
end
|
14
|
+
|
15
|
+
gemspec path: "../"
|
data/lib/devise-security.rb
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require DEVISE_ORM.to_s if DEVISE_ORM.in? [:active_record, :mongoid]
|
4
4
|
require 'active_support/core_ext/integer'
|
5
5
|
require 'active_support/ordered_hash'
|
6
6
|
require 'active_support/concern'
|
7
7
|
require 'devise'
|
8
8
|
|
9
9
|
module Devise
|
10
|
-
|
11
10
|
# Number of seconds that passwords are valid (e.g 3.months)
|
12
11
|
# Disable pasword expiration with +false+
|
13
12
|
# Expire only on demand with +true+
|
@@ -104,7 +103,6 @@ Devise.add_module :paranoid_verification, controller: :paranoid_verification_cod
|
|
104
103
|
# requires
|
105
104
|
require 'devise-security/routes'
|
106
105
|
require 'devise-security/rails'
|
107
|
-
require
|
108
|
-
require 'devise-security/models/old_password'
|
106
|
+
require "devise-security/orm/#{DEVISE_ORM}"
|
109
107
|
require 'devise-security/models/database_authenticatable_patch'
|
110
108
|
require 'devise-security/models/paranoid_verification'
|
@@ -1,24 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "compatibility/#{DEVISE_ORM}"
|
4
|
+
|
3
5
|
module Devise
|
4
6
|
module Models
|
7
|
+
# These compatibility modules define methods used by devise-security
|
8
|
+
# that may need to be defined or re-defined for compatibility between ORMs
|
9
|
+
# and/or older versions of ORMs.
|
5
10
|
module Compatibility
|
6
11
|
extend ActiveSupport::Concern
|
7
|
-
|
8
|
-
# for backwards compatibility with Rails < 5.1.x
|
9
|
-
unless Devise.activerecord51?
|
10
|
-
def saved_change_to_encrypted_password?
|
11
|
-
encrypted_password_changed?
|
12
|
-
end
|
13
|
-
|
14
|
-
def encrypted_password_before_last_save
|
15
|
-
previous_changes['encrypted_password'].try(:first)
|
16
|
-
end
|
17
|
-
|
18
|
-
def will_save_change_to_encrypted_password?
|
19
|
-
changed_attributes['encrypted_password'].present?
|
20
|
-
end
|
21
|
-
end
|
12
|
+
include "Devise::Models::Compatibility::#{DEVISE_ORM.to_s.classify}".constantize
|
22
13
|
end
|
23
14
|
end
|
24
15
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Devise
|
2
|
+
module Models
|
3
|
+
module Compatibility
|
4
|
+
module ActiveRecord
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
unless Devise.activerecord51?
|
7
|
+
# When the record was saved, was the +encrypted_password+ changed?
|
8
|
+
# @return [Boolean]
|
9
|
+
def saved_change_to_encrypted_password?
|
10
|
+
encrypted_password_changed?
|
11
|
+
end
|
12
|
+
|
13
|
+
# The encrypted password that existed before the record was saved
|
14
|
+
# @return [String]
|
15
|
+
# @return [nil] if an +encrypted_password+ had not been set
|
16
|
+
def encrypted_password_before_last_save
|
17
|
+
previous_changes['encrypted_password'].try(:first)
|
18
|
+
end
|
19
|
+
|
20
|
+
# When the record is saved, will the +encrypted_password+ be changed?
|
21
|
+
# @return [Boolean]
|
22
|
+
def will_save_change_to_encrypted_password?
|
23
|
+
changed_attributes['encrypted_password'].present?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Devise
|
2
|
+
module Models
|
3
|
+
module Compatibility
|
4
|
+
module Mongoid
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Will saving this record change the +email+ attribute?
|
8
|
+
# @return [Boolean]
|
9
|
+
def will_save_change_to_email?
|
10
|
+
changed.include? 'email'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Will saving this record change the +encrypted_password+ attribute?
|
14
|
+
# @return [Boolean]
|
15
|
+
def will_save_change_to_encrypted_password?
|
16
|
+
changed.include? 'encrypted_password'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -10,7 +10,7 @@ module Devise
|
|
10
10
|
new_password_confirmation = params[:password_confirmation]
|
11
11
|
|
12
12
|
result = if valid_password?(current_password) && new_password.present? && new_password_confirmation.present?
|
13
|
-
|
13
|
+
update(params, *options)
|
14
14
|
else
|
15
15
|
self.assign_attributes(params, *options)
|
16
16
|
self.valid?
|
@@ -22,7 +22,11 @@ module Devise
|
|
22
22
|
|
23
23
|
# Updates +last_activity_at+, called from a Warden::Manager.after_set_user hook.
|
24
24
|
def update_last_activity!
|
25
|
-
|
25
|
+
if respond_to?(:update_column)
|
26
|
+
self.update_column(:last_activity_at, Time.now.utc)
|
27
|
+
elsif defined? Mongoid
|
28
|
+
self.update_attribute(:last_activity_at, Time.now.utc)
|
29
|
+
end
|
26
30
|
end
|
27
31
|
|
28
32
|
# Tells if the account has expired
|
@@ -103,7 +107,7 @@ module Devise
|
|
103
107
|
# @example Overwritten version to blank out the object.
|
104
108
|
# def self.delete_all_expired_for(time = 90.days)
|
105
109
|
# expired_for(time).each do |u|
|
106
|
-
# u.
|
110
|
+
# u.update first_name: nil, last_name: nil
|
107
111
|
# end
|
108
112
|
# end
|
109
113
|
def delete_all_expired_for(time)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class OldPassword
|
4
|
+
include Mongoid::Document
|
5
|
+
|
6
|
+
## Database authenticatable
|
7
|
+
field :encrypted_password, type: String
|
8
|
+
validates_presence_of :encrypted_password
|
9
|
+
field :password_salt, type: String
|
10
|
+
|
11
|
+
field :password_archivable_type, type: String
|
12
|
+
validates_presence_of :password_archivable_type
|
13
|
+
|
14
|
+
field :password_archivable_id, type: String
|
15
|
+
validates_presence_of :password_archivable_id
|
16
|
+
index({ password_archivable_type: 1, password_archivable_id: 1 }, name: :index_password_archivable)
|
17
|
+
|
18
|
+
include Mongoid::Timestamps
|
19
|
+
|
20
|
+
belongs_to :password_archivable, polymorphic: true
|
21
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'compatibility'
|
4
|
+
require_relative "#{DEVISE_ORM}/old_password"
|
4
5
|
|
5
6
|
module Devise
|
6
7
|
module Models
|
@@ -11,7 +12,7 @@ module Devise
|
|
11
12
|
include Devise::Models::DatabaseAuthenticatable
|
12
13
|
|
13
14
|
included do
|
14
|
-
has_many :old_passwords, as: :password_archivable, dependent: :destroy
|
15
|
+
has_many :old_passwords, class_name: 'OldPassword', as: :password_archivable, dependent: :destroy
|
15
16
|
before_update :archive_password, if: :will_save_change_to_encrypted_password?
|
16
17
|
validate :validate_password_archive, if: :password_present?
|
17
18
|
end
|
@@ -38,11 +39,15 @@ module Devise
|
|
38
39
|
# @return [true] if current password was used previously
|
39
40
|
# @return [false] if disabled or not previously used
|
40
41
|
def password_archive_included?
|
41
|
-
return false unless max_old_passwords
|
42
|
-
|
42
|
+
return false unless max_old_passwords.positive?
|
43
|
+
|
44
|
+
old_passwords_including_cur_change = old_passwords.order(created_at: :desc).limit(max_old_passwords).pluck(:encrypted_password)
|
43
45
|
old_passwords_including_cur_change << encrypted_password_was # include most recent change in list, but don't save it yet!
|
44
46
|
old_passwords_including_cur_change.any? do |old_password|
|
45
|
-
|
47
|
+
# NOTE: we deliberately do not do mass assignment here so that users that
|
48
|
+
# rely on `protected_attributes_continued` gem can still use this extension.
|
49
|
+
# See issue #68
|
50
|
+
self.class.new.tap { |object| object.encrypted_password = old_password }.valid_password?(password)
|
46
51
|
end
|
47
52
|
end
|
48
53
|
|
@@ -60,11 +65,15 @@ module Devise
|
|
60
65
|
|
61
66
|
private
|
62
67
|
|
63
|
-
#
|
68
|
+
# Archive the last password before save and delete all to old passwords from archive
|
69
|
+
# @note we check to see if an old password has already been archived because
|
70
|
+
# mongoid will keep re-triggering this callback when we add an old password
|
64
71
|
def archive_password
|
65
|
-
if max_old_passwords
|
72
|
+
if max_old_passwords.positive?
|
73
|
+
return true if old_passwords.where(encrypted_password: encrypted_password_was).exists?
|
74
|
+
|
66
75
|
old_passwords.create!(encrypted_password: encrypted_password_was) if encrypted_password_was.present?
|
67
|
-
old_passwords.order(:
|
76
|
+
old_passwords.order(created_at: :desc).offset(max_old_passwords).destroy_all
|
68
77
|
else
|
69
78
|
old_passwords.destroy_all
|
70
79
|
end
|
@@ -39,6 +39,7 @@ module Devise::Models
|
|
39
39
|
# @return [Boolean]
|
40
40
|
def need_change_password!
|
41
41
|
return unless password_expiration_enabled?
|
42
|
+
|
42
43
|
need_change_password
|
43
44
|
save(validate: false)
|
44
45
|
end
|
@@ -51,6 +52,7 @@ module Devise::Models
|
|
51
52
|
# @return [void]
|
52
53
|
def need_change_password
|
53
54
|
return unless password_expiration_enabled?
|
55
|
+
|
54
56
|
self.password_changed_at = nil
|
55
57
|
end
|
56
58
|
alias expire_password need_change_password
|
@@ -70,6 +72,7 @@ module Devise::Models
|
|
70
72
|
def password_change_requested?
|
71
73
|
return false unless password_expiration_enabled?
|
72
74
|
return false if new_record?
|
75
|
+
|
73
76
|
password_changed_at.nil?
|
74
77
|
end
|
75
78
|
|
@@ -79,6 +82,7 @@ module Devise::Models
|
|
79
82
|
return false if new_record?
|
80
83
|
return false unless password_expiration_enabled?
|
81
84
|
return false if expire_password_on_demand?
|
85
|
+
|
82
86
|
password_changed_at < expire_password_after.seconds.ago
|
83
87
|
end
|
84
88
|
alias password_expired? password_too_old?
|
@@ -89,6 +93,7 @@ module Devise::Models
|
|
89
93
|
# @note called as a +before_save+ hook
|
90
94
|
def update_password_changed
|
91
95
|
return unless (new_record? || encrypted_password_changed?) && !password_changed_at_changed?
|
96
|
+
|
92
97
|
self.password_changed_at = Time.zone.now
|
93
98
|
end
|
94
99
|
|
@@ -90,8 +90,8 @@ module Devise
|
|
90
90
|
|
91
91
|
def has_uniqueness_validation_of_login?
|
92
92
|
validators.any? do |validator|
|
93
|
-
|
94
|
-
|
93
|
+
validator_orm_klass = DEVISE_ORM == :active_record ? ActiveRecord::Validations::UniquenessValidator : ::Mongoid::Validatable::UniquenessValidator
|
94
|
+
validator.kind_of?(validator_orm_klass) && validator.attributes.include?(login_attribute)
|
95
95
|
end
|
96
96
|
end
|
97
97
|
|
@@ -9,6 +9,7 @@ module DeviseSecurity::Patches
|
|
9
9
|
end
|
10
10
|
|
11
11
|
private
|
12
|
+
|
12
13
|
def check_security_question
|
13
14
|
# only find via email, not login
|
14
15
|
resource = resource_class.find_or_initialize_with_error_by(:email, params[resource_name][:email], :not_found)
|
@@ -19,4 +20,3 @@ module DeviseSecurity::Patches
|
|
19
20
|
end
|
20
21
|
end
|
21
22
|
end
|
22
|
-
|
@@ -4,7 +4,7 @@ module DeviseSecurity
|
|
4
4
|
module Generators
|
5
5
|
# Generator for Rails to create or append to a Devise initializer.
|
6
6
|
class InstallGenerator < Rails::Generators::Base
|
7
|
-
LOCALES = %w[en es de fr it tr].freeze
|
7
|
+
LOCALES = %w[en es de fr it ja tr].freeze
|
8
8
|
|
9
9
|
source_root File.expand_path('../../templates', __FILE__)
|
10
10
|
desc 'Install the devise security extension'
|
@@ -1,5 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
if DEVISE_ORM == :active_record
|
4
|
+
class ApplicationRecord < ActiveRecord::Base
|
5
|
+
self.abstract_class = true
|
6
|
+
end
|
7
|
+
else
|
8
|
+
class ApplicationRecord
|
9
|
+
include Mongoid::Document
|
10
|
+
end
|
5
11
|
end
|
@@ -1,7 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class CaptchaUser <
|
4
|
-
self.table_name = 'users'
|
3
|
+
class CaptchaUser < ApplicationUserRecord
|
5
4
|
devise :database_authenticatable, :password_archivable,
|
6
5
|
:paranoid_verification, :password_expirable
|
6
|
+
if DEVISE_ORM == :mongoid
|
7
|
+
require './test/dummy/app/models/mongoid/mappings'
|
8
|
+
include ::Mongoid::Mappings
|
9
|
+
end
|
7
10
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ConfirmableFields
|
2
|
+
extend ::ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
include Mongoid::Document
|
6
|
+
|
7
|
+
## Confirmable
|
8
|
+
field :confirmation_token, type: String
|
9
|
+
field :confirmed_at, type: Time
|
10
|
+
field :confirmation_sent_at, type: Time
|
11
|
+
field :unconfirmed_email, type: String # Only if using reconfirmable
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module DatabaseAuthenticatableFields
|
2
|
+
extend ::ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
include Mongoid::Document
|
6
|
+
|
7
|
+
## Database authenticatable
|
8
|
+
field :username, type: String
|
9
|
+
field :email, type: String, default: ""
|
10
|
+
#validates_presence_of :email
|
11
|
+
|
12
|
+
field :encrypted_password, type: String, default: ""
|
13
|
+
validates_presence_of :encrypted_password
|
14
|
+
|
15
|
+
include Mongoid::Timestamps
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module LockableFields
|
2
|
+
extend ::ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
include Mongoid::Document
|
6
|
+
|
7
|
+
field :failed_attempts, type: Integer, default: 0 # Only if lock strategy is :failed_attempts
|
8
|
+
field :unlock_token, type: String # Only if unlock strategy is :email or :both
|
9
|
+
field :locked_at, type: Time
|
10
|
+
include Mongoid::Timestamps
|
11
|
+
index({ unlock_token: 1 }, { unique: true })
|
12
|
+
end
|
13
|
+
end
|