devise 3.2.4 → 4.0.0

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 (178) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/.travis.yml +33 -17
  4. data/CHANGELOG.md +57 -1033
  5. data/CODE_OF_CONDUCT.md +22 -0
  6. data/CONTRIBUTING.md +2 -0
  7. data/Gemfile +5 -5
  8. data/Gemfile.lock +138 -115
  9. data/MIT-LICENSE +1 -1
  10. data/README.md +124 -65
  11. data/Rakefile +2 -1
  12. data/app/controllers/devise/confirmations_controller.rb +7 -3
  13. data/app/controllers/devise/omniauth_callbacks_controller.rb +8 -4
  14. data/app/controllers/devise/passwords_controller.rb +16 -6
  15. data/app/controllers/devise/registrations_controller.rb +22 -10
  16. data/app/controllers/devise/sessions_controller.rb +42 -14
  17. data/app/controllers/devise/unlocks_controller.rb +5 -2
  18. data/app/controllers/devise_controller.rb +63 -29
  19. data/app/mailers/devise/mailer.rb +4 -0
  20. data/app/views/devise/confirmations/new.html.erb +7 -3
  21. data/app/views/devise/mailer/password_change.html.erb +3 -0
  22. data/app/views/devise/passwords/edit.html.erb +14 -5
  23. data/app/views/devise/passwords/new.html.erb +7 -3
  24. data/app/views/devise/registrations/edit.html.erb +19 -9
  25. data/app/views/devise/registrations/new.html.erb +18 -7
  26. data/app/views/devise/sessions/new.html.erb +16 -7
  27. data/app/views/devise/shared/{_links.erb → _links.html.erb} +2 -2
  28. data/app/views/devise/unlocks/new.html.erb +7 -3
  29. data/bin/test +13 -0
  30. data/config/locales/en.yml +19 -16
  31. data/devise.gemspec +3 -4
  32. data/gemfiles/{Gemfile.rails-3.2-stable → Gemfile.rails-4.1-stable} +6 -6
  33. data/gemfiles/Gemfile.rails-4.1-stable.lock +167 -0
  34. data/gemfiles/{Gemfile.rails-head → Gemfile.rails-4.2-stable} +6 -6
  35. data/gemfiles/Gemfile.rails-4.2-stable.lock +189 -0
  36. data/gemfiles/Gemfile.rails-5.0-beta +37 -0
  37. data/gemfiles/Gemfile.rails-5.0-beta.lock +199 -0
  38. data/lib/devise/controllers/helpers.rb +94 -27
  39. data/lib/devise/controllers/rememberable.rb +9 -2
  40. data/lib/devise/controllers/sign_in_out.rb +2 -9
  41. data/lib/devise/controllers/store_location.rb +11 -3
  42. data/lib/devise/controllers/url_helpers.rb +7 -7
  43. data/lib/devise/encryptor.rb +22 -0
  44. data/lib/devise/failure_app.rb +72 -23
  45. data/lib/devise/hooks/activatable.rb +3 -4
  46. data/lib/devise/hooks/csrf_cleaner.rb +3 -1
  47. data/lib/devise/hooks/timeoutable.rb +13 -8
  48. data/lib/devise/mailers/helpers.rb +1 -1
  49. data/lib/devise/mapping.rb +6 -2
  50. data/lib/devise/models/authenticatable.rb +32 -28
  51. data/lib/devise/models/confirmable.rb +55 -22
  52. data/lib/devise/models/database_authenticatable.rb +32 -19
  53. data/lib/devise/models/lockable.rb +5 -5
  54. data/lib/devise/models/recoverable.rb +44 -20
  55. data/lib/devise/models/rememberable.rb +54 -27
  56. data/lib/devise/models/timeoutable.rb +0 -6
  57. data/lib/devise/models/trackable.rb +5 -3
  58. data/lib/devise/models/validatable.rb +3 -3
  59. data/lib/devise/models.rb +1 -1
  60. data/lib/devise/omniauth/url_helpers.rb +62 -4
  61. data/lib/devise/parameter_sanitizer.rb +176 -61
  62. data/lib/devise/rails/routes.rb +76 -59
  63. data/lib/devise/rails/warden_compat.rb +1 -10
  64. data/lib/devise/rails.rb +2 -11
  65. data/lib/devise/strategies/authenticatable.rb +15 -6
  66. data/lib/devise/strategies/database_authenticatable.rb +5 -4
  67. data/lib/devise/strategies/rememberable.rb +13 -3
  68. data/lib/devise/test_helpers.rb +12 -7
  69. data/lib/devise/token_generator.rb +1 -41
  70. data/lib/devise/version.rb +1 -1
  71. data/lib/devise.rb +150 -58
  72. data/lib/generators/active_record/devise_generator.rb +28 -4
  73. data/lib/generators/active_record/templates/migration.rb +3 -3
  74. data/lib/generators/active_record/templates/migration_existing.rb +3 -3
  75. data/lib/generators/devise/controllers_generator.rb +44 -0
  76. data/lib/generators/devise/install_generator.rb +15 -0
  77. data/lib/generators/devise/orm_helpers.rb +1 -18
  78. data/lib/generators/devise/views_generator.rb +14 -3
  79. data/lib/generators/templates/README +1 -1
  80. data/lib/generators/templates/controllers/README +14 -0
  81. data/lib/generators/templates/controllers/confirmations_controller.rb +28 -0
  82. data/lib/generators/templates/controllers/omniauth_callbacks_controller.rb +28 -0
  83. data/lib/generators/templates/controllers/passwords_controller.rb +32 -0
  84. data/lib/generators/templates/controllers/registrations_controller.rb +60 -0
  85. data/lib/generators/templates/controllers/sessions_controller.rb +25 -0
  86. data/lib/generators/templates/controllers/unlocks_controller.rb +28 -0
  87. data/lib/generators/templates/devise.rb +36 -28
  88. data/lib/generators/templates/markerb/confirmation_instructions.markerb +1 -1
  89. data/lib/generators/templates/markerb/password_change.markerb +3 -0
  90. data/lib/generators/templates/markerb/reset_password_instructions.markerb +1 -1
  91. data/lib/generators/templates/markerb/unlock_instructions.markerb +1 -1
  92. data/lib/generators/templates/simple_form_for/passwords/edit.html.erb +1 -1
  93. data/lib/generators/templates/simple_form_for/registrations/new.html.erb +1 -1
  94. data/lib/generators/templates/simple_form_for/sessions/new.html.erb +2 -2
  95. data/test/controllers/custom_registrations_controller_test.rb +40 -0
  96. data/test/controllers/custom_strategy_test.rb +7 -5
  97. data/test/controllers/helper_methods_test.rb +22 -0
  98. data/test/controllers/helpers_test.rb +41 -1
  99. data/test/controllers/inherited_controller_i18n_messages_test.rb +51 -0
  100. data/test/controllers/internal_helpers_test.rb +19 -15
  101. data/test/controllers/load_hooks_controller_test.rb +19 -0
  102. data/test/controllers/passwords_controller_test.rb +5 -4
  103. data/test/controllers/sessions_controller_test.rb +24 -21
  104. data/test/controllers/url_helpers_test.rb +7 -1
  105. data/test/devise_test.rb +48 -8
  106. data/test/failure_app_test.rb +107 -19
  107. data/test/generators/active_record_generator_test.rb +6 -26
  108. data/test/generators/controllers_generator_test.rb +48 -0
  109. data/test/generators/install_generator_test.rb +14 -3
  110. data/test/generators/views_generator_test.rb +8 -1
  111. data/test/helpers/devise_helper_test.rb +10 -12
  112. data/test/integration/authenticatable_test.rb +37 -21
  113. data/test/integration/confirmable_test.rb +54 -14
  114. data/test/integration/database_authenticatable_test.rb +12 -1
  115. data/test/integration/http_authenticatable_test.rb +4 -5
  116. data/test/integration/lockable_test.rb +10 -9
  117. data/test/integration/omniauthable_test.rb +13 -11
  118. data/test/integration/recoverable_test.rb +28 -15
  119. data/test/integration/registerable_test.rb +41 -33
  120. data/test/integration/rememberable_test.rb +51 -7
  121. data/test/integration/timeoutable_test.rb +23 -22
  122. data/test/integration/trackable_test.rb +3 -3
  123. data/test/mailers/confirmation_instructions_test.rb +10 -10
  124. data/test/mailers/reset_password_instructions_test.rb +8 -8
  125. data/test/mailers/unlock_instructions_test.rb +8 -8
  126. data/test/mapping_test.rb +7 -0
  127. data/test/models/authenticatable_test.rb +11 -1
  128. data/test/models/confirmable_test.rb +91 -42
  129. data/test/models/database_authenticatable_test.rb +26 -6
  130. data/test/models/lockable_test.rb +29 -17
  131. data/test/models/recoverable_test.rb +74 -7
  132. data/test/models/rememberable_test.rb +68 -94
  133. data/test/models/trackable_test.rb +28 -0
  134. data/test/models/validatable_test.rb +9 -17
  135. data/test/models_test.rb +15 -6
  136. data/test/omniauth/url_helpers_test.rb +4 -7
  137. data/test/orm/active_record.rb +6 -1
  138. data/test/parameter_sanitizer_test.rb +103 -53
  139. data/test/rails_app/app/active_record/user.rb +1 -0
  140. data/test/rails_app/app/active_record/user_on_engine.rb +7 -0
  141. data/test/rails_app/app/active_record/user_on_main_app.rb +7 -0
  142. data/test/rails_app/app/active_record/user_without_email.rb +8 -0
  143. data/test/rails_app/app/controllers/admins_controller.rb +1 -6
  144. data/test/rails_app/app/controllers/application_controller.rb +5 -2
  145. data/test/rails_app/app/controllers/application_with_fake_engine.rb +30 -0
  146. data/test/rails_app/app/controllers/custom/registrations_controller.rb +31 -0
  147. data/test/rails_app/app/controllers/home_controller.rb +5 -1
  148. data/test/rails_app/app/controllers/users/omniauth_callbacks_controller.rb +3 -3
  149. data/test/rails_app/app/controllers/users_controller.rb +6 -6
  150. data/test/rails_app/app/mailers/users/from_proc_mailer.rb +3 -0
  151. data/test/rails_app/app/mailers/users/mailer.rb +0 -9
  152. data/test/rails_app/app/mailers/users/reply_to_mailer.rb +4 -0
  153. data/test/rails_app/app/mongoid/user_on_engine.rb +39 -0
  154. data/test/rails_app/app/mongoid/user_on_main_app.rb +39 -0
  155. data/test/rails_app/app/mongoid/user_without_email.rb +33 -0
  156. data/test/rails_app/config/application.rb +3 -3
  157. data/test/rails_app/config/boot.rb +4 -4
  158. data/test/rails_app/config/environments/production.rb +6 -2
  159. data/test/rails_app/config/environments/test.rb +13 -3
  160. data/test/rails_app/config/initializers/devise.rb +15 -16
  161. data/test/rails_app/config/initializers/secret_token.rb +1 -6
  162. data/test/rails_app/config/routes.rb +23 -3
  163. data/test/rails_app/db/migrate/20100401102949_create_tables.rb +2 -2
  164. data/test/rails_app/lib/shared_user.rb +1 -1
  165. data/test/rails_app/lib/shared_user_without_email.rb +26 -0
  166. data/test/rails_app/lib/shared_user_without_omniauth.rb +13 -0
  167. data/test/rails_test.rb +9 -0
  168. data/test/routes_test.rb +33 -16
  169. data/test/support/assertions.rb +2 -3
  170. data/test/support/helpers.rb +13 -6
  171. data/test/support/http_method_compatibility.rb +51 -0
  172. data/test/support/integration.rb +4 -4
  173. data/test/support/webrat/integrations/rails.rb +9 -0
  174. data/test/test_helper.rb +7 -0
  175. data/test/test_helpers_test.rb +43 -38
  176. data/test/test_models.rb +3 -3
  177. metadata +77 -23
  178. data/gemfiles/Gemfile.rails-4.0-stable +0 -29
