Если вы чем-то похожи мне, вы забыли свой пароль более одного раза, особенно на сайтах, которые вы не посещали в течение долгого времени. Вы, вероятно, также видели и / или были огорчены сообщениями об изменении пароля, которые содержат ваш пароль в виде простого текста.
К сожалению, рабочий процесс сброса пароля становится недолгим и ограниченным во время разработки приложения. Это не только может привести к разочарованию пользователя, но также может оставить ваше приложение с дырами в безопасности.
Мы рассмотрим, как создать рабочий процесс безопасного сброса пароля. Мы будем использовать NodeJS и MySQL в качестве базовых компонентов. Если вы пишете с использованием другого языка, платформы или базы данных, вы все равно можете воспользоваться общими «советами по безопасности», изложенными в каждом разделе.
Поток пароля для сброса состоит из следующих компонентов:
- Ссылка для отправки пользователя в начало рабочего процесса.
- Форма, позволяющая пользователю отправить свою электронную почту.
- Поиск, который проверяет электронную почту и отправляет электронное письмо по адресу.
- Электронное письмо, содержащее токен сброса с истечением срока действия, который позволяет пользователю сбросить свой пароль.
- Форма, позволяющая пользователю создать новый пароль.
- Сохранение нового пароля и повторный вход пользователя с новым паролем.
Помимо Node, Express и MySQL, мы будем использовать следующие библиотеки:
Sequelize — это ORM базы данных NodeJS, которая упрощает запуск миграций базы данных, а также создает безопасность. Q ueries. Nodemailer — это популярная библиотека электронной почты NodeJS, которую мы будем использовать для отправки электронных писем со сбросом пароля.
Совет по безопасности № 1
Некоторые статьи предполагают, что безопасные потоки паролей могут быть разработаны с использованием JSON Web Tokens (JWT) , что устраняет необходимость в хранении базы данных (и, следовательно, проще в реализации). Мы не используем этот подход на нашем сайте, потому что секреты токенов JWT обычно хранятся прямо в коде. Мы хотим избежать использования «одного секрета» для управления ими всеми (по той же причине, по которой вы не солите пароли с одним и тем же значением), и поэтому нам нужно перенести эту информацию в базу данных.
Установка
Сначала установите Sequelize, Nodemailer и другие связанные библиотеки:
$ npm install --save sequelize sequelize-cli mysql crypto nodemailer
В пути где вы хотите включить ваши рабочие процессы сброса, добавьте необходимые модули. Если вам нужно обновить Express и маршруты, ознакомьтесь с их руководством.
const nodemailer = require ('nodemailer');
и настройте его с помощью электронной почты SMTP учетные данные.
const transport = nodemailer.createTransport ({
хост: process.env.EMAIL_HOST,
порт: process.env.EMAIL_PORT,
безопасный: правда,
auth: {
пользователь: process.env.EMAIL_USER,
пройти: process.env.EMAIL_PASS
}
});
Я использую почтовое решение AWS Simple Email Service, но вы можете использовать что угодно (Mailgun и т. Д.).
Если вы настраиваете это впервые Служба отправки электронной почты, вам нужно потратить некоторое время на настройку соответствующих ключей домена и настройку авторизации. Если вы используете Route 53 вместе с SES, это очень просто и выполняется практически автоматически, поэтому я выбрал его. В AWS есть несколько учебных пособий о том, как SES работает с Route53.
Совет по безопасности № 2
Для хранения учетных данных вне моего кода я использую dotenv, который позволяет мне создавать локальный файл .env с мои переменные среды. Таким образом, при развертывании в рабочей среде я могу использовать различные рабочие ключи, которые не видны в коде, и, следовательно, позволяет ограничивать разрешения моей конфигурации только для определенных членов моей команды.
Настройка базы данных
Поскольку мы собираемся отправлять токены сброса пользователям, нам необходимо сохранить эти токены в базе данных.
Я предполагаю, что в вашей базе данных есть таблица действующих пользователей. Если вы уже используете Sequelize, отлично! Если нет, вы можете освежить в памяти Sequelize и Sequelize CLI.
Если вы еще не использовали Sequelize в своем приложении, вы можете настроить его, выполнив следующую команду в корневой папке вашего приложения:
$ sequelize init
Это создаст несколько новых папок в вашей настройке, включая миграции и модели.
Это также создаст конфигурацию файл. В вашем конфигурационном файле обновите блок development
указав учетные данные для вашего локального сервера баз данных mysql.
Давайте воспользуемся инструментом CLI Sequelize для генерации таблицы базы данных для нас.
$ sequelize модель: create --name ResetToken --attributes электронная почта: строка, токен: строка, срок действия: дата, используется: целое число
$ 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,
`createAt` datetime НЕ NULL,
`updatedAt` datetime НЕ NULL,
`used` int (11) NOT NULL DEFAULT '0',
ПЕРВИЧНЫЙ КЛЮЧ (`id`)
) ENGINE = InnoDB AUTO_INCREMENT = 21 CHARSET ПО УМОЛЧАНИЮ = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
Убедитесь, что это работает должным образом с помощью клиента SQL или командной строки:
mysql> описывает ResetTokens;
+ ------------ + -------------- + ------ + ----- + -------- - + ---------------- +
| Поле | Тип | Null | Ключ | По умолчанию | Extra |
+ ------------ + -------------- + ------ + ----- + -------- - + ---------------- +
| id | int (11) | НЕТ | PRI | NULL | auto_increment |
| электронная почта | Варчар (255) | ДА | | NULL | |
| жетон | Варчар (255) | ДА | | NULL | |
| истечение срока действия | дата и время | ДА | | NULL | |
| создал на | дата и время | НЕТ | | NULL | |
| обновлено на | дата и время | НЕТ | | NULL | |
| используется | int (11) | НЕТ | | 0 | |
+ ------------ + -------------- + ------ + ----- + -------- - + ---------------- +
7 строк в наборе (0,00 с)
Совет по безопасности № 3
Если вы в настоящее время не используете ORM, вам следует подумать об этом. ORM автоматизирует написание и правильное экранирование SQL-запросов, делая ваш код более читабельным и более безопасным по умолчанию. Они помогут вам избежать атак с использованием SQL-инъекций путем правильного экранирования SQL-запросов.
Настройка маршрута сброса пароля
Создание маршрута получения в user.js :
router.get ('/ Forgot-Password', функция (req, res, next) {
res.render ('user / Forgot-Password', {});
});
Затем создайте маршрут POST, который будет использоваться при публикации формы для сброса пароля. В приведенный ниже код я включил несколько важных функций безопасности.
Советы по безопасности № 4-6
- Даже если мы не найдем адрес электронной почты, мы вернемся «хорошо», как наш статус. Мы не хотим, чтобы нежелательные боты выясняли, какие письма являются реальными, а не реальными в нашей базе данных.
- Чем больше случайных байтов вы используете в токене, тем меньше вероятность его взлома. Мы используем 64 случайных байта в нашем генераторе токенов (не используйте меньше 8).
- Срок действия токена истекает через 1 час. Это ограничивает время работы токена сброса.
router.post ('/ Forgot-Password', асинхронная функция (req, res, next) {
// убедитесь, что у вас есть пользователь с этим адресом электронной почты
var email = await User.findOne ({где: {email: req.body.email}});
if (email == null) {
/ **
* мы не хотим говорить злоумышленникам, что
* электронная почта не существует, потому что это позволит
* они используют эту форму, чтобы найти те, которые делают
* существует.
** /
return res.json ({status: 'ok'});
}
/ **
* Срок действия любых токенов, которые были ранее
* установлен для этого пользователя. Что мешает старым токенам
* от использования.
** /
await ResetToken.update ({
б: 1
},
{
где: {
электронная почта: req.body.email
}
});
// Создать токен случайного сброса
var fpSalt = crypto.randomBytes (64) .toString ('base64');
// токен истекает через час
var expireDate = new Date ();
expireDate.setDate (expireDate.getDate () + 1/24);
// вставляем данные токена в БД
await ResetToken.create ({
электронная почта: req.body.email,
expiration: expireDate,
токен: токен,
б: 0
});
// создать электронную почту
константное сообщение = {
от: process.env.SENDER_ADDRESS,
to: req.body.email,
replyTo: process.env.REPLYTO_ADDRESS,
subject: process.env.FORGOT_PASS_SUBJECT_LINE,
текст: 'Чтобы сбросить пароль, нажмите на ссылку ниже. n nhttps: //'+process.env.DOMAIN+'/user/reset-password? token =' + encodeURIComponent (token) + '& email =' + req.body.email
};
// отправить письмо
transport.sendMail (сообщение, функция (ошибка, информация) {
if (err) {console.log (err)}
else {console.log (info); }
});
return res.json ({status: 'ok'});
});
Вы увидите переменную User, указанную выше — что это? Для целей данного руководства мы предполагаем, что у вас есть модель User, которая подключается к вашей базе данных для получения значений. Приведенный выше код основан на Sequelize, но при необходимости вы можете изменить его, если запросите базу данных напрямую (но я рекомендую Sequelize!).
Теперь нам нужно сгенерировать представление. Используя Bootstrap CSS, jQuery и платформу pug, встроенную в платформу Node Express, представление выглядит следующим образом:
extends ../layout
блокировать контент
div.container
div.row
div.col
h1 забыл пароль
p Введите адрес электронной почты ниже. Если он у нас есть, мы отправим вам электронное письмо для сброса.
div.forgot-message.alert.alert-success (style = "display: none;") Получен адрес электронной почты. Если у вас есть электронное письмо с файлом, мы отправим вам электронное письмо для сброса. Пожалуйста, подождите несколько минут и проверьте папку со спамом, если вы ее не видите.
form # ForgotPasswordForm.form-inline (onsubmit = "return false;")
div.form-группа
label.sr-only (for = "email") Адрес электронной почты:
input.form-control.mr-2 # emailFp (type = 'email', name = 'email', placeholder = "Адрес электронной почты")
div.form-group.mt-1.text-центр
button # fpButton.btn.btn-success.mb-2 (type = 'submit') Отправить письмо
скрипт.
$ ('# fpButton'). on ('click', function () {
$ .post ('/ user / Forgot-Password', {
email: $ ('# emailFp'). val (),
}, функция (соответственно) {
. $ ( 'Забыл-сообщение') показать ();
. $ ( '# ForgotPasswordForm') удалить ();
});
});
Вот форма на странице:
На этом этапе вы сможете заполнить форму с адресом электронной почты, который находится в вашей базе данных, а затем получить по электронной почте пароль для сброса пароля по этому адресу. Нажав на ссылку сброса, вы ничего не сделаете.
Настройка маршрута «Сброс пароля»
Теперь давайте продолжим и настроим оставшуюся часть рабочего процесса.
Добавить модуль Sequelize.Op для вашего маршрута:
const Sequelize = require ('sequelize');
const Op = Sequelize.Op;
Теперь давайте создадим маршрут GET для пользователей, которые щелкнули по ссылке сброса пароля. Как вы увидите ниже, мы хотим убедиться, что мы правильно проверяем токен сброса.
Совет по безопасности № 7:
Убедитесь, что вы ищете только токены сброса, которые не срок действия истек и не использовался.
Для демонстрации я также очищаю все токены с истекшим сроком действия при загрузке, чтобы сохранить размер таблицы. Если у вас большой веб-сайт, перенесите его в cronjob.
router.get ('/ reset-password', асинхронная функция (req, res, next) {
/ **
* Этот код очищает все просроченные токены. Вы
* следует переместить это в cronjob, если у вас есть
* большой сайт. Мы просто включаем это здесь как
* демонстрация.
** /
await ResetToken.destroy ({
где: {
истечение срока: {[Op.lt]: Sequelize.fn ('CURDATE')},
}
});
// найти токен
var record = await ResetToken.findOne ({
где: {
электронная почта: req.query.email,
истечение срока: {[Op.gt]: Sequelize.fn ('CURDATE')},
токен: req.query.token,
б: 0
}
});
if (record == null) {
return res.render ('user / reset-password', {
сообщение: «Срок действия токена истек. Пожалуйста, попробуйте сбросить пароль еще раз. ',
showForm: ложь
});
}
res.render ('user / reset-password', {
showForm: правда,
запись: запись
});
});
Теперь давайте создадим POST-маршрут, который будет использоваться после того, как пользователь введет данные своего нового пароля.
Совет по безопасности № с 8 по 11:
- Убедитесь, что пароли соответствуют и соответствуют вашим минимальным требованиям.
- Еще раз проверьте маркер сброса, чтобы убедиться, что он не использовался и у него не истек срок действия. Нам нужно проверить его еще раз, поскольку токен отправляется пользователем через форму.
- Перед сбросом пароля пометьте токен как использованный. Таким образом, если произойдет что-то непредвиденное (например, сбой сервера), пароль не будет сброшен, пока токен все еще действителен.
- Используйте криптографически безопасную случайную соль (в этом случае мы используем 64 случайных числа). байт).
router.post ('/ reset-password', асинхронная функция (req, res, next) {
// сравниваем пароли
if (req.body.password1! == req.body.password2) {
return res.json ({status: 'error', сообщение: 'Пароли не совпадают. Пожалуйста, попробуйте еще раз.'});
}
/ **
* Убедитесь, что пароль действителен (isValidPassword
* функция проверяет, если пароль> = 8 символов, буквенно-цифровой,
* имеет специальные символы и т. д.)
** /
if (! isValidPassword (req.body.password1)) {
return res.json ({status: 'error', сообщение: 'Пароль не соответствует минимальным требованиям. Пожалуйста, повторите попытку.'});
}
var record = await ResetToken.findOne ({
где: {
электронная почта: req.body.email,
истечение срока: {[Op.gt]: Sequelize.fn ('CURDATE')},
токен: req.body.token,
б: 0
}
});
if (record == null) {
return res.json ({status: 'error', сообщение: 'Token not found. Пожалуйста, попробуйте снова сбросить пароль.'});
}
var upd = await ResetToken.update ({
б: 1
},
{
где: {
электронная почта: 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 ({
пароль: новый пароль,
соль: newSalt
},
{
где: {
электронная почта: req.body.email
}
});
return res.json ({status: 'ok', сообщение: 'Пароль сброшен. Пожалуйста, войдите с новым паролем.'});
});
И снова мнение:
расширяет ../layout
блокировать контент
div.container
div.row
div.col
h1 Сбросить пароль
p Введите новый пароль ниже.
если сообщение
div.reset-message.alert.alert-warning # {message}
еще
div.reset-message.alert ( 'дисплей: нет;' стиль =)
если showForm
form # resetPasswordForm (onsubmit = "return false;")
div.form-группа
label (for = "password1") Новый пароль:
input.form-control # password1 (type = 'password', name = 'password1')
small.form-text.text-muted Пароль должен состоять из 8 или более символов.
div.form-группа
метка (for = "password2") Подтвердите новый пароль
input.form-control # password2 (type = 'password', name = 'password2')
small.form-text.text-muted Оба пароля должны совпадать.
input # emailRp (type = 'hidden', name = 'email', value = record.email)
input # tokenRp (type = 'hidden', name = 'token', value = record.token)
div.form-группа
button # rpButton.btn.btn-success (type = 'submit') Сбросить пароль
скрипт.
$ ('# rpButton'). on ('click', function () {
$ .post ('/ user / reset-password', {
password1: $ ('# password1'). val (),
password2: $ ('# password2'). val (),
email: $ ('# emailRp'). val (),
токен: $ ('# tokenRp'). val ()
}, функция (соответственно) {
if (resp.status == 'ok') {
$ (»Сброс-сообщение.) RemoveClass (« тревога-опасность ') addClass (' тревога-успех) шоу () текст (resp.message)....;
$ ( '# ResetPasswordForm') удалить ().
} еще {
$ ( 'Сброс-сообщение.) RemoveClass ( «тревога-успех ') addClass (' тревога-опасность') показать () текст (resp.message)....;
}
});
});
Вот как это должно выглядеть:
Добавить ссылку на страницу входа
И наконец, не забудьте добавить ссылку на этот поток со страницы входа! Как только вы это сделаете, у вас должен получиться рабочий сброс пароля. Обязательно тщательно проверяйте на каждом этапе процесса, чтобы убедиться, что все работает, и ваши токены имеют короткий срок действия и помечены с правильным статусом по мере продвижения рабочего процесса.
Следующие шаги
Надеемся, что это помог вам на пути к кодированию безопасной, удобной для пользователя функции сброса пароля.
- Если вы заинтересованы в получении дополнительной информации о криптографической безопасности, я рекомендую краткий обзор Википедии (предупреждение, оно плотное!).
- Если вы хотите повысить безопасность аутентификации вашего приложения, изучите 2FA. Существует множество различных вариантов.
- Если я напугал вас созданием собственного потока сброса пароля, вы можете положиться на сторонние системы входа в систему, такие как Google и Facebook. PassportJS — это промежуточное программное обеспечение, которое вы можете использовать для NodeJS, которое реализует эти стратегии.