Linking User Accounts

We recently introduced some changes in Account Linking. For all the details see Migration Guide: Account Linking and ID Tokens.

Auth0 supports the linking of user accounts from various identity providers. This allows a user to authenticate from any of their accounts and still be recognized by your app and associated with the same user profile. This feature requires a paid subscription to the Developer, Developer Pro or Enterprise plan (see Pricing).

Note that Auth0 will treat all identities as separate by default. For example, if a user logs in first against the Auth0 database and then via Google or Facebook, these two attempts would appear to Auth0 as two separate users.

You can implement functionality to enable a user to explicitly link accounts. In this scenario, the user would log in with an initial provider, perhaps Google. Your application would provide a link or button to enable them to link another account to the first one. The user would click on this link/button and your application would make a call so that when the user logs in with the second provider, the second account is linked with the first.

Advantages of linking accounts

  • Allows users to log in with any identity provider without creating a separate profile for each
  • Allows registered users to use a new social or passwordless login but continue using their existing profile
  • Allows users that registered using a passwordless login to link to an account with a more complete profile
  • Allows your apps to retrieve user profile data stored in various connections

The linking process

The process of linking accounts merges two existing user profiles into a single one. When linking accounts, a primary account and a secondary account must be specified.

In the example below you can see how the resulting linked profile will be for the sample primary and secondary accounts.

{
  "email": "your0@email.com",
  "email_verified": true,
  "name": "John Doe",
  "given_name": "John",
  "family_name": "Doe",
  "picture": "https://lh3.googleusercontent..../photo.jpg",
  "gender": "male",
  "locale": "en",
  "user_id": "google-oauth2|115015401343387192604",
  "identities": [
    {
        "provider": "google-oauth2",
        "user_id": "115015401343387192604",
        "connection": "google-oauth2",
        "isSocial": true
    }
  ],
  "user_metadata": {
    "color": "red"
  },
  "app_metadata": {
    "roles": [
        "Admin"
    ]
  },
  ...
}
        
{
  "phone_number": "+14258831929",
  "phone_verified": true,
  "name": "+14258831929",
  "updated_at": "2015-10-08T18:35:18.102Z",
  "user_id": "sms|560ebaeef609ee1adaa7c551",
  "identities": [
    {
        "user_id": "560ebaeef609ee1adaa7c551",
        "provider": "sms",
        "connection": "sms",
        "isSocial": false
    }
  ],
  "user_metadata": {
      "color": "blue"
  },
  "app_metadata": {
      "roles": [
          "AppAdmin"
      ]
  },
  ...
}
        
{
  "email": "your@email.com",
  "email_verified": true,
  "name": "John Doe",
  "given_name": "John",
  "family_name": "Doe",
  "picture": "https://lh3.googleusercontent.../photo.jpg",
  "gender": "male",
  "locale": "en",
  "user_id": "google-oauth2|115015401343387192604",
  "identities": [
    {
      "provider": "google-oauth2",
      "user_id": "115015401343387192604",
      "connection": "google-oauth2",
      "isSocial": true
    },
    {
      "profileData": {
          "phone_number": "+14258831929",
          "phone_verified": true,
          "name": "+14258831929"
      },
      "user_id": "560ebaeef609ee1adaa7c551",
      "provider": "sms",
      "connection": "sms",
      "isSocial": false
    }
  ],
  "user_metadata": {
      "color": "red"
  },
  "app_metadata": {
      "roles": [
          "Admin"
      ]
  },
  ...
}
        

Note that:

  • The user_id and all other main profile properties continue to be those of the primary identity
  • The secondary account is now embedded in the identities array of the primary profile
  • The attributes of the secondary account are placed inside the profileData field of the corresponding identity inside the array
  • The user_metadata and app_metadata of the primary account have not changed
  • The user_metadata and app_metadata of the secondary account are discarded
  • There is no automatic merging of user profiles with associated identities
  • The secondary account is removed from the users list

Merging Metadata

Metadata are not automatically merged during account linking. If you want to merge them you have to do it manually, using the Auth0 APIv2 Update User endpoint.

The Auth0 Node.js SDK for APIv2 is also available. You can find sample code for merging metadata before linking using this SDK here.

