Добавление нового метода аутентификации «managed» в Discourse

Продолжение с Future Social Authentication Improvements…

В настоящее время мы переносим всю информацию об «ассоциированных аккаунтах» в единую таблицу базы данных. Это поможет значительно сократить дублирование логики и ускорит разработку в будущем. Например, миграция нашей основной логики Twitter в новую систему сократила количество строк кода с 136 до всего 24 :tada:.

Этот пост не является пошаговым руководством по добавлению нового провайдера аутентификации, но он призван дать общее представление, указывая на соответствующий исходный код там, где это необходимо.

Реализация аутентификатора

Каждый аутентификатор должен реализовывать подкласс Auth::Authenticator. Чтобы использовать новую общую логику, аутентификатор может вместо этого расширять Auth::ManagedAuthenticator. Пример минимальной реализации можно найти в основном аутентификаторе Facebook:

name и register_middleware должны быть переопределены в реализующих классах, а также enable_setting — булево значение настройки сайта, которое администратор использует для включения провайдера.

Аутентификатор должен также объявлять required_settings: настройки сайта, которые должны иметь значение, чтобы аутентификация могла успешно завершиться. Базовый класс использует их для configured?, а enabled? равно enable_setting && configured?, поэтому провайдер с отсутствующими учетными данными никогда не отображается на странице входа, и его маршрут /auth/<name> остается закрытым — иначе нажатие кнопки оставит пользователя на странице ошибок самого провайдера без возможности вернуться. Объявление required_settings также позволяет AuthProviderCredentialsValidator отказать в включении провайдера в первую очередь; подключите его с помощью validator: "AuthProviderCredentialsValidator" в настройке включения.

def enable_setting
  :enable_google_oauth2_logins
end

def required_settings
  %i[google_oauth2_client_id google_oauth2_client_secret]
end

Аутентификатор, который напрямую переопределяет enabled?, отказывается от обоих механизмов контроля.

:information_source: Примечание: для совместимости с multisite важно, чтобы любая специфичная для сайта информация передавалась в omniauth через lambda setup, а не фиксировалась в момент определения. Смотрите все основные аутентификаторы в качестве примеров этого.

Вся логика связи внешних аккаунтов с аккаунтами Discourse обрабатывается Auth::ManagedAuthenticator. Это опирается на то, что провайдер omniauth возвращает данные в формате, определенном в их документации. Если требуется какая-либо манипуляция с этими данными, аутентификаторы могут переопределить метод after_authenticate и манипулировать auth_token по мере необходимости. Например, основной аутентификатор Twitter удаляет всю информацию extra из токена:

Данные хранятся в таблице базы данных user_associated_accounts. provider_uid, info, credentials и extra берутся напрямую из данных, возвращаемых omniauth.

Изображения провайдеров загружаются из info.image при аутентификации и сохраняются в avatar_upload_id каждого аккаунта. Они остаются отдельными от загруженного изображения пользователя и выбираемых пресетов. Выбор аватара предлагает кэшированные изображения от включенных провайдеров в рамках существующих разрешений на аватары.

UserAvatar управляет импортом аватаров, выбором, обновлением и очисткой. Аутентификация планирует получение провайдером через UserAvatar.retrieve_for_associated_account; задания на загрузку и импортеры используют UserAvatar.import_url_for_user, передавая associated_account_id для изображений провайдеров. Удобные методы пользователя и обратные вызовы жизненного цикла аккаунта/загрузки делегируют изменения аватара UserAvatar; разрешения остаются в Guardian.

user_avatars.selected_user_associated_account_id фиксирует явно выбранный провайдер. Последующие загрузки обновляют отображаемый аватар только пока этот провайдер остается выбранным. Новые аккаунты изначально выбирают свой провайдер, если аватар не был назначен; auth_overrides_avatar продолжает применять выбор провайдера. Существующие аватары сохраняют свой внешний вид и не получают провайдера автоматически. Существующие связанные аккаунты заполняют свои выбор провайдеров при следующем входе.

Выбор другого аватара очищает выбор провайдера. Отключение аккаунта сохраняет его текущее отображаемое изображение как локальный снимок и сохраняет любое отдельное загруженное изображение. Неудачные загрузки сохраняют предыдущее изображение. Очередные загрузки отбрасываются, если аккаунт был отключен, перенесен на другого пользователя или теперь предоставляет другой URL изображения.

Выбор Gravatar по-прежнему использует сопоставление ID загрузки. Таким образом, пользовательское или пресетное изображение, идентичное кэшированному Gravatar, может следовать за последующими обновлениями Gravatar.

После того как класс Authenticator определен, его необходимо зарегистрировать. Это должно происходить на ранней стадии жизненного цикла приложения и не может происходить внутри метода after_initialize плагина. Минимальная регистрация может просто содержать ссылку на аутентификатор. В плагине регистрация может быть выполнена с помощью функции auth_provider. Например:

auth_provider authenticator: OpenIDConnectAuthenticator.new()

В ядре регистрация происходит в discourse.rb. Полный список возможных опций AuthProvider можно найти здесь. Текстовое содержимое может быть определено с помощью этих опций, но лучше предоставлять локализуемые строки в client.en.yml, следуя стандартным ключам. Например:

Дополнительные примечания по ManagedAuthenticator от @fantasticfears

ManagedAuthenticator в деталях

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

Discourse управляет информацией о пользователях с помощью двух контроллеров. Users::OmniauthCallbacksController управляет полезной нагрузкой после завершения аутентификации OAuth2. Здесь вызывается after_authenticate. Здесь также используется can_connect_existing_user?.
Есть несколько частных методов, которые можно прочитать, чтобы понять, как работают различные поля данных.

if authenticator.can_connect_existing_user? && current_user
  @auth_result = authenticator.after_authenticate(auth, existing_account: current_user)
else
  @auth_result = authenticator.after_authenticate(auth)
end

UsersController имеет revoke_account, который использует can_revoke? и revoke. Но для того, чтобы метод revoke работал удаленно, вам нужно создать собственную реализацию.

UserAuthenticator — это класс сервиса, помогающий аутентифицировать (проверять подтверждение электронной почты или путь OAuth2) пользователей. Здесь вызывается after_create_account.

Основная логика остается в after_authenticate с классом данных Auth::Result. Мы следуем структуре данных здесь. extra_data будет передан в after_create_account для создания связанных записей.

result.extra_data = {
  provider: auth_token[:provider],
  uid: auth_token[:uid],
  info: auth_token[:info],
  extra: auth_token[:extra],
  credentials: auth_token[:credentials]
}

Он будет пытаться сопоставить и подключить к существующему аккаунту.

Вы можете спросить, почему возможно автоматическое создание аккаунта, но нет User.create. Это делается в UsersController#create.

authentication = UserAuthenticator.new(user, session)

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


Миграция в новую систему

Для обеспечения бесшовного переключения на новую систему данные должны быть мигрированы из старого места хранения. Для основных провайдеров аутентификации это могут быть выделенные таблицы. Для плагинов это могут быть plugin_store_rows или oauth2_user_infos. Минимальные данные, необходимые в строке user_associated_accounts, — это provider_name, provider_uid и user_id. Пример миграции можно посмотреть здесь:

После того как система ManagedAuthenticator будет выпущена в стабильную ветку с v2.2.0, мы начнем мигрировать официальные плагины аутентификации. В этот момент здесь будет добавлен пример миграции plugin_store_row.


Этот документ находится под контролем версий - предложите изменения на github.

23 лайка

@david вся проделанная здесь работа просто супер. Огромное спасибо. Мне также посчастливилось поработать с GitHub - discourse/discourse-development-auth: A discourse plugin which adds a fake authentication provider. For development purposes only. · GitHub, что очень удобно.

Есть лишь одно замечание: эта функция некорректно работает (не открывает всплывающее окно регистрации) с ember cli в локальной среде. Я ломал голову, разрабатывая плагин для добавления провайдера аутентификации, и вдруг мне в голову пришло использовать NO_EMBER_CLI=1, и всё заработало.

7 лайков

Я хотел бы узнать, является ли реализация аутентификатора правильным путём в рамках

Правильно ли я понимаю, что все зарегистрированные аутентификаторы вызываются на раннем этапе работы приложения, и поэтому я мог бы проверить в них, содержится ли в URL имя пользователя и какая-то подсказка для аутентификации по электронной почте, а затем в ответ отобразить форму «Отправьте мне ссылку для входа»?