devise 3.4.1 → 3.5.10

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of devise might be problematic. Click here for more details.

Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +28 -19
  3. data/CHANGELOG.md +193 -104
  4. data/CODE_OF_CONDUCT.md +22 -0
  5. data/CONTRIBUTING.md +2 -0
  6. data/Gemfile +3 -2
  7. data/Gemfile.lock +90 -95
  8. data/MIT-LICENSE +1 -1
  9. data/README.md +55 -34
  10. data/Rakefile +2 -1
  11. data/app/controllers/devise/confirmations_controller.rb +4 -0
  12. data/app/controllers/devise/omniauth_callbacks_controller.rb +4 -0
  13. data/app/controllers/devise/passwords_controller.rb +14 -4
  14. data/app/controllers/devise/registrations_controller.rb +10 -11
  15. data/app/controllers/devise/sessions_controller.rb +7 -2
  16. data/app/controllers/devise/unlocks_controller.rb +3 -0
  17. data/app/controllers/devise_controller.rb +34 -18
  18. data/app/mailers/devise/mailer.rb +4 -0
  19. data/app/views/devise/confirmations/new.html.erb +1 -1
  20. data/app/views/devise/mailer/password_change.html.erb +3 -0
  21. data/app/views/devise/passwords/edit.html.erb +3 -0
  22. data/app/views/devise/registrations/new.html.erb +1 -1
  23. data/app/views/devise/shared/_links.html.erb +1 -1
  24. data/config/locales/en.yml +2 -0
  25. data/devise.gemspec +0 -2
  26. data/gemfiles/Gemfile.rails-3.2-stable.lock +52 -49
  27. data/gemfiles/Gemfile.rails-4.0-stable +1 -0
  28. data/gemfiles/Gemfile.rails-4.0-stable.lock +61 -60
  29. data/gemfiles/Gemfile.rails-4.1-stable +1 -0
  30. data/gemfiles/Gemfile.rails-4.1-stable.lock +66 -65
  31. data/gemfiles/Gemfile.rails-4.2-stable +30 -0
  32. data/gemfiles/Gemfile.rails-4.2-stable.lock +193 -0
  33. data/lib/devise/controllers/helpers.rb +12 -6
  34. data/lib/devise/controllers/rememberable.rb +9 -2
  35. data/lib/devise/controllers/sign_in_out.rb +2 -8
  36. data/lib/devise/controllers/store_location.rb +3 -1
  37. data/lib/devise/controllers/url_helpers.rb +7 -9
  38. data/lib/devise/encryptor.rb +22 -0
  39. data/lib/devise/failure_app.rb +48 -13
  40. data/lib/devise/hooks/timeoutable.rb +5 -7
  41. data/lib/devise/mapping.rb +1 -0
  42. data/lib/devise/models/authenticatable.rb +20 -26
  43. data/lib/devise/models/confirmable.rb +51 -17
  44. data/lib/devise/models/database_authenticatable.rb +17 -11
  45. data/lib/devise/models/lockable.rb +5 -1
  46. data/lib/devise/models/recoverable.rb +23 -15
  47. data/lib/devise/models/rememberable.rb +56 -22
  48. data/lib/devise/models/timeoutable.rb +0 -6
  49. data/lib/devise/models/trackable.rb +1 -2
  50. data/lib/devise/models/validatable.rb +3 -3
  51. data/lib/devise/models.rb +1 -1
  52. data/lib/devise/rails/routes.rb +27 -18
  53. data/lib/devise/rails.rb +1 -1
  54. data/lib/devise/strategies/authenticatable.rb +7 -4
  55. data/lib/devise/strategies/database_authenticatable.rb +1 -1
  56. data/lib/devise/strategies/rememberable.rb +13 -6
  57. data/lib/devise/test_helpers.rb +2 -2
  58. data/lib/devise/version.rb +1 -1
  59. data/lib/devise.rb +37 -36
  60. data/lib/generators/active_record/templates/migration.rb +1 -1
  61. data/lib/generators/active_record/templates/migration_existing.rb +1 -1
  62. data/lib/generators/devise/views_generator.rb +14 -3
  63. data/lib/generators/templates/controllers/README +2 -2
  64. data/lib/generators/templates/controllers/omniauth_callbacks_controller.rb +1 -1
  65. data/lib/generators/templates/controllers/registrations_controller.rb +2 -2
  66. data/lib/generators/templates/controllers/sessions_controller.rb +1 -1
  67. data/lib/generators/templates/devise.rb +17 -11
  68. data/lib/generators/templates/markerb/confirmation_instructions.markerb +1 -1
  69. data/lib/generators/templates/markerb/password_change.markerb +3 -0
  70. data/lib/generators/templates/markerb/reset_password_instructions.markerb +1 -1
  71. data/lib/generators/templates/markerb/unlock_instructions.markerb +1 -1
  72. data/lib/generators/templates/simple_form_for/passwords/edit.html.erb +1 -1
  73. data/lib/generators/templates/simple_form_for/registrations/new.html.erb +1 -1
  74. data/test/controllers/custom_registrations_controller_test.rb +6 -1
  75. data/test/controllers/helper_methods_test.rb +21 -0
  76. data/test/controllers/helpers_test.rb +5 -0
  77. data/test/controllers/inherited_controller_i18n_messages_test.rb +51 -0
  78. data/test/controllers/internal_helpers_test.rb +4 -4
  79. data/test/controllers/load_hooks_controller_test.rb +19 -0
  80. data/test/controllers/passwords_controller_test.rb +1 -1
  81. data/test/controllers/sessions_controller_test.rb +3 -3
  82. data/test/devise_test.rb +3 -3
  83. data/test/failure_app_test.rb +40 -0
  84. data/test/generators/views_generator_test.rb +7 -0
  85. data/test/integration/database_authenticatable_test.rb +11 -0
  86. data/test/integration/omniauthable_test.rb +12 -10
  87. data/test/integration/recoverable_test.rb +13 -0
  88. data/test/integration/rememberable_test.rb +50 -3
  89. data/test/integration/timeoutable_test.rb +13 -18
  90. data/test/mailers/confirmation_instructions_test.rb +1 -1
  91. data/test/mapping_test.rb +6 -0
  92. data/test/models/confirmable_test.rb +93 -37
  93. data/test/models/database_authenticatable_test.rb +20 -0
  94. data/test/models/lockable_test.rb +29 -7
  95. data/test/models/recoverable_test.rb +62 -7
  96. data/test/models/rememberable_test.rb +68 -97
  97. data/test/models/validatable_test.rb +5 -5
  98. data/test/models_test.rb +15 -6
  99. data/test/rails_app/app/active_record/user_without_email.rb +8 -0
  100. data/test/rails_app/app/controllers/admins_controller.rb +0 -5
  101. data/test/rails_app/app/controllers/custom/registrations_controller.rb +10 -0
  102. data/test/rails_app/app/mongoid/user_without_email.rb +33 -0
  103. data/test/rails_app/config/application.rb +1 -1
  104. data/test/rails_app/config/environments/production.rb +6 -2
  105. data/test/rails_app/config/environments/test.rb +7 -2
  106. data/test/rails_app/config/initializers/devise.rb +12 -15
  107. data/test/rails_app/config/routes.rb +6 -3
  108. data/test/rails_app/lib/shared_user.rb +1 -1
  109. data/test/rails_app/lib/shared_user_without_email.rb +26 -0
  110. data/test/rails_test.rb +9 -0
  111. data/test/support/helpers.rb +4 -0
  112. data/test/support/integration.rb +2 -2
  113. data/test/test_helpers_test.rb +22 -7
  114. data/test/test_models.rb +2 -2
  115. data/test/time_helpers.rb +137 -0
  116. metadata +26 -4
