Invite-Only Applications

Many SaaS apps allow self-service provisioning, where users can register themselves and begin using the app. Other types of apps, however, do not allow such signups. Instead, the customer (typically an organization of some type) pay upfront for a number of users, and only the end user with the appropriate credentials may sign up and access the app. In such cases, you can use an invite-only workflow for authorization purposes.

Example Scenario: Analystick

In this tutorial, we will work through a sample setup for the fictional company, Analystick.

Analystick is a multi-tenant SaaS solution offering cloud-based analytics. Customers purchasing licenses send Analystick lists of users whom they want to access the application.

You can handle this requirement in Auth0 using an Enterprise Connection (using federation) with the individual customers using ADFS, SAML-P, and so on. This allows the customer to authenticate users with their own Active Directory specifying who gets access to the app.

The invite-only authorization flow includes the following steps:

  1. Creating new users in Analystick and bulk importing the same users into Auth0
  2. Triggering the email verification process via Auth0
  3. Triggering the password reset process via Auth0

Setup your Client

You can store all Analystick end users in a single database, since everyone will provide their unique corporate email addresses.

To prevent users from signing themselves up and adding themselves to the database connection, be sure to select the Disable Sign Ups option on the connection to make sure users can only be created on the backend.

You will need to create a client in the Dashboard with the correct parameters:

  • Name: give your application a clear name as this will be used in the emails being sent out during the invite-only workflow
  • Client Type: this will be a regular web application.
  • Allowed Callback URLs: this should be the URL of your app

Since this client needs to access the Management API, you'll need to authorize it and set its scopes as follows:

  • Go to the APIs section of the Dashboard.
  • Select Auth0 Management API.
  • Click over to the Non-interactive Clients tab.
  • Find the client you just created, and set its toggle to Authorized.
  • Use the down arrow to open up the scopes selection area. Select the following scopes: read:users, update:users, delete:users, create:users, and create:user_tickets.
  • Click Update.

Authorize Client

Import Users

Every user that exists in Analystick should be created in your Auth0 database connection as well. Auth0 offers a bulk user import functionality for this purpose.

Email Verification

Once you've created the user in Auth0, you'll send the appropriate POST call to the Create an Email Verification Ticket endpoint to trigger an email that verifies the user's email.

Be sure to update the following placeholder values:

  • MGMT_API_ACCESS_TOKEN: replace with your API access token
  • YOUR_APP_CALLBACK_URL: replace with the callback/return URL for your app
  • USER_ID: replace with the Auth0 user ID for the end user

curl --request POST \
  --url 'https://YOUR_AUTH0_DOMAIN/api/v2/tickets/email-verification' \
  --header 'authorization: Bearer MGMT_API_ACCESS_TOKEN' \
  --data '{ "result_url": "YOUR_APP_CALLBACK_URL", "user_id": "USER_ID", "ttl_sec": 0 }'
