المصادقة الثنائية بكلمات المرور لمرة واحدة (OTP) في Ruby on Rails باستخدام ROTP
تطبيق المصادقة الثنائية بكلمات مرور لمرة واحدة طريقة ممتازة لإضافة طبقة أمان إضافية لمستخدميكم.

ما هي المصادقة الثنائية بـ OTP؟
المصادقة الثنائية (2FA) تعني أن النظام يطلب خطوتين لتسجيل دخول المستخدم، مثل اسم المستخدم وكلمة المرور، ثم رمز يُرسل عبر البريد الإلكتروني أو رسالة SMS. أما المصادقة الثنائية بكلمة مرور لمرة واحدة (OTP 2FA) فهي أسهل على المستخدم، لأن الرمز يُولَّد على جهازه بفضل سرّ (secret) مشترك بين التطبيق والمستخدم. ولإعداد OTP، يمسح المستخدم أولًا رمز QR يحتوي على هذا السر، باستخدام تطبيق مثل Google Authenticator أو 1Password. وبعد الإعداد يستطيع جهاز المستخدم توليد رمز OTP يبقى صالحًا لفترة زمنية محددة، تتراوح عادة بين 15 و30 ثانية.
الإعداد
مع rtop gem يصبح إعداد المصادقة الثنائية بـ OTP بسيطًا جدًا. سأفترض أن لديكم نموذج مستخدم مع نظام مصادقة قائم، وغالبًا ما يكون Devise. لنثبّت rotp وrqrcode حتى نتمكن من توليد رموز QR تمسحونها من مدير كلمات المرور المفضل لديكم.
# Gemfile
gem 'rotp'
gem 'rqrcode'
إعداد ROTP
لإعداد ROTP نحتاج إلى إضافة عمود otp_secret وعمود last_otp_at (من نوع integer، لا date) إلى جدول User، وسنستخدمهما لتوليد رمز OTP التالي والتحقق منه.
bin/rails generate migration add_otp_secret_to_users otp_secret:string last_otp_at:integer
bin/rails db:migrate
توليد السر والتحقق من أول رمز OTP للمستخدم
نحتاج الآن إلى السماح للمستخدمين بإعداد OTP. أين تضعون ذلك في تطبيقكم قرار يعود إليكم، ومن منظور تجربة المستخدم هناك خياران:
- حماية OTP إلزامية
- بعد التسجيل في التطبيق، أعيدوا توجيه المستخدم إلى صفحة إعداد OTP ليضبط سر OTP الخاص به
- حماية OTP اختيارية
- اسمحوا للمستخدم بتفعيل حماية OTP أو إيقافها
- بعد التفعيل، أعيدوا توجيهه إلى صفحة إعداد OTP ليضبط سر OTP الخاص به
- بعد الإيقاف، احذفوا سر OTP
- اسمحوا للمستخدم بتفعيل حماية OTP أو إيقافها
لإنشاء otp_secret نولّد سلسلة base32 باستخدام مكتبة ROTP، ثم نتحقق من أن المستخدم أعدّ تطبيق المصادقة لديه بشكل صحيح قبل حفظ السر، حتى لا نحرمه من الدخول إلى حسابه.
سأنفذ كل ذلك داخل الـ controller لتسهيل العرض، لكنني أنصح بنقله إلى مكان آخر، مثل form objects أو service objects أو interactors.
# app/controllers/otp_secrets_controller.rb
class OtpSecretsController < ApplicationController
def new
@otp_secret = ROTP::Base32.random
totp = ROTP::TOTP.new(
@otp_secret, issuer: 'YourAppName'
)
@qr_code = RQRCode::QRCode
.new(totp.provisioning_uri(current_user.email))
.as_png(resize_exactly_to: 200)
.to_data_url
end
def create
@otp_secret = params[:otp_secret]
totp = ROTP::TOTP.new(
@otp_secret, issuer: 'YourAppName'
)
last_otp_at = totp.verify(
params[:otp_attempt], drift_behind: 15
)
if last_otp_at
current_user.update(
otp_secret: @otp_secret, last_otp_at: last_otp_at
)
redirect_to(
after_otp_configuration_path,
notice: 'Successfully configured OTP protection for your account'
)
else
flash.now[:alert] = 'The code you provided was invalid!'
@qr_code = RQRCode::QRCode
.new(totp.provisioning_uri(current_user.email))
.as_png(resize_exactly_to: 200)
.to_data_url
render :new
end
end
end
الـ view
نحتاج الآن إلى إنشاء نموذج يعرض الـ action المسمى new أعلاه
# app/views/otp_secrets/new.html.erb
<%= form_tag otp_secrets_path, method: :post do %>
<%= hidden_field_tag :otp_secret %>
<%= image_tag @qr_code %>
<%= text_field_tag :otp_attempt, label: "<strong>Verify (enter a one-time password)"</strong> %>
<% end %>
يُفترض أن يكون لديكم الآن شيء يشبه هذا هذا كل ما يلزم لإعداد المستخدم. بقي فقط أن نُلزم المستخدم بإدخال رمز OTP عند تسجيل الدخول
التحقق من رموز OTP عند تسجيل الدخول
إذا كنتم تُلزمون جميع المستخدمين بتأمين حساباتهم بـ OTP، فالأمر بسيط: أضيفوا حقل إدخال نصي إلى نموذج تسجيل الدخول وتحققوا منه. أما إذا كان OTP اختياريًا، فعليكم التحقق مما إذا كان مفعّلًا أثناء تسجيل الدخول، وتأجيل إتمام الدخول حتى يتحقق المستخدم من رمز OTP بنجاح. منطق التحقق من OTP بسيط للغاية. لقد نفذناه مرة من قبل، وكل ما نحتاجه تعديل بسيط ليشمل last_otp_attempt
last_otp_at = totp.verify(
otp_attempt, after: user.last_otp_at, drift_behind: 15
)
if last_otp_at
user.update(last_otp_at: last_otp_at)
else
# Return an error to the user
end
أفكار إضافية
هذا كل ما في الأمر. يكفيكم هذا المقال للبدء، لكن هناك أمور أخرى تستحق التفكير:
- يجب تشفير
otp_secretلحماية مستخدميكم في حال تسرّب قاعدة البيانات. أنا أستخدم attr_encrypted. - عليكم إجراء بعض التحقق للتأكد من أن سر OTP لم يُعبث به. أفضّل استخدام ActiveSupport::MessageEncryptor لأتأكد من أن ما أرسلته إلى المستخدم هو نفسه ما يعود إليّ، ثم أفك ترميز السر وأعيد ترميزه للتأكد من أنه فعلًا سلسلة base32.
- إذا كنتم تستخدمون عملية تسجيل دخول من خطوتين، فتأكدوا من أن مستخدميكم:
- لا يُعدّون مسجّلين فعليًا حتى تتحققوا من رمز OTP بنجاح.
- أو أنشئوا before_action يتحقق من أنهم أدخلوا رمز OTP صحيحًا بعد آخر تسجيل دخول. والخيار الثاني أعلى تكلفة بكثير لأنه يُنفَّذ مع كل طلب.


