confirmation, reset password, unlock等需要token寄送的流程都會失效,使用者會看到「token 是無效的」。
這是3.1.0的backwards incompatible changes,新版的devise針對了token的部分做了加密流程,也就是在confirmation或reset password mail中帶的url的token並不會跟database中confirmation_token與reset_passwork_token一樣。所以舊版程式碼的mail將confirmation_token與reset_passwork_token直接帶入url就會造成「token無效問題」。
下面是DeviseCHANGELOG的說明:
另外在plataformatec的blog也有提到:
Store digested tokens in the database
In previous versions, Devise stored the tokens for confirmation, reset password and unlock directly in the database. This meant that somebody with read access to the database could use such tokens to sign in as someone else by, for example, resetting their password.
In Devise 3.1, we store an encrypted token in the database and the actual token is sent only via e-mail to the user. This means that:
修正方式很簡單,只要將mail中的token改成新版mailer傳來的token即可。
diff --git a/app/views/users/mailer/confirmation_instructions.html.erb b/app/views/users/mailer/confirmation_instructions.html.erb
index 2c84295..9d7d18e 100644
--- a/app/views/users/mailer/confirmation_instructions.html.erb
+++ b/app/views/users/mailer/confirmation_instructions.html.erb
@@ -3,7 +3,7 @@
<%= t('.instruction', :default => "You can confirm your account email through the link below:") %>
<%= link_to t('.action', :default => "Confirm my account"),
- confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) %>
+ confirmation_url(@resource, :confirmation_token => @token) %>
<%= t(".statement1") %>
<%= t(".statement2") %>
diff --git a/app/views/users/mailer/reset_password_instructions.html.erb b/app/views/users/mailer/reset_password_instructions.html.erb
index fa5bcec..8232037 100644
--- a/app/views/users/mailer/reset_password_instructions.html.erb
+++ b/app/views/users/mailer/reset_password_instructions.html.erb
@@ -2,7 +2,7 @@
<%= t('.instruction', :default => "Someone has requested a link to change your password, and you can do this through the link below.") %>
-<%= link_to t('.action', :default => "Change my password"), edit_password_url(@resource, :reset_password_token => @resource.reset_password_token) %>
+<%= link_to t('.action', :default => "Change my password"), edit_password_url(@resource, :reset_password_token => @token) %>
<%= t('.instruction_2') %>
<%= t('.instruction_3') %>
diff --git a/app/views/users/mailer/unlock_instructions.html.erb b/app/views/users/mailer/unlock_instructions.html.erb
index 8bd4887..54264d4 100644
--- a/app/views/users/mailer/unlock_instructions.html.erb
+++ b/app/views/users/mailer/unlock_instructions.html.erb
@@ -4,4 +4,4 @@
<%= t('.instruction', :default => "Click the link below to unlock your account:") %>
-<%= link_to t('.action', :default => "Unlock my account"), unlock_url(@resource, :unlock_token => @resource.unlock_token) %>
+<%= link_to t('.action', :default => "Unlock my account"), unlock_url(@resource, :unlock_token => @token) %>
不過要注意的是,一旦更新完上production,原本已經存在的confirmation, reset password, unlock token都會失效。devise提供了一個設定config.allow_insecure_token_lookup = true
讓新版的devise可以延用舊的token,但文件上建議為了安全性的考量不要長期使用這個設定。
比較好的做法是要將擁有這些token的帳號撈出來並重新寄送相對應的mail。下面是個簡單的rake來處理這個問題:
namespace :devise_upgrade do
TEST_ACCOUNT = "test.example"
desc "Resent confirmation and reset password mail"
task resend: :environment do
if Rails.env.production?
puts "In production mode"
c_users = User.where(confirmed_at: nil)
p_users = User.where("reset_password_token IS NOT NULL")
else
# NOTE: should send email to test account only
puts "In \#{Rails.env} mode"
c_users = User.where("confirmed_at IS NULL and email LIKE \"%\#{TEST_ACCOUNT}%\"")
p_users = User.where("reset_password_token IS NOT NULL and email LIKE \"%\#{TEST_ACCOUNT}%\"")
end
puts "Found \#{c_users.count} accounts which need to send the confirmation mail"
c_users.each do |cu|
puts "Send to: \#{cu.email}"
cu.resend_confirmation_instructions
end
puts "Found \#{p_users.count} accounts which need to send the reset password mail:"
p_users.each do |pu|
puts "Send to: \#{pu.email}"
pu.send_reset_password_instructions
end
end
end
升級成新版的devise之後,新註冊的user,在點選confirmation mail中的link做第一次confirmation時會失敗,但重寄confirmation mail後的第二次confirmation又會成功。
情況應該與這篇SO - Devise Confirmation invalid on first send是相同的問題,上面的SO提到,如果在RegistrationsController#create
中update user object,就會讓confirmation_token被意外的重新產生,變成email裡的token會與database的token對不上。實際從log發現,我們的程式碼會在user的after_create中更新欄位,這個動作就會造成confirmation_token被重新更新。
Started POST "/users" for 127.0.0.1 at 2014-11-12 17:16:02 +0800
Processing by Users::RegistrationsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"hbtvAY4Ds+jB/bVJsMl1LRk/P5Dr3bRskdt9EcI2aQE=", "user"=>{"email"=>"test@example.com", "name"=>"Kait007", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "referral_code"=>"", "is_policy_accepted"=>"1"}}
(0.2ms) BEGIN
User Exists (0.9ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = BINARY 'test@example.com' LIMIT 1
User Load (25.0ms) SELECT `users`.* FROM `users` WHERE `users`.`confirmation_token` = '0c5701d484010871605fb35a45bac7898e6461de2210783c092148c10e76df70' ORDER BY `users`.`id` ASC LIMIT 1
SQL (3.6ms) INSERT INTO `users` (`confirmation_sent_at`, `confirmation_token`, `created_at`, `email`, `encrypted_password`, `is_policy_accepted`, `name`, `referral_code`, `slug`, `updated_at`) VALUES ('2014-11-12 09:16:02', '0c5701d484010871605fb35a45bac7898e6461de2210783c092148c10e76df70', '2014-11-12 09:16:02', 'test@example.com', '$2a$10$jEKk/TiCe76nKB8iYmJN3.sj6Y.Y0AoUmaWr.q9vPVK5s40yW7/vu', 1, 'User 001', '', 'e533885908fd4943', '2014-11-12 09:16:02')
User Load (21.5ms) SELECT `users`.* FROM `users` WHERE `users`.`confirmation_token` = '3fb4b1a2ecfb67a4e92d7b9777576da24547a7bf3ff9df13627ee5ab93908a34' ORDER BY `users`.`id` ASC LIMIT 1
SQL (5.4ms) UPDATE `users` SET `api_key` = '960d00f3-0684-4cdc-9e22-b147a7a84820', `api_secret` = 'c4221dc7-eede-4392-924b-0e4c063152b5', `confirmation_sent_at` = '2014-11-12 09:16:02', `confirmation_token` = '3fb4b1a2ecfb67a4e92d7b9777576da24547a7bf3ff9df13627ee5ab93908a34', `created_at` = '2014-11-12 09:16:02', `encrypted_password` = '$2a$10$jEKk/TiCe76nKB8iYmJN3.sj6Y.Y0AoUmaWr.q9vPVK5s40yW7/vu', `id` = 7508, `is_policy_accepted` = 1, `name` = 'User 001', `referral_code` = '', `slug` = 'e533885908fd4943', `unconfirmed_email` = 'test@example.com', `updated_at` = '2014-11-12 09:16:02' WHERE `users`.`id` = 7508
(0.7ms) COMMIT
Redirected to http://localhost:1212/
Completed 302 Found in 191ms (ActiveRecord: 57.4ms)
將user model中的gen_key
從after_create
階段提前至before_create
的階段,並去掉gen_key
中的save
(因為save會在create的時候做掉)。
diff --git a/app/models/user.rb b/app/models/user.rb
index d25c05c..afcb63d 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -100,7 +100,7 @@ class User < ActiveRecord::Base
# callbacks
- after_create :gen_api
+ before_create :gen_api
# other
def to_param
@@ -110,7 +110,7 @@ class User < ActiveRecord::Base
def gen_api
self.api_secret = SecureRandom.uuid
self.api_key = SecureRandom.uuid
- self.save(validate: false)
end