PingOne Platform APIs

Check Assertion (FIDO Device)

POST {{authPath}}/{{envID}}/deviceAuthentications/{{deviceAuthID}}

The multi-factor authentication flow for a FIDO device checks the authenticator assertion response, which contains the signed challenge needed to complete the MFA flow. The MFA actions service validates the challenge.

The following sample shows the POST /{{envID}}/deviceAuthentications/{{deviceAuthID}} operation to validate the assertion used in the multi-factor authentication flow. This operation uses the application/vnd.pingidentity.assertion.check+json custom media type as the content type in the request header.

The ASSERTION_REQUIRED flow state includes the publicKeyCredentialRequestOptions response property that specifies the public key credential request options object generated for the selected device that is used to call the navigator.credentials.get() on the browser to generate the assertion.

Device authentication

A FIDO2 biometrics device flow uses functions from the Web Authentication API (webauthn API) to manage device authentication. The following sample JavaScript code will help you implement the webauthn API for browser-based operations.

For more information about the Web Authentication API, refer to Web Authentication: An API for accessing Public Key Credentials.

Call the navigator.credentials.get method using the publicKeyCredentialOptions returned from the assertion.check action of the Flows service (refer to Assertion Check). As an example, refer to the WebAuthnAuthentication function in the following code sample:

var authAbortController = window.PublicKeyCredential ? new AbortController() : null;
var authAbortSignal = window.PublicKeyCredential ? authAbortController.signal : null;

window.abortWebAuthnSignal = function abortWebAuthnSignal() {
    authAbortController.abort();
    authAbortController = new AbortController();
    authAbortSignal = authAbortController.signal;
}

window.IsWebAuthnSupported = function IsWebAuthnSupported() {
    if (!window.PublicKeyCredential) {
        console.log("Web Authentication API is not supported on this browser.");
        return false;
    }
    return true;
}

window.isWebAuthnPlatformAuthenticatorAvailable = function isWebAuthnPlatformAuthenticatorAvailable() {
    var timer;
    var p1 = new Promise(function(resolve) {
        timer = setTimeout(function() {
            resolve(false);
        }, 1000);
    });
    var p2 = new Promise(function(resolve) {
        if (IsWebAuthnSupported() && window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable) {
            resolve(
	            window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().catch(function(err) {
                    console.log(err);
                    return false;
                }));
        }
        else {
            resolve(false);
        }
    });
    return Promise.race([p1, p2]).then(function (res) {
        clearTimeout(timer);
        console.log("isWebAuthnPlatformAuthenticatorAvailable - " +  res);
        return res;
    });
}

window.WebAuthnPlatformAuthentication = function WebAuthnPlatformAuthentication(publicKeyCredentialRequestOptions) {
    return new Promise(function(resolve, reject) {
        isWebAuthnPlatformAuthenticatorAvailable().then(function (result) {
            if (result) {
                resolve(Authenticate(publicKeyCredentialRequestOptions));
            }
            reject(Error("UnSupportedBrowserError"));
        });
    });
}

function Authenticate(publicKeyCredentialRequestOptions) {
    return new Promise(function(resolve, reject) {
        var options = JSON.parse(publicKeyCredentialRequestOptions);
        var publicKeyCredential = {};
        publicKeyCredential.challenge = new Uint8Array(options.challenge);
        if ('allowCredentials' in options) {
            publicKeyCredential.allowCredentials = credentialListConversion(options.allowCredentials);
        }
        if ('rpId' in options) {
            publicKeyCredential.rpId = options.rpId;
        }
        if ('timeout' in options) {
            publicKeyCredential.timeout = options.timeout;
        }
        if ('userVerification' in options) {
            publicKeyCredential.userVerification = options.userVerification;
        }
        console.log(publicKeyCredential);
        navigator.credentials.get({"publicKey": publicKeyCredential})
            .then(function (assertion) {
                // Send new credential info to server for verification and registration.
                console.log(assertion);
                var publicKeyCredential = {};
                if ('id' in assertion) {
                    publicKeyCredential.id = assertion.id;
                }
                if ('rawId' in assertion) {
                    publicKeyCredential.rawId = toBase64Str(assertion.rawId);
                }
                if ('type' in assertion) {
                    publicKeyCredential.type = assertion.type;
                }
                var response = {};
                response.clientDataJSON = toBase64Str(assertion.response.clientDataJSON);
                response.authenticatorData = toBase64Str(assertion.response.authenticatorData);
                response.signature = toBase64Str(assertion.response.signature);
                response.userHandle = toBase64Str(assertion.response.userHandle);
                publicKeyCredential.response = response;
                resolve(JSON.stringify(publicKeyCredential));
            }).catch(function (err) {
            // No acceptable authenticator or user refused consent. Handle appropriately.
            console.log(err);
            reject(Error(err.name));
        });
    });
}

