Migrating Discourse Users to Auth0

A guide on how to migrate Discourse users to Auth0. This is not a simple operation, so while this lays out the overall procedure for migration, the specific queries used may need to be tweaked for your circumstances. It’s recommend that this be done by a developer who understands what is going on in each step.

I originally posted this on the Auth0 Community forums and also discussed it on the Discourse meta forums. As mentioned in the Auth0 Community post, the relevant Auth0 guides are

Step 1. Export users from Discourse

Using Discourse’s Data Explorer Plugin , use a query like the one below to generate a user list, which should include, as a minimum email, email_verified and password_hash.

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

For more details on what’s going on here please see Export password hashes in the PHC format - sysadmin - Discourse Meta. Note in particular the replies where I’ve mentioned why I tweaked the original query posted by David from the Discourse team.

Step 2. Create an Auth0 users JSON file

This is where you convert the Discourse users JSON file you downloaded to a format that you can import into Auth0. Take a look at the schema for the auth0 JSON user import file first to understand what it should look like

I suggest you use a simple script to do this, so you can clearly see what’s going on and review the output. For example

#!/usr/bin/ruby
require 'json'

input = JSON.parse(File.read(ARGV[0]))
output = []

input["rows"].each do |row|
  output.push(
    email: row[0],
    email_verified: row[1],
    custom_password_hash: {
      algorithm: "pbkdf2",
      hash: {
        value: row[2],
        encoding: "utf8"
      }
    }
  )
end

File.open("output.json", "w") do |f|
  f.write(output.to_json)
end

Generate Auth0 JSON

Here’s an example user JSON produced by that script

{
    "email": "angus+1@fake-email.com",
    "email_verified": true,
    "custom_password_hash": {
      "algorithm": "pbkdf2",
      "hash": {
        "value": "$pbkdf2-sha256$i=64000,l=32$eedf758e855bf13f96f9c48c14d486fb$3t2t87idnzb3Xzqwi0zXn95uJBwKZlo5Sk4yCg4jxR8",
        "encoding": "utf8"
      }
    }
 }

Step 3. Post your users JSON file to Auth0

Now we have out Auth0 users JSON file ready, we now want to send it to Auth0 to be imported using the Auth0 Management API. This API is probably already setup in your Auth0 tenant. We’re specifically going to be using the “Create import users job” action, which will process our import as a background job

There are multiple ways to send off this request. Again I suggest a simple script so you know exactly what’s going on

#!/usr/bin/ruby

require 'rest-client'

response = RestClient.post(
  "https://[your_tenant_name].auth0.com/api/v2/jobs/users-imports", {
    users: File.new("output.json"),
    connection_id: "[connection_id_of_database]",
    send_completion_email: true
  }, {
    "authorization": "Bearer [your_api_token]",
    "content-type": "multipart/form-data"
  }
)

puts response.body

When this completes the registered admin on your Auth0 tenant will get an email (because we set “send_completion_email” to true) with a report on the job. If all goes well, your users will now be in Auth0.

Step 4. Test!

The key thing that needs testing is whether user’s passwords have been imported properly. If you read the meta.discourse.org and community.auth0.com topics I linked to at the start of this post you’ll see that how that is handled may have changed slightly in Auth0. It may change again.

Please feel free to post here with any questions, observations or corrections about this process. I’d be happy to engage :slight_smile:

1 Like