@@ -2,18 +2,25 @@ module Devise
2
2
  module Controllers
3
3
  # A module that may be optionally included in a controller in order
4
4
  # to provide remember me behavior. Useful when signing in is done
5
- # through a callback, like in Omniauth.
5
+ # through a callback, like in OmniAuth.
6
6
  module Rememberable
7
7
  # Return default cookie values retrieved from session options.
8
8
  def self.cookie_values
9
9
  Rails.configuration.session_options.slice(:path, :domain, :secure)
10
10
  end
11
11
 
12
+ def remember_me_is_active?(resource)
13
+ return false unless resource.respond_to?(:remember_me)
14
+ scope = Devise::Mapping.find_scope!(resource)
15
+ _, token, generated_at = cookies.signed[remember_key(resource, scope)]
16
+ resource.remember_me?(token, generated_at)
17
+ end
18
+
12
19
  # Remembers the given resource by setting up a cookie
13
20
  def remember_me(resource)
14
21
  return if env["devise.skip_storage"]
15
22
  scope = Devise::Mapping.find_scope!(resource)
16
- resource.remember_me!(resource.extend_remember_period)
23
+ resource.remember_me!
17
24
  cookies.signed[remember_key(resource, scope)] = remember_cookie_values(resource)
18
25
  end
19
26
 
