Запуск создания аккаунта/входа во внешнем сервисе при входе пользователя в Discourse

На моём сайте Discourse установлен JS-плагин. Этот JS использует PocketBase для хранения данных. PocketBase — это «серверless»-сервис на базе SQLite, позволяющий хранить файлы и JSON-объекты. В PocketBase есть отличная система аутентификации на основе JWT: как только у пользователя появляется токен аутентификации, он может безопасно записывать данные напрямую в PocketBase (без проксирования через бэкенд-сервер и т.п.) прямо из JS на стороне клиента.

Я пытаюсь найти способ автоматически создавать учётную запись в PocketBase, когда пользователь входит в Discourse.

Моя первая попытка заключалась в том, чтобы JS-плагин делал запрос к определённому пути на сервере, передавая куки аутентификации Discourse. Затем этот путь должен был проксироваться через nginx к сервису («прокси входа»), который мог бы декодировать куки аутентификации и определять пользователя. После получения подтверждённых данных о пользователе прокси входа мог бы сделать специальный запрос в PocketBase, получить оттуда токен аутентификации PocketBase и вернуть его клиенту. Затем клиентский JS мог бы использовать этот токен для последующих запросов напрямую к PocketBase.

Однако у меня возникли трудности с декодированием куки аутентификации Discourse (я предполагаю, что правильная кука — _t, но не вижу простого способа извлечь данные о пользователе, к тому же меня беспокоит, что структура этих данных может измениться).

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

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

У меня есть приложение на Next.js, где клиентская часть должна иметь валидный JWT для вызова моего бэкенд-API, если существует сессия в Discourse.

Для этого я использую Discourse как провайдера идентификации через DiscourseConnect.

В моём случае я делаю это одним клиентским вызовом fetch с { credentials: "include" }, что работает только потому, что у меня всё настроено в рамках одного домена, а вызов fetch прозрачно следует за перенаправлениями.

Мой клиент запрашивает кастомный эндпоинт /auth/token, который проверяет наличие _t (просто чтобы избежать бессмысленного перенаправления) и возвращает редирект на защищённый URL /session/sso_provider, построенный согласно документации в связанной теме, с параметрами nonce/sso/sig и return_sso_url, указывающим на кастомный /auth/callback. Этот коллбэк извлекает данные, отправленные Discourse, формирует и возвращает JWT-токен, который клиент может использовать с этого момента.

Думаю, ваш случай использования можно решить аналогичным образом.

Отлично, я попробую. Большое спасибо.

@renato, если не возражаете, у меня несколько вопросов.

Значит ли это, что мне нужно делегировать всю аутентификацию управления пользователями приложению Connect? Я не уверен, хочу ли я этого.

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

Однако, когда я начал изучать Discourse Connect, я забеспокоился, что теперь мне нужно создать и поддерживать совершенно новое приложение для управления аутентификацией пользователей, а я пока не совсем понимаю, как правильно определить его масштаб.

Мой ответ исходит из предположения, что вы используете Discourse в качестве провайдера идентификации (с его интерфейсами входа/регистрации), и хотите оставить всё именно так.

На стороне Discourse его включение так же просто, как

Однако вы упомянули, что создаёте плагин.

Если вы реализуете «путь на сервере» как новое действие контроллера в плагине Discourse, вы сможете получить пользователя из сессии, вызвать сторонние сервисы и вернуть JWT вашему клиенту.

Большое спасибо за обсуждение.

Я прочитал ссылку: Use Discourse as an identity provider (SSO, DiscourseConnect) - #8 by reverend_paco

Но, как я понимаю, это относится к случаю, когда нужно перенаправлять пользователей на сторонний сайт и управлять аутентификацией там.

В моём случае у меня есть JS-скрипт, который выполняется на сайте Discourse, и я хочу, чтобы этот JS обращался к пути на том же сервере и получал обратно cookie для PocketBase.

На самом деле я использую nginx-прокси перед Discourse, поэтому просто добавил специальный маршрут /pb/auth (например). Когда мой JS обращается к этому маршруту, бэкенд-прокси-сервер (не являющийся частью Discourse) принимает соединение и пытается декодировать сессионный cookie _t.

Я поступал так, потому что это казалось немного проще, чем добавлять плагин для Discourse (я меньше знаком с ним и с настройкой разработки и т.д.). Если дело сводится к простому декодированию cookie с использованием base64 и хешированию sha, я думал, что это обеспечит защищённую полезную нагрузку для определения пользователя.

Но если вы считаете, что существует простой способ создать плагин, добавляющий этот маршрут в Discourse, я очень заинтересован в этом. Это кажется правильным подходом в долгосрочной перспективе. Но я старый программист на Perl, поэтому предпочитаю самый ленивый путь, и мой маршрут через nginx казался мне ещё более ленивым. :slight_smile:

Совершенно наоборот: если у вас есть отдельный «сайт» (в данном примере PocketBase) и вы хотите, чтобы Discourse был источником истины для управления пользователями и аутентификацией — как в моём примере с Next.js.

