Introduction

In this guide you will learn how a browser-based FIDO login using WebAuthn can be performed programmatically via REST APIs using IBM Security Verify. FIDO supports first factor user authentication using a FIDO2-based authenticator registered at the same site. This authenticator could be built into the user's device (e.g. Mac Touch bar) or could be a portable hardware key connected via USB, Bluetooth, or NFC.

For more background, check out the check out FIDO2 Concepts.

If you want to skip direct to setting up a working demonstration application, check out this blog.

👍

Simpler integration with federation

Instead of supporting FIDO2 natively in your application (as described here), an alternative is to integrate with IBM Security Verify using OpenID Connect (or SAML) and let it take care of authentication outside your application. That way you get support for all of our authentication capabilities (including FIDO2).

Pre-requisites

The WebAuthn API that enables FIDO2 support in browsers can only be accessed from a page loaded with HTTPS. This means your application server must support HTTPS connections.

Browsers must connect to your application server using a fully qualified host name which is registered as an origin in the FIDO2 relying party definition. localhost will not work. This means your application server must be in DNS or, for testing, you can use an entry in a local hosts file.

You must have a FIDO2 Relying Party definition in your IBM Security Verify tenant which has a Relying party identifier and origins set for your web application. See Create a FIDO Relying Party for details.

The API client used by the application must have the following permissions in IBM Security Verify:

  • Manage Authenticator Registrations for all users (for registration)
  • Authenticate any user (to run login flow)
  • Read users and groups (to lookup user data)

For details on creating an API client, see Create an API client.

The application must have acquired an Access Token. If the application is a privileged client, it can use the Client Credentials flow. Otherwise it must use the Access Token obtained via OIDC or an OAuth user flow (for example Authorization Code flow or PolicyAuth flow).

🚧

Registration must also be implemented in the application

FIDO2 authentication only allows authenticators registered for a specific Relying Party to be used for authentication to that Relying Party. This is enforced by DNS domain. As a result, you can't use an authenticator registered using the end-user launchpad for authentication within a custom application.

This guide also covers registration.

Variables

The following variables are needed to run the flows described in this guide:

VariableExample ValueDescription
tenant_urltenant.verify.ibm.comThe URL of your IBM Security Verify tenant.
access_tokeneWn4Z5xChc3q9B9dqbGgFlsHDh7uhAOnmNeKW5EzThe access token obtained from the token endpoint.
originhttps://www.example.comThe base URL used to access your web application. This must be an origin in your Relying Party definition.

Look up Relying Party ID

An IBM Security Verify tenant can support multiple FIDO2 Relying Parties. When calling the FIDO2 Server endpoints, the ID of the Relying Party must be included. You will now lookup this Relying party ID using the origin as a key.

curl -X POST "https://${tenant_url}/v2.0/factors/fido2/relyingparties" -H "Content-Type: application/json" -H "Authorization: Bearer ${access_token}" --data "{\"origin\": \"${origin}\"}"
//Pre-requisites
//var axios = require('axios');
//var tenant_url = "Tenant URL";
//var access_token = "Access Token";
//var origin = "Web App Origin";

async function getRpId(token, origin) {

  var request = {
    method: 'post',
    url: 'https://' + tenant_url + '/v2.0/factors/fido2/relyingparties',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Content-Type': 'application/json'
    },
    data: {
      'origin': origin
    }
  }

  var response = await axios(request);
  var rpId = response.data.fido2[0].id;
  return (rpId);
}

var fido2rp_id = getRpId(access_token, origin);

The JSON response to this call has the following format:

{
  "origin": "https://www.example.com",
  "fido2": [{
    "id": "56753c0b-6c6b-4b4b-8885-dedaec7754a8",
    "rpId": "example.com",
    "name": "Example RP",
    "attestationOptionsPath": "/v2.0/factors/fido2/relyingparties/56753c0b-6c6b-4b4b-8885-dedaec7754a8/attestation/options",
    "assertionOptionsPath": "/v2.0/factors/fido2/relyingparties/56753c0b-6c6b-4b4b-8885-dedaec7754a8/assertion/options",
    "attestationResultPath": "/v2.0/factors/fido2/relyingparties/56753c0b-6c6b-4b4b-8885-dedaec7754a8/attestation/result",
    "assertionResultPath": "/v2.0/factors/fido2/relyingparties/56753c0b-6c6b-4b4b-8885-dedaec7754a8/assertion/result"
  }]
}

