GDPR: Track consent with your custom UI

In this tutorial we will see how you can use auth0.js or the Auth0 APIs to ask for consent information and save the input at the user's metadata.

The contents of this document are not intended to be legal advice, nor should they be considered a substitute for legal assistance. The final responsibility for understanding and complying with GDPR resides with you, though Auth0 will assist you in meeting GDPR requirements where possible.

Overview

We will capture consent information, under various scenarios, and save this at the user's metadata.

All scenarios will save the following properties at the user's metadata:

  • a consentGiven property, with true/false values, shows if the user has provided consent (true) or not (false)
  • a consentTimestamp property, holding the Unix timestamp of when the user provided consent

For example:

{
  "consentGiven": "true"
  "consentTimestamp": "1525101183"
}

We will see four different implementations for this:

  1. one that displays a flag, works for database connections, and uses the auth0.js library to create the user (used by Single Page Applications)
  2. one that displays a flag, works for database connections, and uses the Authentication API to create the user (used by Regular Web Apps)
  3. one that displays a flag, works for social connections, and uses the Management API to update the user's information (used either by SPAs or Regular Web Apps)
  4. one that redirects to another page where the Terms & Conditions and/or privacy policy information can be reviewed and consent info can be provided (used either by SPAs or Regular Web Apps)

Option 1: Use auth0.js

In this section, we will use a simple Single Page Application and customize the login widget to add a flag which users can use to provide consent information. Instead of building an app from scratch, we will use Auth0's JavaScript Quickstart sample. We will also use Auth0's Universal Login Page so we can implement a Universal Login experience, instead of embedding the login in our app.

