Сброс пароля и временные токены

Сброс пароля в системах аутентификации почти всегда опирается на комбинацию двух механизмов: одноразовые временные токены и хеширование новых паролей через bcrypt.js. Разделение этих задач принципиально: токен отвечает за временное подтверждение действия, bcrypt.js — за безопасное хранение итогового пароля.


Типичный процесс строится вокруг нескольких последовательных шагов:

  1. Запрос на восстановление доступа
  2. Генерация временного токена
  3. Сохранение токена и срока его действия
  4. Отправка ссылки с токеном
  5. Проверка токена при переходе по ссылке
  6. Установка нового пароля с его хешированием через bcrypt.js

Ключевой принцип — ни один из этапов не должен раскрывать текущий пароль или позволять его восстановление из базы данных.


Роль bcrypt.js в механизме сброса

Библиотека bcrypt.js применяется исключительно для хранения нового пароля. Она не участвует в генерации или проверке временных токенов.

Особенности bcrypt.js:

  • адаптивная вычислительная сложность (cost factor)
  • устойчивость к перебору
  • встроенная работа с солью (salt)
  • одностороннее хеширование

Пример базового использования:

import bcrypt fr om 'bcryptjs';

const saltRounds = 12;

async function hashPassword(password) {
  const hash = await bcrypt.hash(password, saltRounds);
  return hash;
}

async function verifyPassword(password, hash) {
  return await bcrypt.compare(password, hash);
}

В контексте сброса пароля bcrypt.js применяется на финальном этапе — при сохранении нового значения пароля.


Генерация временных токенов

Токен сброса пароля должен обладать несколькими свойствами:

  • криптографическая случайность
  • ограниченный срок жизни
  • однократное использование
  • невозможность предсказания

Чаще всего используется модуль crypto:

import crypto fr om 'crypto';

function generateResetToken() {
  return crypto.randomBytes(32).toString('hex');
}

Полученный токен не хранится в открытом виде в базе данных. Это важный момент безопасности.


Хранение токена: хеширование вместо открытого значения

Если токен хранится в базе данных в исходном виде, компрометация базы приводит к возможности мгновенного использования всех активных ссылок сброса.

Поэтому применяется хеширование токена перед сохранением. В отличие от паролей, bcrypt.js здесь используется редко, поскольку:

  • токены короткоживущие
  • не требуют адаптивной стоимости вычисления
  • важна скорость проверки

Чаще применяется SHA-256:

import crypto from 'crypto';

function hashToken(token) {
  return crypto.createHash('sha256').update(token).digest('hex');
}

В базе данных хранится только хеш:

{
  userId: "123",
  resetTokenHash: "...",
  resetTokenExpires: Date.now() + 1000 * 60 * 15
}

Полный цикл запроса на сброс пароля

1. Создание токена

const token = generateResetToken();
const tokenHash = hashToken(token);

2. Сохранение в базе данных

await db.users.update({
  wh ere: { email },
  data: {
    resetTokenHash: tokenHash,
    resetTokenExpires: Date.now() + 15 * 60 * 1000
  }
});

3. Отправка ссылки

Формируется URL:

https://example.com/reset-password?token=TOKEN&email=user@mail.com

Отправляется именно исходный токен, а не его хеш.


Проверка токена

При получении запроса на сброс выполняется сопоставление:

const receivedHash = hashToken(tokenFromRequest);

const user = await db.users.findUnique({
  wh ere: { email }
});

const isValid =
  user.resetTokenHash === receivedHash &&
  user.resetTokenExpires > Date.now();

Если условия не выполняются — токен считается недействительным.


Установка нового пароля через bcrypt.js

После успешной проверки токена выполняется хеширование нового пароля:

import bcrypt fr om 'bcryptjs';

const saltRounds = 12;

async function setNewPassword(userId, newPassword) {
  const passwordHash = await bcrypt.hash(newPassword, saltRounds);

  await db.users.update({
    wh ere: { id: userId },
    data: {
      passwordHash,
      resetTokenHash: null,
      resetTokenExpires: null
    }
  });
}

Сброс токена после использования является обязательным шагом, предотвращающим повторное применение ссылки.


Почему bcrypt.js не используется для токенов

Хотя технически возможно хешировать токен через bcrypt.js, на практике это создаёт лишнюю нагрузку и не даёт преимуществ:

  • bcrypt рассчитан на медленное вычисление
  • токены требуют быстрых операций проверки
  • токены не предназначены для хранения секретов долгосрочно

Поэтому оптимальная схема:

  • bcrypt.js → пароли
  • SHA-256 или HMAC → токены

Защита от атак и уязвимостей

Ограничение времени жизни токена

15–30 минут является распространённым диапазоном. Более длительное время увеличивает риск компрометации.


Одноразовое использование

После успешного сброса:

  • токен удаляется
  • любые повторные запросы отклоняются

Rate limiting

Запросы на генерацию токенов должны ограничиваться:

  • по IP
  • по email
  • по устройству

Это снижает вероятность перебора.


Защита от утечки через URL

Токен передаётся в query string, что требует:

  • использования HTTPS
  • отсутствия логирования query параметров на сервере
  • осторожности с реферерами браузера

Сравнение токенов без утечки времени

При сравнении хешей важно избегать утечек через timing attacks. Для этого используется:

import crypto from 'crypto';

crypto.timingSafeEqual(
  Buffer.from(hashA),
  Buffer.from(hashB)
);

Интеграция с системой аутентификации

Сброс пароля всегда является частью более широкой системы:

  • регистрация (bcrypt.js)
  • вход (bcrypt.js compare)
  • восстановление доступа (token + bcrypt.js)
  • обновление пароля (bcrypt.js)

Такая структура позволяет изолировать ответственность компонентов:

  • токены подтверждают действие
  • bcrypt.js защищает данные пользователя

Типичные ошибки реализации

Хранение токена в открытом виде

Создаёт прямую угрозу компрометации аккаунтов при утечке базы.


Отсутствие срока действия

Токен без TTL фактически становится постоянным ключом доступа.


Повторное использование токена

Необнуляемые токены позволяют многократный сброс пароля.


Использование bcrypt для токенов без необходимости

Увеличивает нагрузку и снижает отзывчивость системы без добавления безопасности.


Структура данных пользователя

Практическая модель в базе данных:

{
  id: "123",
  email: "user@mail.com",
  passwordHash: "...bcrypt...",
  resetTokenHash: null,
  resetTokenExpires: null
}

Разделение полей позволяет чётко отделить постоянные и временные данные.


Дополнительные меры усиления безопасности

  • привязка токена к user-agent
  • ограничение количества активных токенов на пользователя
  • логирование попыток сброса
  • уведомления о смене пароля после успешной операции

Такие меры уменьшают поверхность атаки даже при частичной компрометации системы.


Поведение при параллельных запросах

Если генерируется новый токен до истечения старого:

  • предыдущий автоматически инвалидируется
  • в базе остаётся только последний актуальный хеш

Это предотвращает накопление активных ссылок восстановления.


Связь bcrypt.js с общей моделью безопасности

bcrypt.js выступает финальным барьером защиты. Даже при наличии доступа к базе данных:

  • пароль остаётся необратимо хешированным
  • восстановление исходного значения невозможно
  • перебор требует значительных вычислительных ресурсов

Сочетание временных токенов и bcrypt.js формирует двухуровневую систему:

  • временная авторизация через токен
  • постоянная защита через хеш пароля