@@ -6,7 +6,7 @@ module Devise
6
6
  # Return true if the given scope is signed in session. If no scope given, return
7
7
  # true if any scope is signed in. Does not run authentication hooks.
8
8
  def signed_in?(scope=nil)
9
- [ scope || Devise.mappings.keys ].flatten.any? do |_scope|
9
+ [scope || Devise.mappings.keys].flatten.any? do |_scope|
10
10
  warden.authenticate?(scope: _scope)
11
11
  end
12
12
  end
@@ -90,13 +90,7 @@ module Devise
90
90
  session.keys.grep(/^devise\./).each { |k| session.delete(k) }
91
91
  end
92
92
 
93
- def expire_data_after_sign_out!
94
- # session.keys will return an empty array if the session is not yet loaded.
95
- # This is a bug in both Rack and Rails.
96
- # A call to #empty? forces the session to be loaded.
97
- session.empty?
98
- session.keys.grep(/^devise\./).each { |k| session.delete(k) }
99
- end
93
+ alias :expire_data_after_sign_out! :expire_data_after_sign_in!
100
94
  end
101
95
  end
102
96
  end
@@ -35,7 +35,9 @@ module Devise
35
35
  session_key = stored_location_key_for(resource_or_scope)
36
36
  uri = parse_uri(location)
37
37
  if uri
38
- session[session_key] = [uri.path.sub(/\A\/+/, '/'), uri.query].compact.join('?')
38
+ path = [uri.path.sub(/\A\/+/, '/'), uri.query].compact.join('?')
39
+ path = [path, uri.fragment].compact.join('#')
40
+ session[session_key] = path
39
41
  end
40
42
  end
41
43
 
@@ -42,16 +42,14 @@ module Devise
42
42
  [:path, :url].each do |path_or_url|
43
43
  actions.each do |action|
44
44
  action = action ? "#{action}_" : ""
45
- method = "#{action}#{module_name}_#{path_or_url}"
45
+ method = :"#{action}#{module_name}_#{path_or_url}"
46
46
 
47
- class_eval <<-URL_HELPERS, __FILE__, __LINE__ + 1
48
- def #{method}(resource_or_scope, *args)
49
- scope = Devise::Mapping.find_scope!(resource_or_scope)
50
- router_name = Devise.mappings[scope].router_name
51
- context = router_name ? send(router_name) : _devise_route_context
52
- context.send("#{action}\#{scope}_#{module_name}_#{path_or_url}", *args)
53
- end
54
- URL_HELPERS
47
+ define_method method do |resource_or_scope, *args|
48
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
49
+ router_name = Devise.mappings[scope].router_name
50
+ context = router_name ? send(router_name) : _devise_route_context
51
+ context.send("#{action}#{scope}_#{module_name}_#{path_or_url}", *args)
52
+ end
55
53
  end
56
54
  end
57
55
  end
@@ -0,0 +1,22 @@
1
+ require 'bcrypt'
2
+
3
+ module Devise
4
+ module Encryptor
5
+ def self.digest(klass, password)
6
+ if klass.pepper.present?
7
+ password = "#{password}#{klass.pepper}"
8
+ end
9
+ ::BCrypt::Password.create(password, cost: klass.stretches).to_s
10
+ end
11
+
12
+ def self.compare(klass, encrypted_password, password)
13
+ return false if encrypted_password.blank?
14
+ bcrypt = ::BCrypt::Password.new(encrypted_password)
15
+ if klass.pepper.present?
16
+ password = "#{password}#{klass.pepper}"
17
+ end
18
+ password = ::BCrypt::Engine.hash_secret(password, bcrypt.salt)
19
+ Devise.secure_compare(password, encrypted_password)
20
+ end
21
+ end
22
+ end
@@ -22,9 +22,12 @@ module Devise
22
22
  @respond.call(env)
