У Змейки уже есть вход по коду на почту, таблица лидеров и магазин скинов. Дальше в проекте появляются деньги или личные данные — и однажды закрадывается мысль: а что, если кто-то украдёт доступ к чужому аккаунту? Пароль подобрали, почту взломали, сессию перехватили в открытом Wi-Fi — вариантов много. Здесь на сцену выходит двухфакторная аутентификация, сокращённо 2FA: второй рубеж проверки, который не спасает от первой пробоины, зато не даёт ей стать катастрофой. Разберёмся, что это за механизм, чем он отличается от уже знакомого кода на почту и как его добавить в свой проект без лишней теории.
Содержание
- Что такое 2FA простыми словами
- Чем 2FA отличается от кода на почту при входе
- Библиотеки и код: как это выглядит на практике
- Как работает TOTP технически
- Как добавить 2FA в своё приложение
- Резервные коды на случай потери телефона
- Когда 2FA нужна, а когда это лишнее трение
- Безопасное хранение секретного ключа на сервере
- Ограничения 2FA, которые стоит знать заранее
- Частые вопросы
- Заключение
Что такое 2FA простыми словами
Двухфакторная аутентификация — это когда для входа нужно подтвердить личность не одним способом, а двумя разными. Первый фактор — обычно пароль или уже открытая сессия. Второй — что-то ещё, что есть только у настоящего владельца аккаунта.
В классификации факторов входа их обычно делят на три категории: то, что ты знаешь (пароль, PIN), то, что у тебя есть (телефон, физический ключ), и то, чем ты являешься (отпечаток пальца, лицо). 2FA требует факторы из разных категорий. Если бы приложение просило ввести пароль, а потом ещё раз пароль — это была бы не двухфакторная проверка, а просто два раза один и тот же фактор.
Самая распространённая реализация второго фактора для небольших приложений — TOTP, Time-based One-Time Password, одноразовый код на основе времени. Это шестизначное число вроде 483920, которое живёт около тридцати секунд и потом сменяется на новое. Такие коды генерирует не сервер и не почта, а отдельное приложение на телефоне пользователя — Google Authenticator, Яндекс Ключ, Authy и похожие. Именно про такую 2FA пойдёт речь дальше.
Логика проста: даже если пароль от аккаунта попал в руки постороннего человека — слили базу другого сайта, подсмотрели через плечо, подобрали по словарю, — войти он всё равно не сможет. Ему не хватит кода, который в этот момент существует только в приложении на конкретном физическом телефоне владельца.
Разница на бытовом примере: пароль — это ключ от квартиры, который можно скопировать незаметно. Код из приложения-аутентификатора — это как если бы дверь ещё спрашивала одноразовый штамп, который печатает только твой личный блокнот, обновляющий страницы каждые тридцать секунд. Украсть ключ мало — нужен ещё и блокнот в кармане.
Чем 2FA отличается от кода на почту при входе
В статье про OTP, сессии и токены уже разбирался похожий на первый взгляд механизм: пользователь вводит email, получает шестизначный код на почту и попадает внутрь без пароля вообще. Путаница между этим OTP и 2FA встречается постоянно, поэтому стоит развести их явно, по трём признакам.
Что заменяет, а что дополняет. OTP на почту — это самостоятельный способ входа, он полностью заменяет пароль. Пользователь один раз вводит email, получает код, и всё — пароля в системе просто нет. 2FA работает иначе: она встаёт поверх уже существующего входа. Пользователь сначала вводит обычный пароль (или у него уже открыта авторизованная сессия), и только потом, дополнительным шагом, — код из приложения. Убери 2FA, и вход всё равно останется рабочим через пароль; убери OTP на почту в текущей схеме Змейки, и входить будет вообще нечем.
Откуда берётся код. Код на почту прилетает через интернет: сервер отправляет письмо, письмо доходит до почтового ящика, пользователь его читает. Если в этот момент нет связи с почтовым сервером или у пользователя лежит письмо в спаме — код не придёт. Код в TOTP-приложении считается локально на телефоне, без единого запроса в сеть. Самолёт, метро без связи, зона с плохим приёмом — неважно: приложение само знает текущее время и само переводит его в шестизначное число.
Что произойдёт при компрометации почты. Здесь разница особенно важная. Если у злоумышленника есть доступ к почтовому ящику пользователя, а вход в приложение сделан только через OTP на почту, — он спокойно войдёт: попросит код, код придёт на скомпрометированную почту, вход состоится. Если же на аккаунте включена 2FA через приложение-аутентификатор, взлом одной только почты ничего не даёт: секрет для генерации кода живёт на телефоне, а не в почтовом ящике, и почта в этой цепочке вообще не участвует.
Отсюда практический вывод для проекта на курсе: OTP на почту и 2FA решают разные задачи и прекрасно уживаются вместе. Можно оставить вход по коду на почту как основной способ, а 2FA добавить сверху для чувствительных операций — например, для входа в панель администратора магазина скинов или перед выводом заработанных внутриигровых баллов.
Коротко разницу удобно свести в таблицу.
| OTP на почту | 2FA через приложение-аутентификатор | |
|---|---|---|
| Роль в системе входа | Заменяет пароль полностью | Дополняет уже существующий вход |
| Откуда берётся код | Приходит письмом через интернет | Считается локально на телефоне |
| Нужна ли сеть в момент проверки | Да, письмо должно дойти | Нет, работает даже офлайн |
| Что случится при взломе почты | Вход останется возможным для взломщика | Ничего: почта не участвует в проверке |
| Типичное место в проекте | Основной вход в приложение | Дополнительный шаг для чувствительных действий |
Библиотеки и код: как это выглядит на практике
Прежде чем переходить к пошаговому плану подключения, полезно увидеть, как задача решается буквально в нескольких строках. Библиотека otpauth для Node.js берёт на себя и генерацию секрета, и построение ссылки для QR-кода, и проверку введённого кода:
import { Secret, TOTP } from "otpauth";
// Шаг 1: при включении 2FA генерируем секрет и настраиваем TOTP
const secret = new Secret({ size: 20 });
const totp = new TOTP({
issuer: "Змейка",
label: "player@example.com",
secret,
});
// Шаг 2: строку totp.toString() кодируем в QR-код и показываем пользователю
const qrUrl = totp.toString(); // otpauth://totp/...
// Шаг 3: при входе проверяем код, который пользователь ввёл вручную
const delta = totp.validate({ token: userInput, window: 1 });
// delta === null, если код неверный; иначе — насколько «окон» разошлись часы
Параметр window: 1 разрешает небольшой запас по времени — проверяются текущее тридцатисекундное окно и одно соседнее, чтобы лёгкое расхождение часов на телефоне не приводило к отказу при правильном коде. Секрет из переменной secret — это ровно то значение, которое дальше нужно зашифровать перед сохранением в базу, а не хранить рядом в открытом виде.
Как работает TOTP технически
TOTP описан в открытом стандарте RFC 6238, и именно поэтому Google Authenticator, Яндекс Ключ, Authy и десятки других приложений одинаково понимают один и тот же секрет — стандарт один на всех, а не проприетарная технология конкретной компании.
Механизм построен на общем секрете и общем времени. Разберём шаги по порядку.
Шаг 1. Пользователь включает 2FA в настройках аккаунта. Сервер генерирует случайную последовательность байтов — секретный ключ. Обычно это 16–20 байт, закодированные в Base32 для удобства передачи.
Шаг 2. Сервер показывает секрет как QR-код. QR-код кодирует не картинку ради красоты, а обычную ссылку вида otpauth://totp/Название:пользователь?secret=СЕКРЕТ&issuer=Название. Приложение-аутентификатор умеет читать такие ссылки через камеру телефона.
Шаг 3. Пользователь сканирует QR-код приложением. С этого момента и у сервера, и у приложения на телефоне хранится одна и та же секретная строка. Сервер сохраняет её в своей базе рядом с записью пользователя, приложение — в своём локальном хранилище на телефоне.
Шаг 4. Оба независимо считают один и тот же код. Формула TOTP берёт секретный ключ и текущее время, округлённое до тридцатисекундного интервала, и через криптографическую хеш-функцию HMAC-SHA1 превращает эту пару в шестизначное число. Поскольку у сервера и телефона один и тот же секрет и примерно синхронизированные часы, оба независимо получают одинаковый код — без единого сетевого запроса между ними в момент генерации.
Шаг 5. При входе пользователь вводит код, сервер сверяет. Пользователь смотрит на экран телефона, переписывает шестизначное число в форму входа. Сервер по своему секрету и своему текущему времени считает, каким должен быть правильный код, и сравнивает с введённым. Обычно проверяется не только текущее тридцатисекундное окно, но и соседнее — чтобы небольшое расхождение часов на телефоне не ломало вход.
flowchart TB
A["Пользователь включает 2FA"] --> B["Сервер создаёт секрет и QR-код"]
B --> C["Приложение-аутентификатор сканирует QR-код"]
C --> D["При входе пользователь вводит код из приложения"]
D --> E{"Сервер сверяет код"}
E -->|"код верный"| F["Доступ разрешён"]
Важная деталь: связь между сервером и приложением-аутентификатором происходит только один раз, в момент сканирования QR-кода. Дальше оба генератора работают полностью автономно, синхронизированные лишь общим секретом и общим временем — в этом и разница с кодом на почту, которому нужен живой сетевой запрос при каждом входе.
Параметр issuer в ссылке QR-кода — не формальность. Именно он определяет подпись, под которой запись появится в списке приложения-аутентификатора: если у пользователя уже подключено пять сервисов, читаемое название вроде «Змейка» помогает не перепутать код от игры с кодом от почты или банка. Указывай туда понятное имя проекта, а не техническое имя базы данных или внутренний идентификатор.
Как добавить 2FA в своё приложение
Писать формулу TOTP с нуля вручную незачем — она уже реализована в проверенных открытых библиотеках, и переизобретать криптографию самому означает копить риск лишних ошибок. Задача сводится к тому, чтобы правильно подключить готовый инструмент и провести пользователя через понятный интерфейс.
Библиотеки для Node.js. Пакеты otpauth и speakeasy — оба умеют генерировать секрет, строить ссылку для QR-кода и проверять введённый пользователем код по формуле из RFC 6238. otpauth — более новая и активно поддерживаемая библиотека без внешних зависимостей, speakeasy — старый и проверенный вариант, но его развитие практически остановилось. Для нового проекта разумнее взять otpauth. QR-код из полученной ссылки рисует отдельный пакет, например qrcode, — он превращает текстовую строку в картинку, которую можно показать на экране.
Встроенная поддержка в backend-сервисах. Если проект уже использует Supabase для авторизации и базы данных — как в модуле курса про облачное хранилище рекордов, — присмотрись сначала к встроенному MFA (Multi-Factor Authentication): у Supabase есть готовый API для регистрации TOTP-фактора, генерации QR-кода и проверки кода при входе, без необходимости подключать сторонние библиотеки и хранить секреты вручную. Это экономит время и снимает часть ответственности за безопасность хранения секретов с разработчика.
Схема вызовов у Supabase простая и повторяет тот же алгоритм, что описан выше: mfa.enroll() регистрирует новый TOTP-фактор и возвращает данные для QR-кода, mfa.challenge() запускает проверку при входе, а mfa.verify() сверяет код, который ввёл пользователь. Секрет при таком подходе вообще не проходит через код приложения — он создаётся и хранится на стороне Supabase, а разработчику остаётся только выстроить экран с QR-кодом и полем для ввода кода. Для проекта на курсе, где Supabase уже настроен под рекорды и таблицу лидеров, это обычно более короткий путь, чем подключать otpauth и продумывать шифрование секрета самостоятельно.
Практический план подключения. Вне зависимости от конкретной библиотеки последовательность действий похожая:
- Добавь в настройках аккаунта пункт «Двухфакторная аутентификация» с кнопкой включения.
- По нажатию сервер генерирует секрет и строку для QR-кода, отдаёт их на фронтенд.
- Покажи QR-код на экране и рядом — тот же секрет текстом, на случай если у пользователя не работает камера и нужно ввести код в приложение вручную.
- Попроси пользователя сразу же ввести код из приложения, чтобы подтвердить, что сканирование прошло успешно, и только после этого сохрани секрет в базе как активный.
- При каждом следующем входе после проверки пароля показывай отдельный экран «Введи код из приложения» и сверяй его на сервере тем же методом библиотеки.
- Сразу же вместе с включением 2FA сгенерируй и покажи резервные коды — про них отдельно ниже.
Проверка кода на практике — это одна функция вроде totp.validate({ token, window: 1 }), которая возвращает true или false. Вся сложность прячется внутри библиотеки, а от разработчика требуется аккуратно хранить секрет и не пропустить шаг с резервными кодами.
Резервные коды на случай потери телефона
Телефоны теряются, разбиваются, крадут, а иногда просто садится батарея в важный момент. Если единственный способ подтвердить личность — код из приложения на конкретном устройстве, потеря этого устройства означает потерю доступа к собственному аккаунту. Поэтому резервные коды — не необязательное украшение функции 2FA, а обязательная её часть.
Резервный код — это одноразовый код, который сервер генерирует заранее, в момент включения 2FA, и показывает пользователю один раз. Обычно генерируют от восьми до десяти таких кодов сразу. Пользователь должен сохранить их в надёжном месте — распечатать, записать в блокнот, положить в менеджер паролей — до того, как они понадобятся, а не после того, как телефон уже потерян.
Каждый резервный код можно использовать только один раз: как только сервер принял такой код вместо обычного из приложения, он помечается использованным и больше не подходит. Это отличает резервные коды от кода из приложения, который просто сменяется каждые тридцать секунд сам по себе.
На сервере резервные коды хранятся не в открытом виде, а хешированными — тем же способом, каким принято хранить пароли. Показать пользователю их можно только один раз, в момент генерации; после этого сервер хранит только хеши и не способен показать коды снова, только сгенерировать новый набор взамен старого.
Практический совет для интерфейса: рядом с кнопкой включения 2FA сразу предусмотри кнопку «Показать резервные коды ещё раз» — она недоступна в буквальном смысле (сервер не хранит открытый текст), но должна честно объяснять пользователю, что если коды потеряны, единственный выход — сгенерировать новый набор и заново сохранить его в надёжном месте.
На экране входа резервный код принимает та же форма, что и обычный код из приложения, — обычно достаточно добавить под полем ввода ссылку «Нет доступа к приложению? Введи резервный код». Различать формат внутри одной и той же формы необязательно: и шестизначный TOTP-код, и резервный код можно проверять одним и тем же полем, если сервер сам понимает по длине и формату, какую именно проверку выполнять.
Когда 2FA нужна, а когда это лишнее трение
2FA решает конкретную проблему — защиту аккаунта при компрометации пароля. Но у любой дополнительной проверки есть цена: лишний шаг перед входом, который часть пользователей воспринимает как неудобство, а часть — вовсе бросает попытку войти на середине.
Стоит добавлять, когда в приложении есть хотя бы одно из следующего:
- хранятся деньги пользователя или доступ к платежам — например, встроенный магазин с реальной оплатой;
- есть персональные данные сверх минимума — адрес, телефон, документы;
- существует административный доступ, через который можно менять данные других пользователей или настройки всего проекта;
- аккаунт уже становился целью атак или явно привлекателен для мошенников — например, публичный проект с большой аудиторией.
Можно не добавлять или добавлять по желанию, если:
- проект учебный или демонстрационный, вроде базовой версии Змейки с локальным рекордом в
localStorage; - в аккаунте нет ничего ценнее никнейма и личного результата в игре;
- аудитория — несколько человек, которым важнее быстрый и простой вход, чем дополнительная защита.
Хорошая практика — не заставлять всех пользователей включать 2FA принудительно, а предложить её как опцию в настройках, обязательную только для ролей с повышенным доступом — например, для администратора проекта. Это баланс между безопасностью там, где она действительно нужна, и удобством там, где цена лишнего шага выше пользы от него.
Стоит также учитывать возраст и техническую подготовку аудитории. Если приложение рассчитано на людей, впервые устанавливающих приложение-аутентификатор, объясни это отдельным коротким текстом рядом с QR-кодом — сама идея «отсканируй код камерой другого приложения» неочевидна тем, кто раньше этого не делал.
Пример из логики курса: пока Змейка — игра с локальным рекордом, включать 2FA незачем. Как только в проекте появляется магазин с реальной оплатой скинов или личный кабинет с историей покупок, разумно предложить 2FA сначала владельцу проекта и модераторам, у которых есть доступ к панели администратора, а обычным игрокам — сделать её опцией в настройках, а не обязательным условием входа.
Безопасное хранение секретного ключа на сервере
Секрет для генерации TOTP-кодов — по сути ключ, которым может воспользоваться каждый, кто его прочитает: зная секрет и текущее время, легко посчитать правильный шестизначный код и пройти проверку от чужого имени. Поэтому обращаться с ним нужно так же аккуратно, как с любым другим чувствительным ключом, — тема хранения секретов и токенов подробно разобрана в отдельной статье про хранение секретов и токенов.
Ключевые правила для секрета 2FA:
Никогда не в открытом виде в базе. Секрет обязательно шифруется перед сохранением в таблицу пользователей, а расшифровывается только в момент проверки кода на сервере. Если базу данных когда-нибудь скопируют — а такие утечки случаются даже у крупных компаний, — открытый секрет означает, что 2FA всех пользователей разом перестаёт защищать хоть что-то.
Ключ шифрования — отдельно от базы. Ключ, которым шифруется секрет 2FA, хранится в переменных окружения сервера, а не рядом с самой базой данных. Если утечёт только база — секреты остаются зашифрованными и бесполезными для того, кто их украл.
Секрет никогда не уходит на фронтенд повторно. После того как пользователь один раз отсканировал QR-код и подтвердил включение 2FA, сервер больше не должен показывать этот секрет ни в каком виде — ни в ответах API, ни в логах, ни в отладочных сообщениях. Единственный легитимный способ увидеть секрет заново — отключить старую 2FA и подключить её заново с новым секретом.
Логи — под отдельным вниманием. Секреты и введённые пользователем коды не должны попадать в файлы логов целиком. Если для отладки нужно видеть, что происходит с проверкой кода, логируй факт успеха или неудачи, а не сами значения.
Отдельно стоит проверить, что резервный секретный ключ шифрования не попадает в git вместе с кодом — это одна из самых частых практических ошибок, разобранная в той же статье про хранение секретов.
Ограничения 2FA, которые стоит знать заранее
2FA заметно поднимает планку для атакующего, но не превращает аккаунт в неприступную крепость и создаёт свои собственные неудобства.
Потеря телефона без резервных кодов — реальная проблема. Если пользователь потерял устройство и не сохранил резервные коды заранее, единственный оставшийся путь — обращение в поддержку с ручным подтверждением личности другим способом. Это неизбежное следствие самой идеи «код есть только на этом устройстве», и продумать процесс восстановления доступа стоит заранее, а не в момент первого обращения расстроенного пользователя.
Дополнительный шаг входа — трение для всех. Каждый вход требует достать телефон, открыть приложение-аутентификатор, найти нужную запись, переписать код, пока он не сменился. Для аккаунта с деньгами это оправданная цена. Для казуальной игры — заметное препятствие, из-за которого часть пользователей просто перестаёт заходить.
Рассинхронизация часов ломает проверку. Формула TOTP завязана на текущее время. Если часы на сервере или на телефоне пользователя сильно разошлись — а такое бывает при неправильно настроенном часовом поясе или сбое синхронизации, — коды перестают совпадать. Библиотеки закладывают небольшой запас на этот случай, но серьёзный сбой времени всё равно приводит к тому, что даже правильный код сервер не принимает.
2FA не защищает от всех видов атак. Фишинговый сайт, который выглядит точь-в-точь как настоящий, может попросить пользователя ввести и пароль, и текущий код из приложения — и тут же использовать их для входа в реальный сервис, пока код ещё не истёк. От такой атаки в реальном времени защищают более сложные механизмы вроде аппаратных ключей безопасности, а не TOTP сам по себе. Для большинства проектов курса этот риск не критичен, но знать о нём стоит — на эту и смежные темы есть отдельный разбор в статье про безопасность вайбкодинга.
Смена телефона требует ручных действий. Приложения-аутентификаторы привязывают секрет к конкретной установке на конкретном устройстве. При покупке нового телефона пользователю нужно заранее перенести записи через встроенное резервное копирование приложения или, если такой функции нет, отключить и повторно настроить 2FA на новом устройстве. Без предупреждения об этом заранее люди нередко теряют доступ именно в момент смены телефона, а не из-за атаки.
Шестизначный код можно попытаться подобрать. Пространство вариантов у шестизначного кода — миллион комбинаций, и без ограничений на количество попыток его в теории можно перебирать. Поэтому сервер обязан ограничивать число попыток ввода кода в единицу времени — так же, как это обычно делается для паролей, — иначе часть защиты, которую даёт 2FA, обесценивается плохой реализацией проверки.
Не заменяет остальные меры безопасности. 2FA — это одна конкретная защита от одной конкретной проблемы: кражи или подбора пароля. Она не отменяет необходимость правильно хранить пароли в базе, ограничивать доступ к данным на сервере и следить за остальными механизмами авторизации.
Частые вопросы
Можно ли использовать 2FA вместо пароля, а не вместе с ним?
Формально можно, если совсем убрать пароль и оставить только код из приложения-аутентификатора после ввода email. Но тогда это перестаёт быть двухфакторной проверкой в строгом смысле — остаётся только один фактор, просто другой. Смысл 2FA именно в комбинации двух независимых проверок.
Что делать, если пользователь потерял телефон и не сохранил резервные коды?
Единственный оставшийся путь — ручное восстановление доступа через поддержку с проверкой личности другим способом, например через привязанную почту с дополнительными вопросами. Поэтому интерфейс включения 2FA обязан явно и настойчиво предлагать сохранить резервные коды сразу, до того как они понадобятся.
Нужно ли делать 2FA обязательной для всех пользователей?
Не обязательно. Для большинства проектов разумнее сделать её опцией в настройках аккаунта и обязательной только для ролей с повышенным доступом — например, для администратора. Принудительная 2FA для всех повышает трение входа и подходит в основном сервисам, где риски компрометации аккаунта высоки для каждого пользователя без исключения.
Чем Google Authenticator отличается от Яндекс Ключ и похожих приложений?
С точки зрения стандарта TOTP — ничем принципиальным: оба читают одну и ту же ссылку из QR-кода и считают код по одной и той же формуле RFC 6238. Разница в интерфейсе, дополнительных функциях вроде облачного резервного копирования записей и в том, каким приложением удобнее пользоваться конкретному человеку.
Можно ли протестировать 2FA локально, без реального телефона?
Да, для разработки существуют консольные и онлайн-инструменты, которые по секрету считают текущий TOTP-код так же, как это делает приложение на телефоне. Та же библиотека otpauth, которой подключается проверка на сервере, умеет и сгенерировать текущий код по секрету — этого достаточно, чтобы прогонять автотесты входа без ручного набора цифр из приложения. Это удобно для автоматических тестов и отладки, но перед запуском в проде обязательно проверь весь путь с настоящим приложением-аутентификатором на реальном устройстве.
Замедляет ли 2FA обычную работу с приложением после входа?
Нет. Дополнительная проверка происходит один раз при входе, а не при каждом действии внутри приложения. После успешного входа с кодом дальше работает обычная сессия — так же, как она устроена и без 2FA, — и лишний шаг больше не появляется, пока пользователь не выйдет и не залогинится заново.
Заключение
Двухфакторная аутентификация — не замена входу по коду на почту, а дополнительный барьер поверх него, нужный там, где цена взлома аккаунта выше цены одного лишнего шага при входе. TOTP устроен просто: общий секрет, общее время, независимый расчёт кода на сервере и на телефоне пользователя. Подключается он через готовые библиотеки или встроенную поддержку backend-сервиса вроде Supabase, без необходимости писать криптографию самому.
Для учебной Змейки достаточно OTP на почту, разобранного раньше. Но в момент, когда в проекте появляются деньги, административный доступ или чувствительные данные пользователей, стоит вернуться к этой статье и добавить 2FA — вместе с резервными кодами, шифрованием секрета на сервере и понятным объяснением для пользователей, зачем нужен этот дополнительный шаг.
Читай дальше
Все статьиНе просто статьи — тебя доведут до результата
В практикуме за 2499 ₽ рядом живая команда практикующих разработчиков и маркетологов: ведём по шагам до твоего работающего приложения. Не «ролики и сам разбирайся» — помогаем на каждом затыке.
Перейти к практикуму