Добавление SSO после регистрации множества пользователей — как их мигрировать?

Всем привет.

Какой правильный способ импортировать всех существующих пользователей Discourse на intercoin.app из Discourse? Есть ли какой-то REST-эндпоинт, который возвращает всех пользователей с их хешированными паролями и солями, среди прочего? Где ссылка на алгоритм хеширования на GitHub? Мне, вероятно, придется написать код на нашей стороне, чтобы использовать тот же алгоритм хеширования с введенным паролем и солью, если наш собственный не сработает, чтобы позволить этим пользователям войти в систему. Я думаю, что пункт #2 актуален всякий раз, когда пользователь Discourse включает SSO позже (как мы), поэтому решение этой проблемы поможет и другим пользователям Discourse.

Интересный подход.

В user.rb

  def confirm_password?(password)
    return false unless password_hash && salt
    self.password_hash == hash_password(password, salt)
  end

  def hash_password(password, salt)
    raise StandardError.new("пароль слишком длинный") if password.size > User.max_password_length
    Pbkdf2.hash_password(password, salt, Rails.configuration.pbkdf2_iterations, Rails.configuration.pbkdf2_algorithm)
  end

А вот код Pbkdf2: discourse/lib/pbkdf2.rb at 201228162c277b9833bb2988388553fdbfb39521 · discourse/discourse · GitHub

Отлично! Теперь какой HTTP-эндпоинт мне нужно вызвать, чтобы получить всю информацию о пользователе, включая хэш пароля и соль?

Я предполагаю, что такого эндпоинта нет для публичного доступа (зачем облегчать хакерам задачу?). Так что мне делать? Подключаться напрямую к базе данных MySQL? Написать плагин для Discourse?

По сути, это ваши варианты, да.

Документация схемы базы данных где-то доступна?

Как подключиться к базе данных PostgreSQL в Docker? Извините, если это глупый вопрос.

Я только что поговорил с нашей командой, и они согласны: я готов заплатить кому-то за создание небольшого плагина для Discourse, который будет через endpoint выдавать JSON-информацию о пользователе по его email-адресу.

Я обнаружил, что в файле discourse/app/models/user.rb at main · discourse/discourse · GitHub есть «password_hash», salt, имя и имя пользователя. Но email там отсутствует. Для этого я вижу таблицу user_email: discourse/app/models/user_email.rb at main · discourse/discourse · GitHub.

Таким образом, плагин будет искать в таблице user_email запись по email, затем находить user_id, получать строку пользователя и отправлять все «безопасные» поля, включая salt.

Для дополнительной безопасности запросы могут быть подписаны с помощью HMAC, используя общий секрет, который можно передать плагику.

Кто-нибудь хочет заняться этим? Напишите мне или ответьте здесь и сообщите, как с вами связаться. Надеюсь, задача простая (несколько запросов SELECT и проверка HMAC, если секрет задан). Мы будем читать JSON.

Я бы просто использовал существующий admin/users/list/active.json и расширил ответ хешированными паролями.

Также придерживайтесь существующего механизма аутентификации API, не изобретайте велосипед.

То есть вы предлагаете создать разовый скрипт, который импортирует всех пользователей вместе с их солями и паролями?

Ладно, но это всё равно должно быть плагином, не так ли? Поэтому было бы здорово, если бы кто-то из команды Discourse создал такой плагин.

Я бы, вероятно, использовал плагин Data Explorer для экспорта нужной вам информации. Это будет намного проще, чем писать новый плагин.

Как узнать значение этой конфигурации? Каково значение по умолчанию, указано ли оно где-то в коде? @RGJ

root@server:~# cd /var/discourse/
root@server:/var/discourse# ./launcher enter app
Определена архитектура x86_64.
root@server:/var/www/discourse# rails c
[1] pry(main)> Rails.configuration.pbkdf2_iterations
=> 64000
[2] pry(main)>

Спасибо! Хорошо, @RGJ, у меня несколько быстрых вопросов:

Библиотека xorcist — это просто более быстрое побитовое XOR-сложение строк, верно? Что произойдет, если один из символов станет 0, потому что ‘a’ был XOR-сложен с ‘a’? Не являются ли строки завершаемыми нулем?

Моя цель — портировать это в PHP, поэтому любая помощь с вашей стороны (например, информация о том, как реализовать это в PHP) будет очень кстати.

Также, что делает эта строка? ret.bytes.map { |b| ("0" + b.to_s(16))[-2..-1] }.join("")

$u = hash_hmac('sha256', $password, $salt . pack('N', 1));
$ret = $u = hash_hmac('sha256', $password, $u);
for ($i=2; $i < $iterations; ++$i) {
  $u = hash_hmac('sha256', $password, $u);
  $ret = ($ret ^ $u);
}
// todo: выяснить, что делает RUBY в этой последней строке