23
23
  end
24
24
 
25
+ # Try retrieving the URL options from the parent controller (usually
26
+ # ApplicationController). Instance methods are not supported at the moment,
27
+ # so only the class-level attribute is used.
25
28
  def self.default_url_options(*args)
26
- if defined?(ApplicationController)
27
- ApplicationController.default_url_options(*args)
29
+ if defined?(Devise.parent_controller.constantize)
30
+ Devise.parent_controller.constantize.try(:default_url_options) || {}
28
31
  else
29
32
  {}
30
33
  end
@@ -48,18 +51,31 @@ module Devise
48
51
  end
49
52
 
50
53
  def recall
51
- env["PATH_INFO"] = attempted_path
52
- flash.now[:alert] = i18n_message(:invalid)
54
+ config = Rails.application.config
55
+
56
+ if config.try(:relative_url_root)
57
+ base_path = Pathname.new(config.relative_url_root)
58
+ full_path = Pathname.new(attempted_path)
59
+
60
+ env["SCRIPT_NAME"] = config.relative_url_root
61
+ env["PATH_INFO"] = '/' + full_path.relative_path_from(base_path).to_s
62
+ else
63
+ env["PATH_INFO"] = attempted_path
64
+ end
65
+
66
+ flash.now[:alert] = i18n_message(:invalid) if is_flashing_format?
53
67
  self.response = recall_app(warden_options[:recall]).call(env)
54
68
  end
55
69
 
56
70
  def redirect
57
71
  store_location!
58
- if flash[:timedout] && flash[:alert]
59
- flash.keep(:timedout)
60
- flash.keep(:alert)
61
- else
62
- flash[:alert] = i18n_message
72
+ if is_flashing_format?
73
+ if flash[:timedout] && flash[:alert]
74
+ flash.keep(:timedout)
75
+ flash.keep(:alert)
76
+ else
77
+ flash[:alert] = i18n_message
78
+ end
63
79
  end
64
80
  redirect_to redirect_url
65
81
  end
@@ -91,7 +107,7 @@ module Devise
91
107
 
92
108
  def redirect_url
93
109
  if warden_message == :timeout
94
- flash[:timedout] = true
110
+ flash[:timedout] = true if is_flashing_format?
95
111
 
96
112
  path = if request.get?
97
113
  attempted_path
@@ -105,15 +121,28 @@ module Devise
105
121
  end
106
122
  end
107
123
 
124
+ def route(scope)
125
+ :"new_#{scope}_session_url"
126
+ end
127
+
108
128
  def scope_url
109
129
  opts = {}
110
- route = :"new_#{scope}_session_url"
130
+ route = route(scope)
111
131
  opts[:format] = request_format unless skip_format?
112
132
 
113
133
  config = Rails.application.config
114
- opts[:script_name] = (config.relative_url_root if config.respond_to?(:relative_url_root))
115
134
 
116
- context = send(Devise.available_router_name)
135
+ # Rails 4.2 goes into an infinite loop if opts[:script_name] is unset
136
+ if (Rails::VERSION::MAJOR >= 4) && (Rails::VERSION::MINOR >= 2)
137
+ opts[:script_name] = (config.relative_url_root if config.respond_to?(:relative_url_root))
138
+ else
139
+ if config.respond_to?(:relative_url_root) && config.relative_url_root.present?
140
+ opts[:script_name] = config.relative_url_root
141
+ end
142
+ end
143
+
144
+ router_name = Devise.mappings[scope].router_name || Devise.available_router_name
145
+ context = send(router_name)
117
146
 
118
147
  if context.respond_to?(route)
119
148
  context.send(route, opts)
@@ -205,6 +234,12 @@ module Devise
205
234
  Devise.navigational_formats.include?(request_format)
206
235
  end
207
236
 
237
+ # Check if flash messages should be emitted. Default is to do it on
238
+ # navigational formats
239
+ def is_flashing_format?
240
+ is_navigational_format?
241
+ end
242
+
208
243
  def request_format
209
244
  @request_format ||= request.format.try(:ref)
210
245
  end