Use the Management API

The Auth0 Management API provides the Link a user account endpoint, which can be invoked in two ways.

  1. With an Access Token that contains the update:current_user_identities scope, the user_id of the primary account as part of the URL, and the secondary account's ID Token in the payload:

curl --request POST \
  --url 'https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities' \
  --header 'authorization: Bearer ACCESS_TOKEN' \
  --header 'content-type: application/json' \
  --data '{"link_with":"SECONDARY_ACCOUNT_ID_TOKEN"}'
var client = new RestClient("https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities");
var request = new RestRequest(Method.POST);
request.AddHeader("content-type", "application/json");
request.AddHeader("authorization", "Bearer ACCESS_TOKEN");
request.AddParameter("application/json", "{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io/ioutil"
)

func main() {

	url := "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities"

	payload := strings.NewReader("{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("authorization", "Bearer ACCESS_TOKEN")
	req.Header.Add("content-type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := ioutil.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.post("https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities")
  .header("authorization", "Bearer ACCESS_TOKEN")
  .header("content-type", "application/json")
  .body("{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}")
  .asString();
var settings = {
  "async": true,
  "crossDomain": true,
  "url": "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities",
  "method": "POST",
  "headers": {
    "authorization": "Bearer ACCESS_TOKEN",
    "content-type": "application/json"
  },
  "processData": false,
  "data": "{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}"
}

$.ajax(settings).done(function (response) {
  console.log(response);
});
var request = require("request");

var options = { method: 'POST',
  url: 'https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities',
  headers: 
   { 'content-type': 'application/json',
     authorization: 'Bearer ACCESS_TOKEN' },
  body: { link_with: 'SECONDARY_ACCOUNT_ID_TOKEN' },
  json: true };

request(options, function (error, response, body) {
  if (error) throw new Error(error);

  console.log(body);
});
#import <Foundation/Foundation.h>

NSDictionary *headers = @{ @"authorization": @"Bearer ACCESS_TOKEN",
                           @"content-type": @"application/json" };
NSDictionary *parameters = @{ @"link_with": @"SECONDARY_ACCOUNT_ID_TOKEN" };

NSData *postData = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:nil];

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities"]
                                                       cachePolicy:NSURLRequestUseProtocolCachePolicy
                                                   timeoutInterval:10.0];
[request setHTTPMethod:@"POST"];
[request setAllHTTPHeaderFields:headers];
[request setHTTPBody:postData];

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
                                            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                                                if (error) {
                                                    NSLog(@"%@", error);
                                                } else {
                                                    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
                                                    NSLog(@"%@", httpResponse);
                                                }
                                            }];
[dataTask resume];
$curl = curl_init();

curl_setopt_array($curl, array(
  CURLOPT_URL => "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => "{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}",
  CURLOPT_HTTPHEADER => array(
    "authorization: Bearer ACCESS_TOKEN",
    "content-type: application/json"
  ),
));

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
  echo "cURL Error #:" . $err;
} else {
  echo $response;
}
import http.client

conn = http.client.HTTPSConnection("")

payload = "{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}"

headers = {
    'authorization': "Bearer ACCESS_TOKEN",
    'content-type': "application/json"
    }

conn.request("POST", "/YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities", payload, headers)

res = conn.getresponse()
data = res.read()

print(data.decode("utf-8"))
require 'uri'
require 'net/http'

url = URI("https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities")

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["authorization"] = 'Bearer ACCESS_TOKEN'
request["content-type"] = 'application/json'
request.body = "{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}"

response = http.request(request)
puts response.read_body
import Foundation

let headers = [
  "authorization": "Bearer ACCESS_TOKEN",
  "content-type": "application/json"
]
let parameters = ["link_with": "SECONDARY_ACCOUNT_ID_TOKEN"]

let postData = NSJSONSerialization.dataWithJSONObject(parameters, options: nil, error: nil)

var request = NSMutableURLRequest(URL: NSURL(string: "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities")!,
                                        cachePolicy: .UseProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.HTTPMethod = "POST"
request.allHTTPHeaderFields = headers
request.HTTPBody = postData

let session = NSURLSession.sharedSession()
let dataTask = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    println(error)
  } else {
    let httpResponse = response as? NSHTTPURLResponse
    println(httpResponse)
  }
})

