Linking Social and Database Accounts in Auth0

Madeline Ford Ryerson
Extra Credit-A Tech Blog by Guild
6 min readMay 17, 2019

--

Here at Guild, we recently switched to using Auth0 for all of our authentication flows. We have used Auth0 to support SSO for some of our clients for the better part of a year, but now everyone who enters our application — through SSO (we’ll refer to these as ‘social users’) or through email/password (and these as ‘database users’) — passes through Auth0.

Because we have a legacy database containing email/password combos, we chose to use Auth0’s custom database connection feature so that existing users would not have to reset their password when we went live with our new Auth0 flow.

Here’s the catch: historically, we have allowed our social users to also set a personal email and password, which they can use as an alternative way to login in the future. Since you can’t set a custom email or a password on a social user in Auth0, we had to figure out a way to let Auth0 know about this alternate identity.

If you’ve used Auth0’s custom database solution, you might think the easiest solution would be to just wait until the social user logged in using the email/password strategy, and let the custom database scripts create the database user. However, if we used this approach, the social and database identities would not be linked in Auth0. Not only would this cost more (Auth0 charges per user), but it would also make identity resolution, and features such as account updates, trickier down the road.

The solution we came up with was to manually create a database user for every social user that was created, and to link the two accounts in Auth0. What follows is a technical overview of how we accomplished this.

Part 1: Creating the database user in Auth0

Step 1: Instantiate Auth0Client with token

We are using the Ruby Auth0 client which provides access to both the Management and Authentication Auth0 APIs. For reference, here are the docs for this library.

Creating and linking accounts are features that use the Management API, which requires authenticating with a token. Unfortunately, this functionality is not yet built into the Ruby client (at least at the time of this writing… you can take a look at the issue on Github here), so we had to manually fetch a token. Our final implementation for setting up the client looked like this:

def client
@client ||= Auth0Client.new(client_params)
end
def client_params
{
client_id: ENV[‘AUTH0_CLIENT_ID’],
domain: ENV[‘AUTH0_DOMAIN’],
token: fetch_token
}
end
def fetch_token
fetch_token_data[‘access_token’]
end
def fetch_token_data
url = URI(“https://#{ENV[‘AUTH0_DOMAIN’]}/oauth/token")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Post.new(url)
request[‘content-type’] = ‘application/json’
request.body = JSON.dump({ grant_type: ‘client_credentials’,
client_id: ENV[‘CLIENT_ID’],
client_secret: ENV[‘CLIENT_SECRET’],
audience: ENV[‘API_IDENTIFIER’] })
JSON.parse(http.request(request).read_body)
end

See Auth0’s docs on Getting Access Tokens for Production for more information on this oauth/token endpoint. Also, note that you might have to manually turn on the ‘client credentials’ grant for your application. You can find this under ‘Advanced Settings > Grant Types’ on your application’s Settings page.

Also, be sure that you have granted access for your Auth0 API in the Auth0 Dashboard. You can double check by going to ‘APIs > Auth0 Management API > Machine to Machine Applications’, and verifying that your application is toggled on.

Step 2: Create the user — Take 1

We thought this step would be as simple as calling the create_user method with the email and password the social user set, but quickly ran into a problem: Auth0 runs the getUser() database script before creating a database user, which searches for an existing user by email. By default, if a user is found, getUser()returns the user and halts the createUser flow. Otherwise, getUser() returns callback(null) and the createUser flow continues.

Since we had already stored the social user’s email in our database, getUser() was halting the createUser flow. We had to figure out a way to let getUser() return callback(null) in the case when we were trying to manually create a database user.

Step 3: Update the getUser database script

To accomplish this, we had to do two things: (1) keep track of whether a user had a database account already, and (2) tell the getUser database script to return callback(null) if a database user did *not* already exist.

For (1), we created a column on our Users table called auth0_ids, which is simply an array of strings representing the user_idfor each Auth0 identity. For example, a newly created Facebook social user would have something like auth0_ids: ['facebook|abc123']and a newly created database user would have something like auth0_ids:['auth0|abc123']. This way, we could identify if an incoming user was a social user for whom we needed to create a database account.

Next we had to update the return of our getUser() script to take this into account:

if (user_does_not_have_database_record) {
return callback(null);
} else {
callback(null, {
id: user.uuid,
email: user.email,
given_name: user.first_name,
family_name: user.last_name,
phone_number: user.phone
});
}

In other words: if the user found by email does not have a database id in the array of auth0_ids, we want to continue along with the createUser flow so that one can be created. Otherwise, continue as normal and return the found user.

Step 4: Create the user — Take 2

We successfully made it to the createUser script! Then we ran into two errors…

Step 5: Update the createUser database script

The first error we encountered was This user already exists — we were hitting the uniqueness constraint on email when the createUser database script ran, since we already had a user in our database with the incoming email (i.e. the same issue we encountered in the getUserscript, just further down the flow). To get around this we added an ON_CONFLICTclause to our query:

var userQuery = ‘INSERT INTO users(email, updated_at, ...) VALUES ($1, $2, ...) ON CONFLICT (email) DO UPDATE SET updated_at=$2 RETURNING *’;

This accomplished exactly what we wanted: it let the createUser script run successfully so that Auth0 would create a new user, but we would not create a duplicate user in our database.

The next error we ran into was very vague: {“statusCode":400,"error":"Bad Request","message":"Sandbox Error: undefined"}

We eventually tracked this down to a bcrypt issue: in this custom flow, we were passing an already-encrypted password to the createUser script. We got around this by detecting if the incoming password was plaintext (normal create user flow) or already encrypted (custom flow), and then using the correct choice in the user query:

let useIncoming;
try {
useIncoming = bcrypt.getRounds(user.password) > 0;
}
catch(error) {
useIncoming = false;
}
bcrypt.hash(user.password, 10, function (err, hashedPassword) {
const passwordToUse = useIncoming ? user.password : hashedPassword;
var userQuery = ‘INSERT INTO users(email, password, updated_at, ...) VALUES ($1, $2, $3, ...) ON CONFLICT (email) DO UPDATE SET updated_at=$3 RETURNING *’;
client.query(userQuery, [user.email.toLowerCase(), passwordToUse, new Date(), new Date()], function (err, result) {

In other words: if the incoming password has already been encrypted, use it as-is when creating the user, otherwise, use the bcrypt hashedPassword.

Step 6: Create the user — Take 3

At this point, we were finally ready to use the create_user method mentioned back in Step 2:

body = {
‘user_id’: ‘’,
‘connection’: ‘Guild’,
‘email’: email,
‘password’: password,
‘email_verified’: true,
‘verify_email’: false
}
client.create_user(user.full_name, body)

Note: connection is simply the name of our custom database connection

Part 2: Linking the accounts

Our last task was to link the newly created database user with the original social user. In the code snippet below, secondary_user_id is the ID of the user returned in Step 6 above (for example, 'auth0|abc123'), and primary_user_idis the ID of the original social user (for example 'facebook|abc123'):

body = {
‘user_id’: secondary_user_id,
‘provider’: ‘auth0’,
‘connection_id’: ENV[‘CONNECTION_ID’]
}
client.link_user_account(primary_user_id, body)

Note 1: The docs say that connection_id is optional, but as far as we could tell it is required when the secondary account is a database user.

Note 2: We could not make this api-based linkage work when the primary account was a database user and the secondary account was a social user.

After linking the two accounts, you should see both the social and the database identities in the ‘identities’ field on the user record in Auth0. The array will look something like this:

[
{
“provider”: “facebook”,
“user_id”: “facebook|abc123”,
“isSocial”: true
},
{
“profileData”: {
“email”: “test@test.com”,
“given_name”: “John”,
“family_name”: “Doe”,
},
“user_id”: 1234567,
“provider”: “auth0”,
“connection”: “Guild”,
“isSocial”: false
}
]

Our custom flow ensures that every social user signing up on our platform has a corresponding database user, and we can locate them in Auth0 using either identity. Additionally, because the accounts are linked, we are (or the user is) able to make user_metatdata changes on the database user, and have them associated with the overarching User, not just the database identity.

If you are interested in learning with us and working on a platform that will help millions of working adults go back to school, check out our careers page.

--

--