إنشاء تدفقات كلمة مرور آمنة باستخدام NodeJS و MySQL

نشرت: 2022-03-10
ملخص سريع ↬ وظيفة إعادة تعيين كلمة المرور هي حصص الجدول لأي تطبيق سهل الاستخدام. يمكن أن يكون أيضًا كابوسًا أمنيًا. باستخدام NodeJS و MySQL ، يوضح دارشان كيفية إنشاء تدفق آمن لإعادة تعيين كلمة المرور بنجاح حتى تتمكن من تجنب هذه المزالق.

إذا كنت مثلي ، فقد نسيت كلمة مرورك أكثر من مرة ، خاصة على المواقع التي لم تزرها منذ فترة. ربما تكون قد شاهدت أيضًا و / أو تعرضت للإهانة من خلال إعادة تعيين رسائل البريد الإلكتروني الخاصة بكلمة المرور التي تحتوي على كلمة المرور الخاصة بك في نص عادي.

لسوء الحظ ، فإن سير عمل إعادة تعيين كلمة المرور يكتسب اهتمامًا قصيرًا واهتمامًا محدودًا أثناء تطوير التطبيق. لا يؤدي هذا إلى تجربة مستخدم محبطة فحسب ، بل قد يؤدي أيضًا إلى ترك تطبيقك به ثغرات أمنية كبيرة.

سنغطي كيفية إنشاء سير عمل آمن لإعادة تعيين كلمة المرور. سنستخدم NodeJS و MySQL كمكونات أساسية لدينا. إذا كنت تكتب باستخدام لغة أو إطار عمل أو قاعدة بيانات مختلفة ، فلا يزال بإمكانك الاستفادة من اتباع "إرشادات الأمان" العامة الموضحة في كل قسم.

يتكون تدفق إعادة تعيين كلمة المرور من المكونات التالية:

  • ارتباط لإرسال المستخدم إلى بداية سير العمل.
  • نموذج يتيح للمستخدم إرسال بريده الإلكتروني.
  • بحث يتحقق من صحة البريد الإلكتروني ويرسل بريدًا إلكترونيًا إلى العنوان.
  • بريد إلكتروني يحتوي على رمز إعادة التعيين مع انتهاء الصلاحية والذي يسمح للمستخدم بإعادة تعيين كلمة المرور الخاصة به.
  • نموذج يتيح للمستخدم إنشاء كلمة مرور جديدة.
  • حفظ كلمة المرور الجديدة والسماح للمستخدم بتسجيل الدخول مرة أخرى باستخدام كلمة المرور الجديدة.

إلى جانب Node و Express و MySQL ، سنستخدم المكتبات التالية:

  • تكملة ORM
  • Nodemailer

Sequelize هي قاعدة بيانات NodeJS ORM تسهل تشغيل عمليات ترحيل قاعدة البيانات بالإضافة إلى استعلامات إنشاء الأمان. Nodemailer هي مكتبة بريد إلكتروني شهيرة لـ NodeJS سنستخدمها لإرسال رسائل البريد الإلكتروني الخاصة بإعادة تعيين كلمة المرور.

نصيحة أمنية # 1

تشير بعض المقالات إلى أنه يمكن تصميم تدفقات كلمات المرور الآمنة باستخدام JSON Web Tokens (JWT) ، مما يلغي الحاجة إلى تخزين قاعدة البيانات (وبالتالي يسهل تنفيذها). لا نستخدم هذا الأسلوب على موقعنا ، لأن أسرار رمز JWT عادةً ما يتم تخزينها بشكل صحيح في التعليمات البرمجية. نريد تجنب وجود "سر واحد" للحكم عليهم جميعًا (لنفس السبب الذي يجعلك لا تستخدم كلمات المرور بنفس القيمة) ، وبالتالي نحتاج إلى نقل هذه المعلومات إلى قاعدة بيانات.

المزيد بعد القفز! أكمل القراءة أدناه ↓

تثبيت

أولاً ، قم بتثبيت Sequelize و Nodemailer والمكتبات الأخرى المرتبطة بها:

 $ npm install --save sequelize sequelize-cli mysql crypto nodemailer