dataTask.resume()

An Access Token that contains the update:current_user_identities scope, can only be used to update the information of the currently logged-in user. Therefore this method is suitable for scenarios where the user initiates the linking process.

The following must apply:

  • The secondary account's ID Token must be signed with RS256
  • The aud claim in the secondary account's ID Token must identify the client, and hold the same value with the azp claim of the Access Token used to make the request.
  1. With an Access Token that contains the update:users scope, the user_id of the primary account as part of the URL, and the user_id of the secondary account in the payload:

curl --request POST \
  --url 'https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities' \
  --header 'authorization: Bearer ACCESS_TOKEN' \
  --header 'content-type: application/json' \
  --data '{"provider":"SECONDARY_ACCOUNT_PROVIDER", "user_id": "SECONDARY_ACCOUNT_USER_ID"}'
var client = new RestClient("https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities");
var request = new RestRequest(Method.POST);
request.AddHeader("content-type", "application/json");
request.AddHeader("authorization", "Bearer ACCESS_TOKEN");
request.AddParameter("application/json", "{\"provider\":\"SECONDARY_ACCOUNT_PROVIDER\", \"user_id\": \"SECONDARY_ACCOUNT_USER_ID\"}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io/ioutil"
)

func main() {

	url := "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities"

	payload := strings.NewReader("{\"provider\":\"SECONDARY_ACCOUNT_PROVIDER\", \"user_id\": \"SECONDARY_ACCOUNT_USER_ID\"}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("authorization", "Bearer ACCESS_TOKEN")
	req.Header.Add("content-type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := ioutil.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.post("https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities")
  .header("authorization", "Bearer ACCESS_TOKEN")
  .header("content-type", "application/json")
  .body("{\"provider\":\"SECONDARY_ACCOUNT_PROVIDER\", \"user_id\": \"SECONDARY_ACCOUNT_USER_ID\"}")
  .asString();
var settings = {
  "async": true,
  "crossDomain": true,
  "url": "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities",
  "method": "POST",
  "headers": {
    "authorization": "Bearer ACCESS_TOKEN",
    "content-type": "application/json"
  },
  "processData": false,
  "data": "{\"provider\":\"SECONDARY_ACCOUNT_PROVIDER\", \"user_id\": \"SECONDARY_ACCOUNT_USER_ID\"}"
}

$.ajax(settings).done(function (response) {
  console.log(response);
});
var request = require("request");

var options = { method: 'POST',
  url: 'https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities',
  headers: 
   { 'content-type': 'application/json',
     authorization: 'Bearer ACCESS_TOKEN' },
  body: 
   { provider: 'SECONDARY_ACCOUNT_PROVIDER',
     user_id: 'SECONDARY_ACCOUNT_USER_ID' },
  json: true };

request(options, function (error, response, body) {
  if (error) throw new Error(error);

  console.log(body);
});
#import <Foundation/Foundation.h>

NSDictionary *headers = @{ @"authorization": @"Bearer ACCESS_TOKEN",
                           @"content-type": @"application/json" };
NSDictionary *parameters = @{ @"provider": @"SECONDARY_ACCOUNT_PROVIDER",
                              @"user_id": @"SECONDARY_ACCOUNT_USER_ID" };

NSData *postData = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:nil];

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities"]
                                                       cachePolicy:NSURLRequestUseProtocolCachePolicy
                                                   timeoutInterval:10.0];
[request setHTTPMethod:@"POST"];
[request setAllHTTPHeaderFields:headers];
[request setHTTPBody:postData];

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
                                            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                                                if (error) {
                                                    NSLog(@"%@", error);
                                                } else {
                                                    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
                                                    NSLog(@"%@", httpResponse);
                                                }
                                            }];
[dataTask resume];
$curl = curl_init();

curl_setopt_array($curl, array(
  CURLOPT_URL => "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => "{\"provider\":\"SECONDARY_ACCOUNT_PROVIDER\", \"user_id\": \"SECONDARY_ACCOUNT_USER_ID\"}",
  CURLOPT_HTTPHEADER => array(
    "authorization: Bearer ACCESS_TOKEN",
    "content-type: application/json"
  ),
));

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
  echo "cURL Error #:" . $err;
} else {
  echo $response;
}
import http.client

