В большинстве примеров двухфакторной аутентификации второй фактор запрашивается при входе в систему. Это защищает от кражи паролей, но мало помогает в другой распространенной ситуации: когда злоумышленник уже завладел действительным сессионным cookie-файлом.
После того как пользователь вошел в систему, сессия может оставаться активной в течение нескольких дней или недель. Если кто-то получит доступ к этому cookie-файлу, ему не нужно будет снова проходить процедуру входа. С точки зрения приложения, пользователь уже прошел аутентификацию.
Django — это высокоуровневый веб-фреймворк на Python, который способствует быстрой разработке и чистому, прагматичному проектированию. Он поставляется со встроенными средствами аутентификации, управления сессиями, ORM и системой проверки форм. В этом руководстве мы затронем все четыре этих компонента.
Поэтому для некоторых приложений целесообразно вводить второй фактор не при входе, а непосредственно перед выполнением критически важных действий. Хорошими примерами таких действий, перед которыми может потребоваться дополнительный фактор аутентификации, являются: передача билета, перевод денег, ротация ключа API или изменение счета для выплат.
Этот шаблон обычно называют пошаговой аутентификацией (step-up authentication). Пользователь входит в систему обычным способом и может без помех пользоваться низкорисковыми частями приложения. Когда он пытается выполнить более ответственное действие, приложение запрашивает дополнительный этап проверки.
В этом руководстве мы создадим такой процесс пошаговой аутентификации в Django с использованием Vonage Verify API. Успешная проверка будет сохраняться в текущей сессии в течение пяти минут и аннулироваться после выполнения одного защищенного действия. Мы также обработаем основные случаи сбоев API и протестируем весь процесс без отправки реальных сообщений.
tl;dr: Ознакомьтесь с кратким руководством в репозитории Vonage Community на GitHub.
tl;dr: Ознакомьтесь с кратким руководством в репозитории Vonage Community на GitHub.
Страница ввода кода в приложении Setlist, показывающая замаскированный номер получателя и широкое моноширинное поле ввода с цифрами 9 1 4 8 9 7.
История вопроса
Пошаговая аутентификация против 2FA при входе
2FA при входе защищает доступ к самой учетной записи. Каждая новая сессия требует второго фактора, независимо от того, открывает ли пользователь консоль биллинга или читает страницу справки. Это уместно, когда большая часть приложения содержит конфиденциальные данные. Банковские приложения и административные системы — типичные примеры.
Пошаговая аутентификация использует другой подход. Обычная активность использует существующую аутентифицированную сессию, в то время как конкретные действия с высоким уровнем риска требуют недавней проверки вторым фактором. Это может быть полезно, когда только небольшая часть приложения нуждается в усиленной защите. Это также позволяет ограничить дополнительные неудобства для пользователя только теми действиями, которые их оправдывают.
Многие системы используют оба подхода. 2FA при входе может защищать доступ к учетной записи, а пошаговая аутентификация добавляет еще одну проверку перед особо ответственными операциями. В этом руководстве мы сосредоточимся на второй части.
Что вы создадите: приложение Django для передачи билетов
Пример приложения называется Setlist. Оно хранит концертные билеты. Пользователь входит в систему, просматривает свои билеты и может передать билет другому пользователю. Передача билета — это критически важная операция в данном примере, так как она перемещает актив, имеющий рыночную стоимость, из учетной записи, и отправитель не может отменить это действие.
Вот полный процесс с точки зрения пользователя:
- Ада входит в систему обычным способом и просматривает свои билеты — дополнительная проверка не требуется.
Ада входит в систему обычным способом и просматривает свои билеты — дополнительная проверка не требуется.
- Ада нажимает «Передать» (Transfer) на билете. Поскольку это защищенное действие, приложение перенаправляет ее на процесс проверки.
Ада нажимает «Передать» (Transfer) на билете. Поскольку это защищенное действие, приложение перенаправляет ее на процесс проверки.
- Если у Ады уже есть подтвержденный номер телефона в профиле, Vonage отправляет код на этот номер. Если нет, она сначала вводит его.
Если у Ады уже есть подтвержденный номер телефона в профиле, Vonage отправляет код на этот номер. Если нет, она сначала вводит его.
- Ада вводит шестизначный код. Vonage подтверждает его правильность.
Ада вводит шестизначный код. Vonage подтверждает его правильность.
- Приложение фиксирует проверку, обновляет ключ сессии и завершает передачу — билет теперь отображается в учетной записи Грейс.
Приложение фиксирует проверку, обновляет ключ сессии и завершает передачу — билет теперь отображается в учетной записи Грейс.
- Если Ада попытается передать другой билет сразу после этого, ей придется пройти проверку снова. Один код не может авторизовать несколько передач.
Если Ада попытается передать другой билет сразу после этого, ей придется пройти проверку снова. Один код не может авторизовать несколько передач.
Просмотр билетов использует обычную аутентифицированную сессию. Для передачи требуется проверочный код. VerificationRequiredMixin обеспечивает соблюдение этого правила: без недавней проверки в сессии любая попытка перейти к представлению передачи перенаправляет пользователя прямо на процесс проверки.
Повторно используемая часть находится в приложении Django под названием stepup. Оно не содержит никакой логики, специфичной для билетов, поэтому тот же код можно будет применить к другим защищенным действиям в будущем.
Страница «Мои билеты» в Setlist, где перечислены четыре концертных билета, каждый из которых показывает место проведения, дату, место, номер заказа и номинальную стоимость от $62 до $145, с кнопкой «Передать».
Настройка
Предварительные требования
Вам понадобятся:
- Python 3.12 или новее. Пример был создан с использованием Django 5.2.17 LTS и версии 4.8 SDK Vonage для Python.
Python 3.12 или новее. Пример был создан с использованием Django 5.2.17 LTS и версии 4.8 SDK Vonage для Python.
- Учетная запись Vonage API.
Учетная запись Vonage API.
- Телефон, способный принимать SMS. Пока ваша учетная запись находится в пробном режиме, сообщения могут быть отправлены только на номера, которые вы добавили как тестовые. Добавьте свой телефон в разделе Ваши номера перед тестированием процесса.
Телефон, способный принимать SMS. Пока ваша учетная запись находится в пробном режиме, сообщения могут быть отправлены только на номера, которые вы добавили как тестовые. Добавьте свой телефон в разделе Ваши номера перед тестированием процесса.
Настройка и запуск приложения Django
Клонируйте репозиторий и создайте виртуальное окружение:
Установите зависимости:
Скопируйте файл примера окружения:
Затем добавьте свои учетные данные Vonage:
Создайте базу данных и загрузите демонстрационные данные:
Запустите Django:
Откройте http://127.0.0.1:8000/ и войдите как ada с паролем setlist-demo. Также есть вторая учетная запись, grace, которую мы будем использовать в качестве получателя при передаче билетов.
Вот и все! Приложение готово к использованию!
Что Vonage Verify API берет на себя
Прежде чем переходить к коду Django, стоит разделить обязанности приложения и обязанности Vonage Verify.
Если бы мы реализовали проверочные коды самостоятельно, нам пришлось бы генерировать их, хранить, устанавливать срок действия, обеспечивать лимиты повторных попыток, безопасно сравнивать отправленные значения и решать, что делать после слишком большого количества неверных попыток. Verify берет на себя эти задачи. Приложение инициирует проверку, отправляя номер телефона в API и получая идентификатор запроса. Позже оно отправляет этот идентификатор вместе с кодом, который ввел пользователь — Verify решает, является ли код действительным, и весь жизненный цикл запроса управляется на стороне Vonage.
В результате этому проекту не нужны таблица кодов, столбец срока действия или задача по очистке.
Verify также может обрабатывать резервные каналы (fallback). Его рабочий процесс представляет собой упорядоченный список способов доставки. Если первый канал не приводит к завершению проверки в течение channel_timeout, Vonage может перейти к следующему. Для примера достаточно одного SMS-канала. Если вам нужен резервный вариант, вы можете добавить после него VoiceChannel(to=number), чтобы та же самая проверка могла продолжиться с помощью телефонного звонка, если SMS не придет.
Создание потока дополнительной аутентификации (Step-Up Authentication)
Хранение подтвержденного номера телефона
Модель пользователя по умолчанию в Django не имеет поля для номера телефона. Для нового проекта вы можете решить добавить его в пользовательскую модель. Однако в существующем приложении замена модели пользователя может стать гораздо более масштабным изменением, чем сама функция аутентификации.
Для этого примера номер телефона хранится в модели «один-к-одному» внутри приложения stepup:
Используйте здесь settings.AUTH_USER_MODEL вместо прямого импорта django.contrib.auth.models.User. Прямой импорт User работает до тех пор, пока в проекте используется модель пользователя по умолчанию. Использование настройки означает, что это приложение продолжит работать, даже если в проекте используется пользовательская модель.
Различие между number и confirmed_at также полезно. Номер, введенный в форму, не обязательно является подтвержденным. В этом приложении неподтвержденный номер остается в сессии, пока идет процесс проверки. База данных обновляется только после того, как пользователь успешно вводит код, отправленный на этот номер.
Хранение проверки в сессии Django
Сессия — это место, где должно находиться состояние дополнительной аутентификации. Первая реализация может устанавливать его после успешной проверки:
Проблема в том, что значение оставалось бы действительным в течение всего времени жизни сессии, если бы что-то явно его не удалило. Сессии Django обычно живут гораздо дольше, чем должна длиться проверка. Значение по умолчанию SESSION_COOKIE_AGE составляет две недели.
Для дополнительной аутентификации нам важно знать не только то, была ли проведена проверка, но и когда она произошла. В примере сохраняется временная метка:
Время жизни (TTL) по умолчанию в примере составляет пять минут. Это дает пользователю достаточно времени, чтобы получить сообщение, ввести код и завершить защищенное действие, не превращая проверку в долгосрочное разрешение.
Хранение этого состояния в сессии также важно, когда один и тот же пользователь вошел в систему на нескольких устройствах. Если пользователь проходит проверку на ноутбуке, сессия на его телефоне не должна становиться подтвержденной автоматически. Состояние на уровне сессии обеспечивает такое поведение без необходимости дополнительного отслеживания устройств.
Проверка параметра перенаправления next
Поток проверки должен возвращать пользователя на страницу, которую он запрашивал изначально. Распространенный способ реализации этого — использование параметра next. Важная деталь заключается в том, что next — это ввод, контролируемый пользователем.
Это небезопасно:
Злоумышленник может создать URL, например:
Пользователь посетит реальное приложение, завершит его реальный процесс проверки, а затем будет перенаправлен на сайт, контролируемый злоумышленником.
Django предоставляет для этого случая функцию url_has_allowed_host_and_scheme():
Эта функция ограничивает перенаправления текущим хостом, а также отлавливает такие формы, как протокольно-относительные URL, например //evil.example. Это та же проверка, которая нужна процессу входа в систему Django для собственного параметра next.
Защита представлений Django с помощью примеси (Mixin) проверки
Далее нам нужен защитник, который можно прикрепить к конфиденциальным представлениям.
Эта примесь расширяет LoginRequiredMixin, а не заменяет ее. В Django mixins — это способ компоновки повторно используемого поведения в представлениях на основе классов: каждая примесь добавляет отдельный фрагмент логики, и несколько примесей можно комбинировать в одном представлении. Это позволяет разделить здесь два условия: анонимный пользователь должен войти в систему, в то время как аутентифицированный пользователь без недавней проверки должен завершить процесс проверки. Django уже знает, как обрабатывать первый случай, поэтому эта примесь добавляет только второй.
Защита представления становится простой:
Представления, не требующие дополнительной аутентификации, продолжают использовать LoginRequiredMixin. В Setlist представления TicketListView и TicketDetailView остаются обычными аутентифицированными представлениями. Пользователь может просматривать свои билеты, не имея под рукой телефона.
Интеграция Vonage Verify API в Django
Настройка Python-клиента Vonage
Весь код, специфичный для Vonage, находится в одном модуле. Представления Django вызывают небольшой интерфейс, а не вникают в то, как работают запросы SDK, ошибки или объекты ответов.
Verify v2 поддерживает аутентификацию JWT и Basic. Передача ключа API и секрета выбирает аутентификацию Basic, что ограничивает этот пример двумя переменными окружения. Для продакшена идентификатор приложения плюс закрытый ключ — лучший вариант по умолчанию, и он также требуется, если вы хотите получать асинхронные обратные вызовы о статусе.
Запуск проверки
Первый вызов API запускает проверку:
Здесь есть два блока try, потому что у функции есть две разные точки отказа. Я столкнулся с этим при тестировании случая с неверным номером. Объекты запросов Vonage SDK v4 являются моделями Pydantic, поэтому что-то вроде:
может вызвать pydantic.ValidationError во время создания объекта запроса. На тот момент HTTP-запрос еще не был выполнен, поэтому HttpRequestError никогда его не увидит. Разделение валидации модели и HTTP-ошибок на разные блоки делает это различие явным и предотвращает превращение некорректного ввода в неожиданный ответ 500.
Проверка кода
Отправка кода требует меньше усилий:
SDK вызывает исключение при неудачных проверках, поэтому приложению не нужно проверять отдельный флаг успеха.
Обработка ошибок Vonage Verify API
Главный вопрос для пользовательского интерфейса заключается не только в том, какая HTTP-ошибка произошла. Нам нужно решить, может ли пользователь продолжить работу с текущим запросом проверки или этот запрос завершен и ему нужно начать сначала. Вспомогательная функция возвращает как сообщение, так и флаг перезапуска:
Значение False означает, что существующий запрос проверки все еще пригоден для использования. Пользователь остается на форме ввода кода и может попробовать еще раз. Значение True означает, что приложению следует очистить ожидающее состояние и вернуть пользователя к началу процесса проверки.
Случай с HTTP 410 особенно полезен, поскольку Verify принудительно ограничивает количество неверных попыток за нас. Нам не нужно поддерживать отдельный счетчик в Django.
Запуск проверки имеет свое собственное сопоставление ошибок. Например:
Код 401 или 403 указывает на проблему с учетными данными или правами доступа приложения в Vonage — полный список кодов ошибок см. в справочнике по API Vonage. Полная информация должна находиться в логах сервера, а не в сообщении, отображаемом пользователю.
Код 409 — еще один случай, который стоит обработать. Это может произойти, когда проверка для того же номера уже выполняется, например, если кто-то дважды отправил форму запуска.
Создание представлений проверки в Django
Поток в браузере состоит из двух шагов:
- Запуск проверки для номера телефона.
Запуск проверки для номера телефона.
- Отправка кода, полученного на этот телефон.
Отправка кода, полученного на этот телефон.
Представление номера телефона
Первое представление обрабатывает номер телефона и создает запрос Verify.
В первой ветке логики есть важное ограничение безопасности. Как только у пользователя подтвержден номер телефона, это представление не позволяет ему указать другой. В противном случае злоумышленник, узнавший пароль пользователя, мог бы войти в систему, ввести свой номер телефона и самостоятельно пройти второй этап проверки. Изменение подтвержденного номера должно быть отдельным процессом управления учетной записью, в идеале защищенным проверкой через существующий номер.
Для нового номера приложение пока ничего не записывает в VerifiedPhone. Отправленное значение остается в сессии до тех пор, пока код, отправленный на этот номер, не будет подтвержден.
Представление ввода кода
Второе представление проверяет код:
Вызов cycle_key() заслуживает внимания. Django обновляет ключ сессии во время входа в систему для защиты от фиксации сессии. Если злоумышленнику удалось установить известный идентификатор сессии до аутентификации, старый идентификатор становится бесполезным после входа пользователя. Успешная проверка второго фактора — это еще одна граница аутентификации, поэтому в примере ключ сессии обновляется и там.
Полировка и подводные камни
Улучшение ввода одноразового кода на мобильных устройствах
Форма проверки также дает браузеру несколько полезных подсказок:
inputmode="numeric" побуждает мобильные браузеры показывать цифровую клавиатуру. autocomplete="one-time-code" позволяет поддерживаемым мобильным платформам предлагать входящий SMS-код прямо из уведомления. Это небольшое дополнение, которое устраняет самую утомительную часть процесса проверки.
Две ловушки Django, которых следует избегать
При создании примера возникли две проблемы, связанные с самим Django, а не с Verify API. Ни одна из них не является сложной, если знать, что происходит, но обе легко упустить из виду.
Нормализация номера телефона перед валидацией поля
Люди часто вводят номера с пробелами или ведущим знаком «плюс»:
Это допустимый ввод для формы, даже если значение, которое мы в конечном итоге отправляем в Vonage, должно состоять только из цифр. Моим первым побуждением было нормализовать его в clean_number(). Проблема в том, что Django запускает валидаторы полей до метода clean_<field> формы. Это означает, что регулярное выражение E.164 видит исходное значение, включая «+» и пробелы, и отклоняет его с сообщением валидатора («Введите номер в международном формате, только цифры») до того, как clean_number() получит шанс что-либо изменить.
Метод to_python() пользовательского поля выполняется раньше:
К тому времени, когда запускаются валидаторы, они получают уже нормализованное представление.
Не загружайте защищенные объекты в dispatch()
Первая версия представления передачи билета загружала билет в dispatch():
Это работает некорректно для анонимных запросов. Для анонимного пользователя request.user.pk равен None. Поиск билета завершается неудачей и возвращает 404 до того, как LoginRequiredMixin, который также работает через dispatch(), получит возможность перенаправить пользователя на страницу входа.
Лучший подход — позволить миксинам доступа обрабатывать dispatch() и загружать объект позже:
Затем представление может использовать self.ticket из get() и post() после того, как аутентификация уже была проверена.
Использование проверки после передачи
Недавняя проверка позволяет пользователю перейти к представлению передачи билета, но нужно принять еще одно политическое решение. Должна ли эта проверка авторизовать каждое конфиденциальное действие, выполненное в течение пятиминутного TTL, или только то действие, которое пользователь изначально намеревался выполнить?
Для этого примера одна проверка авторизует одну передачу. Сама передача происходит в рамках транзакции базы данных:
Как только передача проходит успешно, состояние повышения прав удаляется из сессии. Если пользователь захочет передать еще один билет, ему придется снова пройти процесс проверки. Это строже, чем полагаться только на пятиминутный TTL, но соответствует политике, которую мы внедряем: код одобрил одну операцию с высоким уровнем риска.
Запись о передаче также хранит идентификатор запроса Verify, связанный с действием. Это дает приложению контрольный журнал, связывающий передачу с конкретным запросом на проверку. Если передача будет оспорена позже, запись будет содержать более полезные доказательства, чем просто логическое значение, указывающее на то, что сессия была проверена в тот момент.
Аутентификация может помочь защитить важные действия для ваших пользователей, такие как передача билетов.
Тестирование без отправки SMS
В проекте 47 тестов. Ни один из них не обращается к сети, не требует учетных данных и не отправляет сообщения.
Это работает, потому что HTTP-исключения Vonage оборачивают requests.Response. Тест может создать такой объект вручную и передать его в тот же класс исключения, который вызвал бы SDK:
Внедрите эту фабрику http_error() в start_verification() или check_code(), и каждая ветка обработки ошибок станет трехстрочным тестом: неверный код, истекший запрос, слишком много попыток, ограничение скорости, параллельный запрос и неверные учетные данные. В репозитории также есть тесты, которые проверяют сами политики безопасности, включая то, что подтвержденный пользователь не может перенаправлять коды на новый номер.
Запустите полный набор тестов с помощью:
Следующие шаги
Приложение stepup намеренно сделано небольшим и автономным. Чтобы использовать тот же шаблон в других местах, добавьте VerificationRequiredMixin к представлениям, требующим более строгой аутентификации. Это может включать смену пароля, добавление способа вывода средств, удаление учетной записи, смену ключа API или раскрытие секрета.
Стоит включить в этот обзор и конфиденциальные операции чтения. Отображение ключа API или учетных данных для восстановления может иметь такие же последствия, как и их изменение.
Есть несколько вещей, которые этот пример намеренно опускает. Первое — это восстановление учетной записи. Как написано, потеря доступа к подтвержденному телефону также означает потерю возможности передавать билеты. Реальному приложению обычно нужны резервные коды, процесс восстановления с поддержкой службы поддержки или другой фактор восстановления.
Второе — изменение подтвержденного номера телефона. Это должен быть выделенный процесс, обычно защищенный проверкой через существующий номер.
Приложение также должно добавить свои собственные ограничения скорости для эндпоинтов. Verify защищает API-сторону процесса проверки, но это не мешает кому-либо повторно отправлять запросы на ваш эндпоинт Django.
Наконец, Verify поддерживает Silent Authentication. Silent Authentication может подтвердить номер телефона через оператора связи без необходимости ввода кода пользователем, с SMS в качестве резервного варианта, когда тихая проверка невозможна. Общий поток приложения остается похожим, но взаимодействие с пользователем может быть более плавным.
Полный исходный код доступен по адресу .
Итоги
В этом руководстве мы создали поток аутентификации с повышением прав в Django с нуля. Вот что охватывает готовая реализация:
- Vonage Verify API обрабатывает генерацию кода, доставку, срок действия, лимиты повторных попыток и принудительное выполнение попыток. Приложению нужно только запустить запрос на проверку, а затем проверить отправленный код.
Vonage Verify API обрабатывает генерацию кода, доставку, срок действия, лимиты повторных попыток и принудительное выполнение попыток. Приложению нужно только запустить запрос на проверку, а затем проверить отправленный код.
- VerificationRequiredMixin позволяет защитить любое представление на основе классов одной строкой кода. Он работает в связке с LoginRequiredMixin, поэтому анонимные пользователи перенаправляются на страницу входа, а аутентифицированные, но не прошедшие проверку пользователи — на процесс верификации.
VerificationRequiredMixin позволяет защитить любое представление на основе классов одной строкой кода. Он работает в связке с LoginRequiredMixin, поэтому анонимные пользователи перенаправляются на страницу входа, а аутентифицированные, но не прошедшие проверку пользователи — на процесс верификации.
- Состояние на основе сессии с коротким TTL ограничивает верификацию одним устройством и пятиминутным окном. Никакой утечки между устройствами, никаких долгосрочных разрешений.
Состояние на основе сессии с коротким TTL ограничивает верификацию одним устройством и пятиминутным окном. Никакой утечки между устройствами, никаких долгосрочных разрешений.
- Одна верификация — одно действие. Использование состояния повышения прав после перевода означает, что один код не может незаметно авторизовать все тикеты в аккаунте.
Одна верификация — одно действие. Использование состояния повышения прав после перевода означает, что один код не может незаметно авторизовать все тикеты в аккаунте.
- Безопасные перенаправления, ротация сессий и привязка к номеру телефона закрывают наиболее распространенные пробелы в реализации: атаки с открытым перенаправлением, фиксацию сессии и перенаправление учетных данных.
Безопасные перенаправления, ротация сессий и привязка к номеру телефона закрывают наиболее распространенные пробелы в реализации: атаки с открытым перенаправлением, фиксацию сессии и перенаправление учетных данных.
- 47 тестов, ноль реальных SMS-сообщений. Фабрика http_error() позволяет протестировать каждую ветку ошибок путем внедрения поддельного HTTP-исключения, сохраняя набор тестов быстрым и не требующим учетных данных.
47 тестов, ноль реальных SMS-сообщений. Фабрика http_error() позволяет протестировать каждую ветку ошибок путем внедрения поддельного HTTP-исключения, сохраняя набор тестов быстрым и не требующим учетных данных.