Я бы начал с прочтения

Отлично, я с нетерпением это прочитаю. Я начал изучать образец плагина-каркаса (GitHub - discourse/discourse-plugin-skeleton: Template for Discourse plugins · GitHub), но остался немного разочарован, так как у него вообще нет документации.

С первого взгляда у меня возникает вопрос: добавляет ли этот учебник код в базовую установку Rails для Discourse? Я не против делать так, если это официальный способ, но это кажется опасным, и было бы лучше реализовать это как плагин (который можно легко удалить или отключить). Кроме того, не стоит ли мне беспокоиться, что это сломает обновления Discourse, если мой код не будет в репозитории GitHub?

Например, здесь:

Это значит, что мне действительно нужно зайти в контейнер (./launcher enter app) и затем отредактировать /var/www/app/controllers/snack_controller.rb?

И я на самом деле только что следовал этим инструкциям. Мне не удаётся заставить маршрут /admin/snack.json работать, даже после выполнения ./launcher rebuild app.

Этот учебник, похоже, написан около восьми лет назад. Действительно ли это правильный способ делать вещи?

Существуют и другие руководства: дата вверху указывает на дату создания темы, но всё, что находится в разделе Documentation, должно быть актуальным. Если вы обнаружите какие-либо проблемы, сообщите нам.

Вы можете ознакомиться с кодом существующего плагина #plugin для справки.

Нет:

У меня сложилось впечатление, что это ещё не было обновлено.

Я думаю, что сейчас этот файл находится по адресу https://github.com/discourse/discourse/blob/main/app/assets/javascripts/admin/addon/routes/admin-route-map.js, и он был переименован в 2020 году.

Хорошо, я попытался следовать инструкциям. Я попробовал использовать команду rake plugin:create[pocketbase-auth] внутри контейнера (поскольку rake, вероятно, недоступен снаружи, верно). Но это привело к полному провалу, так как у меня не настроен git внутри контейнера.

Из дальнейшего чтения я понял, что для отображения плагина в разделе администратора нужно указать репозиторий git этого плагина. Однако я еще не дошел до этапа, когда у меня есть хотя бы минимально рабочая версия плагина, и у меня ничего не находится внутри репозитория git.

РЕДАКТИРОВАНИЕ: Я невнимательно прочитал, и действительно, это требует настройки для разработки, что четко указано в самом начале. Я займусь этим и вернусь к этому позже.

Я подозреваю, что эти трудности возникают потому, что обычно плагины разрабатываются в среде «dev» Discourse, а не внутри docker-контейнера, который я запускаю. Это нормально, но я бы хотел, чтобы руководства для разработчиков плагинов начинались именно с этого предположения и инструкций о том, как работать таким образом. Рекомендуемый способ запуска Discourse — использование docker (что мне нравится), но, на мой взгляд, существует разрыв между тем, как запускать всё внутри docker, и тем, как выполнять разработку согласно документации.


# rake plugin:create[pocketbase-auth]
Клонирование 'https://github.com/discourse/discourse-plugin-skeleton' в '/var/www/discourse/plugins/pocketbase-auth'...
Инициализация репозитория git...
hint: Использование 'master' в качестве имени для начальной ветки. Это имя ветки по умолчанию
hint: может быть изменено. Чтобы настроить имя начальной ветки, используемое во всех
hint: ваших новых репозиториях (чтобы подавить это предупреждение), выполните:
hint: 
hint: 	git config --global init.defaultBranch <name>
hint: 
hint: Имена, которые обычно выбирают вместо 'master', — это 'main', 'trunk' и
hint: 'development'. Недавно созданную ветку можно переименовать с помощью команды:
hint: 
hint: 	git branch -m <name>
Инициализирован пустой репозиторий Git в /var/www/discourse/plugins/pocketbase-auth/.git/
Идентичность автора неизвестна

*** Пожалуйста, укажите, кто вы.

Выполните:

  git config --global user.email "you@example.com"
  git config --global user.name "Ваше Имя"

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

fatal: не удалось автоматически определить адрес электронной почты (получено 'discourse@community-public-do-vm-app.(none)')
rake aborted!
Команда завершилась с ошибкой 128: git
/var/www/discourse/lib/tasks/plugin.rake:356:in `system'
/var/www/discourse/lib/tasks/plugin.rake:356:in `block (2 levels) in <main>'
/var/www/discourse/lib/tasks/plugin.rake:346:in `chdir'
/var/www/discourse/lib/tasks/plugin.rake:346:in `block in <main>'
/usr/local/bin/bundle:25:in `load'
/usr/local/bin/bundle:25:in `<main>'
Задачи: TOP => plugin:create
(Полный трассировочный отчет можно получить, запустив задачу с флагом --trace)

Обновление: я последовал вашему совету и разработал плагин. Он отлично работает и делает именно то, что мне было нужно. Спасибо за помощь.