conn = http.client.HTTPSConnection("")

payload = "{\"provider\":\"SECONDARY_ACCOUNT_PROVIDER\", \"user_id\": \"SECONDARY_ACCOUNT_USER_ID\"}"

headers = {
    'authorization': "Bearer ACCESS_TOKEN",
    'content-type': "application/json"
    }

conn.request("POST", "/YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities", payload, headers)

res = conn.getresponse()
data = res.read()

print(data.decode("utf-8"))
require 'uri'
require 'net/http'

url = URI("https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities")

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["authorization"] = 'Bearer ACCESS_TOKEN'
request["content-type"] = 'application/json'
request.body = "{\"provider\":\"SECONDARY_ACCOUNT_PROVIDER\", \"user_id\": \"SECONDARY_ACCOUNT_USER_ID\"}"

response = http.request(request)
puts response.read_body
import Foundation

let headers = [
  "authorization": "Bearer ACCESS_TOKEN",
  "content-type": "application/json"
]
let parameters = [
  "provider": "SECONDARY_ACCOUNT_PROVIDER",
  "user_id": "SECONDARY_ACCOUNT_USER_ID"
]

let postData = NSJSONSerialization.dataWithJSONObject(parameters, options: nil, error: nil)

var request = NSMutableURLRequest(URL: NSURL(string: "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities")!,
                                        cachePolicy: .UseProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.HTTPMethod = "POST"
request.allHTTPHeaderFields = headers
request.HTTPBody = postData

let session = NSURLSession.sharedSession()
let dataTask = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    println(error)
  } else {
    let httpResponse = response as? NSHTTPURLResponse
    println(httpResponse)
  }
})

dataTask.resume()

The SECONDARY_ACCOUNT_USER_ID and SECONDARY_ACCOUNT_PROVIDER can be deduced by the unique ID of the user. So for example, if the user ID is google-oauth2|108091299999329986433, set the google-oauth2 part as the provider, and the 108091299999329986433 part as the user_id at your request.

Instead of the provider and user_id, you can send the secondary account's ID Token as part of the payload:


curl --request POST \
  --url 'https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities' \
  --header 'authorization: Bearer ACCESS_TOKEN' \
  --header 'content-type: application/json' \
  --data '{"link_with":"SECONDARY_ACCOUNT_ID_TOKEN"}'
var client = new RestClient("https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities");
var request = new RestRequest(Method.POST);
request.AddHeader("content-type", "application/json");
request.AddHeader("authorization", "Bearer ACCESS_TOKEN");
request.AddParameter("application/json", "{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io/ioutil"
)

func main() {

	url := "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities"

	payload := strings.NewReader("{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("authorization", "Bearer ACCESS_TOKEN")
	req.Header.Add("content-type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := ioutil.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.post("https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities")
  .header("authorization", "Bearer ACCESS_TOKEN")
  .header("content-type", "application/json")
  .body("{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}")
  .asString();
var settings = {
  "async": true,
  "crossDomain": true,
  "url": "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities",
  "method": "POST",
  "headers": {
    "authorization": "Bearer ACCESS_TOKEN",
    "content-type": "application/json"
  },
  "processData": false,
  "data": "{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}"
}

$.ajax(settings).done(function (response) {
  console.log(response);
});
var request = require("request");

var options = { method: 'POST',
  url: 'https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities',
  headers: 
   { 'content-type': 'application/json',
     authorization: 'Bearer ACCESS_TOKEN' },
  body: { link_with: 'SECONDARY_ACCOUNT_ID_TOKEN' },
  json: true };

request(options, function (error, response, body) {
  if (error) throw new Error(error);

  console.log(body);
});
#import <Foundation/Foundation.h>

NSDictionary *headers = @{ @"authorization": @"Bearer ACCESS_TOKEN",
                           @"content-type": @"application/json" };
NSDictionary *parameters = @{ @"link_with": @"SECONDARY_ACCOUNT_ID_TOKEN" };

NSData *postData = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:nil];

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities"]
                                                       cachePolicy:NSURLRequestUseProtocolCachePolicy
                                                   timeoutInterval:10.0];
[request setHTTPMethod:@"POST"];
[request setAllHTTPHeaderFields:headers];
[request setHTTPBody:postData];

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
                                            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                                                if (error) {
                                                    NSLog(@"%@", error);
                                                } else {
                                                    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
                                                    NSLog(@"%@", httpResponse);
                                                }
                                            }];
