Skip to content

Commit 354e502

Browse files
author
José Valim
committed
Only allow insecure token lookup if a flag is given
1 parent 3cdbf15 commit 354e502

File tree

14 files changed

+108
-81
lines changed

14 files changed

+108
-81
lines changed

lib/devise.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ module Strategies
5050
mattr_accessor :secret_key
5151
@@secret_key = nil
5252

53-
# Secret key used by the key generator
54-
mattr_accessor :token_generator
55-
@@token_generator = nil
53+
# Allow insecure token lookup. Must be used
54+
# temporarily just for migration.
55+
mattr_accessor :allow_insecure_token_lookup
56+
@@allow_insecure_tokens_lookup = false
5657

5758
# Custom domain or key for cookies. Not set by default
5859
mattr_accessor :rememberable_options
@@ -260,6 +261,10 @@ module Strategies
260261
mattr_accessor :paranoid
261262
@@paranoid = false
262263

264+
# Stores the token generator
265+
mattr_accessor :token_generator
266+
@@token_generator = nil
267+
263268
# Default way to setup Devise. Run rails generate devise_install to create
264269
# a fresh initializer with all configuration values.
265270
def self.setup

lib/devise/models/confirmable.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,13 +276,16 @@ def send_confirmation_instructions(attributes={})
276276
# If the user is already confirmed, create an error for the user
277277
# Options must have the confirmation_token
278278
def confirm_by_token(confirmation_token)
279-
original_token = confirmation_token
279+
original_token = confirmation_token
280280
confirmation_token = Devise.token_generator.digest(self, :confirmation_token, confirmation_token)
281+
281282
confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_token)
282-
unless confirmable.persisted?
283+
if !confirmable.persisted? && Devise.allow_insecure_token_lookup
283284
confirmable = find_or_initialize_with_error_by(:confirmation_token, original_token)
284285
end
286+
285287
confirmable.confirm! if confirmable.persisted?
288+
confirmable.confirmation_token = original_token
286289
confirmable
287290
end
288291

lib/devise/models/lockable.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,15 @@ def send_unlock_instructions(attributes={})
162162
# Options must have the unlock_token
163163
def unlock_access_by_token(unlock_token)
164164
original_token = unlock_token
165-
unlock_token = Devise.token_generator.digest(self, :unlock_token, unlock_token)
165+
unlock_token = Devise.token_generator.digest(self, :unlock_token, unlock_token)
166166

167167
lockable = find_or_initialize_with_error_by(:unlock_token, unlock_token)
168-
unless lockable.persisted?
168+
if !lockable.persisted? && Devise.allow_insecure_token_lookup
169169
lockable = find_or_initialize_with_error_by(:unlock_token, original_token)
170170
end
171171

172172
lockable.unlock_access! if lockable.persisted?
173+
lockable.unlock_token = original_token
173174
lockable
174175
end
175176

lib/devise/models/recoverable.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,11 @@ def reset_password_token
112112
# containing an error in reset_password_token attribute.
113113
# Attributes must contain reset_password_token, password and confirmation
114114
def reset_password_by_token(attributes={})
115-
original_token = attributes[:reset_password_token]
115+
original_token = attributes[:reset_password_token]
116116
reset_password_token = Devise.token_generator.digest(self, :reset_password_token, original_token)
117117

118118
recoverable = find_or_initialize_with_error_by(:reset_password_token, reset_password_token)
119-
unless recoverable.persisted?
119+
if !recoverable.persisted? && Devise.allow_insecure_token_lookup
120120
recoverable = find_or_initialize_with_error_by(:reset_password_token, original_token)
121121
end
122122

@@ -127,6 +127,8 @@ def reset_password_by_token(attributes={})
127127
recoverable.errors.add(:reset_password_token, :expired)
128128
end
129129
end
130+
131+
recoverable.reset_password_token = original_token
130132
recoverable
131133
end
132134

lib/devise/token_generator.rb

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,23 @@ def initialize(key_generator)
1010
end
1111

1212
def digest(klass, column, value)
13-
value.present? && OpenSSL::HMAC.hexdigest("SHA1", key_for(klass, column), value.to_s)
13+
value.present? && OpenSSL::HMAC.hexdigest("SHA1", key_for(column), value.to_s)
1414
end
1515

1616
def generate(klass, column)
17-
adapter = klass.to_adapter
18-
key = key_for(klass, column)
17+
key = key_for(column)
1918

2019
loop do
2120
raw = Devise.friendly_token
2221
enc = OpenSSL::HMAC.hexdigest("SHA1", key, raw)
23-
break [raw, enc] unless adapter.find_first({ column => enc })
22+
break [raw, enc] unless klass.to_adapter.find_first({ column => enc })
2423
end
2524
end
2625

2726
private
2827