@@ -6,7 +6,6 @@ module Devise
6
6
  # page based on current scope and mapping. If no scope is given, redirect
7
7
  # to the default_url.
8
8
  class FailureApp < ActionController::Metal
9
- include ActionController::RackDelegation
10
9
  include ActionController::UrlFor
11
10
  include ActionController::Redirecting
12
11
 
@@ -22,9 +21,12 @@ module Devise
22
21
  @respond.call(env)
23
22
  end
24
23
 
24
+ # Try retrieving the URL options from the parent controller (usually
25
+ # ApplicationController). Instance methods are not supported at the moment,
26
+ # so only the class-level attribute is used.
25
27
  def self.default_url_options(*args)
26
- if defined?(ApplicationController)
27
- ApplicationController.default_url_options(*args)
28
+ if defined?(Devise.parent_controller.constantize)
29
+ Devise.parent_controller.constantize.try(:default_url_options) || {}
28
30
  else
29
31
  {}
30
32
  end
@@ -48,18 +50,40 @@ module Devise
48
50
  end
49
51
 
50
52
  def recall
51
- env["PATH_INFO"] = attempted_path
52
- flash.now[:alert] = i18n_message(:invalid)
53
- self.response = recall_app(warden_options[:recall]).call(env)
53
+ config = Rails.application.config
54
+
55
+ header_info = if config.try(:relative_url_root)
56
+ base_path = Pathname.new(config.relative_url_root)
57
+ full_path = Pathname.new(attempted_path)
58
+
59
+ { "SCRIPT_NAME" => config.relative_url_root,
60
+ "PATH_INFO" => '/' + full_path.relative_path_from(base_path).to_s }
61
+ else
62
+ { "PATH_INFO" => attempted_path }
63
+ end
64
+
65
+ header_info.each do | var, value|
66
+ if request.respond_to?(:set_header)
67
+ request.set_header(var, value)
68
+ else
69
+ env[var] = value
70
+ end
71
+ end
72
+
73
+ flash.now[:alert] = i18n_message(:invalid) if is_flashing_format?
74
+ # self.response = recall_app(warden_options[:recall]).call(env)
75
+ self.response = recall_app(warden_options[:recall]).call(request.env)
54
76
  end
