Migrate to Access Tokens for Account Linking

Previously you could use ID tokens to link and unlink user accounts for some use cases. Auth0 is deprecating this functionality. You will now need to use access tokens in all cases.

This deprecation is in response to a potential security vulnerability. Auth0 strongly recommends that you update your code as soon as possible.

Features affected

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 as 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 (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.

  • For unlinking accounts, you can no longer use an ID token at the Authorization header. You must use an access token instead.

There are several ways you can link and unlink accounts. In the following list you can see the use cases and how the changes affect them.

Use Case Status
Use the Management API POST /api/v2/users/{id}/identies endpoint and send the primary account's ID token in the Authorization header. Affected
Use the Management API POST /api/v2/users/{id}/identies endpoint and send an access token (with scope update:users) in the authorization header, and the secondary account's user_id in the payload. Not affected
Use the Management API POST /api/v2/users/{id}/identies endpoint and 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
Use the Management API POST /api/v2/users/{id}/identies endpoint and send an access token in the Authorization header and the secondary account's ID token in the payload. New use case
Use the auth0.js library and the primary account's ID token to instantiate auth0.Management. Affected
Use the auth0.js library and an access token (with scope update:users) to instantiate auth0.Management. Not affected
Use the auth0.js library and an access token (with scope update:current_user_identites) to instantiate auth0.Management. Affected
Use the Management API DELETE /api/v2/users/{id}/identities/{provider}/{user_id} endpoint and send the primary account's ID token in the Authorization header. Affected
Use the Management API DELETE /api/v2/users/{id}/identities/{provider}/{user_id} endpoint and send an access token in the Authorization header. Not affected

Actions

Review all your calls to the account linking Identities endpoint and update those that make use of the vulnerable flow described above. You can update your calls to either of the following:

  • Client-side / user-initiated linking scenarios: For client-side linking scenarios, make the call to the Identities endpoint using an access token with the update:current_user_identities scope, and provide the ID token of the secondary account in the payload (link_with). This ID token must be obtained through an OAuth/OIDC-conformant flow.

  • Server-side linking scenarios: For server-side linking scenarios, make the call to Identities endpoint using an access token with the update:users scope and provide the user_id of the secondary account in the payload.

See Link User Accounts for details.

To link user accounts you can either call 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 accounts using your app.

Prior to the deprecation you could use the primary user's ID token or access token (which contained the update:current_user_identities scope) to authenticate with the Management API and use the Link a User Account endpoint.

Now you must get an access token (containing 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.

  1. Get an access token with the update:current_user_identities scope as shown in the following example. (The example uses the implicit flow which is the recommended OAuth 2.0 flow for client-side apps, however, you can get access tokens for any application type.)

  2. Using the previous method using an ID token, your code would look similar to this:

    to configure this snippet with your account

      https://YOUR_DOMAIN/authorize?
          scope=openid
          &response_type=id_token
          &client_id=YOUR_CLIENT_ID
          &redirect_uri=https://YOUR_APP/callback
          &nonce=NONCE
          &state=OPAQUE_VALUE
    
    
    Using the new method using an access token, your code will look similar to this:

    to configure this snippet with your account

      https://YOUR_DOMAIN/authorize?
          audience=https://YOUR_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=NONCE
          &state=OPAQUE_VALUE
    
    

  3. To get an access token that can access the Management API:

    1. Set the audience to https://YOUR_DOMAIN/api/v2/.

    2. Ask for the scope ${scope}.

    3. Set the response_type to id_token token so Auth0 will send 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_DOMAIN/",
            "sub": "auth0|5a620d29a840170a9ef43672",
            "aud": "https://YOUR_DOMAIN/api/v2/",
            "iat": 1521031317,
            "exp": 1521038517,
            "azp": "YOUR_CLIENT_ID",
            "scope": "${scope}"
          }
      
      
      Notice that the aud is set to your tenant's API URI, the scope to ${scope}, and the sub to the user ID of the logged-in user.

  4. The following must apply:

    1. The secondary account's ID Token must be signed with RS256.

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

  5. 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 also remains the same.

        {
          "method": "POST",
          "url": "https://YOUR_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities",
          "httpVersion": "HTTP/1.1",
          "headers": [{
          "name": "Authorization",
          "value": "Bearer ACCESS_TOKEN"
          },
          {
          "name": "content-type",
          "value": "application/json"
          }],
          "postData" : {
          "mimeType": "application/json",
          "text": "{\"link_with\":\"SECONDARY_ACCOUNT_ID_TOKEN\"}"
          }
        }
    
    

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.

  1. Get an access token with the update:current_user_identities scope, then use this token to instantiate auth0.Management. The final call to linkUser remains the same.

  2. Using the previous method using an ID token, your code would look similar to this:

    to configure this snippet with your account

        // get an ID Token
        var webAuth = new auth0.WebAuth({
          clientID: 'YOUR_CLIENT_ID',
          domain: 'YOUR_DOMAIN',
          redirectUri: 'https://YOUR_APP/callback',
          scope: 'openid',
          responseType: 'id_token'
        });
        // create a new instance
        var auth0Manage = new auth0.Management({
          domain: 'YOUR_DOMAIN',
          token: 'ID_TOKEN'
        });
    
    
    Using the new method using an access token, your code will look similar to this:

    to configure this snippet with your account

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

    1. Asks for both an Id token and an access token in response (responseType: `token id_token`).

    2. Sets the Management API as the intended audience of the token (audience: `https://YOUR_DOMAIN/api/v2/`).

    3. Asks for the required permission (scope: `update:current_user_identities`).

    4. Authenticates with the Management API using the 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 new method introduces 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_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_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities");
var request = new RestRequest(Method.POST);
request.AddHeader("authorization", "Bearer ACCESS_TOKEN");
request.AddHeader("content-type", "application/json");
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_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_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 axios = require("axios").default;

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

axios.request(options).then(function (response) {
  console.log(response.data);
}).catch(function (error) {
  console.error(error);
});
#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_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, [
  CURLOPT_URL => "https://YOUR_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 => [
    "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_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'
require 'openssl'

url = URI("https://YOUR_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"] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://YOUR_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(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 to unlink accounts then you must update your code to use access tokens.

  1. First, you must get an access token with the update:current_user_identities scope.

  2. Using the previous method using an ID token, your code would look similar to this:

    to configure this snippet with your account

        https://YOUR_DOMAIN/authorize?
          scope=openid
          &response_type=id_token
          &client_id=YOUR_CLIENT_ID
          &redirect_uri=https://YOUR_APP/callback
          &nonce=NONCE
          &state=OPAQUE_VALUE
    
    
    Using the new method using an access token, your code will look similar to this:

    to configure this snippet with your account

        https://YOUR_DOMAIN/authorize?
          audience=https://YOUR_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=NONCE
          &state=OPAQUE_VALUE
    
    

  3. To get an access token that can access the Management API:

    1. Set the audience to https://YOUR_DOMAIN/api/v2/.

    2. Ask for the scope ${scope}.

    3. Set the response_type to id_token token so Auth0 will send both an ID token and an access token. If we decode the access token and review its contents we can see the following:

      to configure this snippet with your account

          {
            "iss": "https://YOUR_DOMAIN/",
            "sub": "auth0|5a620d29a840170a9ef43672",
            "aud": "https://YOUR_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.

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

  5. Using the previous method, your call would look similar to this:

      DELETE https://YOUR_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID
        Authorization: 'Bearer ID_TOKEN-OR-ACCESS_TOKEN'
    
    
    Using the new method, you call will look similar to this:
      DELETE https://YOUR_DOMAIN/api/v2/users/PRIMARY_ACCOUNT_USER_ID/identities/SECONDARY_ACCOUNT_PROVIDER/SECONDARY_ACCOUNT_USER_ID
        Authorization: 'Bearer ACCESS_TOKEN'
    
    

Security considerations

We have identified a weakness in a particular account linking flow that could allow it to be misused in specific circumstances. We have found no evidence that this has been used maliciously but have decided to deprecate the flow to prevent that ever happening.

Therefore, Auth0 requires customers using the affected account linking flow to migrate to a more secure implementation before 19 October 2018. Migration paths are provided in this guide, which should not result in any lost functionality.

On or after 19 October 2018 the affected account linking flow will be disabled and you will experience run-time errors.

You are impacted if you call the Post Identities endpoint using a token (ID or access token) with the scope update:current_user_identities in the Authorization header and include the secondary account's user_id in the payload. No other use cases are impacted.

Learn more