PHC 形式でのパスワードハッシュのエクスポート

Discourse のパスワード保存システムに関する現在の詳細は、discourse/docs/SECURITY.md at main · discourse/discourse · GitHub で確認できます。執筆時点では、600,000 回のイテレーションで PBKDF2-SHA256 を使用しています。

状況によっては、Discourse からハッシュ化されたパスワードをすべてエクスポートし、別のシステムにインポートしたい場合があります。例えば、Discourse 組み込みの認証からカスタム SSO システムへ移行する場合などが挙げられます。データベースから元のパスワードを抽出することは不可能であるため、移行先のシステムは同じハッシュアルゴリズムを同じパラメータで実行できる必要があります。

パスワードデータは user_passwords テーブルに格納されており、password_hashpassword_saltpassword_algorithm の各カラムを含みます。password_algorithm カラムには完全な PHC アルゴリズムプレフィックス(例: $pbkdf2-sha256$i=600000,l=32$)が格納されます。イテレーション数が時間経過とともに増加している場合、ユーザーごとにこの値が異なる可能性があります。

Data Explorer を使用して、コンピュータが読み取れる形式で情報をエクスポートできます。

SELECT users.id, users.username, up.password_salt, up.password_hash, up.password_algorithm
FROM users
INNER JOIN user_passwords up ON users.id = up.user_id
WHERE users.id > 0

これにより、Discourse 独自の形式でデータがエクスポートされます。Salt は hex エンコードされ、パスワードハッシュも hex エンコードされます。

一部の外部システムは、パスワードハッシュ関数の出力をアルゴリズムを跨いで表現することを目的とした PHC ストリング形式 をサポートしています。pbkdf2-sha256 の場合、このストリングにはアルゴリズムの種類、イテレーション数、base64 エンコードされた Salt、base64 エンコードされたハッシュが含まれます。幸いにも、Postgres は単一のクエリでこれらすべてを処理できます。

各 Discourse ユーザーに対して PHC ストリングを生成するには、Data Explorer で以下のようなクエリを使用できます。

SELECT users.id, users.username,
  concat(
    up.password_algorithm,
    replace(encode(up.password_salt::bytea, 'base64'), '=', ''),
    '$',
    replace(encode(decode(up.password_hash, 'hex'), 'base64'), '=', '')
  ) as phc
FROM users
INNER JOIN user_passwords up ON users.id = up.user_id
WHERE users.id > 0

Auth0 を使用している場合は、代わりに以下のクエリを使用してください。

SELECT
    user_emails.email,
    users.active as email_verified,
    concat(
        up.password_algorithm,
        replace(encode(up.password_salt::bytea, 'base64'), '=', ''),
        '$',
        replace(encode(decode(up.password_hash, 'hex'), 'base64'), '=', '')
    ) as password_hash
FROM users
INNER JOIN user_passwords up ON users.id = up.user_id
INNER JOIN user_emails 
ON users.id = user_emails.user_id 
AND user_emails.primary IS TRUE
AND users.id > 0
「いいね!」 13

余計なお世話かもしれませんが、私は(友好的な Auth0 チームの助けを借りて)Auth0 にユーザーパスワードをインポートするための有効な PHC 文字列を生成するように、サンプルクエリを微調整しました。

また、以下の行を変更して、ソルトを base64 でエンコードしました。

salt,

replace(encode(users.salt::bytea, 'base64'), '=', ''),

に変更しました。
詳細はこちら(Discourse ユーザーとそのパスワードを Auth0 にインポートする手順を含む)をご覧ください。

「いいね!」 1

@angus さん、ありがとうございます。これは興味深いですね。OP に記載されたクエリを使って、ある顧客が Auth0 へのユーザーインポートに成功した事例があるからです。インポートプロセスに変更があったのでしょうか?記憶では、PHC 文字列を Auth0 にインポートできる機能は去年の 11 月にはまだ非常に新しいものだったと思います :thinking:

「いいね!」 3

はい、私もその点について疑問に思い、同じように考えていました。

PHC 仕様の記述も少し曖昧で、塩(salt)が Base64 エンコードされている必要があるのかどうかも明確ではありませんでした。

塩は、[a-zA-Z0-9/+.-](小文字、大文字、数字、/、+、.、-)の文字の並びで構成されます。関数仕様は、有効な塩の値のセットと、このフィールドの最大長を定義しなければなりません。任意のバイナリ塩を扱う関数は、定義された範囲または範囲のセットに収まる長さを持つバイナリ値の Base64 エンコードをそのフィールドとして定義すべきです。

「いいね!」 2

話題がそれてしまい申し訳ありませんが、Auth0からDiscourseへパスワードハッシュをインポートした方はいますか?この移行を検討しているため、ご助言いただければ幸いです。有料のAuth0顧客ではないため、パスワードハッシュのエクスポートに費用をかける前に、これが可能かどうかを確認したかったのです。

よろしくお願いいたします。

Discourse コアではパスワードのインポートはサポートされていませんが、このサードパーティ製プラグインの適応版を使用することで実現できる可能性があります。

「いいね!」 4

ありがとうございます!GitHub ではリポジトリを見つけましたが、こちらのメタではトピックが見つかりませんでした。

「いいね!」 1

@angus さん、こちらで整理を進めています。

元のコードブロックは以下のようになっているべきでしょうか?

SELECT id, username,
  concat(
    '$pbkdf2-sha256$i=64000,l=32$',
    replace(encode(users.salt::bytea, 'base64'), '=', ''),
    '$',
    replace(encode(decode(password_hash, 'hex'), 'base64'), '=', '')
  ) as phc
FROM users

また、以下のような内容を含めるべきでしょうか?

Discourse のパスワードを Auth0 にインポートする方法の詳細については、Bulk User Import Custom Password Hash Issue - Auth0 Community をご覧ください。

Auth0 から Discourse へデータを移行する場合は、こちらが役立つかもしれません:https://meta.discourse.org/t/migrated-password-hashes-support/19512。

「いいね!」 3

はい、その通りです。

各インポートには特定の注意が必要であることを追加することをお勧めします。扱うユーザーデータはユースケースによって異なるため、これらのクエリをそのままコピー&ペーストするだけではいけません。

また、PHC からのパスワードのエクスポートは必ずしも Auth0 専用というわけではないので、単に「例」として言及する方が良いかもしれません。

私がエクスポートに使用した完全なクエリは以下の通りです。

SELECT
    user_emails.email,
    users.active as email_verified,
    concat(
        '$pbkdf2-sha256$i=64000,l=32$',
        replace(encode(users.salt::bytea, 'base64'), '=', ''),
        '$',
        replace(encode(decode(users.password_hash, 'hex'), 'base64'), '=', '')
    ) as password_hash
FROM users
INNER JOIN user_emails 
ON users.id = user_emails.user_id 
AND user_emails.primary IS TRUE
AND users.id > 0
「いいね!」 2