55
77
 
56
78
  def redirect
57
79
  store_location!
58
- if flash[:timedout] && flash[:alert]
59
- flash.keep(:timedout)
60
- flash.keep(:alert)
61
- else
62
- flash[:alert] = i18n_message
80
+ if is_flashing_format?
81
+ if flash[:timedout] && flash[:alert]
82
+ flash.keep(:timedout)
83
+ flash.keep(:alert)
84
+ else
85
+ flash[:alert] = i18n_message
86
+ end
63
87
  end
64
88
  redirect_to redirect_url
65
89
  end
@@ -78,6 +102,9 @@ module Devise
78
102
  options[:resource_name] = scope
79
103
  options[:scope] = "devise.failure"
80
104
  options[:default] = [message]
105
+ auth_keys = scope_class.authentication_keys
106
+ keys = auth_keys.respond_to?(:keys) ? auth_keys.keys : auth_keys
107
+ options[:authentication_keys] = keys.join(I18n.translate(:"support.array.words_connector"))
81
108
  options = i18n_options(options)
82
109
 
83
110
  I18n.t(:"#{scope}.#{message}", options)
@@ -88,7 +115,7 @@ module Devise
88
115
 
89
116
  def redirect_url
90
117
  if warden_message == :timeout
91
- flash[:timedout] = true
118
+ flash[:timedout] = true if is_flashing_format?
92
119
 