[dataTask resume];
$curl = curl_init();

curl_setopt_array($curl, array(
  CURLOPT_URL => "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => "{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}",
  CURLOPT_HTTPHEADER => array(
    "authorization: Bearer ACCESS_TOKEN",
    "content-type: application/json"
  ),
));

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
  echo "cURL Error #:" . $err;
} else {
  echo $response;
}
import http.client

conn = http.client.HTTPSConnection("")

payload = "{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}"

headers = {
    'authorization': "Bearer ACCESS_TOKEN",
    'content-type': "application/json"
    }

conn.request("POST", "/YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities", payload, headers)

res = conn.getresponse()
data = res.read()

print(data.decode("utf-8"))
require 'uri'
require 'net/http'

url = URI("https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities")

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["authorization"] = 'Bearer ACCESS_TOKEN'
request["content-type"] = 'application/json'
request.body = "{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}"

response = http.request(request)
puts response.read_body
import Foundation

let headers = [
  "authorization": "Bearer ACCESS_TOKEN",
  "content-type": "application/json"
]
let parameters = ["link_with": "SECONDARY_ACCOUNT_ID_TOKEN"]

let postData = NSJSONSerialization.dataWithJSONObject(parameters, options: nil, error: nil)

var request = NSMutableURLRequest(URL: NSURL(string: "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities")!,
                                        cachePolicy: .UseProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.HTTPMethod = "POST"
request.allHTTPHeaderFields = headers
request.HTTPBody = postData

let session = NSURLSession.sharedSession()
let dataTask = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    println(error)
  } else {
    let httpResponse = response as? NSHTTPURLResponse
    println(httpResponse)
  }
})

dataTask.resume()

The following must apply in case you send the ID Token as part of the payload:

  • The secondary account's ID Token must be signed with RS256
  • The aud claim in the secondary account's ID Token must identify the client, and hold the same value with the azp claim of the Access Token used to make the request.

Note also that since the Access Token contains the update:users scope, it can be used to update the information of any user. Therefore this method is intended for use in server-side code only.

Use Auth0.js

Instead of calling directly the API, you can use the Auth0.js library.

First, you must get an Access Token that can be used to call the Management API. You can do it by specifying the https://YOUR_AUTH0_DOMAIN/api/v2/ audience when initializing Auth0.js. You will get the Access Token as part of the authentication flow. Alternatively, you can use the checkSession method.

Once you have the Access Token, you can create a new auth0.Management instance by passing it the account's Auth0 domain, and the Access Token.

For more information and sample scripts, see Auth0.js > User management.

Scenarios

In this section we will see some scenarios that implement account linking:

For security purposes, link accounts only if both e-mails are verified.

Automatic account linking

You can implement automatic linking by setting up a Rule that will run upon user login and link accounts with the same e-mail address.

The rule is an example of linking accounts in server-side code using the Auth0 Management API Link a user account endpoint where you have both the primary and secondary user IDs and an Management API Access Token with update:users scope.

Note, that if the primary account changes during the authorization transaction (for example, the account the user has logged in with, becomes a secondary account to some other primary account), you could get an error in the Authorization Code flow or an ID Token with the wrong sub claim in the token flow. To avoid this, set context.primaryUser = 'auth0|user123' in the rule after account linking. This will tell the authorization server to use the user with id auth0|user123 for the rest of the flow.

For a rule template on automatic account linking, see Link Accounts with Same Email Address. If you want to merge metadata as well, see Link Accounts with Same Email Address while Merging Metadata.

User-initiated account linking

Typically, account linking will be initiated by an authenticated user. Your app must provide the UI, such as a Link accounts button on the user's profile page.

Sample user profile page