29-
def key_for(klass, column)
30-
@key_generator.generate_key("#{klass.name} #{column}")
28+
def key_for(column)
29+
@key_generator.generate_key(column.to_s)
3130
end
3231
end
3332

test/controllers/passwords_controller_test.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@ class PasswordsControllerTest < ActionController::TestCase
44
tests Devise::PasswordsController
55
include Devise::TestHelpers
66

7-
def setup
7+
setup do
88
request.env["devise.mapping"] = Devise.mappings[:user]
9-
109
@user = create_user
11-
@user.send_reset_password_instructions
10+
@raw = @user.send_reset_password_instructions
1211
end
1312

1413
def put_update_with_params
1514
put :update, "user" => {
16-
"reset_password_token" => @user.reset_password_token, "password" => "123456", "password_confirmation" => "123456"
15+
"reset_password_token" => @raw, "password" => "123456", "password_confirmation" => "123456"
1716
}
1817
end
1918

test/integration/confirmable_test.rb

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ def resend_confirmation
2828

2929
test 'user should receive a confirmation from a custom mailer' do
3030
User.any_instance.stubs(:devise_mailer).returns(Users::Mailer)
31-
3231
resend_confirmation
33-
3432
assert_equal ['[email protected]'], ActionMailer::Base.deliveries.first.from
3533
end
3634

@@ -43,7 +41,7 @@ def resend_confirmation
4341
test 'user with valid confirmation token should be able to confirm an account' do
4442
user = create_user(:confirm => false)
4543
assert_not user.confirmed?
46-
visit_user_confirmation_with_token(user.confirmation_token)
44+
visit_user_confirmation_with_token(user.raw_confirmation_token)
4745

4846
assert_contain 'Your account was successfully confirmed.'
4947
assert_current_url '/'
@@ -54,7 +52,7 @@ def resend_confirmation
5452
swap Devise, :confirm_within => 3.days do
5553
user = create_user(:confirm => false, :confirmation_sent_at => 4.days.ago)
5654
assert_not user.confirmed?
57-
visit_user_confirmation_with_token(user.confirmation_token)
55+
visit_user_confirmation_with_token(user.raw_confirmation_token)
5856

5957
assert_have_selector '#error_explanation'
6058
assert_contain /needs to be confirmed within 3 days/
@@ -66,7 +64,7 @@ def resend_confirmation
6664
swap Devise, :confirm_within => 3.days do
6765
user = create_user(:confirm => false, :confirmation_sent_at => 2.days.ago)
6866
assert_not user.confirmed?
69-
visit_user_confirmation_with_token(user.confirmation_token)
67+
visit_user_confirmation_with_token(user.raw_confirmation_token)
7068

7169
assert_contain 'Your account was successfully confirmed.'
7270
assert_current_url '/'
@@ -78,7 +76,7 @@ def resend_confirmation
7876
Devise::ConfirmationsController.any_instance.stubs(:after_confirmation_path_for).returns("/?custom=1")
7977

8078
user = create_user(:confirm => false)
81-
visit_user_confirmation_with_token(user.confirmation_token)
79+
visit_user_confirmation_with_token(user.raw_confirmation_token)
8280

8381
assert_current_url "/?custom=1"
8482
end
@@ -87,7 +85,7 @@ def resend_confirmation
8785
user = create_user(:confirm => false)
8886
user.confirmed_at = Time.now
8987
user.save
90-
visit_user_confirmation_with_token(user.confirmation_token)
88+
visit_user_confirmation_with_token(user.raw_confirmation_token)
9189

9290
assert_have_selector '#error_explanation'
9391
assert_contain 'already confirmed'
@@ -98,7 +96,7 @@ def resend_confirmation
9896
user.confirmed_at = Time.now
9997
user.save
10098

101-
visit_user_confirmation_with_token(user.confirmation_token)
99+
visit_user_confirmation_with_token(user.raw_confirmation_token)
102100
assert_contain 'already confirmed'
103101

104102
fill_in 'email', :with => user.email
@@ -108,14 +106,14 @@ def resend_confirmation
108106

109107
test 'sign in user automatically after confirming its email' do
110108
user = create_user(:confirm => false)
111-
visit_user_confirmation_with_token(user.confirmation_token)
109+
visit_user_confirmation_with_token(user.raw_confirmation_token)
112110

113111
assert warden.authenticated?(:user)
114112
end
115113

116114
test 'increases sign count when signed in through confirmation' do
117115
user = create_user(:confirm => false)
118-
visit_user_confirmation_with_token(user.confirmation_token)
116+
visit_user_confirmation_with_token(user.raw_confirmation_token)
119117

120118
user.reload
121119
assert_equal 1, user.sign_in_count
@@ -175,7 +173,7 @@ def resend_confirmation
175173