This works only for database connections (we will use Auth0's infrastructure, instead of setting up our own database).

  1. Go to Dashboard > Applications and create a new application. Choose Single Web Page Applications as type. Go to Settings and set the Allowed Callback URLs to http://localhost:3000.

    This field holds the set of URLs to which Auth0 is allowed to redirect the users after they authenticate. Our sample app will run at http://localhost:3000 hence we set this value.

  2. Copy the Client Id and Domain values. You will need them in a while.

  3. Go to Dashboard > Connections > Database and create a new connection. Click Create DB Connection, set a name for the new connection, and click Save. Go to the connection's Applications tab and make sure your newly created application is enabled.

  4. Download the JavaScript SPA Sample.

  5. Set the Client ID and Domain values.

  6. Go to Dashboard > Hosted Pages. At the Login tab enable the toggle.

  7. At the Default Templates dropdown make sure that Custom Login Form is picked. The code is prepopulated for you.

  8. Set the value of the databaseConnection variable to the name of the database connection your app is using.

    //code reducted for simplicity
    var databaseConnection = 'test-db';
    //code reducted for simplicity
    
  9. To add a field for the consentGiven metadata, add a checkbox at the form. For our example, we will configure the checkbox as checked by default and disabled so the user cannot uncheck it. You can adjust this according to your business needs.

    //code reducted for simplicity
    <div class="form-group">
      <label for="name">I consent with data processing</label>
      <input
        type="checkbox"
        id="userConsent"
        checked disabled>
    </div>
    //code reducted for simplicity
    
  10. Edit the signup function to set the metadata. Note that we set the value of the metadata to a string with the value true and not to a boolean value, and we are using toString to convert the number to a string. This is due to a restriction of the Authentication API Signup endpoint which only accepts strings as values.

    //code reducted for simplicity
    webAuth.redirect.signupAndLogin({
      connection: databaseConnection,
      email: email,
      password: password,
      user_metadata: { consentGiven: 'true', consentTimestamp: Date.now().toString() }
    }, function(err) {
      if (err) displayError(err);
    });
    //code reducted for simplicity
    
  11. To see what the login widget will look like, click the Preview tab.

    Preview custom form with auth0.js

  12. To test this configuration run the application and go to http://localhost:3000. Sign up with a new user. Then go to Dashboard > Users and search for your new user. Go to User Details and scroll down to the Metadata section. At the user_metadata text area you should see the consentGiven metadata set to true.

Option 2: Use the API (Database)

If you serve your login page from your own server, then you can call the Authentication API Signup endpoint directly once the user signs up.

For the same scenario we have been discussing so far, once you sign up a new user, you can use the following snippet to create the user at Auth0 and set the metadata. Remember to replace the value of the consentTimestamp request parameter with the timestamp of when the user provided consent.


curl --request POST \
  --url 'https://YOUR_AUTH0_DOMAIN/dbconnections/signup' \
  --header 'content-type: application/json' \
  --data '{"client_id": "YOUR_CLIENT_ID","email": "YOUR_USER_EMAIL","password": "YOUR_USER_PASSWORD","user_metadata": {"consentGiven": "true", "consentTimestamp": "1525101183" }}'
var client = new RestClient("https://YOUR_AUTH0_DOMAIN/dbconnections/signup");
var request = new RestRequest(Method.POST);
request.AddHeader("content-type", "application/json");
request.AddParameter("application/json", "{\"client_id\": \"YOUR_CLIENT_ID\",\"email\": \"YOUR_USER_EMAIL\",\"password\": \"YOUR_USER_PASSWORD\",\"user_metadata\": {\"consentGiven\": \"true\", \"consentTimestamp\": \"1525101183\" }}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
package main

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

func main() {

	url := "https://YOUR_AUTH0_DOMAIN/dbconnections/signup"

	payload := strings.NewReader("{\"client_id\": \"YOUR_CLIENT_ID\",\"email\": \"YOUR_USER_EMAIL\",\"password\": \"YOUR_USER_PASSWORD\",\"user_metadata\": {\"consentGiven\": \"true\", \"consentTimestamp\": \"1525101183\" }}")

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

	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/dbconnections/signup")
  .header("content-type", "application/json")
  .body("{\"client_id\": \"YOUR_CLIENT_ID\",\"email\": \"YOUR_USER_EMAIL\",\"password\": \"YOUR_USER_PASSWORD\",\"user_metadata\": {\"consentGiven\": \"true\", \"consentTimestamp\": \"1525101183\" }}")
  .asString();
var settings = {
  "async": true,
  "crossDomain": true,
  "url": "https://YOUR_AUTH0_DOMAIN/dbconnections/signup",
  "method": "POST",
  "headers": {
    "content-type": "application/json"
  },
  "processData": false,
  "data": "{\"client_id\": \"YOUR_CLIENT_ID\",\"email\": \"YOUR_USER_EMAIL\",\"password\": \"YOUR_USER_PASSWORD\",\"user_metadata\": {\"consentGiven\": \"true\", \"consentTimestamp\": \"1525101183\" }}"
}

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

var options = { method: 'POST',
  url: 'https://YOUR_AUTH0_DOMAIN/dbconnections/signup',
  headers: { 'content-type': 'application/json' },
  body: 
   { client_id: 'YOUR_CLIENT_ID',
     email: 'YOUR_USER_EMAIL',
     password: 'YOUR_USER_PASSWORD',
     user_metadata: { consentGiven: 'true', consentTimestamp: '1525101183' } },
  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" };
NSDictionary *parameters = @{ @"client_id": @"YOUR_CLIENT_ID",
                              @"email": @"YOUR_USER_EMAIL",
                              @"password": @"YOUR_USER_PASSWORD",
                              @"user_metadata": @{ @"consentGiven": @"true", @"consentTimestamp": @"1525101183" } };

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

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://YOUR_AUTH0_DOMAIN/dbconnections/signup"]
                                                       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/dbconnections/signup",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => "{\"client_id\": \"YOUR_CLIENT_ID\",\"email\": \"YOUR_USER_EMAIL\",\"password\": \"YOUR_USER_PASSWORD\",\"user_metadata\": {\"consentGiven\": \"true\", \"consentTimestamp\": \"1525101183\" }}",
  CURLOPT_HTTPHEADER => array(
    "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 = "{\"client_id\": \"YOUR_CLIENT_ID\",\"email\": \"YOUR_USER_EMAIL\",\"password\": \"YOUR_USER_PASSWORD\",\"user_metadata\": {\"consentGiven\": \"true\", \"consentTimestamp\": \"1525101183\" }}"

headers = { 'content-type': "application/json" }

conn.request("POST", "/YOUR_AUTH0_DOMAIN/dbconnections/signup", payload, headers)

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

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

url = URI("https://YOUR_AUTH0_DOMAIN/dbconnections/signup")

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["content-type"] = 'application/json'
request.body = "{\"client_id\": \"YOUR_CLIENT_ID\",\"email\": \"YOUR_USER_EMAIL\",\"password\": \"YOUR_USER_PASSWORD\",\"user_metadata\": {\"consentGiven\": \"true\", \"consentTimestamp\": \"1525101183\" }}"

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

let headers = ["content-type": "application/json"]
let parameters = [
  "client_id": "YOUR_CLIENT_ID",
  "email": "YOUR_USER_EMAIL",
  "password": "YOUR_USER_PASSWORD",
  "user_metadata": [
    "consentGiven": "true",
    "consentTimestamp": "1525101183"
  ]
]

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

var request = NSMutableURLRequest(URL: NSURL(string: "https://YOUR_AUTH0_DOMAIN/dbconnections/signup")!,
                                        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()

Note that we set the value of the metadata to a string with the value true and not to a boolean value due to the API restriction that accepts strings as values, not booleans.

If setting boolean values is a requirement for you, you can use the Management API instead. In this scenario you sign up your user as usual, and then you call the Update User endpoint of the Management API to set the required metadata after the user has been created. For details on how to do that keep reading, the next paragraph uses that endpoint.

Option 3: Use the API (Social)

If you use social connections, then you cannot use the Authentication API to create the user at Auth0, since that endpoint works only for database connections.

What you have to do instead is let your user sign up with the social provider (which will create a user record at Auth0) and then use the Management API to update the user's information.

Before you call the Management API you need to get a valid token. For details on how to do that see How to Get an Access Token for the Management API.

Get a token from an SPA

The linked article uses the Client Credentials OAuth 2.0 grant to get a token, which you cannot use from an app running on the browser. What you can use instead is the Implicit Grant. Set the audience request parameter to https://YOUR_AUTH0_DOMAIN/api/v2/ and the scope parameter to the scope create:current_user_metadata. You can use the Access Token you will get at the response to call the Update User endpoint of the Management API.

Once you have a valid token, use the following snippet to update the user's metadata.


curl --request POST \
  --url 'https://YOUR_AUTH0_DOMAIN/api/v2/users/%7BUSER_ID%7D' \
  --header 'authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'content-type: application/json' \
  --data '{"user_metadata": {"consentGiven":true, "consentTimestamp": "1525101183"}}'
var client = new RestClient("https://YOUR_AUTH0_DOMAIN/api/v2/users/%7BUSER_ID%7D");
var request = new RestRequest(Method.POST);
request.AddHeader("content-type", "application/json");
request.AddHeader("authorization", "Bearer YOUR_ACCESS_TOKEN");
request.AddParameter("application/json", "{\"user_metadata\": {\"consentGiven\":true, \"consentTimestamp\": \"1525101183\"}}", 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/%7BUSER_ID%7D"

	payload := strings.NewReader("{\"user_metadata\": {\"consentGiven\":true, \"consentTimestamp\": \"1525101183\"}}")

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

	req.Header.Add("authorization", "Bearer YOUR_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/%7BUSER_ID%7D")
  .header("authorization", "Bearer YOUR_ACCESS_TOKEN")
  .header("content-type", "application/json")
  .body("{\"user_metadata\": {\"consentGiven\":true, \"consentTimestamp\": \"1525101183\"}}")
  .asString();
var settings = {
  "async": true,
  "crossDomain": true,
  "url": "https://YOUR_AUTH0_DOMAIN/api/v2/users/%7BUSER_ID%7D",
  "method": "POST",
  "headers": {
    "authorization": "Bearer YOUR_ACCESS_TOKEN",
    "content-type": "application/json"
  },
  "processData": false,
  "data": "{\"user_metadata\": {\"consentGiven\":true, \"consentTimestamp\": \"1525101183\"}}"
}

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

var options = { method: 'POST',
  url: 'https://YOUR_AUTH0_DOMAIN/api/v2/users/%7BUSER_ID%7D',
  headers: 
   { 'content-type': 'application/json',
     authorization: 'Bearer YOUR_ACCESS_TOKEN' },
  body: 
   { user_metadata: { consentGiven: true, consentTimestamp: '1525101183' } },
  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 YOUR_ACCESS_TOKEN",
                           @"content-type": @"application/json" };
NSDictionary *parameters = @{ @"user_metadata": @{ @"consentGiven": @YES, @"consentTimestamp": @"1525101183" } };

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

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://YOUR_AUTH0_DOMAIN/api/v2/users/%7BUSER_ID%7D"]
                                                       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/%7BUSER_ID%7D",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => "{\"user_metadata\": {\"consentGiven\":true, \"consentTimestamp\": \"1525101183\"}}",
  CURLOPT_HTTPHEADER => array(
    "authorization: Bearer YOUR_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 = "{\"user_metadata\": {\"consentGiven\":true, \"consentTimestamp\": \"1525101183\"}}"

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

conn.request("POST", "/YOUR_AUTH0_DOMAIN/api/v2/users/%7BUSER_ID%7D", 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/%7BUSER_ID%7D")

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 YOUR_ACCESS_TOKEN'
request["content-type"] = 'application/json'
request.body = "{\"user_metadata\": {\"consentGiven\":true, \"consentTimestamp\": \"1525101183\"}}"

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

let headers = [
  "authorization": "Bearer YOUR_ACCESS_TOKEN",
  "content-type": "application/json"
]
let parameters = ["user_metadata": [
    "consentGiven": true,
    "consentTimestamp": "1525101183"
  ]]

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

var request = NSMutableURLRequest(URL: NSURL(string: "https://YOUR_AUTH0_DOMAIN/api/v2/users/%7BUSER_ID%7D")!,
                                        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()

Note that in order to make this call you need to know the unique user_id. You can retrieve this from the sub claim of the ID Token, if you got one from the response. Alternatively, if all you have is the email, you can retrieve the Id by calling another endpoint of the Management API. For more information see Search Users by Email.

Option 4: Redirect to another page

If you want to display more information to your user, then upon signup you can redirect to another page where you ask for consent and any additional info, and then redirect back to finish the authentication transaction. This can be done with redirect rules. That same rule can be used to save the consent information at the user's metadata so we can track this information and not ask for consent upon next login.

For simplicity, we will use this sample consent form. This is a form we have hosted for you using a webtask, but later on we will see how to host your own version of this form (with your own URL). You can find the webtask's code at Auth0 Redirect Rules repo.

If you are implementing this from a regular web app, hosting your own form, then you can also save the consent information at the user_metadata using the Management API's Update User endpoint.

  1. First, we will add the rule. Go to Dashboard > Rules and click Create Rule. At the Rules Templates select empty rule. Change the default rule's name (empty rule) to something descriptive, for example Redirect to consent form.

  2. Add the following JavaScript code and save your changes. The code redirects the user to the CONSENT_FORM_URL URL (we will configure this at the next step). Once the user hits Submit at the consent form, the rule runs again as part of the callback. At this point we persist the information at the user_metadata.

    function redirectToConsentForm (user, context, callback) {
      var consentGiven = user.user_metadata && user.user_metadata.consentGiven;
    
      // redirect to consent form if user has not yet consented
      if (!consentGiven && context.protocol !== 'redirect-callback') {
        var auth0Domain = auth0.baseUrl.match(/([^:]*:\/\/)?([^\/]+\.[^\/]+)/)[2];
    
        context.redirect = {
          url: configuration.CONSENT_FORM_URL +
            (configuration.CONSENT_FORM_URL.indexOf('?') === -1 ? '?' : '&') +
            'auth0_domain=' + encodeURIComponent(auth0Domain)
        };
      }
    
      // if user clicked 'I agree' on the consent form, persist it to their profile
      // so they don't get prompted again
      if (context.protocol === 'redirect-callback') {
        if (context.request.body.confirm === 'yes') {
          user.user_metadata = user.user_metadata || {};
          user.user_metadata.consentGiven = true;
          user.user_metadata.consentTimestamp = Date.now();
    
          auth0.users.updateUserMetadata(user.user_id, user.user_metadata)
            .then(function(){
              callback(null, user, context);
            })
            .catch(function(err){
              callback(err);
            });
        } else {
          callback(new UnauthorizedError('User did not consent!'));
        }
      }
    
      callback(null, user, context);
    }
    
  3. Go back to Dashboard > Rules, scroll down, and under Settings, create a Key/Value pair as follows:

  • Key: CONSENT_FORM_URL
  • Value: https://wt-peter-auth0_com-0.run.webtask.io/simple-redirect-rule-consent-form

If you want to work with your own implementation of the consent form webtask, you can host your own version of the webtask.js script. For instructions see Consent Form Setup.

To learn more about redirect rules, see Redirect Users from Rules.

If you plan on using this approach in a production environment, make sure to review Trusted Callback URL's and Data Integrity (both sections address some security concerns).

To test this configuration:

  1. Run the application and go to http://localhost:3000
  2. Sign up with a new user. You will be navigated to the consent form.
  3. Check the I agree flag and click Submit
  4. Go to Dashboard > Users and search for your new user
  5. Go to User Details and scroll down to the Metadata section.
  6. At the user_metadata text area you should see the consentGiven metadata set to true, and the consentTimestamp set to the Unix timestamp of the moment the user consented

Application Sign Up widget

That's it, you are done!