93
120
  path = if request.get?
94
121
  attempted_path
@@ -96,26 +123,38 @@ module Devise
96
123
  request.referrer
97
124
  end
98
125
 
99
- path || scope_path
126
+ path || scope_url
100
127
  else
101
- scope_path
128
+ scope_url
102
129
  end
103
130
  end
104
131
 
105
- def scope_path
132
+ def route(scope)
133
+ :"new_#{scope}_session_url"
134
+ end
135
+
136
+ def scope_url
106
137
  opts = {}
107
- route = :"new_#{scope}_session_path"
138
+ route = route(scope)
108
139
  opts[:format] = request_format unless skip_format?
109
140
 
110
141
  config = Rails.application.config
111
- opts[:script_name] = (config.relative_url_root if config.respond_to?(:relative_url_root))
112
142
 
113
- context = send(Devise.available_router_name)
143
+ if config.respond_to?(:relative_url_root)
144
+ # Rails 4.2 goes into an infinite loop if opts[:script_name] is unset
145
+ rails_4_2 = (Rails::VERSION::MAJOR >= 4) && (Rails::VERSION::MINOR >= 2)
146
+ if config.relative_url_root.present? || rails_4_2
147
+ opts[:script_name] = config.relative_url_root
148
+ end
149
+ end
150
+
151
+ router_name = Devise.mappings[scope].router_name || Devise.available_router_name
152
+ context = send(router_name)
114
153
 
115
154
  if context.respond_to?(route)
116
155
  context.send(route, opts)
117
- elsif respond_to?(:root_path)
118
- root_path(opts)
156
+ elsif respond_to?(:root_url)
157
+ root_url(opts)
119
158
  else
120
159
  "/"
121
160
  end
@@ -144,7 +183,7 @@ module Devise
144
183
  # It does not make sense to send authenticate headers in ajax requests
145
184
  # or if the user disabled them.
146
185
  def http_auth_header?
147
- Devise.mappings[scope].to.http_authenticatable && !request.xhr?
186
+ scope_class.http_authenticatable && !request.xhr?
148
187
  end
149
188
 
150
189
  def http_auth_body
@@ -167,11 +206,11 @@ module Devise
167
206
  end
168
207
 
169
208
  def warden
170
- env['warden']
209
+ request.respond_to?(:get_header) ? request.get_header("warden") : env["warden"]
171
210
  end
172
211
 
173
212
  def warden_options
174
- env['warden.options']
213
+ request.respond_to?(:get_header) ? request.get_header("warden.options") : env["warden.options"]
175
214
  end
176
215
 
177
216
  def warden_message
@@ -182,6 +221,10 @@ module Devise
182
221
  @scope ||= warden_options[:scope] || Devise.default_scope
183
222
  end
184
223
 
224
+ def scope_class
225
+ @scope_class ||= Devise.mappings[scope].to
226
+ end
227
+
185
228
  def attempted_path
186
229
  warden_options[:attempted_path]
187
230
  end
@@ -198,6 +241,12 @@ module Devise
198
241
  Devise.navigational_formats.include?(request_format)
199
242
  end
200
243
 
244
+ # Check if flash messages should be emitted. Default is to do it on
245
+ # navigational formats
246
+ def is_flashing_format?
247
+ is_navigational_format?
248
+ end
249
+
201
250
  def request_format
202
251
  @request_format ||= request.format.try(:ref)
203
252
  end
@@ -1,7 +1,6 @@
1
- # Deny user access whenever their account is not active yet. All strategies that inherits from
2
- # Devise::Strategies::Authenticatable and uses the validate already check if the user is active_for_authentication?
3
- # before actively signing them in. However, we need this as hook to validate the user activity
4
- # in each request and in case the user is using other strategies beside Devise ones.
1
+ # Deny user access whenever their account is not active yet.
2
+ # We need this as hook to validate the user activity on each request
3
+ # and in case the user is using other strategies beside Devise ones.
5
4
  Warden::Manager.after_set_user do |record, warden, options|
6
5
  if record && record.respond_to?(:active_for_authentication?) && !record.active_for_authentication?
7
6
  scope = options[:scope]
@@ -1,5 +1,7 @@
1
1
  Warden::Manager.after_authentication do |record, warden, options|
2
- if Devise.clean_up_csrf_token_on_authentication
2
+ clean_up_for_winning_strategy = !warden.winning_strategy.respond_to?(:clean_up_csrf?) ||
3
+ warden.winning_strategy.clean_up_csrf?
4
+ if Devise.clean_up_csrf_token_on_authentication && clean_up_for_winning_strategy
3
5
  warden.request.session.try(:delete, :_csrf_token)
4
6
  end
5
7
  end
@@ -7,22 +7,27 @@ 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
- proxy = Devise::Hooks::Proxy.new(warden)
13
13
 
14
- if record.timedout?(last_request_at) && !env['devise.skip_timeout']
15
- Devise.sign_out_all_scopes ? proxy.sign_out : proxy.sign_out(scope)
14
+ if last_request_at.is_a? Integer
15
+ last_request_at = Time.at(last_request_at).utc
16
+ elsif last_request_at.is_a? String
17
+ last_request_at = Time.parse(last_request_at)
18
+ end
16
19
 
17
- if record.respond_to?(:expire_auth_token_on_timeout) && record.expire_auth_token_on_timeout
18
- record.reset_authentication_token!
19
- end
20
+ proxy = Devise::Hooks::Proxy.new(warden)
20
21
 
22
+ if record.timedout?(last_request_at) &&
23
+ !env['devise.skip_timeout'] &&
24
+ !proxy.remember_me_is_active?(record)
25
+ Devise.sign_out_all_scopes ? proxy.sign_out : proxy.sign_out(scope)
21
26
  throw :warden, scope: scope, message: :timeout
22
27
  end
23
28
 
24
29
  unless env['devise.skip_trackable']
25
- warden.session(scope)['last_request_at'] = Time.now.utc
30
+ warden.session(scope)['last_request_at'] = Time.now.utc.to_i
26
31
  end
27
32
  end