function credentialListConversion(list) {
    var credList = [];
    for (var i=0; i < list.length; i++) {
        var cred = {
            type: list[i].type,
            id: new Uint8Array(list[i].id)
        };
        if (list[i].transports) {
            cred.transports = list[i].transports;
        }
        credList.push(cred);
    }
    return credList;
}

function toBase64Str(bin){
    return btoa(String.fromCharCode.apply(null, new Uint8Array(bin)));
}

const isWebAuthnSupported = () => {
  if (!window.PublicKeyCredential) {
    return false;
  }
  return true;
};

function getCompatibility() {
  return isWebAuthnPlatformAuthenticatorAvailable()
      .then((result) => {
        if (result) {
          return 'FULL';
        } else if (isWebAuthnSupported()) {
          return 'SECURITY_KEY_ONLY';
        } else {
          return 'NONE';
        }
      })
      .catch(() => {
        if (isWebAuthnSupported()) {
          return 'SECURITY_KEY_ONLY';
        } else {
          return 'NONE';
        }
      });
}

Prerequisites

Request Model
Property Type Required?

origin

String

Required

assertion

String

Required

compatibility

String

Optional

Refer to the Device authentications data model for full property descriptions.

Headers

Authorization      Bearer {{accessToken}}

Content-Type      application/vnd.pingidentity.assertion.check+json

Body

raw ( application/vnd.pingidentity.assertion.check+json )

{
    "origin": "https://app.pingone.com",
    "assertion": "{{assertionFromBrowser}}",
    "compatibility" : "FULL"
}

Example Request

  • cURL

  • C#

  • Go

  • HTTP

  • Java

  • jQuery

  • NodeJS

  • Python

  • PHP

  • Ruby

  • Swift

curl --location --globoff '{{authPath}}/{{envID}}/deviceAuthentications/{{deviceAuthID}}' \
--header 'Content-Type: application/vnd.pingidentity.assertion.check+json' \
--header 'Authorization: Bearer {{accessToken}}' \
--data '{
    "origin": "https://app.pingone.com",
    "assertion": "{{assertionFromBrowser}}",
    "compatibility" : "FULL"
}'
var options = new RestClientOptions("{{authPath}}/{{envID}}/deviceAuthentications/{{deviceAuthID}}")
{
  MaxTimeout = -1,
};
var client = new RestClient(options);
var request = new RestRequest("", Method.Post);
request.AddHeader("Content-Type", "application/vnd.pingidentity.assertion.check+json");
request.AddHeader("Authorization", "Bearer {{accessToken}}");
var body = @"{" + "\n" +
@"    ""origin"": ""https://app.pingone.com""," + "\n" +
@"    ""assertion"": ""{{assertionFromBrowser}}""," + "\n" +
@"    ""compatibility"" : ""FULL""" + "\n" +
@"}";
request.AddStringBody(body, DataFormat.Json);
RestResponse response = await client.ExecuteAsync(request);
Console.WriteLine(response.Content);
package main

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

func main() {

  url := "{{authPath}}/{{envID}}/deviceAuthentications/{{deviceAuthID}}"
  method := "POST"

  payload := strings.NewReader(`{
    "origin": "https://app.pingone.com",
    "assertion": "{{assertionFromBrowser}}",
    "compatibility" : "FULL"
}`)

  client := &http.Client {
  }
  req, err := http.NewRequest(method, url, payload)

  if err != nil {
    fmt.Println(err)
    return
  }
  req.Header.Add("Content-Type", "application/vnd.pingidentity.assertion.check+json")
  req.Header.Add("Authorization", "Bearer {{accessToken}}")

  res, err := client.Do(req)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer res.Body.Close()

  body, err := io.ReadAll(res.Body)
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println(string(body))
}
POST /{{envID}}/deviceAuthentications/{{deviceAuthID}} HTTP/1.1
Host: {{authPath}}
Content-Type: application/vnd.pingidentity.assertion.check+json
Authorization: Bearer {{accessToken}}

{
    "origin": "https://app.pingone.com",
    "assertion": "{{assertionFromBrowser}}",
    "compatibility" : "FULL"
}
OkHttpClient client = new OkHttpClient().newBuilder()
  .build();
