Migration Guide: Account Linking and ID Tokens

This guide is part of the Deprecating the usage of ID Tokens on the Auth0 Management API migration, and focuses on the account linking process.

For some use cases you could use ID Tokens to link and unlink user accounts. This functionality is being deprecated. You will have to use Access Tokens in all cases.

The functionality is available and affected users are encouraged to migrate. However the ability to use ID Tokens will not be disabled in the foreseeable future so the mandatory opt-in date for this migration remains open. When this changes, customers will be notified beforehand.

This article will help you migrate your implementation. First, we will see which use cases are affected. We will continue with reviewing how you can use scopes to get tokens with different access rights, and how you can use them in the account linking process.

Summary of changes

The changes in account linking are:

  • You can no longer use an ID Token at the Authorization header, an Access Token must be used instead
  • If you use an Access Token at the Authorization header, with update:users as the granted permission, then you can send at the request's body either the user_id or the ID Token of the secondary account.
  • If you use an Access Token at the Authorization header, with update:current_user_metadata as the granted permission, then you can only send the ID Token of the secondary account in the request's body.
  • If you send the ID Token of the secondary account in the request's body (that is, the use cases described in the previous two bullets), then the following must apply:
    • The ID Token must be signed using RS256 (you can set this value at Dashboard > Clients > Client Settings > Advanced Settings > OAuth)
    • The claim aud of the ID Token, must identify the client, and be the same value with the azp claim of the Access Token

The change in the unlinking of accounts is that you can no longer use an ID Token at the Authorization header. An Access Token must be used instead.

Does this affect me?

There are several ways you can link and unlink accounts. Some change, some remain the same, and a new variation is introduced. In the following matrix you can see a list of the use cases and their status based on this migration.

Use case Status More info
You link user accounts with the Link a user account endpoint of the Management API, and you send the primary account's ID Token in the Authorization header.
Affected
Link current user accounts with the API
You link user accounts with the Link a user account endpoint of the Management API, you send an Access Token (with scope update:users) in the Authorization header, and the secondary account's user_id in the payload
No change
N/A
You link user accounts with the Link a user account endpoint of the Management API, you send an Access Token (with scope update:current_user_identities) in the Authorization header, and the secondary account's user_id in the payload
Affected
Link current user accounts with the API
You link user accounts with the Link a user account endpoint of the Management API, you send an Access Token in the Authorization header, and the secondary account's ID Token in the payload
New use case
Link any user account with the API
You link user accounts with the Auth0.js library, and you use the primary account's ID Token to instantiate auth0.Management
Affected
Link current user accounts with Auth0.js
You link user accounts with the Auth0.js library, and you use an Access Token (with scope update:users) to instantiate auth0.Management
No change
N/A
You link user accounts with the Auth0.js library, and you use an Access Token (with scope update:current_user_identities) to instantiate auth0.Management
Affected
Link current user accounts with Auth0.js
You unlink user accounts with the Unlink a user identity endpoint of the Management API, and you send the primary account's ID Token in the Authorization header
Affected
How to unlink accounts
You unlink user accounts with the Unlink a user identity endpoint of the Management API, and you send an Access Token in the Authorization header
No change
N/A

In order to link accounts you can either call directly the Link a user account endpoint of the Management API, or use the Auth0.js library.

A common use case is to allow the logged-in user to link their various accounts using your app.

Until now you could use the primary user's ID Token or Access Token (which contained the update:current_user_identities scope) in order to authenticate with the Management API and use the Link a user account endpoint.

With this migration, you must get an Access Token (which must contain the update:current_user_identities scope) and use that to authenticate with the API and use the Link a user account endpoint. The payload must be the ID Token of the secondary user.

First, you must get an Access Token with the update:current_user_identities scope.

In the example that follows, we use the Implicit Grant, the recommended OAuth 2.0 flow for client-side apps). You can get Access Tokens though for any application type (see How to get an Access Token).


https://YOUR_AUTH0_DOMAIN/authorize?
  scope=openid
  &response_type=id_token
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://YOUR_APP/callback
  &nonce=CRYPTOGRAPHIC_NONCE
  &state=OPAQUE_VALUE
        

https://YOUR_AUTH0_DOMAIN/authorize?
  audience=https://YOUR_AUTH0_DOMAIN/api/v2/
  &scope=update:current_user_identities
  &response_type=token%20id_token
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://YOUR_APP/callback
  &nonce=CRYPTOGRAPHIC_NONCE
  &state=OPAQUE_VALUE
        

