Orchestration SDKs

Stepping through DaVinci flows

PingOne JavaScript


To authenticate your users the Orchestration SDK for JavaScript DaVinci module must start the flow, and step through each node.

For information on which connectors and fields the DaVinci module supports, refer to Compatibility.

Starting a DaVinci flow

To start a DaVinci flow, call the start() method on your new client object:

Start a DaVinci flow
let node = await davinciClient.start();

Adding custom parameters

When starting a DaVinci module you can add additional key-pair parameters. The DaVinci module will append these parameters as query strings to the initial OAuth 2.0 call to the /authorize endpoint.

You can access these additional OAuth 2.0 parameters in your DaVinci flows by using the authorizationRequest.<customParameter> property.

To add parameters when starting the client, create an object of the key-value pairs and pass it as a query parameter to the start() function:

const query = {
  customQueryParameter: 'customValue'
}

let node = await davinciClient.start({query});

You can add any parameters to the request as required. For example, you could add acr_values to the request to the /authorize endpoint.

Determining DaVinci flow node type

Each step of the flow returns one of four node types:

continue

This type indicates there is input required from the client. The node object for this type contains a list of collector objects, which describe the information it requires from the client.

success

This type indicates the flow is complete, and authentication was successful.

error

This type indicates an error in the data sent to the server. For example, an email address in an incorrect format, or a password that does not meet complexity requirements.

You can correct the error and resubmit to continue the flow.

failure

This type indicates that the flow could not be completed and must be restarted. This can be caused by a server error, or a timeout.

Use node.status to determine which node type the server has returned:

Determine node type using the node.status property
let node = await davinciClient.start();

switch (node.status) {
  case 'continue':
    return renderContinue();
  case 'success':
    return renderSuccess();
  case 'error':
    return renderError();
  default: // Handle 'failure' node type
    return renderFailure();
}

Handling DaVinci flow collectors in continue nodes

The continue node type contains a list of collector objects. These collectors define what information or action to request from the user, or browser.

For a list of supported collectors, refer to Supported PingOne fields and collectors.

The Orchestration SDK for JavaScript groups collectors that have similar traits together into categories. For example, collectors that only require a single primitive value to be returned to the server, such as a username or password string, or a single value from a drop-down list are grouped together in a single value collectors category.

To complete a DaVinci flow, we recommend that you either implement a component for a category of collectors, or implement a component for each collector type that you will encounter in the flow.

Your app iterates through the flow and handles each collector as you encounter it.

Example of iterating collectors and using components
const collectors = davinciClient.getCollectors();
collectors.forEach((collector) => {
  if (collector.type === 'TextCollector') {
    textComponent(
      collector, // Object with the collector details
      davinciClient.update(collector), // Returns an update function for this collector
      davinciClient.validate(collector), // Returns a validate function for this collector
    );
  } else if (collector.type === 'PasswordCollector') {
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
    collector;
    passwordComponent(
      collector, // Object with the collector details
      davinciClient.update(collector), // Returns an update function for this collector
    );
  } else if (collector.type === 'SubmitCollector') {
    submitButtonComponent(
      collector, // Object with the collector details
    );
  } else if (collector.type === 'FlowCollector') {
    flowLinkComponent(
      collector, // Object with the collector details
      davinciClient.flow({
        // Returns a function to call the flow from within component
        action: collector.output.key,
      }),
      renderForm, // Enable re-rendering the form
    );
  }
});

Example 1. Handling TextCollector with a component

This example shows how to update a collector with a value gathered from your user.

Pass both a collector and updater object into a component that renders the appropriate user interface, captures the user’s input, and then updates the collector, ready to return to the server.

Example TextCollector mapping
const collectors = davinciClient.getCollectors();
collectors.map((collector) => {
  if (collector.type === 'TextCollector') {
    renderTextCollector(collector, davinciClient.update(collector));
  }
});

Mutating the node object, the collectors array, or any other properties does not alter the internal state of the DaVinci module.