@@ -7,7 +7,8 @@ Warden::Manager.after_set_user do |record, warden, options|
7
7
  scope = options[:scope]
8
8
  env = warden.request.env
9
9
 
10
- if record && record.respond_to?(:timedout?) && warden.authenticated?(scope) && options[:store] != false
10
+ if record && record.respond_to?(:timedout?) && warden.authenticated?(scope) &&
11
+ options[:store] != false && !env['devise.skip_timeoutable']
11
12
  last_request_at = warden.session(scope)['last_request_at']
12
13
 
13
14
  if last_request_at.is_a? Integer
@@ -18,13 +19,10 @@ Warden::Manager.after_set_user do |record, warden, options|
18
19
 
19
20
  proxy = Devise::Hooks::Proxy.new(warden)
20
21
 
21
- if record.timedout?(last_request_at) && !env['devise.skip_timeout']
22
+ if record.timedout?(last_request_at) &&
23
+ !env['devise.skip_timeout'] &&
24
+ !proxy.remember_me_is_active?(record)
22
25
  Devise.sign_out_all_scopes ? proxy.sign_out : proxy.sign_out(scope)
23
-
24
- if record.respond_to?(:expire_auth_token_on_timeout) && record.expire_auth_token_on_timeout
25
- record.reset_authentication_token!
26
- end
27
-
28
26
  throw :warden, scope: scope, message: :timeout
29
27
  end
30
28
 
@@ -31,6 +31,7 @@ module Devise
31
31
  # Receives an object and find a scope for it. If a scope cannot be found,
32
32
  # raises an error. If a symbol is given, it's considered to be the scope.
33
33
  def self.find_scope!(obj)
34
+ obj = obj.devise_scope if obj.respond_to?(:devise_scope)
34
35
  case obj
35
36
  when String, Symbol
36
37
  return obj.to_sym
@@ -1,3 +1,4 @@
1
+ require 'active_model/version'
1
2
  require 'devise/hooks/activatable'
2
3
  require 'devise/hooks/csrf_cleaner'
3
4
 
@@ -37,7 +38,7 @@ module Devise
37
38
  # calling model.active_for_authentication?. This method is overwritten by other devise modules. For instance,
38
39
  # :confirmable overwrites .active_for_authentication? to only return true if your model was confirmed.
39
40
  #
40
- # You overwrite this method yourself, but if you do, don't forget to call super:
41
+ # You can overwrite this method yourself, but if you do, don't forget to call super:
41
42
  #
42
43
  # def active_for_authentication?
43
44
  # super && special_condition_is_valid?
@@ -95,29 +96,22 @@ module Devise
95
96
  def authenticatable_salt
96
97
  end
97
98
 
98
- array = %w(serializable_hash)
99
- # to_xml does not call serializable_hash on 3.1
100
- array << "to_xml" if Rails::VERSION::STRING[0,3] == "3.1"
101
-
102
- array.each do |method|
103
- class_eval <<-RUBY, __FILE__, __LINE__
104
- # Redefine to_xml and serializable_hash in models for more secure defaults.
105
- # By default, it removes from the serializable model all attributes that
106
- # are *not* accessible. You can remove this default by using :force_except
107
- # and passing a new list of attributes you want to exempt. All attributes
108
- # given to :except will simply add names to exempt to Devise internal list.
109
- def #{method}(options=nil)
110
- options ||= {}
111
- options[:except] = Array(options[:except])
112
-
113
- if options[:force_except]
114
- options[:except].concat Array(options[:force_except])
115
- else
116
- options[:except].concat BLACKLIST_FOR_SERIALIZATION
117
- end
118
- super(options)
119
- end
120
- RUBY
99
+ # Redefine serializable_hash in models for more secure defaults.
100
+ # By default, it removes from the serializable model all attributes that
101
+ # are *not* accessible. You can remove this default by using :force_except
102
+ # and passing a new list of attributes you want to exempt. All attributes
103
+ # given to :except will simply add names to exempt to Devise internal list.
104
+ def serializable_hash(options = nil)
105
+ options ||= {}
106
+ options[:except] = Array(options[:except])
107
+
108
+ if options[:force_except]
109
+ options[:except].concat Array(options[:force_except])
110
+ else
111
+ options[:except].concat BLACKLIST_FOR_SERIALIZATION
112
+ end
113
+
114
+ super(options)
121
115
  end
