Двухфакторная аутентификация (2FA) — один из самых простых способов существенно повысить безопасность учётных записей WordPress. Даже если пароль пользователя украден, злоумышленнику дополнительно понадобится одноразовый код из приложения-аутентификатора.
В WordPress необязательно устанавливать отдельный плагин для реализации 2FA. Систему можно встроить непосредственно в код сайта: добавить поле TOTP-кода в стандартную форму входа, сохранить секрет пользователя в user_meta и проверять одноразовый пароль на этапе аутентификации.
Ниже — полностью самостоятельный пример на PHP без сторонней библиотеки для TOTP.
Важно: пример предназначен прежде всего для разработчиков. Перед установкой на рабочий сайт обязательно сделайте резервную копию и проверьте механизм на тестовой копии сайта.
Что мы будем делать
В результате получим следующую схему:
Логин + пароль + одноразовый код
↓
WordPress проверяет
логин и пароль
↓
Проверка TOTP-кода
↓
Успешная аутентификация
↓
WordPress создаёт cookie
Это важно: WordPress сначала проверяет пароль и TOTP, и только после успешной аутентификации создаёт полноценную сессию. В штатном процессе wp_signon() сначала вызывает wp_authenticate(), а затем wp_set_auth_cookie(), поэтому фильтр authenticate подходит для дополнительной проверки второго фактора.
Сам WordPress предоставляет authenticate для дополнительной проверки учётных данных, а login_form позволяет добавить дополнительные поля в стандартную форму входа.
1. Что понадобится
Нам понадобится:
- WordPress;
- PHP 7.4+;
- HTTPS;
- доступ к
functions.phpдочерней темы; - приложение-аутентификатор: Google Authenticator, Microsoft Authenticator, 1Password и т. п.
Для примера мы не будем устанавливать Composer-пакет или WordPress-плагин. Алгоритм TOTP реализуем непосредственно на PHP.
Это сделано специально для демонстрации принципа. В коммерческом проекте криптографическую реализацию лучше брать из проверенной библиотеки и регулярно обновлять её.
2. Полный код
Создайте резервную копию сайта и откройте:
/wp-content/themes/ВАША-ДОЧЕРНЯЯ-ТЕМА/functions.php
Добавьте следующий код.
<?php
/**
* Simple WordPress TOTP 2FA
*
* Без стороннего WordPress-плагина.
*
* Что делает:
* - добавляет поле TOTP в форму wp-login.php;
* - генерирует секрет;
* - проверяет TOTP;
* - не позволяет войти без 2FA;
* - предоставляет страницу настройки 2FA;
*
* ВАЖНО:
* Это демонстрационный production-oriented пример.
* Перед использованием на критическом проекте проведите аудит.
*/
/* =========================================================
* НАСТРОЙКИ
* ======================================================= */
define( 'MY_2FA_META_SECRET', '_my_2fa_secret' );
define( 'MY_2FA_META_ENABLED', '_my_2fa_enabled' );
/* =========================================================
* BASE32
* ======================================================= */
/**
* Генерация Base32-секрета.
*/
function my_2fa_base32_encode( $data ) {
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$binary = '';
foreach ( str_split( $data ) as $char ) {
$binary .= str_pad(
decbin( ord( $char ) ),
8,
'0',
STR_PAD_LEFT
);
}
$output = '';
$length = strlen( $binary );
for ( $i = 0; $i < $length; $i += 5 ) {
$chunk = substr( $binary, $i, 5 );
if ( strlen( $chunk ) < 5 ) {
$chunk = str_pad( $chunk, 5, '0' );
}
$output .= $alphabet[ bindec( $chunk ) ];
}
return $output;
}
/**
* Декодирование Base32.
*/
function my_2fa_base32_decode( $input ) {
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$input = strtoupper( $input );
$input = preg_replace( '/[^A-Z2-7]/', '', $input );
$binary = '';
for ( $i = 0; $i < strlen( $input ); $i++ ) {
$position = strpos(
$alphabet,
$input[ $i ]
);
if ( false === $position ) {
continue;
}
$binary .= str_pad(
decbin( $position ),
5,
'0',
STR_PAD_LEFT
);
}
$output = '';
for ( $i = 0; $i + 8 <= strlen( $binary ); $i += 8 ) {
$output .= chr(
bindec(
substr( $binary, $i, 8 )
)
);
}
return $output;
}
/* =========================================================
* TOTP
* ======================================================= */
/**
* Генерация 6-значного TOTP-кода.
*/
function my_2fa_totp( $secret, $timestamp = null ) {
if ( null === $timestamp ) {
$timestamp = time();
}
$counter = (int) floor( $timestamp / 30 );
$binary_counter = pack(
'N2',
0,
$counter
);
$secret_binary = my_2fa_base32_decode( $secret );
$hash = hash_hmac(
'sha1',
$binary_counter,
$secret_binary,
true
);
$offset = ord(
substr( $hash, -1 )
) & 0x0F;
$binary_code = substr(
$hash,
$offset,
4
);
$value = unpack(
'N',
$binary_code
);
$value = $value[1] & 0x7FFFFFFF;
return str_pad(
(string) ( $value % 1000000 ),
6,
'0',
STR_PAD_LEFT
);
}
/**
* Проверка TOTP.
*
* Разрешаем небольшое расхождение времени:
* предыдущий, текущий и следующий 30-секундный интервал.
*/
function my_2fa_verify_totp( $secret, $code ) {
$code = preg_replace( '/\D/', '', $code );
if ( 6 !== strlen( $code ) ) {
return false;
}
$time = time();
for ( $offset = -1; $offset <= 1; $offset++ ) {
$expected = my_2fa_totp(
$secret,
$time + ( $offset * 30 )
);
if ( hash_equals( $expected, $code ) ) {
return true;
}
}
return false;
}
/* =========================================================
* ГЕНЕРАЦИЯ СЕКРЕТА
* ======================================================= */
function my_2fa_generate_secret() {
return my_2fa_base32_encode(
random_bytes( 20 )
);
}
/* =========================================================
* OT AUTH URI
* ======================================================= */
/**
* URI, который можно использовать для создания QR-кода.
*/
function my_2fa_get_otpauth_uri( $user, $secret ) {
$issuer = wp_parse_url(
home_url(),
PHP_URL_HOST
);
$issuer = $issuer ?: 'WordPress';
$label = rawurlencode(
$issuer . ':' . $user->user_email
);
return sprintf(
'otpauth://totp/%s?secret=%s&issuer=%s',
$label,
rawurlencode( $secret ),
rawurlencode( $issuer )
);
}
/* =========================================================
* ДОБАВЛЯЕМ ПОЛЕ В СТАНДАРТНУЮ ФОРМУ ВХОДА
* ======================================================= */
add_action(
'login_form',
function () {
?>
<p>
<label for="my_2fa_code">
Код двухфакторной аутентификации<br>
<input
type="text"
name="my_2fa_code"
id="my_2fa_code"
class="input"
value=""
size="20"
inputmode="numeric"
autocomplete="one-time-code"
pattern="[0-9]{6}"
maxlength="6"
>
</label>
</p>
<?php
}
);
/* =========================================================
* ПРОВЕРКА 2FA ПРИ ВХОДЕ
* ======================================================= */
add_filter(
'authenticate',
function ( $user, $username, $password ) {
/*
* Если предыдущая проверка уже завершилась ошибкой,
* ничего не меняем.
*/
if ( is_wp_error( $user ) ) {
return $user;
}
/*
* Если логин/пароль ещё не прошли проверку,
* не вмешиваемся.
*/
if ( ! $user instanceof WP_User ) {
return $user;
}
/*
* Получаем секрет пользователя.
*/
$secret = get_user_meta(
$user->ID,
MY_2FA_META_SECRET,
true
);
/*
* 2FA обязательна для всех пользователей,
* поэтому отсутствие секрета блокирует вход.
*/
if ( empty( $secret ) ) {
return new WP_Error(
'2fa_not_configured',
sprintf(
'<strong>Ошибка:</strong> Для пользователя %s не настроена двухфакторная аутентификация.',
esc_html( $user->user_login )
)
);
}
/*
* Получаем код из формы.
*/
$code = isset( $_POST['my_2fa_code'] )
? wp_unslash( $_POST['my_2fa_code'] )
: '';
/*
* Проверяем код.
*/
if ( ! my_2fa_verify_totp( $secret, $code ) ) {
return new WP_Error(
'invalid_2fa',
'<strong>Ошибка:</strong> Неверный код двухфакторной аутентификации.'
);
}
return $user;
},
30,
3
);
/* =========================================================
* СТРАНИЦА НАСТРОЙКИ 2FA
* ======================================================= */
add_action(
'admin_menu',
function () {
add_users_page(
'Двухфакторная аутентификация',
'Моя 2FA',
'read',
'my-2fa',
'my_2fa_settings_page'
);
}
);
/* =========================================================
* СТРАНИЦА НАСТРОЙКИ
* ======================================================= */
function my_2fa_settings_page() {
if ( ! is_user_logged_in() ) {
return;
}
$user_id = get_current_user_id();
$user = wp_get_current_user();
/*
* Создаём секрет, если его ещё нет.
*/
$secret = get_user_meta(
$user_id,
MY_2FA_META_SECRET,
true
);
if ( empty( $secret ) ) {
$secret = my_2fa_generate_secret();
update_user_meta(
$user_id,
MY_2FA_META_SECRET,
$secret
);
}
$enabled = (bool) get_user_meta(
$user_id,
MY_2FA_META_ENABLED,
true
);
/*
* Проверка первого кода.
*/
if (
isset( $_POST['my_2fa_enable'] )
&& check_admin_referer( 'my_2fa_enable' )
) {
$code = isset( $_POST['my_2fa_setup_code'] )
? wp_unslash( $_POST['my_2fa_setup_code'] )
: '';
if ( my_2fa_verify_totp( $secret, $code ) ) {
update_user_meta(
$user_id,
MY_2FA_META_ENABLED,
1
);
$enabled = true;
echo '<div class="notice notice-success"><p>2FA успешно включена.</p></div>';
} else {
echo '<div class="notice notice-error"><p>Неверный код. Проверьте время на телефоне и попробуйте ещё раз.</p></div>';
}
}
/*
* Сброс 2FA.
*/
if (
isset( $_POST['my_2fa_reset'] )
&& check_admin_referer( 'my_2fa_reset' )
) {
/*
* Не удаляем секрет автоматически.
* Вместо этого создаём новый.
*/
$secret = my_2fa_generate_secret();
update_user_meta(
$user_id,
MY_2FA_META_SECRET,
$secret
);
update_user_meta(
$user_id,
MY_2FA_META_ENABLED,
0
);
$enabled = false;
echo '<div class="notice notice-warning"><p>Старый секрет сброшен. Настройте 2FA заново.</p></div>';
}
$uri = my_2fa_get_otpauth_uri(
$user,
$secret
);
?>
<div class="wrap">
<h1>Двухфакторная аутентификация</h1>
<?php if ( $enabled ) : ?>
<div class="notice notice-success">
<p>
<strong>2FA включена.</strong>
При следующем входе потребуется код из приложения-аутентификатора.
</p>
</div>
<form method="post">
<?php wp_nonce_field( 'my_2fa_reset' ); ?>
<input
type="hidden"
name="my_2fa_reset"
value="1"
>
<?php
submit_button(
'Перегенерировать секрет',
'secondary'
);
?>
</form>
<?php else : ?>
<p>
Откройте приложение-аутентификатор
и добавьте новую учётную запись.
</p>
<p>
Если приложение поддерживает ручной ввод,
используйте следующий секрет:
</p>
<p>
<code>
<?php echo esc_html( $secret ); ?>
</code>
</p>
<p>
Также можно использовать URI:
</p>
<p>
<code style="word-break:break-all;">
<?php echo esc_html( $uri ); ?>
</code>
</p>
<hr>
<h2>Проверка настройки</h2>
<p>
Введите шестизначный код, который сейчас показывает приложение.
</p>
<form method="post">
<?php wp_nonce_field( 'my_2fa_enable' ); ?>
<p>
<input
type="text"
name="my_2fa_setup_code"
inputmode="numeric"
pattern="[0-9]{6}"
maxlength="6"
autocomplete="one-time-code"
>
</p>
<input
type="hidden"
name="my_2fa_enable"
value="1"
>
<?php
submit_button(
'Включить 2FA'
);
?>
</form>
<?php endif; ?>
</div>
<?php
}
3. Как работает этот код
Разберём основные части.
Генерация секрета
При первоначальной настройке создаётся случайный 20-байтный секрет:
$secret = my_2fa_generate_secret();
Внутри используется:
random_bytes(20)
Полученный бинарный массив преобразуется в Base32.
Например, пользователь может получить:
JBSWY3DPEHPK3PXP
Именно этот секрет должен знать только сервер и приложение-аутентификатор пользователя.
4. Как формируется TOTP
TOTP основан на общем секрете и текущем времени.
В нашем примере используется интервал 30 секунд:
$counter = (int) floor( $timestamp / 30 );
Затем рассчитывается HMAC-SHA1:
$hash = hash_hmac(
'sha1',
$binary_counter,
$secret_binary,
true
);
После этого из результата получается шестизначное число.
То есть приложение-аутентификатор и WordPress независимо выполняют один и тот же алгоритм:
секрет + текущее время
↓
HMAC-SHA1
↓
6-значный код
5. Почему разрешены три временных интервала
В коде есть:
for ( $offset = -1; $offset <= 1; $offset++ ) {
Это означает, что сервер проверяет:
предыдущий интервал
текущий интервал
следующий интервал
Каждый интервал длится 30 секунд.
Это компенсирует небольшое расхождение часов между сервером и смартфоном.
При этом мы не разрешаем слишком большое временное окно, поскольку это увеличило бы период действия каждого кода.
6. Добавление поля в форму WordPress
WordPress предоставляет хук login_form, который срабатывает после поля пароля стандартной формы входа. Именно туда мы добавляем поле TOTP.
Получается стандартная форма:
Имя пользователя
[________________]
Пароль
[________________]
Код двухфакторной аутентификации
[________________]
[ Войти ]
Пользователь теперь вводит три значения:
логин
пароль
6-значный код
7. Проверка второго фактора
Ключевая часть находится здесь:
add_filter(
'authenticate',
function ( $user, $username, $password ) {
WordPress передаёт результат проверки логина и пароля в фильтр authenticate. Если пользователь уже успешно определён как WP_User, мы можем проверить второй фактор. WordPress прямо документирует authenticate как механизм для дополнительной проверки аутентификации.
Получаем секрет:
$secret = get_user_meta(
$user->ID,
MY_2FA_META_SECRET,
true
);
user_meta — штатный механизм WordPress для хранения дополнительных данных пользователя.
После этого проверяем код:
if ( ! my_2fa_verify_totp( $secret, $code ) ) {
return new WP_Error(
'invalid_2fa',
'<strong>Ошибка:</strong> Неверный код двухфакторной аутентификации.'
);
}
Возвращение WP_Error останавливает успешную аутентификацию. Это принципиально важно.
8. Почему cookie создаётся только после 2FA
Внутри стандартного процесса WordPress wp_signon() сначала получает результат wp_authenticate(), и только если он не является ошибкой, вызывает wp_set_auth_cookie().
Получается:
POST /wp-login.php
↓
логин + пароль
↓
wp_authenticate()
↓
проверка TOTP
↓
WP_User
↓
wp_set_auth_cookie()
↓
пользователь вошёл
Если TOTP неправильный:
POST /wp-login.php
↓
логин + пароль
↓
wp_authenticate()
↓
TOTP НЕВЕРНЫЙ
↓
WP_Error
↓
cookie НЕ создаётся
Именно поэтому нельзя просто проверять 2FA после события wp_login: на этом этапе пользователь уже успешно вошёл.
9. Первоначальная настройка
После добавления кода войдите в WordPress под нужным пользователем и откройте:
Пользователи → Моя 2FA
Страница создаст секрет.
Например:
JBSWY3DPEHPK3PXP
В приложении-аутентификаторе создайте новую запись и введите этот секрет вручную.
После этого приложение начнёт показывать:
123456
Введите текущий код на странице WordPress и нажмите «Включить 2FA».
После успешной проверки пользователь получает:
_totp_secret = секрет
_totp_enabled = 1
Для обработки формы настройки используется WordPress nonce. WordPress рекомендует применять nonce в формах для защиты от подделки запросов.
10. Важная проблема: как включить 2FA сразу для всех
Здесь есть важный нюанс.
В приведённом примере 2FA обязательна для каждого пользователя, у которого происходит вход. Если у пользователя нет _my_2fa_secret, вход будет запрещён:
if ( empty( $secret ) ) {
return new WP_Error(
'2fa_not_configured',
'Для этой учётной записи не настроена двухфакторная аутентификация.'
);
}
Поэтому нельзя просто установить код на рабочий сайт и выйти из всех аккаунтов.
Сначала пользователи должны зарегистрировать свои секреты.
Для существующего сайта лучше внедрять защиту поэтапно.
Вариант 1 — период настройки
На первом этапе разрешить вход пользователям без 2FA и показать им уведомление:
Ваша учётная запись ещё не защищена 2FA.
Настройте двухфакторную аутентификацию.
После окончания переходного периода сделать наличие секрета обязательным.
Вариант 2 — администратор заранее создаёт секреты
Можно написать отдельную административную страницу, которая генерирует секрет для каждого пользователя.
Но передавать секреты пользователям нужно безопасным способом.
Вариант 3 — обязательная настройка после входа
Более удобный вариант — разрешить пользователю войти один раз, но не давать ему доступ к сайту до завершения настройки 2FA.
Это требует дополнительной логики промежуточной сессии и уже является более серьёзной модификацией процесса авторизации.
11. Как добавить QR-код
В примере намеренно не используется сторонний QR-библиотека.
Мы генерируем стандартный URI:
otpauth://totp/...
Например:
otpauth://totp/example.com:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=example.com
QR-код можно генерировать любой совместимой библиотекой или сервисом, который вы контролируете.
В production-системе я бы не рекомендовал отправлять секрет на случайный внешний QR-сервис: секрет является вторым фактором и должен оставаться конфиденциальным.
Поэтому для полноценной реализации лучше использовать локальную PHP-библиотеку генерации QR-кодов.
12. Где хранится секрет
В нашем варианте секрет хранится в:
wp_usermeta
с ключом:
_my_2fa_secret
То есть база содержит примерно:
user_id: 15
meta_key: _my_2fa_secret
meta_value: JBSWY3DPEHPK3PXP
Это рабочий вариант, но для особо критичных систем можно пойти дальше и шифровать секреты перед сохранением.
Самое главное — не выводить секрет в логи, debug-вывод, REST API или административные страницы, доступные другим пользователям.
13. Почему нельзя хешировать TOTP-секрет как пароль
Пароли WordPress можно хранить в виде одностороннего хеша.
TOTP-секрет устроен иначе.
Серверу необходимо иметь возможность использовать исходный секрет для вычисления:
HMAC(secret, time)
Поэтому обычный password_hash() здесь не подходит.
password_hash() предназначен для создания одностороннего хеша пароля, а TOTP требует возможности использовать секрет для генерации кодов.
Если требуется более высокий уровень защиты, секрет можно шифровать, а не просто хешировать.
14. Почему используется hash_equals()
При проверке кода мы используем:
hash_equals(
$expected,
$code
)
а не:
$expected === $code
hash_equals() предназначен для сравнения секретных строк с защитой от timing attacks. PHP отдельно рекомендует передавать известное секретное значение первым аргументом, а пользовательское — вторым.
Поэтому правильный вариант:
hash_equals( $expected, $code );
15. Что ещё необходимо добавить для production
Предыдущий код показывает полноценный базовый механизм, но для серьёзного проекта я бы не останавливался на нём.
Защита от brute-force
Сейчас атакующий может отправлять много неправильных TOTP-кодов.
Нужно добавить rate limiting.
Например:
5 неправильных кодов
↓
блокировка на 5 минут
Причём желательно учитывать не только IP, но и пользователя.
Резервные коды
Пользователь может потерять смартфон.
Поэтому желательно добавить, например:
8J4K-2F9P
Q7XM-91KD
...
Каждый код должен использоваться только один раз.
Хранить резервные коды лучше в виде хешей, а не открытым текстом.
Защита HTTPS
Весь механизм должен работать через HTTPS.
WordPress рекомендует использовать защищённое соединение для входа, поскольку при обычном HTTP пароль и другие данные авторизации могут быть перехвачены.
Проверить сайт можно хотя бы по адресу:
https://example.com/wp-login.php
а не:
http://example.com/wp-login.php
16. Не забываем про REST API
Есть ещё одна распространённая ошибка.
Можно защитить:
/wp-login.php
и считать, что весь WordPress теперь защищён 2FA.
Это не так.
В зависимости от конфигурации сайта могут существовать:
- REST API;
- XML-RPC;
- Application Passwords;
- кастомные API;
- сторонние интеграции;
- отдельные формы авторизации.
WordPress сам использует различные механизмы проверки cookie и авторизации, поэтому каждый используемый способ доступа нужно рассматривать отдельно.
Особенно важно не сломать API-интеграции после введения обязательной 2FA.
17. Не используйте 2FA для Application Passwords автоматически
Application Password — это не обычный пароль пользователя, вводимый через /wp-login.php.
Например, стороннее приложение может обращаться к WordPress REST API следующим образом:
Application
↓
REST API
↓
Application Password
Оно не проходит через обычную HTML-форму входа.
Поэтому добавленный нами TOTP-код не будет автоматически защищать такие запросы.
Если сайт использует Application Passwords, их модель безопасности нужно проектировать отдельно.
18. Что произойдёт при обычном входе
После установки кода пользователь открывает:
/wp-login.php
и видит:
Имя пользователя
[ admin ]
Пароль
[ ******** ]
Код двухфакторной аутентификации
[ 123456 ]
[ Войти ]
При правильном пароле и правильном коде:
WP_User
↓
wp_set_auth_cookie()
↓
Авторизация
При неправильном TOTP:
WP_Error
↓
Авторизация остановлена
↓
Cookie не создаётся
19. Что будет, если злоумышленник знает пароль
Представим, что пароль администратора:
MyVerySecretPassword
стал известен злоумышленнику.
Без 2FA:
пароль → вход
С нашим механизмом:
пароль
↓
TOTP
↓
вход
Злоумышленнику дополнительно потребуется TOTP-секрет или доступ к приложению-аутентификатору.
Именно в этом заключается основной смысл двухфакторной аутентификации.
20. Важное замечание о безопасности самого кода
Самостоятельная реализация 2FA — это не то же самое, что установить зрелый 2FA-плагин.
У готового решения уже могут быть реализованы:
- recovery codes;
- rate limiting;
- уведомления;
- блокировки;
- управление устройствами;
- резервный способ входа;
- WebAuthn/passkeys;
- защита от различных сценариев обхода;
- миграции данных;
- совместимость с новыми версиями WordPress.
Поэтому собственный код имеет смысл, если вам действительно нужен контроль над реализацией.
21. Куда лучше помещать такой код
Технически пример можно разместить в:
functions.php
Но для безопасности я предпочёл бы отдельный файл:
wp-content/mu-plugins/my-2fa.php
Это не обычный плагин, который пользователь устанавливает через админку, а автоматически загружаемый must-use plugin.
Преимущество в том, что защита не исчезнет при смене или обновлении темы.
Если принципиально требуется вообще отсутствие любого plugin-кода, используйте functions.php дочерней темы.
22. Минимальный план внедрения
Для существующего сайта безопаснее делать внедрение поэтапно:
1. Резервная копия
↓
2. Тестовая копия сайта
↓
3. Установка кода
↓
4. Настройка 2FA администратора
↓
5. Проверка обычного входа
↓
6. Проверка неправильного TOTP
↓
7. Проверка выхода
↓
8. Проверка REST/API-интеграций
↓
9. Настройка остальных пользователей
↓
10. Принудительное включение 2FA
Не включайте обязательную 2FA на production-сайте до того, как проверите возможность восстановления доступа.
Заключение
Двухфакторную аутентификацию в WordPress действительно можно реализовать без стороннего плагина.
В базовом варианте достаточно:
- сгенерировать уникальный TOTP-секрет;
- сохранить его для пользователя;
- добавить поле к стандартной форме входа;
- проверить пароль через стандартный механизм WordPress;
- проверить TOTP;
- только после этого позволить WordPress создать authentication cookie.
WordPress специально предоставляет хуки для расширения процесса аутентификации, а wp_signon() создаёт authentication cookie только после успешного результата wp_authenticate().
Главное — не ограничиваться генерацией шестизначного кода. Для реального проекта необходимо также предусмотреть rate limiting, резервные коды, безопасное хранение TOTP-секретов, HTTPS, восстановление доступа и отдельную защиту API.
И ещё один практический момент: перед включением обязательной 2FA для всех пользователей обязательно оставьте себе гарантированный способ восстановления доступа. Ошибка в коде авторизации может заблокировать не только обычных пользователей, но и администратора сайта.


