Могу ли я использовать API Discourse для аутентификации пользователей в другом приложении?

У меня есть рабочий форум Discourse, где пользователи могут создать локальный вход (имя пользователя + пароль).

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

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

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

Какая конечная точка API используется для проверки имени пользователя и пароля при входе на форум?

Более прямой способ — использовать DiscourseConnect как механизм для проверки пользователей или разместить его перед вашим приложением с помощью discourse-auth-proxy.

Это рекомендуемые методы аутентификации пользователей, которые позволяют не обрабатывать учётные данные для входа напрямую. Кроме того, вам не придётся заниматься деталями двухфакторной аутентификации (2FA).

На самом деле моё «другое приложение» — это десктопное приложение, а не веб-приложение. Я не думаю, что в таком случае discourse-auth-proxy сработает.

На странице DiscourseConnect одним из первых утверждений является следующее:

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

Это ровно противоположно тому, что я хочу сделать: я хочу передать все операции входа в Discourse. Есть ли способ использовать DiscourseConnect для этого?

Да, безусловно.

Сложность заключается в том, что между провайдером (Discourse) и потребителем (вашим приложением) существует общий секрет. Если вы распространяете своё приложение, пользователи получат доступ ко всем секретам в нём.

Размещение auth-proxy перед кастомным минимальным веб-сервисом, который выдаёт подписанный токен вашему приложению, может сработать хорошо.

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

Вы имеете в виду API-ключ? Похоже, можно создать «избирательный» API-ключ, который имеет доступ только к определённым конечным точкам API. Всё ещё неясно, какие именно конечные точки потребуются, если я выберу этот подход. Вы знаете?

Да, минимальный веб-сервис с прокси-аутентификацией может стать хорошим решением; мне придётся немного поэкспериментировать, чтобы это выяснить.

Не совсем — речь идёт о значении discourse connect provider secrets для приложения, которое необходимо установить в сочетании с enable discourse connect provider.

Более подробная информация об этом представлена здесь: Use Discourse as an identity provider (SSO, DiscourseConnect)

Для десктопного приложения вам, возможно, будет полезен метод, использованный в этом примере на React Native:

https://github.com/pmusaraj/discourse-mobile-single-site-app/blob/main/js/Authenticate.js

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

Я понимаю, что подход, который я имею в виду, не будет поддерживать двухфакторную аутентификацию (TFA), если я не реализую её самостоятельно, и не будет поддерживать вход через сторонних провайдеров (Google, Facebook, Discord и т.д.).

Я не думаю, что это явно поддерживается, но я бы создал сессию так же, как это делает пользователь в веб-браузере:


curl 'https://try.discourse.org/session' \
  -H 'sec-ch-ua: "Chromium";v="113", "Not-A.Brand";v="24"' \
  -H 'Discourse-Present: true' \
  -H 'DNT: 1' \
  -H 'X-CSRF-Token: …' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'User-Agent: …' \
  -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \
  -H 'Accept: */*' \
  -H 'Referer: https://try.discourse.org/' \
  -H 'X-Requested-With: XMLHttpRequest' \
  -H 'sec-ch-ua-platform: "Linux"' \
  --data-raw 'login=demouser&password=demopassword&second_factor_method=1&timezone=America%2FToronto' \
  --compressed

и дублировать эту логику входа в приложении.

Насколько я понимаю на данный момент, похоже, что метод, используемый в примере с React Native, можно адаптировать для нашего десктопного приложения (которое написано на Python).

Используемая точка доступа к API — это <site>/session, и она требует имя пользователя, пароль и CSRF-токен. CSRF-токен можно получить по адресу <site>/session/csrf.

Это очень близко к тому, что я искал. Думаю, я попробую это сделать и сообщу, если у меня получится.

Документирована ли точка доступа к API <site>/session где-либо?

Лучший способ получить токен для вашего десктопного приложения — использовать ключи API пользователя.

Вам всё равно потребуется веб-интерфейс: либо встроенный в приложение, либо через открытие браузера. Однако, если вы зарегистрируете ваше приложение как обработчик протокола, используемого мобильными приложениями, вы сможете легко получать токен таким образом. В таком случае браузер потребуется снова только в случае истечения срока действия токена или если пользователь зайдёт с другого устройства.