في المسار حيث تريد تضمين مهام سير عمل إعادة التعيين ، أضف الوحدات النمطية المطلوبة. إذا كنت بحاجة إلى تجديد معلومات عن Express وways ، فاطلع على دليلهم.

 const nodemailer = require('nodemailer');

وقم بتكوينه باستخدام بيانات اعتماد SMTP للبريد الإلكتروني.

 const transport = nodemailer.createTransport({ host: process.env.EMAIL_HOST, port: process.env.EMAIL_PORT, secure: true, auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASS } });

حل البريد الإلكتروني الذي أستخدمه هو خدمة البريد الإلكتروني البسيطة من AWS ، ولكن يمكنك استخدام أي شيء (Mailgun ، إلخ).

إذا كانت هذه هي المرة الأولى التي تقوم فيها بإعداد خدمة إرسال البريد الإلكتروني ، فستحتاج إلى قضاء بعض الوقت في تكوين مفاتيح المجال المناسبة وإعداد التراخيص. إذا كنت تستخدم Route 53 جنبًا إلى جنب مع SES ، فهذا أمر بسيط للغاية ويتم تنفيذه تلقائيًا تقريبًا ، وهذا هو سبب اختياره. لدى AWS بعض البرامج التعليمية حول كيفية عمل SES مع Route53.

نصيحة أمنية رقم 2

لتخزين بيانات الاعتماد بعيدًا عن الكود الخاص بي ، أستخدم dotenv ، والذي يتيح لي إنشاء ملف .env محلي باستخدام متغيرات البيئة الخاصة بي. بهذه الطريقة ، عندما أقوم بالنشر في الإنتاج ، يمكنني استخدام مفاتيح إنتاج مختلفة غير مرئية في التعليمات البرمجية ، وبالتالي تتيح لي تقييد أذونات التهيئة الخاصة بي لأعضاء معينين فقط من فريقي.

إعداد قاعدة البيانات

نظرًا لأننا سنرسل رموز إعادة التعيين إلى المستخدمين ، فنحن بحاجة إلى تخزين هذه الرموز المميزة في قاعدة بيانات.

أفترض أن لديك جدول مستخدمين فعال في قاعدة البيانات الخاصة بك. إذا كنت تستخدم Sequelize بالفعل ، فهذا رائع! إذا لم يكن الأمر كذلك ، فقد ترغب في تحسين ميزة Sequelize و Sequelize CLI.

إذا لم تكن قد استخدمت Sequelize حتى الآن في تطبيقك ، فيمكنك إعداده عن طريق تشغيل الأمر أدناه في المجلد الجذر للتطبيق الخاص بك:

 $ sequelize init

سيؤدي هذا إلى إنشاء عدد من المجلدات الجديدة في الإعداد الخاص بك ، بما في ذلك عمليات الترحيل والنماذج.

سيؤدي هذا أيضًا إلى إنشاء ملف تكوين. في ملف التكوين الخاص بك ، قم بتحديث كتلة development باستخدام بيانات الاعتماد لخادم قاعدة بيانات mysql المحلي.

دعنا نستخدم أداة CLI الخاصة بـ Sequelize لإنشاء جدول قاعدة البيانات لنا.

 $ sequelize model:create --name ResetToken --attributes email:string,token:string,expiration:date,used:integer $ sequelize db:migrate

يحتوي هذا الجدول على الأعمدة التالية:

  • عنوان البريد الإلكتروني للمستخدم ،
  • الرمز الذي تم إنشاؤه ،
  • انتهاء صلاحية هذا الرمز المميز ،
  • سواء تم استخدام الرمز المميز أم لا.

