Skip to content

Commit eb640ed

Browse files
committed
Do not use digests for confirmation tokens
1 parent e538f02 commit eb640ed

File tree

3 files changed

+34
-12
lines changed

3 files changed

+34
-12
lines changed

lib/devise/models/confirmable.rb

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module Models
77
#
88
# Confirmable tracks the following columns:
99
#
10-
# * confirmation_token - An OpenSSL::HMAC.hexdigest of @raw_confirmation_token
10+
# * confirmation_token - A unique random token
1111
# * confirmed_at - A timestamp when the user clicked the confirmation link
1212
# * confirmation_sent_at - A timestamp when the confirmation_token was generated (not sent)
1313
# * unconfirmed_email - An email address copied from the email attr. After confirmation
@@ -29,6 +29,8 @@ module Models
2929
# confirmation.
3030
# * +confirm_within+: the time before a sent confirmation token becomes invalid.
3131
# 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.
3234
#
3335
# == Examples
3436
#
@@ -230,10 +232,13 @@ def pending_any_confirmation
230232
# Generates a new random token for confirmation, and stores
231233
# the time this token is being generated in confirmation_sent_at
232234
def generate_confirmation_token
233-
raw, enc = Devise.token_generator.generate(self.class, :confirmation_token)
234-
@raw_confirmation_token = raw
235-
self.confirmation_token = enc
236-
self.confirmation_sent_at = Time.now.utc
235+
if self.confirmation_token && !confirmation_period_expired?
236+
@raw_confirmation_token = self.confirmation_token
237+
else
238+
raw, _ = Devise.token_generator.generate(self.class, :confirmation_token)
239+
self.confirmation_token = @raw_confirmation_token = raw
240+
self.confirmation_sent_at = Time.now.utc
241+
end
237242
end
238243

239244
def generate_confirmation_token!
@@ -244,6 +249,7 @@ def postpone_email_change_until_confirmation_and_regenerate_confirmation_token
244249
@reconfirmation_required = true
245250
self.unconfirmed_email = self.email
246251
self.email = self.email_was
252+
self.confirmation_token = nil
247253
generate_confirmation_token
248254
end
249255

@@ -293,12 +299,17 @@ def send_confirmation_instructions(attributes={})
293299
# If the user is already confirmed, create an error for the user
294300
# Options must have the confirmation_token
295301
def confirm_by_token(confirmation_token)
296-
original_token = confirmation_token
297-
confirmation_token = Devise.token_generator.digest(self, :confirmation_token, confirmation_token)
302+
confirmable = find_first_by_auth_conditions(confirmation_token: confirmation_token)
303+
unless confirmable
304+
confirmation_digest = Devise.token_generator.digest(self, :confirmation_token, confirmation_token)
305+
confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_digest)
306+
end
307+
308+
# TODO: replace above lines with
309+
# confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_token)
310+
# after enough time has passed that Devise clients do not use digested tokens
298311

299-
confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_token)
300312
confirmable.confirm if confirmable.persisted?
301-
confirmable.confirmation_token = original_token
302313
confirmable
303314
end
304315

test/mailers/confirmation_instructions_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def mail
8686
host, port = ActionMailer::Base.default_url_options.values_at :host, :port
8787

8888
if mail.body.encoded =~ %r{<a href=\"http://#{host}:#{port}/users/confirmation\?confirmation_token=([^"]+)">}
89-
assert_equal Devise.token_generator.digest(user.class, :confirmation_token, $1), user.confirmation_token
89+
assert_equal $1, user.confirmation_token
9090
else
9191
flunk "expected confirmation url regex to match"
9292
end

test/models/confirmable_test.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,12 +291,23 @@ def confirm_user_by_token_with_confirmation_sent_at(confirmation_sent_at)
291291
end
292292
end
293293

294-
test 'always generate a new token on resend' do
294+
test 'do not generate a new token on resend' do
295295
user = create_user
296296
old = user.confirmation_token
297297
user = User.find(user.id)
298298
user.resend_confirmation_instructions
299-
assert_not_equal user.confirmation_token, old
299+
assert_equal user.confirmation_token, old
300+
end
301+
302+
test 'generate a new token after first has expired' do
303+
swap Devise, confirm_within: 3.days do
304+
user = create_user
305+
old = user.confirmation_token
306+
user.update_attribute(:confirmation_sent_at, 4.days.ago)
307+
user = User.find(user.id)
308+
user.resend_confirmation_instructions
309+
assert_not_equal user.confirmation_token, old
310+
end
300311
end
301312

302313
test 'should call after_confirmation if confirmed' do

0 commit comments

Comments
 (0)