Example - PingID FIDO security key


Overview

The examples on this page present the steps comprising the following FIDO security key flows:

  1. Pairing a user’s FIDO security key to their profile using the WebAuthn pairing process.
  2. Authentication using a FIDO security key.

For information on WebAuthn, refer to the World Wide Web Consortium (W3C) WebAuthn reference https://www.w3.org/TR/webauthn/.
For further sample WebAuthn registration and authentication scenarios refer to the W3C WebAuthn scenarios reference https://www.w3.org/TR/webauthn/#sample-scenarios.

FIDO security key pairing flow

The WebAuthn pairing process using the PingID APIs requires the following flow:

  1. Initiate the pairing process using the WebAuthnStartPairing API. For example:

    https://idpxnyl3m.pingidentity.com/pingid/rest/4/webauthnstartpairing/do

    Request Body Parameters:

    "reqBody": {
        "rpId": "pingone.com",
        "rpName": "PingID Web Authentication",
        "userName": "fidouser1",
        "webauthnType": "WebAuthn"
      }

    The API call returns the response body, similar to the following example:

    {
      "responseBody": {
        "publicKeyCredentialOptions": "<publicKeyCredentialOptions >",
        "sessionId": "b6367b8e-1da8-490d-9c6d-6814cb2cfc81",
        "errorMsg": "",
        "errorId": 200,
        "uniqueMsgId": "webs_0vntDNAOWRwTL1W5_RbsNzuol1lu0eKfzRSJeC86QKM",
        "clientData": null
      }
    }
  2. Call the navigator.credentials.create method using the publicKeyCredentialOptions returned from the WebAuthnStartPairing API, per the WebAuthnRegistration 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.WebAuthnRegistration = function Register(publicKeyCredentialCreationOptions) {
        return new Promise(function(resolve, reject) {
            if(!IsWebAuthnSupported()) {
                reject(Error("UnSupportedBrowserError"));
            }
            const options = JSON.parse(publicKeyCredentialCreationOptions);
            const publicKeyCredential = {};
            publicKeyCredential.rp = options.rp;
            publicKeyCredential.user = options.user;
            publicKeyCredential.user.id = strToBin(bin2base64str(options.user.id));
            publicKeyCredential.challenge = strToBin(bin2base64str(options.challenge));
            publicKeyCredential.pubKeyCredParams = options.pubKeyCredParams;
            // Optional parameters
            if ('timeout' in options) {
                publicKeyCredential.timeout = options.timeout;
            }
            if ('excludeCredentials' in options) {
                publicKeyCredential.excludeCredentials = credentialListConversion(options.excludeCredentials);
            }
            if ('authenticatorSelection' in options) {
                publicKeyCredential.authenticatorSelection = options.authenticatorSelection;
            }
            if ('attestation' in options) {
                publicKeyCredential.attestation = options.attestation;
            }
            if ('extensions' in options) {
                publicKeyCredential.extensions = options.extensions;
            }
            console.log(publicKeyCredential);
            navigator.credentials.create({"publicKey": publicKeyCredential, "signal": authAbortSignal})
                .then(function (newCredentialInfo) {
                    // Send new credential info to server for verification and registration.
                    console.log(newCredentialInfo);
                    const publicKeyCredential = {};
            if ('id' in newCredentialInfo) {
                        publicKeyCredential.id = newCredentialInfo.id;
                    }
                    if ('type' in newCredentialInfo) {
                        publicKeyCredential.type = newCredentialInfo.type;
                    }
                    if ('rawId' in newCredentialInfo) {
                        publicKeyCredential.rawId = binToStr(newCredentialInfo.rawId);
                    }
                    if (!newCredentialInfo.response) {
                        throw "Missing 'response' attribute in credential response";
                    }
                    const response = {};
                    response.clientDataJSON = binToStr(newCredentialInfo.response.clientDataJSON);
                    response.attestationObject = Array.from(
                        new Uint8Array(newCredentialInfo.response.attestationObject));
                    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 bin2base64str(bin){
        return base64String = btoa(String.fromCharCode.apply(null, new Uint8Array(bin)));
    }
    
    function strToBin(str) {
        var arr = [];
        var bin = atob(str);
        for (var i=0; i < bin.length; i++) {
            arr[i] = bin.charCodeAt(i);
        }
        return new Uint8Array(arr);
    }
    
    function binToStr(bin) {
        return btoa(String.fromCharCode.apply(null, new Uint8Array(bin)));
    }
    
    function credentialListConversion(list) {
        var credList = [];
        for (var i=0; i < list.length; i++) {
            const 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;
    }
  3. Complete the pairing process, using the WebAuthnFinishPairing API. The publicKeyCredentialJson parameter must be assigned the value returned from the navigator.credentials.create function of the previous step (publicKeyCredential from the code example above). For example:

    https://idpxnyl3m.pingidentity.com/pingid/rest/4/webauthnfinishpairing/do

    Request Body Parameters:

    "reqBody": {
    	    "rpId": "pingone.com",
    	    "sessionId": "b6367b8e-1da8-490d-9c6d-6814cb2cfc81",
    	    "userName": "fidouser1",
    	    "origin": "https://admin.pingone.com",
    	    "publicKeyCredentialJson": "<output returned from navigator.credentials.create  function>"
    	  }

FIDO security key authentication flow

The WebAuthn authentication process using the PingID APIs requires the following flow:

  1. Initiate the authentication process using the StartAuthentication API. For example:

    https://idpxnyl3m.pingidentity.com/pingid/rest/4/startauthentication/do

    Request Body Parameters:

    "reqBody": {
      "spAlias": "web",
      "userName": "fidouser1",
      "deviceId": 1084997072428840000,
      "formParameters": {
    	  "isWebAuthnSupportedByBrowser": "true"
    	  }
      }

    The StartAuthentication API acts as a flow manager. It checks policies and other factors, and returns a status (errorId 30011) to indicate that the FIDO security key must be used. For example:

    "responseBody": {
        "multipleDevicesEnabled": true,
        "userDevices": [
          {
            "enrollment": "2019-02-13 08:39:51.333",
            "sentNotClaimedSms": -1,
            "sentClaimedSms": -1,
            "availableNotClaimedSms": 0,
            "availableClaimedSms": 0,
            "pushEnabled": false,
            "displayID": null,
            "deviceUuid": "0f0eaf16-bc75-b440-0f0e-af16bc75b440",
            "deviceId": 1084997072428840000,
            "countryCode": null,
            "phoneNumber": null,
            "deviceModel": null,
            "appVersion": null,
            "email": null,
            "deviceRole": "PRIMARY",
            "hasWatch": false,
            "nickname": "Security Key 1",
            "osVersion": null,
            "type": "Security Key"
          }
        ],
        "extendedAuthenticationDetails": {
          "adminHelp": "<p style=&quot;color:blue;&quot;></p>",
          "lastSuccessfulLogin": null,
          "gracePeriod": 1504083908422
        },
        "localFallbackDeviceList": null,
        "allowedAuthenticationMethods": null,
        "cookie": null,
        "errorParams": null,
        "authenticatingDeviceId": "0f0eaf16-bc75-b440-0f0e-af16bc75b440",
        "sessionId": "webs_SA87Pd4qhAY6JM-ULRE7-yX-zlPvsQfN3bRzMEB86Gw",
        "errorId": 30011,
        "errorMsg": "webauthn.flow",
        "uniqueMsgId": "webs_SA87Pd4qhAY6JM-ULRE7-yX-zlPvsQfN3bRzMEB86Gw",
        "clientData": null
      }
    }
  2. Invoke the WebAuthnStartAuth API. For example:

    https://idpxnyl3m.pingidentity.com/pingid/rest/4/webauthnstartauth/do

    Request Body Parameters:

    "reqBody": {
        "rpId": "pingone.com",
        "userName": "fidouser1",
        "deviceUuid": "0f0eaf16-bc75-b440-0f0e-af16bc75b440",
        "webauthnType: "WebAuthn"
      }

    The WebAuthnStartAuth API call returns the response body, similar to the following example:

    "responseBody": {
        "publicKeyCredentialOptions": "{\"challenge\":[-105,12,-73,3,-45,-70,59,120,30,100,-65,1,-119,49,61,126,-64,112,-29,-62,-113,87,51,70,-43,20,-8,-104,70,-8,-26,81],\"timeout\":120000,\"rpId\":\"pingone.com\",\"allowCredentials\":[{\"type\":\"public-key\",\"id\":[-108,35,43,99,-118,85,-65,-100,-121,97,-48,-100,-94,67,-35,15,-29,20,-3,-115,13,26,36,14,118,43,34,-53,-6,-59,-124,44,-58,60,-3,3,-19,-91,75,-46,112,-20,-109,121,-111,-80,-17,-64,0,63,-9,65,-87,-105,87,22,33,91,-107,-63,43,29,58,-20]}],\"userVerification\":\"preferred\"}",
        "sessionId": "a6b4ee5f-fbe0-403b-838a-4bb6b0520485",
        "errorId": 200,
        "errorMsg": "",
        "uniqueMsgId": "webs_-uD8DyzzMPORJWbF-M3PZxW1wrSAF-swE3pMjwGlj8k",
       "clientData": null
      }
  3. Call the navigator.credentials.get method using the publicKeyCredentialOptions returned from the WebAuthnStartAuth API in the previous step. 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.WebAuthnAuthentication = function Authenticate(publicKeyCredentialRequestOptions) {
        return new Promise(function(resolve, reject) {
            if(!IsWebAuthnSupported()) {
                reject(Error("UnSupportedBrowserError"));
            }
            const options = JSON.parse(publicKeyCredentialRequestOptions);
            const publicKeyCredential = {};
            publicKeyCredential.challenge = strToBin(bin2base64str(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);
                    const publicKeyCredential = {};
                    if ('id' in assertion) {
                        publicKeyCredential.id = assertion.id;
                    }
                    if ('rawId' in assertion) {
                        publicKeyCredential.rawId = binToStr(assertion.rawId);
                    }
                    if ('type' in assertion) {
                        publicKeyCredential.type = assertion.type;
                    }
                    const response = {};
                    response.clientDataJSON = binToStr(assertion.response.clientDataJSON);
                    response.authenticatorData = binToStr(assertion.response.authenticatorData);
                    response.signature = Array.from(new Uint8Array(assertion.response.signature));
                    response.userHandle = binToStr(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 bin2base64str(bin){
        return base64String = btoa(String.fromCharCode.apply(null, new Uint8Array(bin)));
    }
    
    function strToBin(str) {
        var arr = [];
        var bin = atob(str);
        for (var i=0; i < bin.length; i++) {
            arr[i] = bin.charCodeAt(i);
        }
        return new Uint8Array(arr);
    }
    
    function binToStr(bin) {
        return btoa(String.fromCharCode.apply(null, new Uint8Array(bin)));
    }
    
    function credentialListConversion(list) {
        var credList = [];
        for (var i=0; i < list.length; i++) {
            const 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;
    }
  4. Complete the authentication process, using the AuthOffline API. The AuthOffline API’s publicKeyCredentialJson parameter must be assigned the value returned in the publicKeyCredential parameter of the navigator.credentials.get function of the previous step. For example:

    https://idpxnyl3m.pingidentity.com/pingid/rest/4/authoffline/do

    Request Body Parameters:

    "reqBody": {
        "spAlias": "web",
        "userName": "fidouser1",
        "otp": "{\"id\":\"NclJtG2xeNiqDcvPCvjyipoyk7g4lYLSjuGfXefX-4eIAaxF9BRyVQROHJMWU9F9FuqAlbKvw1L0i-Zq2aYSCw\",\"rawId\":\"NclJtG2xeNiqDcvPCvjyipoyk7g4lYLSjuGfXefX 4eIAaxF9BRyVQROHJMWU9F9FuqAlbKvw1L0i Zq2aYSCw==\",\"type\":\"public-key\",\"response\":{\"clientDataJSON\":\"eyJjaGFsbGVuZ2UiOiIyNzU5eTE3WG51eW82dnJMazNpdDNuT3pMYkw4RmF0eFV1Y2RrVWREb1RZIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9\",\"authenticatorData\":\"SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MBAAABOg==\",\"signature\":[48,69,2,33,0,188,55,238,55,35,174,165,142,28,249,147,76,141,98,81,230,248,204,205,186,5,14,32,145,119,9,88,102,28,48,194,177,2,32,44,3,86,43,30,135,43,92,223,227,183,19,157,170,192,91,37,188,54,137,45,23,155,196,225,19,127,103,83,149,122,61],\"userHandle\":\"\"}}",
        "sessionId": "webs_SA87Pd4qhAY6JM-ULRE7-yX-zlPvsQfN3bRzMEB86Gw",
        "formParameters": {
            "origin": "https://admin.pingone.com", 
            "rpId": "pingone.com",
            "authWebauthnSessionId": "a6b4ee5f-fbe0-403b-838a-4bb6b0520485"
             }
      }