Docs

User Consent and Third-Party Applications

User Consent and Third-Party Applications

The OIDC-conformant authentication pipeline supports defining resource servers (such as APIs) as entities separate from applications. This lets you decouple APIs from the applications that consume them, and also lets you define third-party applications that you might not control or even fully trust.

All applications created from the Dashboard are assumed to be first-party by default.

Third-party applications cannot be created from the Dashboard. They must be created through the Management API, by setting is_first_party: false.

All applications created through Dynamic Client Registration will be third-party.

If a user is authenticating through a third-party application and is requesting authorization to access the user's information or perform some action at an API on their behalf, they will see a consent dialog.

For example:

GET /authorize?
client_id=some_third_party_client
&redirect_uri=https://fabrikam.com/contoso_social
&response_type=token id_token
&scope=openid profile email read:posts write:posts
&audience=https://social.contoso.com
&nonce=...
&state=...
Auth0 consent dialog - Fabrikam Application for Contoso is requesting access to your account

If the user allows the application, this creates a user grant which represents the user's consent to this combination of application, resource server, and scopes.

The application then receives a successful authentication response from Auth0 as usual. Once consent has been given, the user won't see the consent dialog during subsequent logins until consent is revoked explicitly.

Scope descriptions

By default, the consent page will use the scopes' names to prompt for the user's consent. As shown below, you should define scopes using the action:resource_name format.

API Scopes

The consent page groups scopes for the same resource and displays all actions for that resource in a single line. For example, the configuration above would result in Posts: read and write your posts.

If you would like to display the Description field instead, you can do so by setting the tenant's use_scope_descriptions_for_consent to true. This will affect consent prompts for all of the APIs on that tenant.

To set the use_scope_descriptions_for_consent flag, you will need to make the appropriate call to the API:


curl --request PATCH \
  --url 'https://YOUR_DOMAIN/api/v2/tenants/settings' \
  --header 'authorization: Bearer API2_ACCESS_TOKEN' \
  --header 'cache-control: no-cache' \
  --header 'content-type: application/json' \
  --data '{ "flags": { "use_scope_descriptions_for_consent": true } }'
var client = new RestClient("https://YOUR_DOMAIN/api/v2/tenants/settings");
var request = new RestRequest(Method.PATCH);
request.AddHeader("content-type", "application/json");
request.AddHeader("authorization", "Bearer API2_ACCESS_TOKEN");
request.AddHeader("cache-control", "no-cache");
request.AddParameter("application/json", "{ \"flags\": { \"use_scope_descriptions_for_consent\": true } }", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
package main

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

func main() {

	url := "https://YOUR_DOMAIN/api/v2/tenants/settings"

	payload := strings.NewReader("{ \"flags\": { \"use_scope_descriptions_for_consent\": true } }")

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

	req.Header.Add("content-type", "application/json")
	req.Header.Add("authorization", "Bearer API2_ACCESS_TOKEN")
	req.Header.Add("cache-control", "no-cache")

	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.patch("https://YOUR_DOMAIN/api/v2/tenants/settings")
  .header("content-type", "application/json")
  .header("authorization", "Bearer API2_ACCESS_TOKEN")
  .header("cache-control", "no-cache")
  .body("{ \"flags\": { \"use_scope_descriptions_for_consent\": true } }")
  .asString();
var request = require("request");

var options = {
  method: 'PATCH',
  url: 'https://YOUR_DOMAIN/api/v2/tenants/settings',
  headers: {
    'content-type': 'application/json',
    authorization: 'Bearer API2_ACCESS_TOKEN',
    'cache-control': 'no-cache'
  },
  body: {flags: {use_scope_descriptions_for_consent: true}},
  json: true
};

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

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

NSDictionary *headers = @{ @"content-type": @"application/json",
                           @"authorization": @"Bearer API2_ACCESS_TOKEN",
                           @"cache-control": @"no-cache" };
NSDictionary *parameters = @{ @"flags": @{ @"use_scope_descriptions_for_consent": @YES } };

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

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://YOUR_DOMAIN/api/v2/tenants/settings"]
                                                       cachePolicy:NSURLRequestUseProtocolCachePolicy
                                                   timeoutInterval:10.0];
[request setHTTPMethod:@"PATCH"];
[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_DOMAIN/api/v2/tenants/settings",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "PATCH",
  CURLOPT_POSTFIELDS => "{ \"flags\": { \"use_scope_descriptions_for_consent\": true } }",
  CURLOPT_HTTPHEADER => array(
    "authorization: Bearer API2_ACCESS_TOKEN",
    "cache-control: no-cache",
    "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 = "{ \"flags\": { \"use_scope_descriptions_for_consent\": true } }"

headers = {
    'content-type': "application/json",
    'authorization': "Bearer API2_ACCESS_TOKEN",
    'cache-control': "no-cache"
    }

conn.request("PATCH", "/YOUR_DOMAIN/api/v2/tenants/settings", 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/tenants/settings")

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

request = Net::HTTP::Patch.new(url)
request["content-type"] = 'application/json'
request["authorization"] = 'Bearer API2_ACCESS_TOKEN'
request["cache-control"] = 'no-cache'
request.body = "{ \"flags\": { \"use_scope_descriptions_for_consent\": true } }"

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

let headers = [
  "content-type": "application/json",
  "authorization": "Bearer API2_ACCESS_TOKEN",
  "cache-control": "no-cache"
]
let parameters = ["flags": ["use_scope_descriptions_for_consent": true]] as [String : Any]

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

let request = NSMutableURLRequest(url: NSURL(string: "https://YOUR_DOMAIN/api/v2/tenants/settings")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "PATCH"
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()

Handle rejected permissions

If a user decides to reject consent to the application, they will be redirected to the redirect_uri specified in the request with an access_denied error:

HTTP/1.1 302 Found
Location: https://fabrikam.com/contoso_social#
    error=access_denied
    &state=...

Only first-party applications can skip the consent dialog, assuming the resource server they are trying to access on behalf of the user has the "Allow Skipping User Consent" option enabled.

Consent can't be skipped on localhost

Note that this option only allows verifiable first-party applications to skip consent at the moment. As localhost is never a verifiable first-party (because any malicious application may run on localhost for a user), Auth0 will always display the consent dialog for applications running on localhost regardless of whether they are marked as first-party applications. During development, you can work around this by modifying your /etc/hosts file to add an entry such as the following:

127.0.0.1       myapp.example

Similarly, you cannot skip consent (even for first-party applications) if localhost appears in any domain in the Allowed Callback URLs setting (found in Dashboard > Applications > Settings). Make sure to update Allowed Callback URLs, and the callback URL you configured in your application, to match the updated domain-mapping.

Since third-party applications are assumed to be untrusted, they are not able to skip consent dialogs.

If a user has provided consent, but you would like to revoke it, you can do so via Dashboard > Users. Select the user in which you are interested, and switch over to the Authorized Applications tab.

Click Revoke next to the appropriate application.

Password-based flows

When performing a Resource Owner Password Credentials exchange, there is no consent dialog involved. During a password exchange, the user provides their password to the application directly, which is equivalent to granting the application full access to the user's account.

When redirecting to /authorize, the prompt=consent parameter will force users to provide consent, even if they have an existing user grant for that application and requested scopes.

The consent dialog UI cannot be customized or set to a custom domain.

Keep reading