في الخلفية ، يقوم Sequelize-cli بتشغيل استعلام SQL التالي:

 CREATE TABLE `ResetTokens` ( `id` int(11) NOT NULL AUTO_INCREMENT, `email` varchar(255) DEFAULT NULL, `token` varchar(255) DEFAULT NULL, `expiration` datetime DEFAULT NULL, `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, `used` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

تحقق من أن هذا يعمل بشكل صحيح باستخدام عميل SQL أو سطر الأوامر:

 mysql> describe ResetTokens; +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | email | varchar(255) | YES | | NULL | | | token | varchar(255) | YES | | NULL | | | expiration | datetime | YES | | NULL | | | createdAt | datetime | NO | | NULL | | | updatedAt | datetime | NO | | NULL | | | used | int(11) | NO | | 0 | | +------------+--------------+------+-----+---------+----------------+ 7 rows in set (0.00 sec)

نصيحة أمنية # 3

إذا كنت لا تستخدم حاليًا ORM ، فيجب أن تفكر في القيام بذلك. تعمل ORM على أتمتة الكتابة والإفلات المناسب لاستعلامات SQL ، مما يجعل شفرتك أكثر قابلية للقراءة وأكثر أمانًا بشكل افتراضي. سوف تساعدك على تجنب هجمات حقن SQL عن طريق الهروب بشكل صحيح من استعلامات SQL.

قم بإعداد مسار إعادة تعيين كلمة المرور

أنشئ مسار الحصول في user.js :

 router.get('/forgot-password', function(req, res, next) { res.render('user/forgot-password', { }); });

ثم قم بإنشاء مسار POST ، وهو المسار الذي يتم الوصول إليه عند نشر نموذج إعادة تعيين كلمة المرور. في الكود أدناه ، قمت بتضمين اثنين من ميزات الأمان الهامة.

نصائح أمنية # 4-6

  1. حتى إذا لم نعثر على عنوان بريد إلكتروني ، فإننا نرجع "موافق" كحالتنا. لا نريد روبوتات غير مرغوب فيها اكتشاف رسائل البريد الإلكتروني الحقيقية مقابل غير الحقيقية في قاعدة البيانات الخاصة بنا.
  2. كلما زاد عدد وحدات البايت العشوائية التي تستخدمها في الرمز المميز ، قل احتمال اختراقه. نحن نستخدم 64 بايت عشوائي في منشئ الرمز المميز الخاص بنا (لا تستخدم أقل من 8).
  3. تنتهي صلاحية الرمز المميز خلال ساعة واحدة. هذا يحد من النافذة الزمنية التي يعمل فيها رمز إعادة التعيين.
 router.post('/forgot-password', async function(req, res, next) { //ensure that you have a user with this email var email = await User.findOne({where: { email: req.body.email }}); if (email == null) { /** * we don't want to tell attackers that an * email doesn't exist, because that will let * them use this form to find ones that do * exist. **/ return res.json({status: 'ok'}); } /** * Expire any tokens that were previously * set for this user. That prevents old tokens * from being used. **/ await ResetToken.update({ used: 1 }, { where: { email: req.body.email } }); //Create a random reset token var fpSalt = crypto.randomBytes(64).toString('base64'); //token expires after one hour var expireDate = new Date(new Date().getTime() + (60 * 60 * 1000)) //insert token data into DB await ResetToken.create({ email: req.body.email, expiration: expireDate, token: fpSalt, used: 0 }); //create email const message = { from: process.env.SENDER_ADDRESS, to: req.body.email, replyTo: process.env.REPLYTO_ADDRESS, subject: process.env.FORGOT_PASS_SUBJECT_LINE, text: 'To reset your password, please click the link below.\n\nhttps://'+process.env.DOMAIN+'/user/reset-password?token='+encodeURIComponent(token)+'&email='+req.body.email }; //send email transport.sendMail(message, function (err, info) { if(err) { console.log(err)} else { console.log(info); } }); return res.json({status: 'ok'}); });

سترى متغير المستخدم المشار إليه أعلاه - ما هذا؟ لأغراض هذا البرنامج التعليمي ، نفترض أن لديك نموذج مستخدم يتصل بقاعدة البيانات الخاصة بك لاسترداد القيم. يعتمد الكود أعلاه على Sequelize ، ولكن يمكنك التعديل حسب الحاجة إذا قمت بالاستعلام عن قاعدة البيانات مباشرة (لكنني أوصي Sequelize!).

نحن الآن بحاجة إلى إنشاء العرض. باستخدام Bootstrap CSS و jQuery وإطار عمل الصلصال المضمن في إطار عمل Node Express ، يبدو العرض كما يلي:

 extends ../layout block content div.container div.row div.col h1 Forgot password p Enter your email address below. If we have it on file, we will send you a reset email. div.forgot-message.alert.alert-success() Email address received. If you have an email on file we will send you a reset email. Please wait a few minutes and check your spam folder if you don't see it. form#forgotPasswordForm.form-inline(onsubmit="return false;") div.form-group label.sr-only(for="email") Email address: input.form-control.mr-2#emailFp(type='email', name='email', placeholder="Email address") div.form-group.mt-1.text-center button#fpButton.btn.btn-success.mb-2(type='submit') Send email script. $('#fpButton').on('click', function() { $.post('/user/forgot-password', { email: $('#emailFp').val(), }, function(resp) { $('.forgot-message').show(); $('#forgotPasswordForm').remove(); }); });

هذا هو النموذج على الصفحة:

إعادة تعيين كلمة المرور لسير عمل إعادة تعيين كلمة المرور بشكل آمن
نموذج إعادة تعيين كلمة المرور الخاصة بك. (معاينة كبيرة)

في هذه المرحلة ، يجب أن تكون قادرًا على ملء النموذج بعنوان بريد إلكتروني موجود في قاعدة البيانات الخاصة بك ، ثم تلقي بريد إلكتروني لإعادة تعيين كلمة المرور على هذا العنوان. النقر فوق رابط إعادة التعيين لن يفعل أي شيء حتى الآن.

قم بإعداد مسار "إعادة تعيين كلمة المرور"

الآن دعنا نمضي قدمًا ونقوم بإعداد بقية سير العمل.

أضف وحدة Sequelize.Op إلى مسارك:

 const Sequelize = require('sequelize'); const Op = Sequelize.Op;

لنقم الآن ببناء مسار GET للمستخدمين الذين نقروا على رابط إعادة تعيين كلمة المرور. كما سترى أدناه ، نريد التأكد من أننا نتحقق من صحة رمز إعادة التعيين بشكل مناسب.

نصيحة أمنية رقم 7:

تأكد من أنك تبحث فقط عن رموز إعادة التعيين التي لم تنتهِ صلاحيتها ولم يتم استخدامها.

لأغراض العرض التوضيحي ، أقوم أيضًا بمسح جميع الرموز المميزة منتهية الصلاحية عند التحميل هنا للحفاظ على الجدول صغيرًا. إذا كان لديك موقع ويب كبير ، فقم بنقله إلى موقع cronjob.

 router.get('/reset-password', async function(req, res, next) { /** * This code clears all expired tokens. You * should move this to a cronjob if you have a * big site. We just include this in here as a * demonstration. **/ await ResetToken.destroy({ where: { expiration: { [Op.lt]: Sequelize.fn('CURDATE')}, } }); //find the token var record = await ResetToken.findOne({ where: { email: req.query.email, expiration: { [Op.gt]: Sequelize.fn('CURDATE')}, token: req.query.token, used: 0 } }); if (record == null) { return res.render('user/reset-password', { message: 'Token has expired. Please try password reset again.', showForm: false }); } res.render('user/reset-password', { showForm: true, record: record }); });

لنقم الآن بإنشاء مسار POST وهو ما يتم ضربه بمجرد أن يملأ المستخدم تفاصيل كلمة المرور الجديدة الخاصة به.

نصيحة أمنية من 8 إلى 11:

  • تأكد من تطابق كلمات المرور وتلبية الحد الأدنى من متطلباتك.
  • تحقق من رمز إعادة التعيين مرة أخرى للتأكد من أنه لم يتم استخدامه ولم تنته صلاحيته. نحتاج إلى التحقق من ذلك مرة أخرى لأن الرمز يتم إرساله بواسطة المستخدم عبر النموذج.
  • قبل إعادة تعيين كلمة المرور ، ضع علامة على الرمز المميز على أنه مستخدم. بهذه الطريقة ، إذا حدث شيء غير متوقع (تعطل الخادم ، على سبيل المثال) ، فلن تتم إعادة تعيين كلمة المرور بينما لا يزال الرمز المميز صالحًا.
  • استخدم ملحًا عشوائيًا آمنًا مشفرًا (في هذه الحالة ، نستخدم 64 بايت عشوائيًا).
 router.post('/reset-password', async function(req, res, next) { //compare passwords if (req.body.password1 !== req.body.password2) { return res.json({status: 'error', message: 'Passwords do not match. Please try again.'}); } /** * Ensure password is valid (isValidPassword * function checks if password is >= 8 chars, alphanumeric, * has special chars, etc) **/ if (!isValidPassword(req.body.password1)) { return res.json({status: 'error', message: 'Password does not meet minimum requirements. Please try again.'}); } var record = await ResetToken.findOne({ where: { email: req.body.email, expiration: { [Op.gt]: Sequelize.fn('CURDATE')}, token: req.body.token, used: 0 } }); if (record == null) { return res.json({status: 'error', message: 'Token not found. Please try the reset password process again.'}); } var upd = await ResetToken.update({ used: 1 }, { where: { email: req.body.email } }); var newSalt = crypto.randomBytes(64).toString('hex'); var newPassword = crypto.pbkdf2Sync(req.body.password1, newSalt, 10000, 64, 'sha512').toString('base64'); await User.update({ password: newPassword, salt: newSalt }, { where: { email: req.body.email } }); return res.json({status: 'ok', message: 'Password reset. Please login with your new password.'}); }); And again, the view: extends ../layout block content div.container div.row div.col h1 Reset password p Enter your new password below. if message div.reset-message.alert.alert-warning #{message} else div.reset-message.alert() if showForm form#resetPasswordForm(onsubmit="return false;") div.form-group label(for="password1") New password: input.form-control#password1(type='password', name='password1') small.form-text.text-muted Password must be 8 characters or more. div.form-group label(for="password2") Confirm new password input.form-control#password2(type='password', name='password2') small.form-text.text-muted Both passwords must match. input#emailRp(type='hidden', name='email', value=record.email) input#tokenRp(type='hidden', name='token', value=record.token) div.form-group button#rpButton.btn.btn-success(type='submit') Reset password script. $('#rpButton').on('click', function() { $.post('/user/reset-password', { password1: $('#password1').val(), password2: $('#password2').val(), email: $('#emailRp').val(), token: $('#tokenRp').val() }, function(resp) { if (resp.status == 'ok') { $('.reset-message').removeClass('alert-danger').addClass('alert-success').show().text(resp.message); $('#resetPasswordForm').remove(); } else { $('.reset-message').removeClass('alert-success').addClass('alert-danger').show().text(resp.message); } }); });

هذا هو ما ينبغي أن تبدو:

نموذج إعادة تعيين كلمة المرور لسير عمل إعادة تعيين كلمة المرور بشكل آمن
نموذج إعادة تعيين كلمة المرور الخاصة بك. (معاينة كبيرة)

أضف الرابط إلى صفحة تسجيل الدخول الخاصة بك

أخيرًا ، لا تنس إضافة ارتباط إلى هذا التدفق من صفحة تسجيل الدخول الخاصة بك! بمجرد القيام بذلك ، يجب أن يكون لديك تدفق عمل لإعادة تعيين كلمة المرور. تأكد من إجراء الاختبار بدقة في كل مرحلة من مراحل العملية لتأكيد أن كل شيء يعمل وأن الرموز المميزة الخاصة بك لها انتهاء صلاحية قصير ويتم تمييزها بالحالة الصحيحة مع تقدم سير العمل.

الخطوات التالية

نأمل أن يساعدك هذا في طريقك إلى ترميز ميزة إعادة تعيين كلمة مرور آمنة وسهلة الاستخدام.

  • إذا كنت مهتمًا بمعرفة المزيد حول أمان التشفير ، فإنني أوصي بملخص Wikipedia (تحذير ، إنه كثيف!).
  • إذا كنت ترغب في إضافة المزيد من الأمان إلى مصادقة التطبيق الخاص بك ، فابحث في 2FA. هناك الكثير من الخيارات المختلفة هناك.
  • إذا أخفتك من بناء تدفق إعادة تعيين كلمة المرور الخاص بك ، فيمكنك الاعتماد على أنظمة تسجيل الدخول التابعة لجهات خارجية مثل Google و Facebook. PassportJS هو برنامج وسيط يمكنك استخدامه لـ NodeJS الذي ينفذ هذه الاستراتيجيات.