كيف تطبّقون المصادقة بلا كلمة مرور في Ruby on Rails
تحدثنا مؤخرًا عن فوائد المصادقة بلا كلمة مرور. لنرَ الآن كيف نطبّقها عمليًا في Ruby on Rails.

في المقال السابق تحدثت عن فوائد المصادقة بلا كلمة مرور. وفي هذا المقال سأوضح كيف نطبّق رسائل البريد التي تحمل رابط تسجيل دخول سحريًا (magic link) في Ruby on Rails. لمن لا يعرف الفكرة: تتيح روابط تسجيل الدخول السحرية للمستخدم الدخول إلى حسابه من دون إدخال كلمة مرور. فبدلًا من النموذج التقليدي للبريد الإلكتروني وكلمة المرور، يكفي أن يُدخل المستخدم بريده الإلكتروني. يرسل النظام بعد ذلك رابطًا يحتوي على token مرتبط بحسابه. وعندما ينقر المستخدم على الرابط، ينتقل إلى التطبيق ويُسجَّل دخوله إلى حسابه مباشرة. وبهذه الطريقة يمكننا أيضًا تسجيل دخوله في التطبيقات الأصلية (native) باستخدام deeplinks.
لنكتب بعض الكود
أول ما نحتاج إليه هو إضافة عمود جديد إلى جدول Users لتخزين login tokens. سنستخدم UUIDs لتبسيط عملية التوليد.
rails g migration add_login_token_to_users login_token:uuid
لننشئ الآن controller يتولى عمليات تسجيل الدخول. لن يكون هذا الـ controller متوافقًا تمامًا مع RESTful، لأننا لا نستطيع إرسال طلب POST من برنامج البريد لدى المستخدم. عادةً أستخدم magic_logins_controller حتى نحتفظ بـ sessions controller لتسجيل الدخول بكلمة المرور، إن أردنا يومًا الجمع بين الطريقتين.
class MagicLoginsController < ApplicationController
def new; end
def create
if (user = User.find_by(login_token_params))
user.generate_login_token!
LoginMailer.with(user: user).magic_login_email.deliver_later
redirect_to(new_magic_login_path, notice: 'Please check your inbox')
else
redirect_to(new_magic_login_path, alert: 'It looks like that email is not registered')
end
end
private def login_token_params
params.require(:login_token).permit(:email)
end
end
Rails.application.routes.draw do
resources :magic_logins, only: %i[new create]
end
لنُضف view للـ action المسمّى new. لن أضيف أي تنسيق، ويمكنكم تنسيقه كما تشاؤون، سنكتفي بنموذج بسيط كمثال.
<%= form_for(User.new, url: magic_logins_path, method: :post do %>
<%= f.label :email %>
<%= f.email_field :email %>
<%= f.submit 'Send me a magic login link' %>
<% end %>
بعد ذلك نحتاج إلى إضافة method في الـ model المسمّى User لتوليد token جديد.
class User < ApplicationRecord
def generate_login_token!
update_column(:login_token, SecureRandom.uuid)
end
end
لا نحتاج إلى أي تحقق هنا لأننا نستخدم UUIDs، فنحن في أمان. حسنًا، لننشئ الآن _magic_login_email. _
class LoginMailer < ApplicationMailer
def magic_login_email
user = params[:user]
@magic_login_url = magic_logins_url(token: user.login_token)
mail(to: user.email, subject: "Here's your login link")
end
end
<h1>Here's your login link</h1>
<%= link_to 'Log in now', @magic_login_url %>
ربما لاحظتم أننا نستخدم رابط index ونمرّر الـ token في query string. هناك طرق أخرى لفعل ذلك، لكن هذا النهج بسيط ويتماشى مع ما يوفّره Rails أصلًا. وهو بالطبع يخالف مبادئ RESTful، لذلك قد تفضّلون نهجًا آخر. الآن لم يبقَ سوى إضافة منطق تسجيل دخول المستخدمين. سأفترض أنكم تستخدمون Devise. إن كنتم تستخدمون gem آخر أو تديرون الجلسات يدويًا، فستحتاجون إلى تعديل هذا المنطق.
class MagicLoginsController < ApplicationController
def index
redirect_to root_path unless params[:token].present?
user = User.find_by(login_token: params[:token])
if user
sign_in(user)
user.update_column(:login_token, nil)
redirect_to after_sign_in_path, notice: 'Logged in successfully!'
else
redirect_to new_magic_login_path, alert: 'The link you used was invalid. Please request a new login link'
end
end
end
كما ترون، نسجّل دخول المستخدم ثم نحذف login token من قاعدة البيانات، وهذا يمنع استخدام الـ token أكثر من مرة. أصبح كل شيء مترابطًا، ويمكن للمستخدمين الآن تسجيل الدخول عبر الروابط السحرية. وقد ترغبون أيضًا في تحديد مدة صلاحية الـ tokens. لفعل ذلك يكفي أن نضيف عمودًا آخر لتتبّع وقت إنشاء الـ token والتحقق من أنه لم تنتهِ صلاحيته
rails g migration add_login_token_generated_at_to_users login_token_generated_at:datetime
class MagicLoginsController < ApplicationController
def index
redirect_to root_path unless params[:token].present?
user = User.find_by(login_token: params[:token])
if user
if user.login_token_generated_at < 15.minutes.ago
redirect_to new_magic_login_path, alert: 'The link you used has expired. Please request a new login link'
end
sign_in(user)
user.update_column(:login_token, nil)
redirect_to after_sign_in_path, notice: 'Logged in successfully!'
else
redirect_to new_magic_login_path, alert: 'The link you used was invalid. Please request a new login link'
end
end
end
Rails.application.routes.draw do
resources :magic_logins, only: %i[index new create]
end
هذا كل ما في الأمر. التطبيق بسيط جدًا في الحقيقة.