Это близко к правильному решению? Не могли бы вы исправить этот код на PHP?

Это встроенная функция.

$hash = hash_pbkdf2('sha256', 'YourPassword', 'YourSalt', 64000, 64, false);

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

Спасибо вам большое, Ричард! Вы сэкономили мне столько времени, избавив от необходимости реализовывать это в пользовательском PHP!

Да! Мне удалось создать скрипт, который проходит по всем пользователям Discourse и импортирует их в нашу платформу вместе с хешем их парольной фразы.

Вскоре мы сможем позволить любому, у кого есть форум на Discourse, добавлять также события, видеоконференции, медиа и многое другое, при этом Discourse будет жить на вкладке «Обсуждение». Результат можно посмотреть на https://intercoin.app

По сути, мы превращаем любую установку Discourse в современную социальную сеть по образцу Facebook. Мы работали над этими функциями в течение многих лет, и теперь хотим тесно интегрировать их с Discourse и WordPress. Таким образом, пользователи смогут объединять WordPress, Discourse и Qbix и самостоятельно размещать всю свою сообщественную платформу.

Однако у меня осталось две проблемы.

  1. В Qbix мы хешируем пароль на стороне клиента как минимум с помощью sha1(password + userId) перед отправкой на сервер. Даже если используется HTTPS. Мы делаем это для того, чтобы сервер или любой MITM-атакующий НИКОГДА не имел доступа к паролю для его повторного использования на других сайтах. Но Discourse просто отправляет пароль на сервер. Поэтому нам пришлось отключить это хеширование на стороне клиента. Возможно ли выполнить несколько итераций hash_pbkdf2 на стороне клиента, а остальные — на стороне сервера? Я пробовал, но результаты не совпадают:
php > $password = 'abc';
php > $salt = 'def';
php > $a = hash_pbkdf2('sha256', $password, $salt, 64000, 64, false);
php > $b = hash_pbkdf2('sha256', $password, $salt, 1, 64, false);
php > $c = hash_pbkdf2('sha256', $password, $b, 63999, 64, false);
php > echo $a;
9d7a21ae4113bea06d81e0c486f45ab778bb739f19f7a6a305d8401918a9d8a1
php > echo $c;
f42af6861ebcf8560b027276e0d02ad46502636045486057d81be7c4c4aa630e
  1. Возможно ли использовать Discourse исключительно как провайдера SSO, вместо того чтобы наш сайт выступал в роли провайдера SSO? Тогда администраторы форумов Discourse будут ещё более склонны расширять их функциями Qbix, так как вход останется точно таким же и будет обрабатываться на стороне Discourse: Facebook, Google и любые другие провайдеры. Есть ли какая-либо документация о том, какую именно информацию Discourse Connect как провайдер SSO возвращает нашему потребительскому сайту? Включает ли она такие данные, как фото, которое мы можем скачать, имя, фамилия и, как минимум, имя пользователя?

Честно говоря, я не думаю, что передача пароля через HTTPS в Discourse является вашей главной проблемой безопасности на данный момент.

Конечно. Я думаю, что вы получите большинство данных в стандартном сериализаторе пользователя.
Но если этого недостаточно, вы всегда можете использовать API для получения дополнительной информации из Discourse.

Честно говоря, я не думаю, что отправка пароля через HTTPS в Discourse является вашей главной проблемой с безопасностью на данный момент.

Мило. Я вижу ваш SHA1 и понижаю его до MD5 :slight_smile:

Теперь я понимаю, почему PBKDF2 на самом деле не подходит для разделения задачи… Проблема в первой строке:

U1 = PRF(Пароль, Соль + INT_32_BE(i))
U2 = PRF(Пароль, U1)
⋮
Uc = PRF(Пароль, Uc−1)

Есть какие-то идеи, как это разделить? Думаю, я могу просто использовать чистую PHP-библиотеку для пользовательского пространства: pbkdf2/src/PBKDF2.php at master · Spomky-Labs/pbkdf2 · GitHub

Я бы рекомендовал Discourse хешировать данные с солью (подойдёт userId) перед отправкой пароля по сети. Почему бы и нет? Это не должно сделать систему несовместимой с тем, что сейчас хранится в базе данных. Просто выполните первые 100 итераций в JavaScript, а затем вычтите 10 из 64000. У вас уже есть собственная реализация (скопированная из Rails), так что достаточно будет добавить переменную isHashed: если она истинна, выполняйте только «последние» 64K−10 шагов.

Идентификатор пользователя неизвестен до входа, поэтому это не сработает…

10 итераций недостаточно для обеспечения безопасности, а 63990 итераций менее безопасны, чем 64000. Таким образом, хотя разница и незначительна, кажется, что вы заменяете один безопасный метод двумя менее безопасными и добавляете много лишней сложности.

И каков реальный выигрыш?