Migration Guide: Management API and ID Tokens

For some use cases you could use ID Tokens in order to call some of the Users and Device Credentials endpoints of the Management API.

This functionality is being deprecated. You will have to use proper Access Tokens in order to access any of the endpoints of the Management API. Make sure the Allow ID Tokens for Management API v2 Authentication toggle is turned off after completing the migration to Access Tokens.

The grace period for this migration started on March 31, 2018 and at the moment is open-ended. This means that you will still be able to use ID Tokens to access these endpoints. When a mandatory opt-in date is set for this migration customers will be notified beforehand.

Customers are encouraged to migrate to Access Tokens. This guide will help you with that.

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 then see all the ways you can use to get an Access Token. Finally, we will review the changes introduced in the Account Linking process.

Does this affect me?

If you use ID Tokens to call any of the following endpoints, then you are affected by this migration:

Endpoint Use Case
GET /api/v2/users/{id} Retrieve a user's information
GET /api/v2/users/{id}/enrollments Retrieve all Guardian MFA enrollments for a user
PATCH /api/v2/users/{id} Update a user's information
DELETE /api/v2/users/{id}/multifactor/{provider} Delete the multifactor provider settings for a user
POST /api/v2/device-credentials Create a public key for a device
DELETE /api/v2/device-credentials/{id} Delete a device credential
POST/api/v2/users/{id}/identities Link user accounts from various identity providers
DELETE /api/v2/users/{id}/identities/{provider}/{user_id} Unlink user accounts

Note that the last two endpoints are used for Account Linking. To review these changes, see Changes in Account Linking.

Nothing else changes in how the endpoints work. You should expect the same request and response schemas and only need update the token that you use for authorization.

Changes in scopes

The actions you can perform with the Management API depend on the scopes that your Access Token contains. With this migration you can either get a "limited" Access Token that can update only the logged-in user's data, or an Access Token that can update the data of any user. In the following matrix you can see the scopes that your token needs to have per case and per endpoint.

Endpoint Scope for current user Scope for any user
GET /api/v2/users/{id} read:current_user read:users
GET /api/v2/users/{id}/enrollments read:current_user read:users
POST/api/v2/users/{id}/identities update:current_user_identities update:users
DELETE /api/v2/users/{id}/identities/{provider}/{user_id} update:current_user_identities update:users
PATCH /api/v2/users/{id} update:current_user_metadata update:users
PATCH /api/v2/users/{id} create:current_user_metadata update:users
DELETE /api/v2/users/{id}/multifactor/{provider} delete:current_user_metadata update:users
POST /api/v2/device-credentials create:current_user_device_credentials create:device_credentials
DELETE /api/v2/device-credentials/{id} delete:current_user_device_credentials delete:device_credentials

For example, if I get an Access Token that contains the scope read:users I can retrieve the data of any user using the GET /api/v2/users/{id} endpoint. But if my token contains the scope read:current_user I can only retrieve the information of the currently logged-in user (the one that the token was issued for).

Restrictions

The Access Tokens used to access the Management API must hold only one value at the aud claim. If your token contains more than one value, then your request to the Management API will error out.

How to get an Access Token

In this section we will see the changes that are introduced in how you get a token for the aforementioned endpoints. We will see sample scripts side-by-side so you can identify the changes.

There are several variations on how you authenticate a user and get tokens, depending on the technology and the OAuth 2.0 flow you use to authenticate:

The Authorization endpoint

In this section we will use an example to showcase the differences in how you get a token with the Authorization endpoint. Keep in mind though that no matter which endpoint you want to migrate, the changes are the same, the only thing that differs is the scopes you will specify in the request.

In the example below, we want to use the GET User by ID endpoint to retrieve the full profile information of the logged-in user. To do so, first we will authenticate our user (using the Implicit grant) and retrieve the token(s).

On the Legacy (ID Token) panel you can see an implementation of the old approach that gets an ID Token (and then uses it to call the endpoint). On the Current (Access Token) panel you can see the new approach that gets an Access Token as well.


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=read:current_user
  &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 read:current_user
  • 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": "read:current_user"
}

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

Once you have the Access Token you can use it to call the endpoint. 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 GET \
  --url 'https://YOUR_AUTH0_DOMAIN/api/v2/users/USER_ID' \
  --header 'authorization: Bearer YOUR_MGMT_API_ACCESS_TOKEN'