По моему личному опыту, использование ключей API пользователя — гораздо более безопасный и простой вариант, чем попытки работать с эндпоинтами сессий. :slight_smile:

Вот 20 строк кода на Python, которые выполняют примерно ту же задачу, что и код React Native, упомянутый @renato (за исключением совместимости с Discourse 2.5 — она мне не нужна)

Это работает хорошо, при условии, что вы используете базовый вход по имени пользователя и паролю. Я всё ещё изучу альтернативные методы, используя вход через SSO Discourse, настроенный в экземпляре Discourse.

import requests
import json

def discourse_authenticate(url, name, password):
    session = requests.Session()
    session.headers.update({'X-Requested-With': 'XMLHttpRequest'})
    r1 = session.get(url + '/session/csrf')
    csrf_token = json.loads(r1.text).get('csrf')
    r2 = session.post(url + '/session',
        data={
            'login': name,
            'password': password,
            'authenticity_token': csrf_token,
        },
    )
    if r2.status_code != 200:
        return None
    return json.loads(r2.text)

Я попытался применить это, но у меня не получается заставить это работать. Ниже приведён некоторый (упрощённый) код на Python, который генерирует URL для .../session/sso_provider. При попытке его использовать я получаю ошибку Login Error. Не имею понятия, что это означает.

import secrets
import base64
import urllib.parse
import hmac
import hashlib

forum_url = 'https://forum.embeetle.com'
target_url = 'https://embeetle.com/#account'
sso_secret = b'JCLSVcqbAnEPXz2p2xBY'

nonce = secrets.token_urlsafe()
payload = f'nonce={nonce}&return_sso_url={target_url}'
payload_base64 = base64.b64encode(payload.encode('utf-8')).decode()
payload_for_url = urllib.parse.quote(payload_base64)

payload_for_url = 'bm9uY2U9YklKeEU1WWw2OFhjSkJydGlwSU15UTRZeVlMeWd6ZzQyUU9mOFo0SWF5QSZyZXR1cm5fc3NvX3VybD1odHRwczovL2VtYmVldGxlLmNvbS8jYWNjb3VudA%3D%3D'

signature = hmac.new(
    sso_secret, payload_for_url.encode('utf-8'), hashlib.sha256
).hexdigest()

print(f'{forum_url}/session/sso_provider?sso={payload_for_url}&sig={signature}')

Например, один запуск может сгенерировать URL в команде curl ниже:

johan@morla:~/sa\> curl 'https://forum.embeetle.com/session/sso_provider?sso=bm9uY2U9YklKeEU1WWw2OFhjSkJydGlwSU15UTRZeVlMeWd6ZzQyUU9mOFo0SWF5QSZyZXR1cm5fc3NvX3VybD1odHRwczovL2VtYmVldGxlLmNvbS8jYWNjb3VudA%3D%3D&sig=a392ebb81b93ba7411290fbd00240921ae053bbb82998830dda994c8a71853da'
Login Errorjohan@morla:~/sa\> 

Как администратор, включите verbose discourse connect logging, попробуйте снова, а затем проверьте /logs на вашем форуме, чтобы увидеть более подробные сообщения об ошибках, например, https://forum.embeetle.com/logs.

Вы увидите что-то вроде этого:

Вам нужно подписывать полезную нагрузку (payload), а не её кавычки, например:

signature = hmac.new(
  sso_secret, payload_base64.encode('utf-8'), hashlib.sha256
).hexdigest()

И тогда всё заработает!

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

Не волнуйтесь, страница, на которую я перенаправляю, является общедоступной, она предназначена только для тестирования.

В любом случае я уже изменил секрет.

Какие у вас топ-3 предложения по открытым исходным кодам для этой конкретной задачи?

Я использую Nginx, но, возможно, это можно решить с помощью более мощного решения, например Keycloak?

Под auth-proxy я подразумеваю следующее: GitHub - discourse/discourse-auth-proxy: An http proxy that uses the DiscourseConnect protocol to authenticate users · GitHub