122
116
 
123
117
  protected
@@ -252,12 +246,12 @@ module Devise
252
246
  to_adapter.find_first(devise_parameter_filter.filter(tainted_conditions).merge(opts))
253
247
  end
254
248
 
255
- # Find an initialize a record setting an error if it can't be found.
249
+ # Find or initialize a record setting an error if it can't be found.
256
250
  def find_or_initialize_with_error_by(attribute, value, error=:invalid) #:nodoc:
257
251
  find_or_initialize_with_errors([attribute], { attribute => value }, error)
258
252
  end
259
253
 
260
- # Find an initialize a group of attributes based on a list of required attributes.
254
+ # Find or initialize a record with group of attributes based on a list of required attributes.
261
255
  def find_or_initialize_with_errors(required_attributes, attributes, error=:invalid) #:nodoc:
262
256
  attributes = attributes.slice(*required_attributes).with_indifferent_access
263
257
  attributes.delete_if { |key, value| value.blank? }
@@ -5,6 +5,14 @@ module Devise
5
5
  # Confirmation instructions are sent to the user email after creating a
6
6
  # record and when manually requested by a new confirmation instruction request.
7
7
  #
8
+ # Confirmable tracks the following columns:
9
+ #
10
+ # * confirmation_token - A unique random token
11
+ # * confirmed_at - A timestamp when the user clicked the confirmation link
12
+ # * confirmation_sent_at - A timestamp when the confirmation_token was generated (not sent)
13
+ # * unconfirmed_email - An email address copied from the email attr. After confirmation
14
+ # this value is copied to the email attr then cleared
15
+ #
8
16
  # == Options
9
17
  #
10
18
  # Confirmable adds the following options to +devise+:
@@ -16,15 +24,17 @@ module Devise
16
24
  # By default allow_unconfirmed_access_for is zero, it means users always have to confirm to sign in.
17
25
  # * +reconfirmable+: requires any email changes to be confirmed (exactly the same way as
18
26
  # initial account confirmation) to be applied. Requires additional unconfirmed_email
19
- # db field to be setup (t.reconfirmable in migrations). Until confirmed new email is
27
+ # db field to be setup (t.reconfirmable in migrations). Until confirmed, new email is
20
28
  # stored in unconfirmed email column, and copied to email column on successful
21
29
  # confirmation.
22
30
  # * +confirm_within+: the time before a sent confirmation token becomes invalid.
23
31
  # You can use this to force the user to confirm within a set period of time.
32
+ # Confirmable will not generate a new token if a repeat confirmation is requested
33
+ # during this time frame, unless the user's email changed too.
24
34
  #
25
35
  # == Examples
26
36
  #
27
- # User.find(1).confirm! # returns true unless it's already confirmed
37
+ # User.find(1).confirm # returns true unless it's already confirmed
28
38
  # User.find(1).confirmed? # true/false
29
39
  # User.find(1).send_confirmation_instructions # manually send instructions
30
40
  #
@@ -56,7 +66,7 @@ module Devise
56
66
  # Confirm a user by setting it's confirmed_at to actual time. If the user
57
67
  # is already confirmed, add an error to email field. If the user is invalid
58
68
  # add errors
59
- def confirm!
69
+ def confirm(args={})
60
70
  pending_any_confirmation do
61
71
  if confirmation_period_expired?