176174
test 'confirm account with valid confirmation token in XML format should return valid response' do
177175
user = create_user(:confirm => false)
178-
get user_confirmation_path(:confirmation_token => user.confirmation_token, :format => 'xml')
176+
get user_confirmation_path(:confirmation_token => user.raw_confirmation_token, :format => 'xml')
179177
assert_response :success
180178
assert response.body.include? %(<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<user>)
181179
end
@@ -256,7 +254,7 @@ def visit_admin_confirmation_with_token(confirmation_token)
256254
admin = create_admin
257255
admin.update_attributes(:email => '[email protected]')
258256
assert_equal '[email protected]', admin.unconfirmed_email
259-
visit_admin_confirmation_with_token(admin.confirmation_token)
257+
visit_admin_confirmation_with_token(admin.raw_confirmation_token)
260258

261259
assert_contain 'Your account was successfully confirmed.'
262260
assert_current_url '/admin_area/home'
@@ -269,15 +267,17 @@ def visit_admin_confirmation_with_token(confirmation_token)
269267
admin.update_attributes(:email => '[email protected]')
270268
assert_equal '[email protected]', admin.unconfirmed_email
271269

272-
confirmation_token = admin.confirmation_token
270+
raw_confirmation_token = admin.raw_confirmation_token
271+
admin = Admin.find(admin.id)
272+
273273
admin.update_attributes(:email => '[email protected]')
274274
assert_equal '[email protected]', admin.unconfirmed_email
275275

276-
visit_admin_confirmation_with_token(confirmation_token)
276+
visit_admin_confirmation_with_token(raw_confirmation_token)
277277
assert_have_selector '#error_explanation'
278278
assert_contain(/Confirmation token(.*)invalid/)
279279

280-
visit_admin_confirmation_with_token(admin.confirmation_token)
280+
visit_admin_confirmation_with_token(admin.raw_confirmation_token)
281281
assert_contain 'Your account was successfully confirmed.'
282282
assert_current_url '/admin_area/home'
283283
assert admin.reload.confirmed?
@@ -291,7 +291,7 @@ def visit_admin_confirmation_with_token(confirmation_token)
291291

292292
create_second_admin(:email => "[email protected]")
293293

294-
visit_admin_confirmation_with_token(admin.confirmation_token)
294+
visit_admin_confirmation_with_token(admin.raw_confirmation_token)
295295
assert_have_selector '#error_explanation'
296296
assert_contain(/Email.*already.*taken/)
297297
assert admin.reload.pending_reconfirmation?

test/integration/lockable_test.rb

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def send_unlock_request
1313
visit new_user_session_path
1414
click_link "Didn't receive unlock instructions?"
1515

16+
Devise.stubs(:friendly_token).returns("abcdef")
1617
fill_in 'email', :with => user.email
1718
click_button 'Resend unlock instructions'
1819
end
@@ -22,8 +23,11 @@ def send_unlock_request
2223

2324
assert_template 'sessions/new'
2425
assert_contain 'You will receive an email with instructions about how to unlock your account in a few minutes'
26+
27+
mail = ActionMailer::Base.deliveries.last
2528
assert_equal 1, ActionMailer::Base.deliveries.size
26-
assert_equal ['[email protected]'], ActionMailer::Base.deliveries.first.from
29+
assert_equal ['[email protected]'], mail.from
30+
assert_match user_unlock_path(unlock_token: 'abcdef'), mail.body.encoded
2731
end
2832

2933
test 'user should receive the instructions from a custom mailer' do
@@ -75,23 +79,15 @@ def send_unlock_request
7579
end
7680

7781
test "locked user should be able to unlock account" do
78-
user = create_user(:locked => true)
79-
assert user.access_locked?
80-
81-
visit_user_unlock_with_token(user.unlock_token)
82+
user = create_user
83+
raw = user.lock_access!
84+
visit_user_unlock_with_token(raw)
8285

8386
assert_current_url "/users/sign_in"
8487
assert_contain 'Your account has been unlocked successfully. Please sign in to continue.'
85-
8688
assert_not user.reload.access_locked?
8789
end
8890

89-
test "redirect user to sign in page after unlocking its account" do
90-
user = create_user(:locked => true)
91-
visit_user_unlock_with_token(user.unlock_token)
92-
assert_not warden.authenticated?(:user)
93-
end
94-
9591
test "user should not send a new e-mail if already locked" do
9692
user = create_user(:locked => true)
9793
user.failed_attempts = User.maximum_attempts + 1
@@ -153,9 +149,10 @@ def send_unlock_request
153149
end
154150

155151
test 'user with valid unlock token should be able to unlock account via XML request' do
156-
user = create_user(:locked => true)
152+
user = create_user()
153+
raw = user.lock_access!
157154
assert user.access_locked?
158-
get user_unlock_path(:format => 'xml', :unlock_token => user.unlock_token)
155+
get user_unlock_path(:format => 'xml', :unlock_token => raw)
159156
assert_response :success
160157
assert response.body.include? %(<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<user>)
161158
end

0 commit comments

Comments
 (0)