var client = new RestClient("https://YOUR_AUTH0_DOMAIN/api/v2/users/USER_ID");
var request = new RestRequest(Method.GET);
request.AddHeader("authorization", "Bearer YOUR_MGMT_API_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/USER_ID"

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

	req.Header.Add("authorization", "Bearer YOUR_MGMT_API_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.get("https://YOUR_AUTH0_DOMAIN/api/v2/users/USER_ID")
  .header("authorization", "Bearer YOUR_MGMT_API_ACCESS_TOKEN")
  .asString();
var settings = {
  "async": true,
  "crossDomain": true,
  "url": "https://YOUR_AUTH0_DOMAIN/api/v2/users/USER_ID",
  "method": "GET",
  "headers": {
    "authorization": "Bearer YOUR_MGMT_API_ACCESS_TOKEN"
  }
}

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

var options = { method: 'GET',
  url: 'https://YOUR_AUTH0_DOMAIN/api/v2/users/USER_ID',
  headers: { authorization: 'Bearer YOUR_MGMT_API_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 YOUR_MGMT_API_ACCESS_TOKEN" };

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://YOUR_AUTH0_DOMAIN/api/v2/users/USER_ID"]
                                                       cachePolicy:NSURLRequestUseProtocolCachePolicy
                                                   timeoutInterval:10.0];
[request setHTTPMethod:@"GET"];
[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/USER_ID",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "GET",
  CURLOPT_HTTPHEADER => array(
    "authorization: Bearer YOUR_MGMT_API_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 YOUR_MGMT_API_ACCESS_TOKEN" }

conn.request("GET", "/YOUR_AUTH0_DOMAIN/api/v2/users/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/USER_ID")

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

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

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

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

var request = NSMutableURLRequest(URL: NSURL(string: "https://YOUR_AUTH0_DOMAIN/api/v2/users/USER_ID")!,
                                        cachePolicy: .UseProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.HTTPMethod = "GET"
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 Token endpoint

In this section we will use an example to showcase the differences in how you get a token with the Token endpoint. Keep in mind though that no matter which endpoint you want to migrate, the changes are the same, the only thing that differs is the scopes you will specify in the request.

In the example below, we want to use the GET User by ID endpoint to retrieve the full profile information of the logged-in user. To do so, first we will authenticate our user (using the Password Exchange grant) and retrieve the token(s).

On the Legacy (ID Token) script you can see an implementation of the old approach that gets an ID Token (and then uses it to call the endpoint). On the Current (Access Token) script you can see the new approach that gets an Access Token as well.


POST https://YOUR_AUTH0_DOMAIN/oauth/token
Content-Type: application/json
{
  "grant_type": "password",
  "username": "USERNAME",
  "password": "PASSWORD",
  "scope": "openid",
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET",
}
        

POST https://YOUR_AUTH0_DOMAIN/oauth/token
Content-Type: application/json
{
  "grant_type": "password",
  "username": "USERNAME",
  "password": "PASSWORD",
  "audience": "https://YOUR_AUTH0_DOMAIN/api/v2/",
  "scope": "read:current_user",
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET",
}
        

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 read:current_user

Once you have the Access Token you can use it to call the endpoint. 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 GET \
  --url 'https://YOUR_AUTH0_DOMAIN/api/v2/users/USER_ID' \
  --header 'authorization: Bearer YOUR_MGMT_API_ACCESS_TOKEN'
var client = new RestClient("https://YOUR_AUTH0_DOMAIN/api/v2/users/USER_ID");
var request = new RestRequest(Method.GET);
request.AddHeader("authorization", "Bearer YOUR_MGMT_API_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/USER_ID"

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

	req.Header.Add("authorization", "Bearer YOUR_MGMT_API_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.get("https://YOUR_AUTH0_DOMAIN/api/v2/users/USER_ID")
  .header("authorization", "Bearer YOUR_MGMT_API_ACCESS_TOKEN")
  .asString();
var settings = {
  "async": true,
  "crossDomain": true,
  "url": "https://YOUR_AUTH0_DOMAIN/api/v2/users/USER_ID",
  "method": "GET",
  "headers": {
    "authorization": "Bearer YOUR_MGMT_API_ACCESS_TOKEN"
  }
}

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

var options = { method: 'GET',
  url: 'https://YOUR_AUTH0_DOMAIN/api/v2/users/USER_ID',
  headers: { authorization: 'Bearer YOUR_MGMT_API_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 YOUR_MGMT_API_ACCESS_TOKEN" };

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://YOUR_AUTH0_DOMAIN/api/v2/users/USER_ID"]
                                                       cachePolicy:NSURLRequestUseProtocolCachePolicy
                                                   timeoutInterval:10.0];
[request setHTTPMethod:@"GET"];
[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/USER_ID",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "GET",
  CURLOPT_HTTPHEADER => array(
    "authorization: Bearer YOUR_MGMT_API_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 YOUR_MGMT_API_ACCESS_TOKEN" }

conn.request("GET", "/YOUR_AUTH0_DOMAIN/api/v2/users/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/USER_ID")

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

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

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

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

var request = NSMutableURLRequest(URL: NSURL(string: "https://YOUR_AUTH0_DOMAIN/api/v2/users/USER_ID")!,
                                        cachePolicy: .UseProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.HTTPMethod = "GET"
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()

Lock or auth0.js (embedded)

If you embed either Lock v11 or auth0.js v9 in your application, then you are using cross-origin authentication. This is used to authenticate users when the requests come from different domains.

If you use auth0.js to access the Management API and manage your users, then your script will have to be updated.


// 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: 'read:current_user',
  responseType: 'token id_token'
});
// create a new instance
var auth0Manage = new auth0.Management({
  domain: 'YOUR_AUTH0_DOMAIN',
  token: 'ACCESS_TOKEN'
});
        

Changes in Account Linking

The changes in this functionality are the following:

  • You can no longer use an ID Token at the Authorization header
  • 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. The following must apply:
    • The ID Token must be signed using RS256 (you can set this value at Dashboard > Applications > Application Settings > Advanced Settings > OAuth)
    • The claim aud of the ID Token, must identify the application, and be the same value with the azp claim of the Access Token

For a detailed overview of these changes and migration steps per use case, see Migration Guide: Account Linking and ID Tokens.

Keep reading