var client = new RestClient("https://YOUR_AUTH0_DOMAIN/api/v2/tickets/email-verification");
var request = new RestRequest(Method.POST);
request.AddHeader("authorization", "Bearer MGMT_API_ACCESS_TOKEN");
request.AddParameter("undefined", "{ \"result_url\": \"YOUR_APP_CALLBACK_URL\", \"user_id\": \"USER_ID\", \"ttl_sec\": 0 }", 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/tickets/email-verification"

	payload := strings.NewReader("{ \"result_url\": \"YOUR_APP_CALLBACK_URL\", \"user_id\": \"USER_ID\", \"ttl_sec\": 0 }")

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

	req.Header.Add("authorization", "Bearer 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.post("https://YOUR_AUTH0_DOMAIN/api/v2/tickets/email-verification")
  .header("authorization", "Bearer MGMT_API_ACCESS_TOKEN")
  .body("{ \"result_url\": \"YOUR_APP_CALLBACK_URL\", \"user_id\": \"USER_ID\", \"ttl_sec\": 0 }")
  .asString();
var settings = {
  "async": true,
  "crossDomain": true,
  "url": "https://YOUR_AUTH0_DOMAIN/api/v2/tickets/email-verification",
  "method": "POST",
  "headers": {
    "authorization": "Bearer MGMT_API_ACCESS_TOKEN"
  },
  "processData": false,
  "data": "{ \"result_url\": \"YOUR_APP_CALLBACK_URL\", \"user_id\": \"USER_ID\", \"ttl_sec\": 0 }"
}

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

var options = { method: 'POST',
  url: 'https://YOUR_AUTH0_DOMAIN/api/v2/tickets/email-verification',
  headers: { authorization: 'Bearer MGMT_API_ACCESS_TOKEN' },
  body: 
   { result_url: 'YOUR_APP_CALLBACK_URL',
     user_id: 'USER_ID',
     ttl_sec: 0 },
  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 MGMT_API_ACCESS_TOKEN" };
NSDictionary *parameters = @{ @"result_url": @"YOUR_APP_CALLBACK_URL",
                              @"user_id": @"USER_ID",
                              @"ttl_sec": @0 };

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

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://YOUR_AUTH0_DOMAIN/api/v2/tickets/email-verification"]
                                                       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/tickets/email-verification",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => "{ \"result_url\": \"YOUR_APP_CALLBACK_URL\", \"user_id\": \"USER_ID\", \"ttl_sec\": 0 }",
  CURLOPT_HTTPHEADER => array(
    "authorization: Bearer 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("")

payload = "{ \"result_url\": \"YOUR_APP_CALLBACK_URL\", \"user_id\": \"USER_ID\", \"ttl_sec\": 0 }"

headers = { 'authorization': "Bearer MGMT_API_ACCESS_TOKEN" }

conn.request("POST", "/YOUR_AUTH0_DOMAIN/api/v2/tickets/email-verification", 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/tickets/email-verification")

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 MGMT_API_ACCESS_TOKEN'
request.body = "{ \"result_url\": \"YOUR_APP_CALLBACK_URL\", \"user_id\": \"USER_ID\", \"ttl_sec\": 0 }"

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

let headers = ["authorization": "Bearer MGMT_API_ACCESS_TOKEN"]
let parameters = [
  "result_url": "YOUR_APP_CALLBACK_URL",
  "user_id": "USER_ID",
  "ttl_sec": 0
]

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

var request = NSMutableURLRequest(URL: NSURL(string: "https://YOUR_AUTH0_DOMAIN/api/v2/tickets/email-verification")!,
                                        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()

Password Reset

Once you've verified the user's password, you will need to initiate the password change process. To do so, your app shoule make a POST request to Auth0's Management API.

Be sure to replace the placeholder values for your API access token, as well as those within the body of the call, including the callback/return URL for your app and the user's details.


curl --request POST \
  --url 'https://YOUR_AUTH0_DOMAIN/api/v2/tickets/password-change' \
  --header 'authorization: Bearer MGMT_API_ACCESS_TOKEN' \
  --data '{ "result_url": "YOUR_APP_CALLBACK_URL", "user_id": "USER_ID", "new_password": "secret", "connection_id": "con_0000000000000001", "email": "EMAIL", "ttl_sec": 0 }'
var client = new RestClient("https://YOUR_AUTH0_DOMAIN/api/v2/tickets/password-change");
var request = new RestRequest(Method.POST);
request.AddHeader("authorization", "Bearer MGMT_API_ACCESS_TOKEN");
request.AddParameter("undefined", "{ \"result_url\": \"YOUR_APP_CALLBACK_URL\", \"user_id\": \"USER_ID\", \"new_password\": \"secret\", \"connection_id\": \"con_0000000000000001\", \"email\": \"EMAIL\", \"ttl_sec\": 0 }", 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/tickets/password-change"

	payload := strings.NewReader("{ \"result_url\": \"YOUR_APP_CALLBACK_URL\", \"user_id\": \"USER_ID\", \"new_password\": \"secret\", \"connection_id\": \"con_0000000000000001\", \"email\": \"EMAIL\", \"ttl_sec\": 0 }")

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

	req.Header.Add("authorization", "Bearer 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.post("https://YOUR_AUTH0_DOMAIN/api/v2/tickets/password-change")
  .header("authorization", "Bearer MGMT_API_ACCESS_TOKEN")
  .body("{ \"result_url\": \"YOUR_APP_CALLBACK_URL\", \"user_id\": \"USER_ID\", \"new_password\": \"secret\", \"connection_id\": \"con_0000000000000001\", \"email\": \"EMAIL\", \"ttl_sec\": 0 }")
  .asString();
var settings = {
  "async": true,
  "crossDomain": true,
  "url": "https://YOUR_AUTH0_DOMAIN/api/v2/tickets/password-change",
  "method": "POST",
  "headers": {
    "authorization": "Bearer MGMT_API_ACCESS_TOKEN"
  },
  "processData": false,
  "data": "{ \"result_url\": \"YOUR_APP_CALLBACK_URL\", \"user_id\": \"USER_ID\", \"new_password\": \"secret\", \"connection_id\": \"con_0000000000000001\", \"email\": \"EMAIL\", \"ttl_sec\": 0 }"
}

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

var options = { method: 'POST',
  url: 'https://YOUR_AUTH0_DOMAIN/api/v2/tickets/password-change',
  headers: { authorization: 'Bearer MGMT_API_ACCESS_TOKEN' },
  body: 
   { result_url: 'YOUR_APP_CALLBACK_URL',
     user_id: 'USER_ID',
     new_password: 'secret',
     connection_id: 'con_0000000000000001',
     email: 'EMAIL',
     ttl_sec: 0 },
  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 MGMT_API_ACCESS_TOKEN" };
NSDictionary *parameters = @{ @"result_url": @"YOUR_APP_CALLBACK_URL",
                              @"user_id": @"USER_ID",
                              @"new_password": @"secret",
                              @"connection_id": @"con_0000000000000001",
                              @"email": @"EMAIL",
                              @"ttl_sec": @0 };

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

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://YOUR_AUTH0_DOMAIN/api/v2/tickets/password-change"]
                                                       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/tickets/password-change",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => "{ \"result_url\": \"YOUR_APP_CALLBACK_URL\", \"user_id\": \"USER_ID\", \"new_password\": \"secret\", \"connection_id\": \"con_0000000000000001\", \"email\": \"EMAIL\", \"ttl_sec\": 0 }",
  CURLOPT_HTTPHEADER => array(
    "authorization: Bearer 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("")

payload = "{ \"result_url\": \"YOUR_APP_CALLBACK_URL\", \"user_id\": \"USER_ID\", \"new_password\": \"secret\", \"connection_id\": \"con_0000000000000001\", \"email\": \"EMAIL\", \"ttl_sec\": 0 }"

headers = { 'authorization': "Bearer MGMT_API_ACCESS_TOKEN" }

conn.request("POST", "/YOUR_AUTH0_DOMAIN/api/v2/tickets/password-change", 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/tickets/password-change")

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 MGMT_API_ACCESS_TOKEN'
request.body = "{ \"result_url\": \"YOUR_APP_CALLBACK_URL\", \"user_id\": \"USER_ID\", \"new_password\": \"secret\", \"connection_id\": \"con_0000000000000001\", \"email\": \"EMAIL\", \"ttl_sec\": 0 }"

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

let headers = ["authorization": "Bearer MGMT_API_ACCESS_TOKEN"]
let parameters = [
  "result_url": "YOUR_APP_CALLBACK_URL",
  "user_id": "USER_ID",
  "new_password": "secret",
  "connection_id": "con_0000000000000001",
  "email": "EMAIL",
  "ttl_sec": 0
]

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

var request = NSMutableURLRequest(URL: NSURL(string: "https://YOUR_AUTH0_DOMAIN/api/v2/tickets/password-change")!,
                                        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()

Summary

This tutorial walked you through implementing an invite-only sign-up flow using the Management API to customize the sign-up process and the email handling.