Skip to content

Соцсети, 2FA, вход под пользователем ​

Три подсистемы, которые включаются отдельно и у каждой свои подводные камни.

Вход через соцсети ​

Поддержаны Google, Яндекс, Mail.ru, GitHub, Facebook и Telegram. Библиотек не нужно — весь обмен это два запроса к провайдеру.

Регистрация — это тот же вход ​

Отдельной «регистрации через соцсеть» нет и не нужно. Кнопки стоят и в форме входа, и в форме регистрации, ведут в одно место, и первый вход через сеть заводит аккаунт.

Google, Яндекс, GitHub, Mail.ru отдают подтверждённую почту. Если такого пользователя ещё нет — аккаунт создаётся молча, и человек сразу внутри. Ни формы регистрации, ни пароля, ни письма.

  • имя пользователя берётся из имени в соцсети, кириллица транслитерируется, занятое имя получает цифру;
  • почта и аватар переносятся из профиля соцсети;
  • пароль ставится случайный. Человек его не знает и знать не должен: он входит через сеть. Захочет пароль — задаст через «забыли пароль».

Telegram почту не отдаёт никогда, поэтому показывается страница «укажите почту», а после неё приходит обычное письмо подтверждения.

Что нужно, чтобы это заработало ​

Три вещи. Пока не сделаны все три, кнопок на сайте не будет.

  1. Применить миграцию — таблица pba_social_accounts. Пакет применяет её сам; где exec() запрещён, см. быстрый старт.
  2. Завести приложение у провайдера и получить доступы.
  3. Вписать доступы в конфиг.

Провайдер без доступов намеренно не показывается

Кнопка, которая заведомо не сработает, хуже её отсутствия: человек нажимает, получает ошибку провайдера и уходит, решив, что сломан сайт.

Подключение провайдера ​

Адрес возврата в настройках приложения у провайдера — https://ваш-сайт/auth/<провайдер>/callback, например https://example.com/auth/google/callback.

php
'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 — не придирка

Если пускать по непроверенной почте, достаточно завести у такого провайдера аккаунт с чужим адресом, чтобы забрать чужой профиль на сайте. Поэтому там отказ с просьбой войти паролем и привязать сеть в профиле.

Аккаунт, заведённый по набранной руками почте, активируется только после подтверждения по письму — иначе через соцсеть можно было бы занять чужой адрес.

Если провайдер не дал почту ​

php
'social' => ['require_email' => true],   // false — заводить вовсе без почты

Без почты человек не сможет восстановить доступ, если сеть отвалится, поэтому по умолчанию спрашиваем.

Привязка и отвязка в профиле ​

Чанк social.accounts.tpl (уже подключён к форме профиля) показывает список сетей: привязанные с кнопкой «отвязать», остальные — с «привязать».

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

Свой провайдер ​

Класс, отвечающий интерфейсу SocialDriver; для обычного OAuth2 проще унаследовать AbstractDriver и описать четыре адреса:

php
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']),
        );
    }
}
php
'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 задерживается. Тут доставки нет вообще.
  • Работает без связи — приложение показывает код и в самолётном режиме.
  • Почта как второй фактор не годится в принципе: если у человека увели пароль от почты, письмо с кодом придёт туда же, и защиты нет.

Как это выглядит для пользователя ​

  1. В профиле открывает «Подтверждение входа», видит инструкцию и ключ.
  2. С телефона нажимает ссылку — открывается приложение и добавляет аккаунт. С компьютера вводит ключ руками, он показан по четыре знака.
  3. Вводит код, который показало приложение. Пока код не введён, ничего не включается: иначе можно запереть себя, сохранив ключ, который в приложение не попал.
  4. Получает резервные коды и сохраняет их.

Резервные коды ​

Восемь одноразовых кодов на случай потери телефона — без них потерянный телефон означал бы потерянный аккаунт. Показываются ровно один раз, при включении; в базе лежат только хеши, восстановить их нельзя. Использованный код вычёркивается.

Перевыпустить можно там же в профиле, подтвердив текущим паролем. Старые сразу перестают работать.

Настройки ​

php
'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. Обычный менеджер получит «Недостаточно прав», даже если наберёт адрес руками. Проверка идёт по текущей сессии менеджера: менеджер и сайт делят одну сессию, поэтому подделать её, не войдя в панель, нельзя.

Не откроются заблокированные и неактивированные аккаунты — под ними и сам пользователь войти не может.

Как выйти обратно. Обычным «Выйти» на сайте. Сессия менеджера при этом сохраняется — панель останется открытой в своей вкладке.

php
'impersonate_enabled' => false,          // адрес отвечает 404, кнопка исчезает
'redirects' => ['impersonate' => '/profile'],

Записать факт входа (для аудита — кто под кем заходил):

php
'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. Установщик прикрепляет его сам; вручную это нужно только если компонент раскладывали файлами, минуя пакет.

pbAuth — вход, регистрация и профиль для PageBlocks