You need the fido2[0].id element from this response and store as fido2rp_id.

Registration Flow

Required user variables

In addition to the variables listed above, the registration flow also requires the following user information:

VariableExample ValueDescription
user_uuidNjQyMDAwRVBPVQThe internal unique user ID of the user for whom registration is being performed.
user_displaynameTest UserThe display name of the user for whom registration is being performed. If not provided, username will be used.

If the user has previously logged in, it is likely that both of these values are available in the user JSON object acquired either from OIDC or from the SCIM endpoint.

Browser: Trigger registration

For a seamless user experience, FIDO2 flows are usually run within a single web page using background REST calls initiated in client-side JavaScript. These calls are mediated by the Relying Party application server which avoids CORS issues and the need to have API credentials in the browser.

Here is the client-side JavaScript code to initiate the flow. The fidoRegister() call is triggered by some button or user action on the page. The code simply calls a trigger URL in the Relying Party application server (in this case /fido/register).

var locationHostPort = location.hostname 
			+ (location.port ? ':' + location.port : '');
var baseURL = location.protocol + '//' + locationHostPort;  

function fidoRegister() {
  var options = {
    method: 'GET',
     headers: {
       'Accept': 'application/json'
      }
    }

  fetch(baseURL + '/fido/register', options).then(response => {
    var status = response.status;
    response.json().then(data => {
      processAttestationOptionsResponse(status, data);
    });
  });
}

The response to this call will be an Options Response message that can trigger the WebAuthn API in the browser. The processAttestationOptionsResponse function is defined later in this guide.

App Server: Initiate FIDO2 registration at IBM Security Verify

When the App Server receives the trigger request from the browser, it must initiate the FIDO2 registration flow at IBM Security Verify. The options for the registration are passed in this call:

📘

userId not required if using a delegated Access Token

If the Access Token used for this initiation call is associated with an end-user, you do not need to specify the userId attribute. The registration will be performed for the user associated with the Access Token.

//Pre-requisites
//var axios = require('axios');
//var tenant_url = "Tenant URL";
//var access_token = "Access Token";
//var fidorp_id = "Relying Party ID";
//var user_uuid = "User Unique ID";
//var user_displayname = "User Display Name";

app.get('/fido/register', async (req, res) =>{

  // prepare registration options
  var options = {
    userId: user_uuid,
    displayName: user_displayname,
    authenticatorSelection: {
      requireResidentKey: true, //false to support 2FA-only authenticators
      userVerification: "preferred"
    },
    attestation: "none"
  };

  var request = {
    url: 'https://' + tenant_url
          + "/v2.0/factors/fido2/relyingparties/" + await fidorp_id
          + "/attestation/options",
    method: "POST",
    headers: {
      "Content-type": "application/json",
      "Authorization": "Bearer " + access_token
    },
    data: options
  };

  try {
    var response = await axios(request);
    console.log(JSON.stringify(response.data));
    res.json(response.data);
  } catch (e) {
    console.log(e);
    res.json({error: "Initiation Failed"});
  }
});

The response from IBM Security Verify FIDO2 endpoint has the following format:

{
  "rp": {
    "id": "example.com",
    "name": "Example RP"
  },
  "user": {
    "id": "NjQyMDAwRVBPVQ",
    "name": "testuser",
    "displayName": "Test User"
  },
  "timeout": 240000,
  "challenge": "NphwnbdeNU-8sQMQog8RtUMH4qd3zEzHR_UtPKyTTfs",
  "extensions": {
    "credentialProtectionPolicy": "userVerificationOptional",
    "credProtect": 2
  },
  "authenticatorSelection": {
    "requireResidentKey": true,
    "userVerification": "preferred"
  },
  "attestation": "none",
  "pubKeyCredParams": [{
    "alg": -7,
    "type": "public-key"
  }, {
    "alg": -257,
    "type": "public-key"
  }]
}

This response is not processed by the App Server - it is returned as-is to the web browser.

Browser: Convert base64url to ArrayBuffer and call WebAuthn API

The message sent by the IBM Security Verify FIDO2 endpoint includes a number of Base64url-encoded strings. They are encoded this way to allow for transport in JSON but they need to be ArrayBuffer objects when passed into the WebAuthn API.

Here is a simple utility function to perform the conversion in the browser:

function b64urlToArrayBuf(base64url) {
  var binaryString = window.atob(base64url
    .replace(/-/g, '+')
    .replace(/_/g, '/'));
  var byteArray = new Uint8Array(binaryString.length);
  for (var i = 0; i < binaryString.length; i++) {
    byteArray[i] = binaryString.charCodeAt(i);
  }
  return byteArray.buffer;
}

Here is the code which will perform the required conversions and then call the navigator.credentials.create() function:

function processAttestationOptionsResponse(rspStatus, serverOptions) {

  if (rspStatus == 200) {

    // Convert b64url fields into the ArrayBuffer type
    //required by WebAuthn API
    serverOptions.user.id = b64urlToArrayBuf(serverOptions.user.id);
    serverOptions.challenge = b64urlToArrayBuf(serverOptions.challenge);

    if (serverOptions["excludeCredentials"] != null
        && serverOptions["excludeCredentials"].length > 0) {
      for (var i = 0; i < serverOptions["excludeCredentials"].length; i++) {
        var b64uCID = serverOptions.excludeCredentials[i].id;
        serverOptions.excludeCredentials[i].id = b64urlToArrayBuf(b64uCID);
       }
     }

     var credCreateOptions = { "publicKey": serverOptions };

     // call the webauthn API
      navigator.credentials.create(credCreateOptions).then(handleCreateResult);
  } else {
    console.log("Unable to obtain attestation options. rspStatus: "
                + rspStatus + " response: " + serverOptions);
  }
}

When navigator.credentials.create() is called, the browser will take over processing and manage the registration process with the authenticator. It may ask the user to interact with the authenticator or enter a PIN.

When complete, the handleCreateResult function is called. This needs to process the result from the API call and get it back to the FIDO2 Server (via the Relying Party application server).

Browser: Convert ArrayBuffer to Base64url and send result

The result returned from the WebAuthn API includes a number of ArrayBuffer objects which need to be to be Base64url-encoded before they are sent back to the FIDO2 Server.

Here is a simple utility function to perform the conversion in the browser:

function arrayBufToB64url(byteArrayBuffer) {
  var binaryString = '';
  var byteArray = new Uint8Array(byteArrayBuffer);
  for (var i = 0; i < byteArray.byteLength; i++) {
    binaryString += String.fromCharCode(byteArray[i]);
  }
  return window.btoa(binaryString)
    .replace(/\+/g, '-')
    .replace(/\//g, '_');
}

Here is the code which will perform the required conversions and then send the WebAuthn result to the Relying Party application server:

function handleCreateResult(result) {
  // success
  var createResponse = result;
  console.log("Received response from authenticator.");

  // marshall the important parts of the response into an object which
  // we'll later send to the server for validation.
  // convert buffer arrays into base64url for sending in JSON.
  attestationResponseObject = {
    "id": createResponse.id,
    "rawId": createResponse.id,
    "nickname": "My FIDO Authenticator",
    "type": "public-key",
    "response": {
      "clientDataJSON": arrayBufToB64url(createResponse.response.clientDataJSON),
      "attestationObject": arrayBufToB64url(createResponse.response.attestationObject)
    }
  };

  // if there are extensions results, include those
  var clientExtensionResults = createResponse.getClientExtensionResults();
  if (clientExtensionResults != null) {
    attestationResponseObject["getClientExtensionResults"] = clientExtensionResults;
  }

  var options = {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(attestationResponseObject)
  }

  fetch(baseURL + '/fido/attestation/result', options).then(response => {
    var status = response.status;
    response.json().then(data => {
      if (status == 200) {
        // Done - redirect to received URL
        window.location.href = data.url;
      } else {
        console.log("Unexpected HTTP response");
      }
    });
  });
}

After the conversion, the WebAuthn result sent to the Relying Party application server has the following format:

{
  "id": "4Aa44jAC3EKueXBfkBkHS4h0HQU0mSe7gFrFGI4JW2U",
  "rawId": "4Aa44jAC3EKueXBfkBkHS4h0HQU0mSe7gFrFGI4JW2U",
  "nickname": "My FIDO Authenticator",
  "type": "public-key",
  "response": {
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG...IjpmYWxzZX0=",
    "attestationObject": "o2NmbXRkbm9uZWdh...xOXT2pwACnM-I="
  },
  "getClientExtensionResults": {}
}

App Server: Forward WebAuthn result

The Relying Party application server makes only a small change the the WebAuthn result. The enabled attribute is added. This tells IBM Security Verify to enable this authenticator as part of registration. The result is then forwarded to IBM Security Verify:

//Pre-requisites
//var axios = require('axios');
//var tenant_url = "Tenant URL";
//var access_token = "Access Token";
//var fidorp_id = "Relying Party ID";

app.post('/fido/attestation/result', async (req, res) =>{

  var body = req.body;
  body.enabled = true;

  var request = {
    url: 'https://' + tenant_url
          + "/v2.0/factors/fido2/relyingparties/" + await fidorp_id
          + "/attestation/result",
    method: "POST",
    headers: {
      "Content-type": "application/json",
      "Authorization": "Bearer " + access_token
    },
    data: body
  };

  try {
    response = await axios(request);
    console.log(JSON.stringify(response.data));
    if (response.status == 200) {
      req.session.registerData = response.data
      res.json({url:'/fido/registerdone'});
    } else {
      res.json({url:'/fido/error'});
    }
  } catch (e) {
    console.log(e);
    res.json({url:'/fido/error'});
  }
});

The response from the FIDO2 Server has the following format:

{
  "id": "78c686ee-9c09-4c46-b3e1-f401e5ff4526",
  "userId": "642000EPOU",
  "type": "fido2",
  "created": "2021-01-26T19:52:30.730Z",
  "updated": "2021-01-26T19:52:30.730Z",
  "enabled": true,
  "validated": false,
  "attributes": {
    "attestationType": "None",
    "attestationFormat": "none",
    "nickname": "My FIDO Authenticator",
    "userVerified": true,
    "userPresent": true,
    "credentialId": "4Aa44jAC3EKueXBfkBkHS4h0HQU0mSe7gFrFGI4JW2U",
    "credentialPublicKey": "v2EzYi03YTMmYTECYi0xAWItMlgglH...qcAApzPi/w==",
    "rpId": "example.com",
    "counter": 1170
  },
  "references": {
    "rpUuid": "56753c0b-6c6b-4b4b-8885-dedaec7754a8"
  }
}

How the application server and client-side code handle successful (or failed) registration is up to the application. In the code above, the application server returns a URL which the client-side code redirects to. This is either a success page or an error page. In the case of success, the response from the FIDO2 Server is saved in the application session so that the information it contains is available when rendering the success page.

Authentication Flow

The FIDO2 authentication flow is quite similar to the registration flow.

Browser: Trigger registration

For a seamless user experience, FIDO2 flows are usually run within a single web page using background REST calls initiated in client-side JavaScript. These calls are mediated by the Relying Party application server which avoids CORS issues and the need to have API credentials in the browser.

Here is the client-side JavaScript code to initiate the flow. The fidoAuthenticate() call is triggered by some button or user action on the page. The code simply calls a trigger URL in the Relying Party application server (in this case /fido/authenticate).

var locationHostPort = location.hostname 
			+ (location.port ? ':' + location.port : '');
var baseURL = location.protocol + '//' + locationHostPort;  

function fidoAuthenticate() {

  var options = {
    method: 'GET',
    headers: {
      'Accept': 'application/json'
    }
  }

  fetch(baseURL + '/fido/authenticate', options).then(response => {
    var status = response.status;
    response.json().then(data => {
      processAssertionOptionsResponse(status, data);
    });
  });
}

The response to this call will be an Options Response message that can trigger the WebAuthn API in the browser. The processAssertionOptionsResponse function is defined later in this guide.

App Server: Initiate FIDO2 authentication at IBM Security Verify

When the App Server receives the trigger request from the browser, it must initiate the FIDO2 authentication flow at IBM Security Verify. The options for the authentication are passed in this call:

//Pre-requisites
//var axios = require('axios');
//var tenant_url = "Tenant URL";
//var access_token = "Access Token";
//var fidorp_id = "Relying Party ID";

app.get('/fido/authenticate', async (req, res) =>{

  // prepare authentication options
  var options = {
    //userId = user_uuid, if supporting 2FA-only authenticators
    userVerification: "preferred"
  };
  
  var request = {
    url: 'https://' + tenant_url
          + "/v2.0/factors/fido2/relyingparties/" + await fidorp_id
          + "/assertion/options",
    method: "POST",
    headers: {
      "Content-type": "application/json",
      "Authorization": "Bearer " + access_token
    },
    data: options
  };

  try {
    var response = await axios(request);
    console.log(JSON.stringify(response.data));
    res.json(response.data);
  } catch (e) {
    console.log(e);
    res.json({error: "Initiation Failed"});
  }
});

The response from IBM Security Verify FIDO2 endpoint has the following format:

{
  "rpId": "example.com",
  "timeout": 240000,
  "challenge": "k0ytEzA0SJfBBg4g2-ZvtfwqMK9mrdbRf5SyFlH4vaA"
}

This response is not processed by the App Server - it is returned as-is to the web browser.

Browser: Convert base64url to ArrayBuffer and call WebAuthn API

The message sent by the IBM Security Verify FIDO2 endpoint includes a number of Base64url-encoded strings. They are encoded this way to allow for transport in JSON but they need to be ArrayBuffer objects when passed into the WebAuthn API.

Here is a simple utility function to perform the conversion in the browser:

function b64urlToArrayBuf(base64url) {
  var binaryString = window.atob(base64url
    .replace(/-/g, '+')
    .replace(/_/g, '/'));
  var byteArray = new Uint8Array(binaryString.length);
  for (var i = 0; i < binaryString.length; i++) {
    byteArray[i] = binaryString.charCodeAt(i);
  }
  return byteArray.buffer;
}

Here is the code which will perform the required conversions and then call the navigator.credentials.get() function:

function processAssertionOptionsResponse(rspStatus, serverOptions) {

  if (rspStatus == 200) {

    // Convert Base64url fields to ArrayBuffers required by WebAuthn API
    serverOptions.challenge = b64urlToArrayBuf(serverOptions.challenge);

    if (serverOptions["allowCredentials"] != null
        && serverOptions["allowCredentials"].length > 0) {
      for (var i = 0; i < serverOptions["allowCredentials"].length; i++) {
        var b64uCID = serverOptions.allowCredentials[i].id;
        serverOptions.allowCredentials[i].id = b64urlToArrayBuf(b64uCID);
      }
    }

    var credRequestOptions = {
      "publicKey": serverOptions
    };

    // call the webauthn API
    navigator.credentials.get(credRequestOptions).then(handleGetResult);
  } else {
    console.log("Unable to obtain assertion options. Response: "
                + serverOptions);
  }
}

When navigator.credentials.get() is called, the browser will take over processing and manage the authentication process with the authenticator. It may ask the user to interact with the authenticator or enter a PIN.

When complete, the handleGetResult function is called. This needs to process the result from the API call and get it back to the FIDO2 Server (via the Relying Party application server).

Browser: Convert ArrayBuffer to Base64url and send result

The result returned from the WebAuthn API includes a number of ArrayBuffer objects which need to be to be Base64url-encoded before they are sent back to the FIDO2 Server.

Here is a simple utility function to perform the conversion in the browser:

function arrayBufToB64url(byteArrayBuffer) {
  var binaryString = '';
  var byteArray = new Uint8Array(byteArrayBuffer);
  for (var i = 0; i < byteArray.byteLength; i++) {
    binaryString += String.fromCharCode(byteArray[i]);
  }
  return window.btoa(binaryString)
    .replace(/\+/g, '-')
    .replace(/\//g, '_');
}

Here is the code which will perform the required conversions and then send the WebAuthn result to the Relying Party application server:

function handleGetResult(getResponse) {

  // marshall the important parts of the response into an object
  // which we send to the server for validation.
  // ArrayBuffers are converted to Base64url for transmission as JSON.
  assertionResponseObject = {
    "id": getResponse.id,
    "rawId": getResponse.id,
    "type": "public-key",
    "response": {
      "clientDataJSON": arrayBufToB64url(getResponse.response.clientDataJSON),
          "authenticatorData": arrayBufToB64url(getResponse.response.authenticatorData),
          "signature": arrayBufToB64url(getResponse.response.signature),
          "userHandle": arrayBufToB64url(getResponse.response.userHandle)
    }
  };

  // if there are extensions results, include those
  var clientExtensionResults = getResponse.getClientExtensionResults();
  if (clientExtensionResults != null) {
    assertionResponseObject["getClientExtensionResults"] = clientExtensionResults;
  }

  // send to server for result processing
  var options = {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(assertionResponseObject)
  }

  fetch(baseURL + '/fido/assertion/result', options).then(response => {
    var status = response.status;
    response.json().then(data => {
      if (status == 200) {
        // Done - redirect to received URL
        window.location.href = data.url;
      } else {
        console.log("Unexpected HTTP response code: " + status);
      }
    });
  });
}

After the conversion, the WebAuthn result sent to the Relying Party application server has the following format:

{
  "id": "4Aa44jAC3EKueXBfkBkHS4h0HQU0mSe7gFrFGI4JW2U",
  "rawId": "4Aa44jAC3EKueXBfkBkHS4h0HQU0mSe7gFrFGI4JW2U",
  "type": "public-key",
  "response": {
    "clientDataJSON": "eyJ0eXBlIjoid2ViY...WxzZX0=",
    "authenticatorData": "XOHtoHS8ddDaN...lUr0WKLS71KUFAAAFzw==",
    "signature": "MEQCIEANaAq57CsfBwue...Ljl4zVxVTQ==",
    "userHandle": "NjQyMDAwRVBPVQ=="
  },
  "getClientExtensionResults": {}
}

App Server: Forward WebAuthn result

The Relying Party application server makes no change the the WebAuthn result. It is simply forwarded to IBM Security Verify:

//Pre-requisites
//var axios = require('axios');
//var tenant_url = "Tenant URL";
//var access_token = "Access Token";
//var fidorp_id = "Relying Party ID";

app.post('/fido/assertion/result', async (req, res) =>{

  // Add ?returnJwt=true to URL to get back a JWT
  var request = {
    url: 'https://' + tenant_url
          + "/v2.0/factors/fido2/relyingparties/" + await fidorp_id
          + "/assertion/result",
    method: "POST",
    headers: {
      "Content-type": "application/json",
      "Authorization": "Bearer " + access_token
    },
    data: req.body
  };

  try {
    var response = await axios(request);
    req.session.user = await getUserSCIMById(response.data.userId);
    res.json({'url': '/authenticatedone'});
  } catch (e) {
    console.log(e);
    res.json({'url': '/fido/error'});
  }
});

The response from the FIDO2 Server has the following format:

{
  "id": "78c686ee-9c09-4c46-b3e1-f401e5ff4526",
  "userId": "642000EPOU",
  "type": "fido2",
  "created": "2021-01-26T19:52:30.730Z",
  "updated": "2021-01-26T19:52:30.730Z",
  "attempted": "2021-01-27T10:16:56.374Z",
  "enabled": true,
  "validated": true,
  "attributes": {
    "attestationType": "None",
    "attestationFormat": "none",
    "nickname": "My FIDO Authenticator",
    "userVerified": true,
    "userPresent": true,
    "credentialId": "4Aa44jAC3EKueXBfkBkHS4h0HQU0mSe7gFrFGI4JW2U",
    "credentialPublicKey": "v2EzYi03YTMmYTECYi0xAWItMlggl...IsTl09qcAApzPi/w==",
    "rpId": "www.example.com",
    "counter": 1487
  },
  "references": {
    "rpUuid": "56753c0b-6c6b-4b4b-8885-dedaec7754a8"
  }
}

In this response, the userId attribute contains the IBM Security Verify unique user id for the authenticated user. This can be used to obtain user details by calling the IBM Security Verify SCIM interface:

//Pre-requisites
//var axios = require('axios');
//var tenant_url = "Tenant URL";
//var access_token = "Access Token";

async function getUserSCIMById(userid) {

  var request = {
    method: 'get',
    url: 'https://' + tenant_url + '/v2.0/Users/' + userid,
    headers: {'Authorization': 'Bearer ' + access_token}
  };

  try {
    var response = await axios(request);
    return response.data;
  } catch (error) {
    console.log(error);
  };
}

How the application server and client-side code handle successful (or failed) authentication is up to the application. In the code above, the application server returns a URL which the client-side code redirects to. This is either a success page or an error page. In the case of success, the SCIM object for the authenticated user is saved in the application session so that the information it contains is available in subsequent application processing.

📘

FIDO2 as part of policyauth flow

If you use FIDO2 authentication within a policy auth flow, you need to add ?returnJwt=true to the call to /assertion/result endpoint. This will cause IBM Security Verify to return a JWT in an assertion attribute which can be used for a call to the token endpoint.

💎

Jon Harry, IBM Security