The internal data the client stores is immutable and can only be updated using the provided APIs, not through property assignment.

Your renderTextCollector would resemble the following:

Example TextCollector updater component
function renderTextCollector(collector, updater) {
  // ... component logic

  function onClick(event) {
    updater(event.target.value);
  }

  // render code
}

Example 2. Handling FlowCollector with a component

This example shows how change from the current flow to an alternate flow, such as a reset password or registration flow.

To switch flows, call the flow method on the davinciClient passing the key property to identify the new flow.

Example FlowCollector mapping
const collectors = davinciClient.getCollectors();
collectors.map((collector) => {
  if (collector.type === 'FlowCollector') {
    renderFlowCollector(collector, davinciClient.flow(collector));
  }
});

This returns a function you can call when the user interacts with it.

Example flowCollector component
function renderFlowCollector(collector, startFlow) {
  // ... component logic

  function onClick(event) {
    startFlow();
  }

  // render code
}

Example 3. Handling SubmitCollector with a component

This example shows how submit the current node and its collected values back to the server. The collection of the data is already complete so an updater component is not required. This collector only renders the button for the user to submit the collected data.

Example SubmitCollector mapping
const collectors = davinciClient.getCollectors();
collectors.map((collector) => {
  if (collector.type === 'SubmitCollector') {
    renderSubmitCollector(
      collector, // This is the only argument you will need to pass
    );
  }
});

Continuing a DaVinci flow

After collecting the data for a node you can proceed to the next node in the flow by calling the next() method on your DaVinci module object.

This can be the result of a user clicking on the button rendered from the SubmitCollector, from the submit event of an HTML form, or by programmatically triggering the submission in the application layer.

Continue a DaVinci flow using next()
let nextStep = davinciClient.next();

You do not need to pass any parameters into the next method as the DaVinci module internally stores the updated object, ready to return to the PingOne server.

The server responds with a new node object, just like when starting a flow initially.

Loop again through conditional checks on the new node’s type to render the appropriate UI or take the appropriate action.

Handling DaVinci flow error nodes

DaVinci flows return the error node type when it receives data that is incorrect, but you can fix the data and resubmit. For example, an email value submitted in an invalid format or a new password that is too short.

This is different than a failure node type which you cannot resubmit and instead you must restart the entire flow.

You can retain a reference to the node you submit in case the next node you receive is an error type. If so, you can re-render the previous form, and inject the error information from the new error node.

After the user revises the data call next() as you did before.

Handling DaVinci flow failure nodes

DaVinci flows return the failure node type if there has been an issue that prevents the flow from continuing. For example, the flow times out or suffers an HTTP 500 server error.

You should offer to restart the flow on receipt of a failure node type.

Restart a DaVinci flow on receipt of a failure node type
const node = await davinciClient.next();

if (node.status === 'failure') {
  const error = davinciClient.getError();
  renderError(error);

  // ... user clicks button to restart flow
  const freshNode = davinciClient.start();
}

Handling DaVinci flow success nodes

DaVinci flows return the success node type when the user completes the flow and PingOne issues them a session.

On receipt of a success node type you should use the OAuth 2.0 authorization Code and state properties from the node and use them to obtain an access token on behalf of the user.

To obtain an access token, leverage the Orchestration SDK for JavaScript.

Example of obtaining an access token using the Orchestration SDK for JavaScript
// ... other imports
import { davinci } from '@forgerock/davinci-client';
import { Config, TokenManager } from '@forgerock/javascript-sdk';

// ... other config or initialization code

// This Config.set accepts the same config schema as the davinci function
Config.set(config);

const node = await davinciClient.next();

if (node.status === 'success') {
  const clientInfo = davinciClient.getClient();

  const code = clientInfo.authorization?.code || '';
  const state = clientInfo.authorization?.state || '';

  const tokens = await TokenManager.getTokens({
    query: {
      code, state
    }
  });

  // user now has session and OIDC tokens
}