MediaType mediaType = MediaType.parse("application/vnd.pingidentity.assertion.check+json");
RequestBody body = RequestBody.create(mediaType, "{\n    \"origin\": \"https://app.pingone.com\",\n    \"assertion\": \"{{assertionFromBrowser}}\",\n    \"compatibility\" : \"FULL\"\n}");
Request request = new Request.Builder()
  .url("{{authPath}}/{{envID}}/deviceAuthentications/{{deviceAuthID}}")
  .method("POST", body)
  .addHeader("Content-Type", "application/vnd.pingidentity.assertion.check+json")
  .addHeader("Authorization", "Bearer {{accessToken}}")
  .build();
Response response = client.newCall(request).execute();
var settings = {
  "url": "{{authPath}}/{{envID}}/deviceAuthentications/{{deviceAuthID}}",
  "method": "POST",
  "timeout": 0,
  "headers": {
    "Content-Type": "application/vnd.pingidentity.assertion.check+json",
    "Authorization": "Bearer {{accessToken}}"
  },
  "data": JSON.stringify({
    "origin": "https://app.pingone.com",
    "assertion": "{{assertionFromBrowser}}",
    "compatibility": "FULL"
  }),
};

$.ajax(settings).done(function (response) {
  console.log(response);
});
var request = require('request');
var options = {
  'method': 'POST',
  'url': '{{authPath}}/{{envID}}/deviceAuthentications/{{deviceAuthID}}',
  'headers': {
    'Content-Type': 'application/vnd.pingidentity.assertion.check+json',
    'Authorization': 'Bearer {{accessToken}}'
  },
  body: JSON.stringify({
    "origin": "https://app.pingone.com",
    "assertion": "{{assertionFromBrowser}}",
    "compatibility": "FULL"
  })

};
request(options, function (error, response) {
  if (error) throw new Error(error);
  console.log(response.body);
});
import requests
import json

url = "{{authPath}}/{{envID}}/deviceAuthentications/{{deviceAuthID}}"

payload = json.dumps({
  "origin": "https://app.pingone.com",
  "assertion": "{{assertionFromBrowser}}",
  "compatibility": "FULL"
})
headers = {
  'Content-Type': 'application/vnd.pingidentity.assertion.check+json',
  'Authorization': 'Bearer {{accessToken}}'
}

response = requests.request("POST", url, headers=headers, data=payload)

print(response.text)
<?php
require_once 'HTTP/Request2.php';
$request = new HTTP_Request2();
$request->setUrl('{{authPath}}/{{envID}}/deviceAuthentications/{{deviceAuthID}}');
$request->setMethod(HTTP_Request2::METHOD_POST);
$request->setConfig(array(
  'follow_redirects' => TRUE
));
$request->setHeader(array(
  'Content-Type' => 'application/vnd.pingidentity.assertion.check+json',
  'Authorization' => 'Bearer {{accessToken}}'
));
$request->setBody('{\n    "origin": "https://app.pingone.com",\n    "assertion": "{{assertionFromBrowser}}",\n    "compatibility" : "FULL"\n}');
try {
  $response = $request->send();
  if ($response->getStatus() == 200) {
    echo $response->getBody();
  }
  else {
    echo 'Unexpected HTTP status: ' . $response->getStatus() . ' ' .
    $response->getReasonPhrase();
  }
}
catch(HTTP_Request2_Exception $e) {
  echo 'Error: ' . $e->getMessage();
}
require "uri"
require "json"
require "net/http"

url = URI("{{authPath}}/{{envID}}/deviceAuthentications/{{deviceAuthID}}")

http = Net::HTTP.new(url.host, url.port);
request = Net::HTTP::Post.new(url)
request["Content-Type"] = "application/vnd.pingidentity.assertion.check+json"
request["Authorization"] = "Bearer {{accessToken}}"
request.body = JSON.dump({
  "origin": "https://app.pingone.com",
  "assertion": "{{assertionFromBrowser}}",
  "compatibility": "FULL"
})

response = http.request(request)
puts response.read_body
let parameters = "{\n    \"origin\": \"https://app.pingone.com\",\n    \"assertion\": \"{{assertionFromBrowser}}\",\n    \"compatibility\" : \"FULL\"\n}"
let postData = parameters.data(using: .utf8)

var request = URLRequest(url: URL(string: "{{authPath}}/{{envID}}/deviceAuthentications/{{deviceAuthID}}")!,timeoutInterval: Double.infinity)
request.addValue("application/vnd.pingidentity.assertion.check+json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer {{accessToken}}", forHTTPHeaderField: "Authorization")

request.httpMethod = "POST"
request.httpBody = postData

let task = URLSession.shared.dataTask(with: request) { data, response, error in
  guard let data = data else {
    print(String(describing: error))
    return
  }
  print(String(data: data, encoding: .utf8)!)
}

task.resume()