많은 사용자가 이미 가입한 후 SSO를 추가하는 경우, 어떻게 마이그레이션하나요?

안녕하세요, 여러분.

기존의 Discourse 사용자를 intercoin.app로 가져오는 올바른 방법은 무엇인가요? 해시된 비밀번호와 솔트 등을 포함한 모든 사용자를 반환하는 REST 엔드포인트가 있나요? GitHub에서 해시 알고리즘에 대한 링크는 무엇인가요? 만약 우리의 해시 알고리즘이 작동하지 않는다면, 해당 사용자가 로그인할 수 있도록 입력된 비밀번호와 솔트를 동일한 해시 알고리즘으로 처리하기 위해 우리 측에서 코드를 작성해야 할 것 같습니다. Discourse 사용자가 나중에 SSO를 활성화할 때(우리가 하려는 것처럼) #2번 항목이 관련될 것이라고 생각하므로, 이 문제를 해결하면 다른 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("password is too long") 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 플러그인을 작성하는 건가요?

네, 기본적으로 선택지는 바로 그 두 가지입니다.

데이터베이스 스키마가 어딘가에 문서화되어 있나요?

docker에서 postgres 데이터베이스에 연결하는 방법을 알고 싶습니다. 사소한 질문일 수 있지만 죄송합니다.

방금 팀과 이야기를 나눴고, 그들은 동의했습니다. 이메일 주소를 입력하면 해당 사용자의 JSON 정보를 엔드포인트를 통해 노출하는 작은 Discourse 플러그인을 만들어 줄 사람을 고용할 준비가 되어 있습니다.

discourse/app/models/user.rb at main · discourse/discourse · GitHub 에 “password_hash” 소금(salt), 이름, 사용자 이름이 있는 것을 확인했습니다. 하지만 이메일은 없습니다. 이메일은 user_email 모델에서 확인됩니다: discourse/app/models/user_email.rb at main · discourse/discourse · GitHub

즉, 이메일 주소를 주면 플러그인은 user_email 테이블에서 해당 메일 주소를 검색한 후 user_id를 찾아 user 행을 가져오고, 소금을 포함해 모든 “안전한” 필드를 전송합니다.

추가적인 보안을 위해, 플러그인에 제공할 수 있는 공유 비밀 키(shared secret)를 사용하여 HMAC으로 요청에 서명할 수 있습니다.

이 작업을 해볼 사람이 있나요? 메시지를 보내거나 여기에 댓글을 남겨서 연락 방법을 알려주세요. 아마도 단순할 것입니다(비밀 키가 설정된 경우 HMAC 확인과 몇 개의 SELECT 쿼리). 우리는 JSON을 읽을 것입니다.

기존의 admin/users/list/active.json을 그대로 사용하고, 응답에 해시된 비밀번호를 추가하는 방식이 좋습니다.

또한, 기존 API 인증 메커니즘을 그대로 사용하세요. 새로운 것을 굳이 만들 필요는 없습니다.

즉, 모든 사용자의 솔트와 비밀번호를 조인하여 한 번에 가져오는 일회성 작업을 하자는 말씀이시군요?

좋습니다. 하지만 그래도 플러그인으로 만들어야 하지 않나요? 그러니 Discourse에 익숙한 누군가가 그 플러그인을 만들어주면 좋겠습니다.

아마도 데이터 탐색기 플러그인을 사용해 원하는 정보를 내보내는 것이 좋겠습니다. 새로운 플러그인을 작성하는 것보다 훨씬 쉽습니다.

이 설정 값을 어떻게 확인하나요? 기본값은 무엇이며, 코드 어딘가에 있나요? @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인 것이 맞나요? 'a’가 'a’와 XOR되어 0이 되는 문자가 포함된 경우 해당 문자열은 어떻게 되나요? 문자열은 널-종료(null-terminated)가 아닌가요?

제 목표는 이 코드를 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);

일반적으로 해시 알고리즘은 바이너리 데이터를 처리하며, 출력 시 결과 값은 16진수 또는 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. 우리 사이트를 SSO 제공자로 사용하는 대신, Discourse를 SSO 제공자로 사용하는 것이 가능한가요? 그러면 Discourse 포럼 호스트들이 로그인 방식이 그대로 유지되고 Discourse 측에서 처리되므로 Qbix 기능으로 확장할 가능성이 더 높아질 것입니다. Facebook, Google, 기타 모든 서비스처럼요. Discourse Connect가 SSO 제공자로 기능할 때 소비자 사이트로 반환하는 정보의 종류에 대한 문서가 있나요? 다운로드할 수 있는 사진, 이름, 성, 그리고 적어도 사용자 이름은 포함되나요?

솔직히 말씀드리자면, 현재 가장 큰 보안 문제는 Discourse가 HTTPS를 통해 비밀번호를 전송하는 것이 아닐 것입니다.

가능합니다. 표준 사용자 직렬화기(serializer)를 사용하면 대부분의 정보를 얻을 수 있을 것입니다.
하지만 그것이 충분하지 않다면, Discourse에서 더 많은 정보를 가져오기 위해 API를 사용할 수 있습니다.

솔직히 말씀드리면, Discourse가 HTTPS를 통해 비밀번호를 전송하는 것이 현재 가장 큰 보안 문제는 아니라고 생각합니다.

귀엽네요. sha1을 보고 md5로 낮추겠습니다 :slight_smile:

그 pbkdf2가 왜 제대로 분리되지 않는지 이해가 갑니다… 문제는 첫 번째 줄이거든요:

U1 = PRF(Password, Salt + INT_32_BE(i))
U2 = PRF(Password, U1)
⋮
Uc = PRF(Password, Uc−1)

그래도 어떻게 분할할 수 있을까요? 그냥 순수 사용자 영역의 php 라이브러리를 사용하면 될 것 같습니다: pbkdf2/src/PBKDF2.php at master · Spomky-Labs/pbkdf2 · GitHub

Discourse에는 비밀번호를 전송하기 전에 소금(userId를 사용하면 됩니다)을 사용하여 해시하는 것을 권장합니다. 왜 안 하겠습니까? 현재 데이터베이스에 저장하는 것과 호환되지 않아도 되는 것이 아닙니다. 단순히 첫 100회 반복을 JavaScript에서 수행하고, 64000에서 10을 빼면 됩니다. 어차피 커스텀 구현(rails에서 복사한 것)을 가지고 있으므로 isHashed 변수를 함께 보내고, 이 값이 true라면 “마지막” 64K-10 단계만 수행하면 됩니다.

로그인 전에 사용자 ID를 알 수 없으므로 그 방법은 작동하지 않습니다…

10회 반복은 안전하지 않으며, 63,990회 반복은 64,000회 반복보다 안전하지 않습니다. 따라서 미미한 차이이긴 하지만, 한 가지 안전한 방법을 두 가지 덜 안전한 방법과 상당한 추가 복잡성으로 대체하는 것처럼 보입니다.

그리고 실제 이점은 무엇인가요?