62
72
  self.errors.add(:email, :confirmation_period_expired,
@@ -64,7 +74,6 @@ module Devise
64
74
  return false
65
75
  end
66
76
 
67
- self.confirmation_token = nil
68
77
  self.confirmed_at = Time.now.utc
69
78
 
70
79
  saved = if self.class.reconfirmable && unconfirmed_email.present?
@@ -75,7 +84,7 @@ module Devise
75
84
  # We need to validate in such cases to enforce e-mail uniqueness
76
85
  save(validate: true)
77
86
  else
78
- save(validate: false)
87
+ save(validate: args[:ensure_valid] == true)
79
88
  end
80
89
 
81
90
  after_confirmation if saved
@@ -83,6 +92,11 @@ module Devise
83
92
  end
84
93
  end
85
94
 
95
+ def confirm!(args={})
96
+ ActiveSupport::Deprecation.warn "confirm! is deprecated in favor of confirm"
97
+ confirm(args)
98
+ end
99
+
86
100
  # Verifies whether a user is confirmed or not
87
101
  def confirmed?
88
102
  !!confirmed_at
@@ -156,6 +170,7 @@ module Devise
156
170
  # in models to map to a nice sign up e-mail.
157
171
  def send_on_create_confirmation_instructions
158
172
  send_confirmation_instructions
173
+ skip_reconfirmation!
159
174
  end
160
175
 
161
176
  # Callback to overwrite if confirmation is required or not.
@@ -202,7 +217,7 @@ module Devise
202
217
  # confirmation_period_expired? # will always return false
203
218
  #
204
219
  def confirmation_period_expired?
205
- self.class.confirm_within && (Time.now > self.confirmation_sent_at + self.class.confirm_within )
220
+ self.class.confirm_within && self.confirmation_sent_at && (Time.now > self.confirmation_sent_at + self.class.confirm_within)
206
221
  end
207
222
 
208
223
  # Checks whether the record requires any confirmation.
@@ -216,12 +231,15 @@ module Devise
216
231
  end
217
232
 
218
233
  # Generates a new random token for confirmation, and stores
219
- # the time this token is being generated
234
+ # the time this token is being generated in confirmation_sent_at
220
235
  def generate_confirmation_token
221
- raw, enc = Devise.token_generator.generate(self.class, :confirmation_token)
222
- @raw_confirmation_token = raw
223
- self.confirmation_token = enc
224
- self.confirmation_sent_at = Time.now.utc
236
+ if self.confirmation_token && !confirmation_period_expired?
237
+ @raw_confirmation_token = self.confirmation_token
238
+ else
239
+ raw, _ = Devise.token_generator.generate(self.class, :confirmation_token)
240
+ self.confirmation_token = @raw_confirmation_token = raw
241
+ self.confirmation_sent_at = Time.now.utc
242
+ end
225
243
  end
226
244
 
227
245
  def generate_confirmation_token!
@@ -232,6 +250,7 @@ module Devise
232
250
  @reconfirmation_required = true
233
251
  self.unconfirmed_email = self.email
234
252
  self.email = self.email_was
253
+ self.confirmation_token = nil
235
254
  generate_confirmation_token
236
255
  end
237
256
 
@@ -242,13 +261,23 @@ module Devise
242
261
  end
243
262
 
244
263
  def reconfirmation_required?
245
- self.class.reconfirmable && @reconfirmation_required && self.email.present?
264
+ self.class.reconfirmable && @reconfirmation_required && (self.email.present? || self.unconfirmed_email.present?)
246
265
  end
247
266
 
248
267
  def send_confirmation_notification?
249
268
  confirmation_required? && !@skip_confirmation_notification && self.email.present?
250
269
  end
251
270
 
271
+ # A callback initiated after successfully confirming. This can be
272
+ # used to insert your own logic that is only run after the user successfully
273
+ # confirms.
274
+ #
275
+ # Example:
276
+ #
277
+ # def after_confirmation
278
+ # self.update_attribute(:invite_code, nil)
279
+ # end
280
+ #
252
281
  def after_confirmation
253
282
  end
254
283
 
@@ -271,12 +300,17 @@ module Devise
271
300
  # If the user is already confirmed, create an error for the user
272
301
  # Options must have the confirmation_token
273
302
  def confirm_by_token(confirmation_token)
274
- original_token = confirmation_token
275
- confirmation_token = Devise.token_generator.digest(self, :confirmation_token, confirmation_token)
303
+ confirmable = find_first_by_auth_conditions(confirmation_token: confirmation_token)
304
+ unless confirmable
305
+ confirmation_digest = Devise.token_generator.digest(self, :confirmation_token, confirmation_token)
306
+ confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_digest)
307
+ end
308
+
309
+ # TODO: replace above lines with
310
+ # confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_token)
311
+ # after enough time has passed that Devise clients do not use digested tokens
276
312
 
277
- confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_token)
278
- confirmable.confirm! if confirmable.persisted?
279
- confirmable.confirmation_token = original_token
313
+ confirmable.confirm if confirmable.persisted?
280
314
  confirmable
281
315
  end