28
33
  end
@@ -64,7 +64,7 @@ module Devise
64
64
  template_path
65
65
  end
66
66
 
67
- # Setup a subject doing an I18n lookup. At first, it attempts to set a subject
67
+ # Set up a subject doing an I18n lookup. At first, it attempts to set a subject
68
68
  # based on the current mapping:
69
69
  #
70
70
  # en:
@@ -23,16 +23,18 @@ module Devise
23
23
  #
24
24
  class Mapping #:nodoc:
25
25
  attr_reader :singular, :scoped_path, :path, :controllers, :path_names,
26
- :class_name, :sign_out_via, :format, :used_routes, :used_helpers, :failure_app
26
+ :class_name, :sign_out_via, :format, :used_routes, :used_helpers,
27
+ :failure_app, :router_name
27
28
 
28
29
  alias :name :singular
29
30
 
30
31
  # Receives an object and find a scope for it. If a scope cannot be found,
31
32
  # raises an error. If a symbol is given, it's considered to be the scope.
32
33
  def self.find_scope!(obj)
34
+ obj = obj.devise_scope if obj.respond_to?(:devise_scope)
33
35
  case obj
34
36
  when String, Symbol
35
- return obj
37
+ return obj.to_sym
36
38
  when Class
37
39
  Devise.mappings.each_value { |m| return m.name if obj <= m.to }
38
40
  else
@@ -60,6 +62,8 @@ module Devise
60
62
  @sign_out_via = options[:sign_out_via] || Devise.sign_out_via
61
63
  @format = options[:format]
62
64
 
65
+ @router_name = options[:router_name]
66
+
63
67
  default_failure_app(options)
64
68
  default_controllers(options)
65
69
  default_path_names(options)
@@ -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
@@ -170,7 +164,13 @@ module Devise
170
164
  # end
171
165
  #
172
166
  def send_devise_notification(notification, *args)
173
- devise_mailer.send(notification, self, *args).deliver
167
+ message = devise_mailer.send(notification, self, *args)
168
+ # Remove once we move to Rails 4.2+ only.
169
+ if message.respond_to?(:deliver_now)
170
+ message.deliver_now
171
+ else
172
+ message.deliver
173
+ end
174
174
  end
175
175
 
176
176
  def downcase_keys
@@ -246,14 +246,18 @@ module Devise
246
246
  to_adapter.find_first(devise_parameter_filter.filter(tainted_conditions).merge(opts))
247
247
  end
248
248
 
249
- # 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.
250
250
  def find_or_initialize_with_error_by(attribute, value, error=:invalid) #:nodoc:
251
251
  find_or_initialize_with_errors([attribute], { attribute => value }, error)
252
252
  end
253
253
 
254
- # 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.
255
255
  def find_or_initialize_with_errors(required_attributes, attributes, error=:invalid) #:nodoc:
256
- attributes = attributes.slice(*required_attributes)
256
+ attributes = if attributes.respond_to? :permit
257
+ attributes.slice(*required_attributes).permit!.to_h.with_indifferent_access
258
+ else
259
+ attributes.with_indifferent_access.slice(*required_attributes)
260
+ end
257
261
  attributes.delete_if { |key, value| value.blank? }
258
262
 
259
263
  if attributes.size == required_attributes.size
