Соцсети, 2FA, вход под пользователем
Три подсистемы, которые включаются отдельно и у каждой свои подводные камни.
Вход через соцсети
Поддержаны Google, Яндекс, Mail.ru, GitHub, Facebook и Telegram. Библиотек не нужно — весь обмен это два запроса к провайдеру.
Регистрация — это тот же вход
Отдельной «регистрации через соцсеть» нет и не нужно. Кнопки стоят и в форме входа, и в форме регистрации, ведут в одно место, и первый вход через сеть заводит аккаунт.
Google, Яндекс, GitHub, Mail.ru отдают подтверждённую почту. Если такого пользователя ещё нет — аккаунт создаётся молча, и человек сразу внутри. Ни формы регистрации, ни пароля, ни письма.
- имя пользователя берётся из имени в соцсети, кириллица транслитерируется, занятое имя получает цифру;
- почта и аватар переносятся из профиля соцсети;
- пароль ставится случайный. Человек его не знает и знать не должен: он входит через сеть. Захочет пароль — задаст через «забыли пароль».
Telegram почту не отдаёт никогда, поэтому показывается страница «укажите почту», а после неё приходит обычное письмо подтверждения.
Что нужно, чтобы это заработало
Три вещи. Пока не сделаны все три, кнопок на сайте не будет.
- Применить миграцию — таблица
pba_social_accounts. Пакет применяет её сам; гдеexec()запрещён, см. быстрый старт. - Завести приложение у провайдера и получить доступы.
- Вписать доступы в конфиг.
Провайдер без доступов намеренно не показывается
Кнопка, которая заведомо не сработает, хуже её отсутствия: человек нажимает, получает ошибку провайдера и уходит, решив, что сломан сайт.
Подключение провайдера
Адрес возврата в настройках приложения у провайдера — https://ваш-сайт/auth/<провайдер>/callback, например https://example.com/auth/google/callback.
'social' => [
'providers' => [
'google' => [
'client_id' => '…',
'client_secret' => '…',
],
],
],Ключи: google, yandex, mailru, github, facebook, telegram. У Telegram вместо client_id/client_secret — bot_name и bot_token от вашего бота.
Кнопка появится сама: чанк social.buttons.tpl уже подключён к формам входа и регистрации и показывает только настроенных провайдеров.
⚠️ Как решается, кого пускать
Это самое важное место подсистемы, и здесь легко наделать дыр. Порядок такой:
| Условие | Что делаем | |
|---|---|---|
| 1 | аккаунт провайдера уже привязан | пускаем того пользователя |
| 2 | человек уже вошёл на сайте | привязываем сеть к его аккаунту |
| 3 | пришла почта, провайдер за неё ручается, такой пользователь есть | пускаем и привязываем сеть |
| 4 | пришла подтверждённая почта, пользователя нет | заводим нового и пускаем |
| 5 | почта пришла, но провайдер её не проверял, а аккаунт с ней есть | отказ |
| 6 | почты нет вовсе (Telegram) | спрашиваем отдельной страницей |
Пункт 5 — не придирка
Если пускать по непроверенной почте, достаточно завести у такого провайдера аккаунт с чужим адресом, чтобы забрать чужой профиль на сайте. Поэтому там отказ с просьбой войти паролем и привязать сеть в профиле.
Аккаунт, заведённый по набранной руками почте, активируется только после подтверждения по письму — иначе через соцсеть можно было бы занять чужой адрес.
Если провайдер не дал почту
'social' => ['require_email' => true], // false — заводить вовсе без почтыБез почты человек не сможет восстановить доступ, если сеть отвалится, поэтому по умолчанию спрашиваем.
Привязка и отвязка в профиле
Чанк social.accounts.tpl (уже подключён к форме профиля) показывает список сетей: привязанные с кнопкой «отвязать», остальные — с «привязать».
Последнюю сеть у пользователя, заведённого через соцсеть, отвязать нельзя: своего пароля он не знает и остался бы вообще без входа. Как только он задаст пароль — сменой или восстановлением — ограничение снимается само.
Свой провайдер
Класс, отвечающий интерфейсу SocialDriver; для обычного OAuth2 проще унаследовать AbstractDriver и описать четыре адреса:
class MyDriver extends \Boshnik\PbAuth\Social\AbstractDriver
{
public static function key(): string { return 'mysite'; }
protected function authUrl(): string { return 'https://mysite/oauth/authorize'; }
protected function tokenUrl(): string { return 'https://mysite/oauth/token'; }
protected function userUrl(): string { return 'https://mysite/api/me'; }
protected function mapUser(array $response, array $token): \Boshnik\PbAuth\Social\SocialUser
{
return new \Boshnik\PbAuth\Social\SocialUser(
id: (string) $response['id'],
email: (string) $response['email'],
emailVerified: !empty($response['email_confirmed']),
);
}
}'social' => [
'drivers' => ['mysite' => \PageBlocks\App\Social\MyDriver::class],
'providers' => ['mysite' => ['client_id' => '…', 'client_secret' => '…']],
],emailVerified заполняйте честно
Именно от него зависит, пустят ли в существующий аккаунт по совпадению почты — пункт 3 таблицы выше. Поставив true там, где провайдер почту не проверяет, вы своими руками открываете захват чужих профилей.
Подтверждение входа кодом (2FA)
Кроме пароля при входе можно спрашивать шестизначный код из приложения на телефоне. Украденного пароля тогда недостаточно, чтобы войти.
Включается это каждым пользователем для себя в профиле, разделом «Подтверждение входа». Настраивать ничего не нужно — раздел появляется сам.
Код никуда не приходит
Это первое, что вызывает вопрос. Сайт ничего не отправляет — ни SMS, ни письмо, ни push. Никакой службы отправки подключать не надо.
Код показывает приложение-аутентификатор, которое пользователь сам ставит на телефон: Google Authenticator, Microsoft Authenticator, Authy, 1Password, Bitwarden, Aegis, FreeOTP — любое, они взаимозаменяемы. Сайт с ними никак не связан: ни договора, ни ключей, ни оплаты.
секретный ключ — один и тот же
↓ ↓
приложение на телефоне сайт
ключ + текущее время ключ + текущее время
428 913 428 913
↓
человек вводит код на сайтеПри включении сайт показывает секретный ключ, человек заводит его в приложение. Дальше каждые 30 секунд оба независимо считают из ключа и текущего времени шестизначное число. Код не пересылается ни в одну сторону — его неоткуда перехватить.
Почему не письмом и не SMS
- SMS стоит денег и требует договора со шлюзом. Здесь ноль.
- Доставка может не случиться: письмо уходит в спам, SMS задерживается. Тут доставки нет вообще.
- Работает без связи — приложение показывает код и в самолётном режиме.
- Почта как второй фактор не годится в принципе: если у человека увели пароль от почты, письмо с кодом придёт туда же, и защиты нет.
Как это выглядит для пользователя
- В профиле открывает «Подтверждение входа», видит инструкцию и ключ.
- С телефона нажимает ссылку — открывается приложение и добавляет аккаунт. С компьютера вводит ключ руками, он показан по четыре знака.
- Вводит код, который показало приложение. Пока код не введён, ничего не включается: иначе можно запереть себя, сохранив ключ, который в приложение не попал.
- Получает резервные коды и сохраняет их.
Резервные коды
Восемь одноразовых кодов на случай потери телефона — без них потерянный телефон означал бы потерянный аккаунт. Показываются ровно один раз, при включении; в базе лежат только хеши, восстановить их нельзя. Использованный код вычёркивается.
Перевыпустить можно там же в профиле, подтвердив текущим паролем. Старые сразу перестают работать.
Настройки
'two_factor_enabled' => true, // общий выключатель
'two_factor_window' => 1, // сколько соседних 30-секундных интервалов принимать
'two_factor_challenge_ttl' => 300, // секунд между паролем и кодом
'two_factor_attempts' => 5, // неверных кодов до сброса; 0 — без предела
'two_factor_backup_codes' => 8, // сколько резервных кодов выдавать
'two_factor_issuer' => '', // чьё имя покажет приложение; пусто — название сайтаtwo_factor_window существует потому, что часы на телефоне и на сервере всегда немного расходятся. 1 — это ±30 секунд, обычно достаточно. Больше — ослаблять защиту: код живёт дольше.
two_factor_enabled => false — это аварийный выход
Снимает требование кода при входе и убирает раздел из профиля, но сохранённые ключи не стирает. Если что-то пошло не так и люди не могут войти, сайт открывается одной строкой в конфиге, а после починки всё возвращается как было.
Что происходит внутри
До ввода кода пользователь не залогинен. В сессии лежит только его id и срок годности ожидания — ни пароля, ни открытой сессии контекста. Сессия открывается только после верного кода.
Один и тот же код второй раз не принимается: запоминается интервал, которому он подошёл. Иначе подсмотренный через плечо код работал бы ещё полминуты.
Секрет хранится в extended профиля под ключом pbauth — своей таблицы компоненту не понадобилось, и поля, которые кладёт туда сайт, не задеваются.
Если пользователь потерял и телефон, и коды
Из интерфейса — никак, это и есть смысл второго фактора. Снять его может только администратор: Управление → Пользователи, у нужного пользователя очистить в поле «Расширенные поля» (extended) ключ pbauth.
QR-кода нет
Сознательно. Нарисовать QR — это либо сторонняя библиотека, которую на SFTP-сервере не установить (composer там не запускается), либо собственный кодировщик, который негде проверить перед выкладкой. Поэтому вместо картинки:
- ссылка
otpauth://— на телефоне открывает приложение напрямую, это даже быстрее сканирования; - ключ по четыре знака — вводится руками в любом приложении.
Нужен QR — нарисуйте на своей стороне: в чанке form.twoFactor.tpl ссылка несёт атрибут data-pbauth-totp-uri, подключите любую JS-библиотеку и отрисуйте его содержимое.
Не отправляйте это значение на чужой сервис генерации QR
В нём лежит секрет, и вы отдадите его вместе со вторым фактором.
Кнопки на пользователя в менеджере
pbAuth добавляет на пользователя две кнопки. Обе в двух местах: Управление → Пользователи (правый клик по строке) и страница редактирования пользователя (рядом с «Сохранить»).
| Кнопка | Что делает |
|---|---|
| Посмотреть на сайте | открывает публичную страницу пользователя. Ничего не логинит |
| Авторизоваться на сайте | открывает сайт под этим пользователем |
Разница принципиальная: первая просто показывает страницу, вторая подменяет вашу сессию на сайте на пользовательскую.
Авторизоваться на сайте
Нужна, когда надо увидеть сайт глазами конкретного человека — разобраться, почему у него что-то не отображается. Работает сразу, настраивать нечего.
Кому доступно. Только менеджеру с флагом sudo. Обычный менеджер получит «Недостаточно прав», даже если наберёт адрес руками. Проверка идёт по текущей сессии менеджера: менеджер и сайт делят одну сессию, поэтому подделать её, не войдя в панель, нельзя.
Не откроются заблокированные и неактивированные аккаунты — под ними и сам пользователь войти не может.
Как выйти обратно. Обычным «Выйти» на сайте. Сессия менеджера при этом сохраняется — панель останется открытой в своей вкладке.
'impersonate_enabled' => false, // адрес отвечает 404, кнопка исчезает
'redirects' => ['impersonate' => '/profile'],Записать факт входа (для аудита — кто под кем заходил):
'listeners' => [
Dispatcher::AFTER_IMPERSONATE => [LogImpersonation::class],
],В $params придут user (под кого вошли) и manager (кто вошёл).
Посмотреть на сайте
Просто открывает публичную страницу пользователя. Адрес задаётся настройкой pbauth_user_page в Система → Настройки системы:
users/{id} → https://сайт/users/123
profile/{username} → https://сайт/profile/vasya
https://другой.сайт/u/{id} абсолютный адрес используется как естьПусто — кнопки нет. По умолчанию стоит users/{id}; если такой страницы на сайте нет, очистите настройку — иначе кнопка ведёт в 404.
Кнопок в менеджере нет вовсе
Проверьте, что в Система → Плагины есть плагин pbAuth и к нему прикреплено событие OnManagerPageBeforeRender. Установщик прикрепляет его сам; вручную это нужно только если компонент раскладывали файлами, минуя пакет.