In order to get an Access Token that can access the Management API:

  • We set the audience to https://YOUR_AUTH0_DOMAIN/api/v2/
  • We asked for the scope update:current_user_identities
  • We set the response_type to id_token token so Auth0 will sent us both an ID Token and an Access Token

If we decode the Access Token and review its contents we can see the following:

{
  "iss": "https://YOUR_AUTH0_DOMAIN/",
  "sub": "auth0|5a620d29a840170a9ef43672",
  "aud": "https://YOUR_AUTH0_DOMAIN/api/v2/",
  "iat": 1521031317,
  "exp": 1521038517,
  "azp": "YOUR_CLIENT_ID",
  "scope": "update:current_user_identities"
}

Notice that the aud is set to your tenant's API URI, the scope to update:current_user_identities, and the sub to the user ID of the logged in user.

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.

Once you have the Access Token you can use it to link user accounts. This part remains the same, nothing else changes in the request except for the value you use as Bearer token. The response remains also the same.


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()

If you use Auth0.js library to access the Management API and link accounts, then you probably use the ID Token of the user's primary identity to instantiate auth0.Management and use it to link accounts.

With this migration, you must get an Access Token (with the update:current_user_identities scope) and use that to instantiate the object.

First, you must get an Access Token with the update:current_user_identities scope. Then use this token to instantiate auth0.Management. The final call to linkUser remain the same.


// get an ID token
var webAuth = new auth0.WebAuth({
  clientID: 'YOUR_CLIENT_ID',
  domain: 'YOUR_AUTH0_DOMAIN',
  redirectUri: 'https://YOUR_APP/callback',
  scope: 'openid',
  responseType: 'id_token'
});
// create a new instance
var auth0Manage = new auth0.Management({
  domain: 'YOUR_AUTH0_DOMAIN',
  token: 'ID_TOKEN'
});
        

// get an access token
var webAuth = new auth0.WebAuth({
  clientID: 'YOUR_CLIENT_ID',
  domain: 'YOUR_AUTH0_DOMAIN',
  redirectUri: 'https://YOUR_APP/callback',
  audience: 'https://YOUR_AUTH0_DOMAIN/api/v2/˜',
  scope: 'update:current_user_identities',
  responseType: 'token id_token'
});
// create a new instance
var auth0Manage = new auth0.Management({
  domain: 'YOUR_AUTH0_DOMAIN',
  token: 'ACCESS_TOKEN'
});
        

If you get an Access Token for account linking, that contains the update:users scope, and send the secondary account's user_id and provider in the request, then you don't have to make any changes.

However, this migration does introduce an alternative to this. You still use an Access Token that contains the update:users scope to authenticate with the API, but in the request's payload you can send the secondary's account ID Token (instead of user_id and provider).


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:

  • 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.

If you use ID Tokens in order to unlink accounts, then you must update your implementation to use Access Tokens.

First, you must get an Access Token with the update:current_user_identities scope.

Use the sample script that follows as a guide. On the Legacy (ID Token) panel you can see an implementation of the old approach that gets an ID Token. On the Current (Access Token) panel you can see the new approach that gets an Access Token as well.

In order to get an Access Token that can access the Management API:

  • We set the audience to https://YOUR_AUTH0_DOMAIN/api/v2/
  • We asked for the scope update:current_user_identities
  • We set the response_type to id_token token so Auth0 will sent us both an ID Token and an Access Token

If we decode the Access Token and review its contents we can see the following:

{
  "iss": "https://YOUR_AUTH0_DOMAIN/",
  "sub": "auth0|5a620d29a840170a9ef43672",
  "aud": "https://YOUR_AUTH0_DOMAIN/api/v2/",
  "iat": 1521031317,
  "exp": 1521038517,
  "azp": "YOUR_CLIENT_ID",
  "scope": "update:current_user_identities"
}

Notice that the aud is set to your tenant's API URI, the scope to update:current_user_identities, and the sub to the user ID of the logged in user.

Once you have the Access Token, you can call the Unlink a user identity endpoint of the Management API, using it in the Authorization header.


DELETE https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID
Authorization: 'Bearer ID_TOKEN-OR-ACCESS_TOKEN'
        

DELETE https://YOUR_AUTH0_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID
Authorization: 'Bearer ACCESS_TOKEN'
        

Keep reading