Current details about Discourse’s password storage system can be found in discourse/docs/SECURITY.md at main · discourse/discourse · GitHub. At the time of writing, we use PBKDF2-SHA256, with 600,000 iterations.
In some situations, you may want to export all of the hashed passwords from Discourse, and import them into another system. For example, you may be migrating from built-in Discourse authentication to a custom SSO system. Remember, it is impossible to extract the original passwords from the database, so your destination system needs to be capable of running the same hashing algorithm with the same parameters.
Password data is stored in the user_passwords table, which contains password_hash, password_salt, and password_algorithm columns. The password_algorithm column stores the full PHC algorithm prefix (e.g. $pbkdf2-sha256$i=600000,l=32$), which may vary per-user if the iteration count has been increased over time.
You can use data explorer to export the information in a computer-readable format:
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
This will export the data in the native Discourse format. The salt is hex encoded and the password hash is hex encoded.
Some external systems support the PHC string format which aims to be a cross-algorithm way to represent the output of a password hashing function. For pbkdf2-sha256, this string contains the algorithm type, the number of iterations, base64-encoded salt and base64-encoded hash. Fortunately, Postgres can handle all of this for us in a single query.
To generate PHC strings for each Discourse user, you can use a data explorer query like this:
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
If you use Auth0 then you want this instead:
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
Last edited by @JammyDodger 2024-05-25T11:32:20Z
Check document
Perform check on document:
