Docs

Associate an Out-of-Band Authenticator

In this tutorial, you'll learn how to configure your application so users can self-associate out-of-band (OOB) authenticators.

Before you start

Before you can use the MFA APIs, you'll need to enable the MFA grant type for your application. You can enable the MFA grant by going to Applications > Your Application > Advanced Settings > Grant Types and selecting MFA.

1. Get the MFA token

When using Universal Login, users get prompted to enroll an MFA factor the first time they authenticate. If you want to provide end users a way to enroll an additional factor, you need to obtain an Access Token for the https://YOUR_DOMAIN/mfa audience and the enroll scope. You can do it by specifying that audience as part the initial authentication request, or in a different one.

When using the Resource Owner Password Grant, when users try to authenticate without an active authenticator associated with their account, Auth0 will return an mfa_required error when calling the /oauth/token endpoint. The request might look something like this:


curl --request POST \
  --url 'https://YOUR_DOMAIN/oauth/token' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data grant_type=password \
  --data username=user@example.com \
  --data password=pwd \
  --data 'client_id=YOUR_CLIENT_ID' \
  --data client_secret=YOUR_CLIENT_SECRET \
  --data audience=https://someapi.com/api \
  --data scope=read:sample
var client = new RestClient("https://YOUR_DOMAIN/oauth/token");
var request = new RestRequest(Method.POST);
request.AddHeader("content-type", "application/x-www-form-urlencoded");
request.AddParameter("application/x-www-form-urlencoded", "grant_type=password&username=user%40example.com&password=pwd&client_id=%24%7Baccount.clientId%7D&client_secret=YOUR_CLIENT_SECRET&audience=https%3A%2F%2Fsomeapi.com%2Fapi&scope=read%3Asample", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
package main

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

func main() {

	url := "https://YOUR_DOMAIN/oauth/token"

	payload := strings.NewReader("grant_type=password&username=user%40example.com&password=pwd&client_id=%24%7Baccount.clientId%7D&client_secret=YOUR_CLIENT_SECRET&audience=https%3A%2F%2Fsomeapi.com%2Fapi&scope=read%3Asample")

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

	req.Header.Add("content-type", "application/x-www-form-urlencoded")

	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/oauth/token")
  .header("content-type", "application/x-www-form-urlencoded")
  .body("grant_type=password&username=user%40example.com&password=pwd&client_id=%24%7Baccount.clientId%7D&client_secret=YOUR_CLIENT_SECRET&audience=https%3A%2F%2Fsomeapi.com%2Fapi&scope=read%3Asample")
  .asString();
var request = require("request");

var options = {
  method: 'POST',
  url: 'https://YOUR_DOMAIN/oauth/token',
  headers: {'content-type': 'application/x-www-form-urlencoded'},
  form: {
    grant_type: 'password',
    username: 'user@example.com',
    password: 'pwd',
    client_id: 'YOUR_CLIENT_ID',
    client_secret: 'YOUR_CLIENT_SECRET',
    audience: 'https://someapi.com/api',
    scope: 'read:sample'
  }
};

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

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

NSDictionary *headers = @{ @"content-type": @"application/x-www-form-urlencoded" };

NSMutableData *postData = [[NSMutableData alloc] initWithData:[@"grant_type=password" dataUsingEncoding:NSUTF8StringEncoding]];
[postData appendData:[@"&username=user@example.com" dataUsingEncoding:NSUTF8StringEncoding]];
[postData appendData:[@"&password=pwd" dataUsingEncoding:NSUTF8StringEncoding]];
[postData appendData:[@"&client_id=YOUR_CLIENT_ID" dataUsingEncoding:NSUTF8StringEncoding]];
[postData appendData:[@"&client_secret=YOUR_CLIENT_SECRET" dataUsingEncoding:NSUTF8StringEncoding]];
[postData appendData:[@"&audience=https://someapi.com/api" dataUsingEncoding:NSUTF8StringEncoding]];
[postData appendData:[@"&scope=read:sample" dataUsingEncoding:NSUTF8StringEncoding]];

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://YOUR_DOMAIN/oauth/token"]
                                                       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_DOMAIN/oauth/token",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => "grant_type=password&username=user%40example.com&password=pwd&client_id=%24%7Baccount.clientId%7D&client_secret=YOUR_CLIENT_SECRET&audience=https%3A%2F%2Fsomeapi.com%2Fapi&scope=read%3Asample",
  CURLOPT_HTTPHEADER => array(
    "content-type: application/x-www-form-urlencoded"
  ),
));

$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 = "grant_type=password&username=user%40example.com&password=pwd&client_id=%24%7Baccount.clientId%7D&client_secret=YOUR_CLIENT_SECRET&audience=https%3A%2F%2Fsomeapi.com%2Fapi&scope=read%3Asample"

headers = { 'content-type': "application/x-www-form-urlencoded" }

conn.request("POST", "/YOUR_DOMAIN/oauth/token", 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/oauth/token")

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/x-www-form-urlencoded'
request.body = "grant_type=password&username=user%40example.com&password=pwd&client_id=%24%7Baccount.clientId%7D&client_secret=YOUR_CLIENT_SECRET&audience=https%3A%2F%2Fsomeapi.com%2Fapi&scope=read%3Asample"

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

let headers = ["content-type": "application/x-www-form-urlencoded"]

let postData = NSMutableData(data: "grant_type=password".data(using: String.Encoding.utf8)!)
postData.append("&username=user@example.com".data(using: String.Encoding.utf8)!)
postData.append("&password=pwd".data(using: String.Encoding.utf8)!)
postData.append("&client_id=YOUR_CLIENT_ID".data(using: String.Encoding.utf8)!)
postData.append("&client_secret=YOUR_CLIENT_SECRET".data(using: String.Encoding.utf8)!)
postData.append("&audience=https://someapi.com/api".data(using: String.Encoding.utf8)!)
postData.append("&scope=read:sample".data(using: String.Encoding.utf8)!)

let request = NSMutableURLRequest(url: NSURL(string: "https://YOUR_DOMAIN/oauth/token")! 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 response will include the mfa_required error and a mfa_token that you can use when calling the /mfa/associate endpoint to enroll your first authenticator.

{
  "error": "mfa_required",
  "error_description": "Multifactor authentication required",
  "mfa_token": "Fe26...Ha"
}

2. Request association of the authenticator

Next, make a POST request to the /mfa/associate endpoint to request association of the user's authenticator. The Access Token required by this endpoint is the MFA token received in the previous step.

To associate an authenticator where the challenge type is an SMS message containing a code the user provides, make the following POST request to the /mfa/associate endpoint. This will both trigger an MFA challenge for the user and associate the new authenticator.

Be sure to replace the placeholder values in the payload body shown below as appropriate.


curl --request POST \
  --url 'https://YOUR_DOMAIN/mfa/associate' \
  --header 'authorization: Bearer ACCESS_TOKEN' \
  --header 'content-type: application/json' \
  --data '{ "authenticator_types": ["oob"], "oob_channels": ["sms"], "phone_number": "+11...9" }'
var client = new RestClient("https://YOUR_DOMAIN/mfa/associate");
var request = new RestRequest(Method.POST);
request.AddHeader("authorization", "Bearer ACCESS_TOKEN");
request.AddHeader("content-type", "application/json");
request.AddParameter("application/json", "{ \"authenticator_types\": [\"oob\"], \"oob_channels\": [\"sms\"], \"phone_number\": \"+11...9\" }", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
package main

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

func main() {

	url := "https://YOUR_DOMAIN/mfa/associate"

	payload := strings.NewReader("{ \"authenticator_types\": [\"oob\"], \"oob_channels\": [\"sms\"], \"phone_number\": \"+11...9\" }")

	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/mfa/associate")
  .header("authorization", "Bearer ACCESS_TOKEN")
  .header("content-type", "application/json")
  .body("{ \"authenticator_types\": [\"oob\"], \"oob_channels\": [\"sms\"], \"phone_number\": \"+11...9\" }")
  .asString();
var request = require("request");

var options = {
  method: 'POST',
  url: 'https://YOUR_DOMAIN/mfa/associate',
  headers: {authorization: 'Bearer ACCESS_TOKEN', 'content-type': 'application/json'},
  body: {authenticator_types: ['oob'], oob_channels: ['sms'], phone_number: '+11...9'},
  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 ACCESS_TOKEN",
                           @"content-type": @"application/json" };
NSDictionary *parameters = @{ @"authenticator_types": @[ @"oob" ],
                              @"oob_channels": @[ @"sms" ],
                              @"phone_number": @"+11...9" };

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

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://YOUR_DOMAIN/mfa/associate"]
                                                       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_DOMAIN/mfa/associate",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => "{ \"authenticator_types\": [\"oob\"], \"oob_channels\": [\"sms\"], \"phone_number\": \"+11...9\" }",
  CURLOPT_HTTPHEADER => array(
    "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 = "{ \"authenticator_types\": [\"oob\"], \"oob_channels\": [\"sms\"], \"phone_number\": \"+11...9\" }"

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

conn.request("POST", "/YOUR_DOMAIN/mfa/associate", 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/mfa/associate")

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 = "{ \"authenticator_types\": [\"oob\"], \"oob_channels\": [\"sms\"], \"phone_number\": \"+11...9\" }"

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

let headers = [
  "authorization": "Bearer ACCESS_TOKEN",
  "content-type": "application/json"
]
let parameters = [
  "authenticator_types": ["oob"],
  "oob_channels": ["sms"],
  "phone_number": "+11...9"
] as [String : Any]

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

let request = NSMutableURLRequest(url: NSURL(string: "https://YOUR_DOMAIN/mfa/associate")! 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()

If successful, you'll receive a response like this:

{
  "authenticator_type": "oob",
  "oob_channel": "sms",
  "recovery_codes": [ "N3BGPZZWJ85JLCNPZBDW6QXC" ],
  "oob_code": "ata6daXAiOi..."
}

Recovery Codes

If this is the first time the user is associating an authenticator, you'll notice the response includes recovery_codes. Recovery codes are used to access the user's account in the event that they lose access to the account or device used for their second factor authentication. These are one-time usable codes, and new ones are generated as necessary.

3. Confirm the authenticator association

Once the authenticator is associated, it must be used at least once to confirm the association.

To confirm the association of an authenticator using SMS messages for the MFA challenge, make a POST request to the oauth/token endpoint. Now, you can add the oob_code retrieved previously as a parameter in the request.

Be sure to replace the placeholder values in the payload body shown below as appropriate.


curl --request POST \
  --url 'https://YOUR_DOMAIN/oauth/token' \
  --header 'authorization: Bearer ACCESS_TOKEN' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data grant_type=http://auth0.com/oauth/grant-type/mfa-oob \
  --data 'client_id=YOUR_CLIENT_ID' \
  --data mfa_token=YOUR_MFA_TOKEN \
  --data oob_code=ata...i0i \
  --data binding_code=000000
var client = new RestClient("https://YOUR_DOMAIN/oauth/token");
var request = new RestRequest(Method.POST);
request.AddHeader("authorization", "Bearer ACCESS_TOKEN");
request.AddHeader("content-type", "application/x-www-form-urlencoded");
request.AddParameter("application/x-www-form-urlencoded", "grant_type=http%3A%2F%2Fauth0.com%2Foauth%2Fgrant-type%2Fmfa-oob&client_id=%24%7Baccount.clientId%7D&mfa_token=YOUR_MFA_TOKEN&oob_code=ata...i0i&binding_code=000000", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
package main

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

func main() {

	url := "https://YOUR_DOMAIN/oauth/token"

	payload := strings.NewReader("grant_type=http%3A%2F%2Fauth0.com%2Foauth%2Fgrant-type%2Fmfa-oob&client_id=%24%7Baccount.clientId%7D&mfa_token=YOUR_MFA_TOKEN&oob_code=ata...i0i&binding_code=000000")

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

	req.Header.Add("authorization", "Bearer ACCESS_TOKEN")
	req.Header.Add("content-type", "application/x-www-form-urlencoded")

	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/oauth/token")
  .header("authorization", "Bearer ACCESS_TOKEN")
  .header("content-type", "application/x-www-form-urlencoded")
  .body("grant_type=http%3A%2F%2Fauth0.com%2Foauth%2Fgrant-type%2Fmfa-oob&client_id=%24%7Baccount.clientId%7D&mfa_token=YOUR_MFA_TOKEN&oob_code=ata...i0i&binding_code=000000")
  .asString();
var request = require("request");

var options = {
  method: 'POST',
  url: 'https://YOUR_DOMAIN/oauth/token',
  headers: {
    authorization: 'Bearer ACCESS_TOKEN',
    'content-type': 'application/x-www-form-urlencoded'
  },
  form: {
    grant_type: 'http://auth0.com/oauth/grant-type/mfa-oob',
    client_id: 'YOUR_CLIENT_ID',
    mfa_token: 'YOUR_MFA_TOKEN',
    oob_code: 'ata...i0i',
    binding_code: '000000'
  }
};

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

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

NSDictionary *headers = @{ @"authorization": @"Bearer ACCESS_TOKEN",
                           @"content-type": @"application/x-www-form-urlencoded" };

NSMutableData *postData = [[NSMutableData alloc] initWithData:[@"grant_type=http://auth0.com/oauth/grant-type/mfa-oob" dataUsingEncoding:NSUTF8StringEncoding]];
[postData appendData:[@"&client_id=YOUR_CLIENT_ID" dataUsingEncoding:NSUTF8StringEncoding]];
[postData appendData:[@"&mfa_token=YOUR_MFA_TOKEN" dataUsingEncoding:NSUTF8StringEncoding]];
[postData appendData:[@"&oob_code=ata...i0i" dataUsingEncoding:NSUTF8StringEncoding]];
[postData appendData:[@"&binding_code=000000" dataUsingEncoding:NSUTF8StringEncoding]];

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://YOUR_DOMAIN/oauth/token"]
                                                       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_DOMAIN/oauth/token",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => "grant_type=http%3A%2F%2Fauth0.com%2Foauth%2Fgrant-type%2Fmfa-oob&client_id=%24%7Baccount.clientId%7D&mfa_token=YOUR_MFA_TOKEN&oob_code=ata...i0i&binding_code=000000",
  CURLOPT_HTTPHEADER => array(
    "authorization: Bearer ACCESS_TOKEN",
    "content-type: application/x-www-form-urlencoded"
  ),
));

$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 = "grant_type=http%3A%2F%2Fauth0.com%2Foauth%2Fgrant-type%2Fmfa-oob&client_id=%24%7Baccount.clientId%7D&mfa_token=YOUR_MFA_TOKEN&oob_code=ata...i0i&binding_code=000000"

headers = {
    'authorization': "Bearer ACCESS_TOKEN",
    'content-type': "application/x-www-form-urlencoded"
    }

conn.request("POST", "/YOUR_DOMAIN/oauth/token", 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/oauth/token")

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/x-www-form-urlencoded'
request.body = "grant_type=http%3A%2F%2Fauth0.com%2Foauth%2Fgrant-type%2Fmfa-oob&client_id=%24%7Baccount.clientId%7D&mfa_token=YOUR_MFA_TOKEN&oob_code=ata...i0i&binding_code=000000"

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

let headers = [
  "authorization": "Bearer ACCESS_TOKEN",
  "content-type": "application/x-www-form-urlencoded"
]

let postData = NSMutableData(data: "grant_type=http://auth0.com/oauth/grant-type/mfa-oob".data(using: String.Encoding.utf8)!)
postData.append("&client_id=YOUR_CLIENT_ID".data(using: String.Encoding.utf8)!)
postData.append("&mfa_token=YOUR_MFA_TOKEN".data(using: String.Encoding.utf8)!)
postData.append("&oob_code=ata...i0i".data(using: String.Encoding.utf8)!)
postData.append("&binding_code=000000".data(using: String.Encoding.utf8)!)

let request = NSMutableURLRequest(url: NSURL(string: "https://YOUR_DOMAIN/oauth/token")! 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()

If the call was successful, you'll receive a response in the below format, containing the Access Token:

{
  "access_token": "eyJ...i",
  "expires_in": 600,
  "scope": "enroll read:authenticators remove:authenticators",
  "token_type": "Bearer"
}

At this point, the authenticator is fully associated and ready to be used, and you have an Access Token for the user.

You can check at any point to verify whether an authenticator has been confirmed by calling the mfa/authenticators endpoint. If the authenticator is confirmed, the value returned for active is true.