@@ -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,21 +24,22 @@ 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 set up (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
  #
31
41
  module Confirmable
32
42
  extend ActiveSupport::Concern
33
- include ActionView::Helpers::DateHelper
34
43
 
35
44
  included do
36
45
  before_create :generate_confirmation_token, if: :confirmation_required?
@@ -56,7 +65,7 @@ module Devise
56
65
  # Confirm a user by setting it's confirmed_at to actual time. If the user
57
66
  # is already confirmed, add an error to email field. If the user is invalid
58
67
  # add errors
59
- def confirm!
68
+ def confirm(args={})
60
69
  pending_any_confirmation do
61
70
  if confirmation_period_expired?
62
71
  self.errors.add(:email, :confirmation_period_expired,
@@ -64,10 +73,9 @@ module Devise
64
73
  return false
65
74
  end
66
75
 
67
- self.confirmation_token = nil
68
76
  self.confirmed_at = Time.now.utc
69
77
 
70
- saved = if self.class.reconfirmable && unconfirmed_email.present?
78
+ saved = if pending_reconfirmation?
71
79
  skip_reconfirmation!
72
80
  self.email = unconfirmed_email
73
81
  self.unconfirmed_email = nil
@@ -75,7 +83,7 @@ module Devise
75
83
  # We need to validate in such cases to enforce e-mail uniqueness
76
84
  save(validate: true)
77
85
  else
78
- save(validate: false)
86
+ save(validate: args[:ensure_valid] == true)
79
87
  end
80
88
 
81
89
  after_confirmation if saved
@@ -83,6 +91,11 @@ module Devise
83
91
  end
84
92
  end
85
93
 
94
+ def confirm!(args={})
95
+ ActiveSupport::Deprecation.warn "confirm! is deprecated in favor of confirm"
96
+ confirm(args)
97
+ end
98
+
86
99
  # Verifies whether a user is confirmed or not
87
100
  def confirmed?
88
101
  !!confirmed_at
@@ -166,7 +179,7 @@ module Devise
166
179
  # Checks if the confirmation for the user is within the limit time.
167
180
  # We do this by calculating if the difference between today and the
168
181
  # confirmation sent date does not exceed the confirm in time configured.
169
- # Confirm_within is a model configuration, must always be an integer value.
182
+ # allow_unconfirmed_access_for is a model configuration, must always be an integer value.
170
183
  #
171
184
  # Example:
172
185
  #
@@ -202,7 +215,7 @@ module Devise
202
215
  # confirmation_period_expired? # will always return false
203
216
  #
204
217
  def confirmation_period_expired?
205
- self.class.confirm_within && (Time.now > self.confirmation_sent_at + self.class.confirm_within )
218
+ self.class.confirm_within && self.confirmation_sent_at && (Time.now > self.confirmation_sent_at + self.class.confirm_within)
206
219
  end
207
220
 
208
221
  # Checks whether the record requires any confirmation.
@@ -216,12 +229,15 @@ module Devise
216
229
  end
217
230
 
218
231
  # Generates a new random token for confirmation, and stores
219
- # the time this token is being generated
232
+ # the time this token is being generated in confirmation_sent_at
220
233
  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
234
+ if self.confirmation_token && !confirmation_period_expired?
235
+ @raw_confirmation_token = self.confirmation_token
236
+ else
237
+ raw, _ = Devise.token_generator.generate(self.class, :confirmation_token)
238
+ self.confirmation_token = @raw_confirmation_token = raw
239
+ self.confirmation_sent_at = Time.now.utc
240
+ end
225
241
  end
226
242
 
227
243
  def generate_confirmation_token!
@@ -232,23 +248,34 @@ module Devise
232
248
  @reconfirmation_required = true
233
249
  self.unconfirmed_email = self.email
234
250
  self.email = self.email_was
251
+ self.confirmation_token = nil
235
252
  generate_confirmation_token
236
253
  end
237
254
 
238
255
  def postpone_email_change?
239
- postpone = self.class.reconfirmable && email_changed? && !@bypass_confirmation_postpone && !self.email.blank?
256
+ postpone = self.class.reconfirmable && email_changed? && email_was.present? && !@bypass_confirmation_postpone && self.email.present?
240
257
  @bypass_confirmation_postpone = false
241
258
  postpone
242
259
  end
243
260
 
244
261
  def reconfirmation_required?
245
- self.class.reconfirmable && @reconfirmation_required && !self.email.blank?
262
+ self.class.reconfirmable && @reconfirmation_required && self.email.present?
246
263
  end
247
264
 
248
265
  def send_confirmation_notification?
249
- confirmation_required? && !@skip_confirmation_notification && !self.email.blank?
266
+ confirmation_required? && !@skip_confirmation_notification && self.email.present?
250
267
  end
251
268
 
269
+ # A callback initiated after successfully confirming. This can be
270
+ # used to insert your own logic that is only run after the user successfully
271
+ # confirms.
272
+ #
273
+ # Example:
274
+ #
275
+ # def after_confirmation
276
+ # self.update_attribute(:invite_code, nil)
277
+ # end
278
+ #
252
279
  def after_confirmation
253
280
  end
254
281
 
@@ -271,17 +298,23 @@ module Devise
271
298
  # If the user is already confirmed, create an error for the user
272
299
  # Options must have the confirmation_token
273
300
  def confirm_by_token(confirmation_token)
274
- original_token = confirmation_token
275
- confirmation_token = Devise.token_generator.digest(self, :confirmation_token, confirmation_token)
301
+ confirmable = find_first_by_auth_conditions(confirmation_token: confirmation_token)
302
+ unless confirmable
303
+ confirmation_digest = Devise.token_generator.digest(self, :confirmation_token, confirmation_token)
304
+ confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_digest)
305
+ end
306
+
307
+ # TODO: replace above lines with
308
+ # confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_token)
309
+ # after enough time has passed that Devise clients do not use digested tokens
276
310
 
277
- confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_token)
278
- confirmable.confirm! if confirmable.persisted?
279
- confirmable.confirmation_token = original_token
311
+ confirmable.confirm if confirmable.persisted?
280
312
  confirmable