282
316
 
@@ -1,10 +1,9 @@
1
1
  require 'devise/strategies/database_authenticatable'
2
- require 'bcrypt'
3
2
 
4
3
  module Devise
5
- # Digests the password using bcrypt.
6
4
  def self.bcrypt(klass, password)
7
- ::BCrypt::Password.create("#{password}#{klass.pepper}", cost: klass.stretches).to_s
5
+ ActiveSupport::Deprecation.warn "Devise.bcrypt is deprecated; use Devise::Encryptor.digest instead"
6
+ Devise::Encryptor.digest(klass, password)
8
7
  end
9
8
 
10
9
  module Models
@@ -13,7 +12,7 @@ module Devise
13
12
  #
14
13
  # == Options
15
14
  #
16
- # DatabaseAuthenticable adds the following options to devise_for:
15
+ # DatabaseAuthenticatable adds the following options to devise_for:
17
16
  #
18
17
  # * +pepper+: a random string used to provide a more secure hash. Use
19
18
  # `rake secret` to generate new keys.
@@ -28,6 +27,8 @@ module Devise
28
27
  extend ActiveSupport::Concern
29
28
 
30
29
  included do
30
+ after_update :send_password_change_notification, if: :send_password_change_notification?
31
+
31
32
  attr_reader :password, :current_password
32
33
  attr_accessor :password_confirmation
33
34
  end
@@ -42,12 +43,9 @@ module Devise
42
43
  self.encrypted_password = password_digest(@password) if @password.present?
43
44
  end
44
45
 
45
- # Verifies whether an password (ie from sign in) is the user password.
46
+ # Verifies whether a password (ie from sign in) is the user password.
46
47
  def valid_password?(password)
47
- return false if encrypted_password.blank?
48
- bcrypt = ::BCrypt::Password.new(encrypted_password)
49
- password = ::BCrypt::Engine.hash_secret("#{password}#{self.class.pepper}", bcrypt.salt)
50
- Devise.secure_compare(password, encrypted_password)
48
+ Devise::Encryptor.compare(self.class, encrypted_password, password)
51
49
  end
52
50
 
53
51
  # Set password and password confirmation to nil
@@ -137,6 +135,10 @@ module Devise
137
135
  encrypted_password[0,29] if encrypted_password
138
136
  end
139
137
 
138
+ def send_password_change_notification
139
+ send_devise_notification(:password_change)
140
+ end
141
+
140
142
  protected
141
143
 
142
144
  # Digests the password using bcrypt. Custom encryption should override
@@ -145,11 +147,15 @@ module Devise
145
147
  # See https://github.com/plataformatec/devise-encryptable for examples
146
148
  # of other encryption engines.
147
149
  def password_digest(password)
148
- Devise.bcrypt(self.class, password)
150
+ Devise::Encryptor.digest(self.class, password)
151
+ end
152
+
153
+ def send_password_change_notification?
154
+ self.class.send_password_change_notification && encrypted_password_changed?
149
155
  end
150
156
 
151
157
  module ClassMethods
152
- Devise::Models.config(self, :pepper, :stretches)
158
+ Devise::Models.config(self, :pepper, :stretches, :send_password_change_notification)
153
159
 
154
160
  # We assume this method already gets the sanitized values from the
155
161
  # DatabaseAuthenticatable strategy. If you are using this method on
@@ -155,6 +155,9 @@ module Devise
155
155
  end
156
156
 
157
157
  module ClassMethods
158
+ # List of strategies that are enabled/supported if :both is used.
159
+ BOTH_STRATEGIES = [:time, :email]
160
+
158
161
  # Attempt to find a user by its unlock keys. If a record is found, send new
159
162
  # unlock instructions to it. If not user is found, returns a new user
160
163
  # with an email not found error.
@@ -181,7 +184,8 @@ module Devise
181
184
 
182
185
  # Is the unlock enabled for the given unlock strategy?
183
186
  def unlock_strategy_enabled?(strategy)
184
- [:both, strategy].include?(self.unlock_strategy)
187
+ self.unlock_strategy == strategy ||
188
+ (self.unlock_strategy == :both && BOTH_STRATEGIES.include?(strategy))
185
189
  end
186
190
 
187
191
  # Is the lock enabled for the given lock strategy?