Exporter les hachages de mot de passe au format PHC

Les détails actuels concernant le système de stockage des mots de passe de Discourse sont disponibles sur discourse/docs/SECURITY.md at main · discourse/discourse · GitHub. Au moment de la rédaction de ce document, nous utilisons PBKDF2-SHA256 avec 600 000 itérations.

Dans certaines situations, vous souhaiterez peut-être exporter tous les mots de passe hachés de Discourse pour les importer dans un autre système. Par exemple, vous migrez peut-être de l’authentification intégrée de Discourse vers un système SSO personnalisé. Rappelez-vous qu’il est impossible d’extraire les mots de passe originaux de la base de données ; votre système de destination doit donc être capable d’exécuter le même algorithme de hachage avec les mêmes paramètres.

Les données relatives aux mots de passe sont stockées dans la table user_passwords, qui contient les colonnes password_hash, password_salt et password_algorithm. La colonne password_algorithm stocke le préfixe complet de l’algorithme PHC (par exemple $pbkdf2-sha256$i=600000,l=32$), qui peut varier d’un utilisateur à l’autre si le nombre d’itérations a été augmenté au fil du temps.

Vous pouvez utiliser l’explorateur de données pour exporter ces informations dans un format lisible par ordinateur :

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

Cela exportera les données au format natif de Discourse. Le sel est encodé en hexadécimal et le hachage du mot de passe l’est également.

Certains systèmes externes prennent en charge le format de chaîne PHC, qui vise à représenter la sortie d’une fonction de hachage de mots de passe de manière inter-algorithmes. Pour pbkdf2-sha256, cette chaîne contient le type d’algorithme, le nombre d’itérations, le sel encodé en base64 et le hachage encodé en base64. Heureusement, Postgres peut gérer tout cela pour nous dans une seule requête.

Pour générer des chaînes PHC pour chaque utilisateur Discourse, vous pouvez utiliser une requête d’explorateur de données comme celle-ci :

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

Si vous utilisez Auth0, utilisez plutôt cette requête :

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 « J'aime »

Just a note here that I (with some help from the friendly Auth0 team) ended up tweaking the example query to generate valid PHC strings for importing user passwords into Auth0.

I also encoded the salt as base64 by changing this line

salt,

to

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

See further here (including a step by step on how to import Discourse users and their passwords into Auth0).

1 « J'aime »

Thanks @angus - this is interesting because we have had a customer use the query in the OP to successfully import users to Auth0. I wonder if something has changed in their import process - IIRC the ability to import PHC strings to Auth0 was very new back in November :thinking:

3 « J'aime »

Yeah, I was wondering about that, and thought the same.

I also wasn’t quite sure of the language in the PHC specification. Not sure if this means the salt must be B64 encoded or not.

The salt consists in a sequence of characters in: [a-zA-Z0-9/+.-] (lowercase letters, uppercase letters, digits, /, +, . and -). The function specification MUST define the set of valid salt values and a maximum length for this field. Functions that work over arbitrary binary salts SHOULD define that field to be the B64 encoding for a binary value whose length falls in a defined range or set of ranges.

2 « J'aime »

Sorry for getting off-topic, but has anyone imported password hashes from Auth0 to Discourse? I’m thinking of doing this migration so any help would be appreciated. I’m not a paying Auth0 customer so I just wanted to know if this is feasible before paying for the password hash export.

Thanks.

Importing passwords is not supported by Discourse core, although it might be possible using an adapted version of this third-party plugin:

4 « J'aime »

Thank you! I’ve found the repo on GitHub but not the topic here on meta.

1 « J'aime »

Hey @angus, we’re cleaning things up here.

Is it true that the OP code block should read:

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

and include something like:

For more information about importing Discourse passwords to Auth0 see Bulk User Import Custom Password Hash Issue - Auth0 Community.

To move data from Auth0 to Discourse, this might help: Migrated password hashes support.

3 « J'aime »

Yup, that looks good.

You may want to add something in about each import needing specific attention, as the user data being handled will differ depending on the use case, i.e. don’t just copy / paste these queries.

Also, exporting passwords in PHC is not necessarily only for Auth0, so perhaps that should just be refered to as an “example”.

The full query I used for my export was

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 « J'aime »