281
313
  end
282
314
 
283
315
  # Find a record for confirmation by unconfirmed email field
284
316
  def find_by_unconfirmed_email_with_errors(attributes = {})
317
+ attributes = attributes.slice(*confirmation_keys).permit!.to_h if attributes.respond_to? :permit
285
318
  unconfirmed_required_attributes = confirmation_keys.map { |k| k == :email ? :unconfirmed_email : k }
286
319
  unconfirmed_attributes = attributes.symbolize_keys
287
320
  unconfirmed_attributes[:unconfirmed_email] = unconfirmed_attributes.delete(:email)
@@ -1,19 +1,18 @@
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
11
- # Authenticatable Module, responsible for encrypting password and validating
12
- # authenticity of a user while signing in.
10
+ # Authenticatable Module, responsible for hashing the password and
11
+ # validating the authenticity of a user while signing in.
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
@@ -36,18 +37,18 @@ module Devise
36
37
  [:encrypted_password] + klass.authentication_keys
37
38
  end
38
39
 
39
- # Generates password encryption based on the given value.
40
+ # Generates a hashed password based on the given value.
41
+ # For legacy reasons, we use `encrypted_password` to store
42
+ # the hashed password.
40
43
  def password=(new_password)
44
+ attribute_will_change! 'password'
41
45
  @password = new_password
42
46
  self.encrypted_password = password_digest(@password) if @password.present?
43
47
  end
44
48
 
45
- # Verifies whether an password (ie from sign in) is the user password.
49
+ # Verifies whether a password (ie from sign in) is the user password.
46
50
  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)
51
+ Devise::Encryptor.compare(self.class, encrypted_password, password)
51
52
  end
52
53
 
53
54
  # Set password and password confirmation to nil
@@ -55,9 +56,13 @@ module Devise
55
56
  self.password = self.password_confirmation = nil
56
57
  end
57
58
 
58
- # Update record attributes when :current_password matches, otherwise returns
59
- # error on :current_password. It also automatically rejects :password and
60
- # :password_confirmation if they are blank.
59
+ # Update record attributes when :current_password matches, otherwise
60
+ # returns error on :current_password.
61
+ #
62
+ # This method also rejects the password field if it is blank (allowing
63
+ # users to change relevant information like the e-mail without changing
64
+ # their password). In case the password field is rejected, the confirmation
65
+ # is also rejected as long as it is also blank.
61
66
  def update_with_password(params, *options)
62
67
  current_password = params.delete(:current_password)
63
68
 
@@ -133,19 +138,27 @@ module Devise
133
138
  encrypted_password[0,29] if encrypted_password
134
139
  end
135
140
 
141
+ def send_password_change_notification
142
+ send_devise_notification(:password_change)
143
+ end
144
+
136
145
  protected
137
146
 
138
- # Digests the password using bcrypt. Custom encryption should override
147
+ # Hashes the password using bcrypt. Custom hash functions should override
139
148
  # this method to apply their own algorithm.
140
149
  #
141
150
  # See https://github.com/plataformatec/devise-encryptable for examples
142
- # of other encryption engines.
151
+ # of other hashing engines.
143
152
  def password_digest(password)
144
- Devise.bcrypt(self.class, password)
153
+ Devise::Encryptor.digest(self.class, password)
154
+ end
155
+
156
+ def send_password_change_notification?
157
+ self.class.send_password_change_notification && encrypted_password_changed?
145
158
  end
146
159
 
147
160
  module ClassMethods
148
- Devise::Models.config(self, :pepper, :stretches)
161
+ Devise::Models.config(self, :pepper, :stretches, :send_password_change_notification)
149
162
 
150
163
  # We assume this method already gets the sanitized values from the
151
164
  # DatabaseAuthenticatable strategy. If you are using this method on