You can follow the Account Linking Using Client Side Code tutorial or view the Auth0 jQuery Single Page App Account Linking Sample on Github for implementation details.

Suggested account linking

As with automatic linking, in this scenario you will set up a Rule that will link accounts with the same verified e-mail address. However, instead of completing the link automatically on authentication, your app will first prompt the user to link their identities.

Sample linking suggestion

You can follow the Account Linking Using Server Side Code tutorial or view the Auth0 Node.js Regular Web App Account Linking Sample on Github for implementation details.

Unlinking accounts

The Auth0 Management API V2 also provides an Unlink a user account endpoint which can be used with either of these two scopes:

  • update:current_user_identities: when you call the endpoint from client-side code where you have an Access Token with this scope
  • update:users: when you call the endpoint from server-side code where you have an Access Token with this scope

curl --request DELETE \
  --url 'https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID' \
  --header 'authorization: Bearer ACCESS_TOKEN'
var client = new RestClient("https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID");
var request = new RestRequest(Method.DELETE);
request.AddHeader("authorization", "Bearer ACCESS_TOKEN");
IRestResponse response = client.Execute(request);
package main

import (
	"fmt"
	"net/http"
	"io/ioutil"
)

func main() {

	url := "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID"

	req, _ := http.NewRequest("DELETE", url, nil)

	req.Header.Add("authorization", "Bearer ACCESS_TOKEN")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := ioutil.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.delete("https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID")
  .header("authorization", "Bearer ACCESS_TOKEN")
  .asString();
var settings = {
  "async": true,
  "crossDomain": true,
  "url": "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID",
  "method": "DELETE",
  "headers": {
    "authorization": "Bearer ACCESS_TOKEN"
  }
}

$.ajax(settings).done(function (response) {
  console.log(response);
});
var request = require("request");

var options = { method: 'DELETE',
  url: 'https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID',
  headers: { authorization: 'Bearer ACCESS_TOKEN' } };

request(options, function (error, response, body) {
  if (error) throw new Error(error);

  console.log(body);
});
#import <Foundation/Foundation.h>

NSDictionary *headers = @{ @"authorization": @"Bearer ACCESS_TOKEN" };

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID"]
                                                       cachePolicy:NSURLRequestUseProtocolCachePolicy
                                                   timeoutInterval:10.0];
[request setHTTPMethod:@"DELETE"];
[request setAllHTTPHeaderFields:headers];

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
                                            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                                                if (error) {
                                                    NSLog(@"%@", error);
                                                } else {
                                                    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
                                                    NSLog(@"%@", httpResponse);
                                                }
                                            }];
[dataTask resume];
$curl = curl_init();

curl_setopt_array($curl, array(
  CURLOPT_URL => "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "DELETE",
  CURLOPT_HTTPHEADER => array(
    "authorization: Bearer ACCESS_TOKEN"
  ),
));

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
  echo "cURL Error #:" . $err;
} else {
  echo $response;
}
import http.client

conn = http.client.HTTPSConnection("")

headers = { 'authorization': "Bearer ACCESS_TOKEN" }

conn.request("DELETE", "/YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID", headers=headers)

res = conn.getresponse()
data = res.read()

print(data.decode("utf-8"))
require 'uri'
require 'net/http'

url = URI("https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE

request = Net::HTTP::Delete.new(url)
request["authorization"] = 'Bearer ACCESS_TOKEN'

response = http.request(request)
puts response.read_body
import Foundation

let headers = ["authorization": "Bearer ACCESS_TOKEN"]

var request = NSMutableURLRequest(URL: NSURL(string: "https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID")!,
                                        cachePolicy: .UseProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.HTTPMethod = "DELETE"
request.allHTTPHeaderFields = headers

let session = NSURLSession.sharedSession()
let dataTask = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    println(error)
  } else {
    let httpResponse = response as? NSHTTPURLResponse
    println(httpResponse)
  }
})

dataTask.resume()

The result of the unlinking process is the following:

  • The secondary account is removed from the identities array of the primary account
  • A new secondary user account is created
  • The secondary account will have no metadata

If your goal is to delete the secondary identity entirely, you must first unlink the accounts, and then delete the newly created secondary account.