Launching a flow with the widget
Launch a prepared flow with a widget.
This method is effective when you want to launch the flow within the current page instead of redirecting the user.
|
Configuring the Flow
Prepare a flow to be launched with the widget.
About this task
|
Steps
-
Click the Flows tab.
-
Find the flow and click ... → Edit
-
Add a nonce to the input schema.
When you launch the flow, you provide a nonce value. This value is returned when the flow completes, letting you verify the flow’s completion.
-
Click Flow Options ( ) → Input Schema.
-
Click Add.
-
In the Parameter Name field, enter
nonce. -
In the Data Type list, select String.
-
Select Required.
-
Click Save.
-
-
At the end of the success path, add a PingOne Authentication node or an HTTP node to send a success response.
-
At the end of any failure paths, add a PingOne Authentication node or an HTTP node to send an error response.
-
Click Save.
-
Click Deploy.
Creating an Application
Create an application in DaVinci to enable your flow.
About this task
| If you want to use an existing application to launch the flow, you can start at step 5. |
Steps
-
Click the Applications tab.
-
Click Add Application.
The Add Application modal opens.
-
In the Name field, enter a name for the application.
-
Click Create.
-
Find the application and click Edit.
-
On the General tab, note the following parameters:
-
Note the Company ID.
-
-
Create a flow policy:
-
Click the Flow Policy tab.
-
Click Add Flow Policy.
-
In the Name field, enter a name for the flow policy.
-
In the flow list, select your flow.
-
In the version list, select Latest Version.
-
Click Create Flow Policy.
The Edit Your Weight Distribution modal opens.
This example only uses one flow, but if your flow policy included multiple flows or flow versions, you could use this modal to split traffic between them.
-
Click Save Flow Policy.
-
Note the Policy ID of your flow policy.
-
Adding CORS Settings in PingOne
Add the domain from which the flow will be launched to the DaVinci application in PingOne to prevent CORS issues.
Steps
-
Sign on to PingOne and go to Applications > Applications.
-
Click the PingOne DaVinci Connection application.
You can also add the CORS setting to any other PingOne application. For tracking and consistency, we recommend adding the CORS setting to the PingOne DaVinci application.
-
Click the Configuration tab.
-
Click the Pencil icon.
-
In the CORS Settings section, select Allow specific origins.
-
In the Allowed Origins field, enter the domain from which you plan to launch the flow.
-
Click Save.
Integration example
This example is a simple hello-world application that launches a flow using the widget. The HTML code specifies the flow policy ID. A server-side JavaScript process securely provides the API key and company ID. The example checks for an existing user session, which is stored in a cookie created by the Return Success node during a previous flow execution.
|
The following example won’t work unless you add your region-specific information. Replace any instances of
|
|
The following example uses the |
HTML invocation code
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8" />
<head>
<title>Simple HTML/JS widget Testing</title>
<link rel="stylesheet" href="https://assets.pingone.<region>/ux/end-user-nano/0.1.0-alpha.9/end-user-nano.css" />
<link rel="stylesheet" href="https://assets.pingone.<region>/ux/astro-nano/0.1.0-alpha.11/icons.css" />
</head>
<body>
<h3>Simple HTML/JS widget Test</h3>
<br />
<p>Widget will be displayed below</p>
<br />
<div id="widget" class="dvWidget"></div>
<script type="text/javascript" src="https://assets.pingone.<region>/davinci/latest/davinci.js"></script>
<script>
// Only the policyId is needed on the frontend.
// API keys and companyId are securely managed server-side in bxi-server.js.
const policyId = "<policy_id>";
// Helper function to set a cookie with security flags
const setCookie = (name, value, options = {}) => {
let cookieString = `${name}=${encodeURIComponent(value)}`;
// Set expiration (default: session cookie, or 1 day if specified)
if (options.maxAge !== undefined) {
cookieString += `; Max-Age=${options.maxAge}`;
}
// Security flags for best practices
cookieString += "; Secure"; // Only transmit over HTTPS
cookieString += "; SameSite=Strict"; // Prevent CSRF attacks
// Note: HttpOnly flag cannot be set from JavaScript and must be set server-side
// For full security, consider having your backend endpoint set the cookie
document.cookie = cookieString;
};
// Callback functions
const successCallback = (response) => {
console.log(response);
// Store DaVinci session token in a secure cookie (1 day expiration).
// The server reads this as "DV-ST" to maintain session continuity.
setCookie("DV-ST", response.sessionToken, { maxAge: 86400 });
};
const errorCallback = (error) => {
console.log(error);
};
const onCloseModal = () => {
console.log("onCloseModal");
};
// Initialize and render the DaVinci widget
(async () => {
try {
// Request a DaVinci SDK token from the backend server.
// The server handles API key management and session token forwarding
// so that sensitive credentials are never exposed to the browser.
const tokenResponse = await fetch("/dvtoken", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include", // Send cookies (DV-ST) to the server
body: JSON.stringify({ policyId }),
});
if (!tokenResponse.ok) {
const errorData = await tokenResponse.json();
throw new Error(errorData.error || "Failed to retrieve DaVinci SDK token from server.");
}
// Server returns { token, companyId, apiRoot }
const { token, companyId, apiRoot } = await tokenResponse.json();
const props = {
config: {
method: "runFlow",
apiRoot: apiRoot,
accessToken: token,
includeHttpCredentials: true,
companyId,
policyId,
// parameters: {
// nonce: "string to validate",
// },
},
useModal: false,
successCallback,
errorCallback,
onCloseModal,
};
// Invoke the Widget
console.log(props);
davinci.skRenderScreen(
document.querySelector(".dvWidget"),
props
);
} catch (error) {
console.error("Error initializing DaVinci widget:", error);
}
})();
</script>
</body>
</html>
Server-side JavaScript code
const Fastify = require('fastify');
const fastifyCookie = require('@fastify/cookie');
const fastifyCors = require('@fastify/cors');
const fastifyStatic = require('@fastify/static');
const dotenv = require('dotenv');
const path = require('path');
// Load environment variables from .env file (relative to this script's directory)
dotenv.config({ path: path.join(dirname, '.env') });
// Initialize Fastify with logging enabled
const fastify = Fastify({ logger: true });
// Register the @fastify/cookie plugin for request.cookies support
fastify.register(fastifyCookie);
// Register CORS to allow browser requests (credentials: true for cookie support)
fastify.register(fastifyCors, {
origin: true,
credentials: true,
});
// Serve static files (HTML, CSS, JS) from the current directory
fastify.register(fastifyStatic, {
root: path.join(dirname),
prefix: '/',
});
// Get a dv token from the server, we do this in server.js as a security best practice so
// API Keys don't need to be exposed on the front-end.
fastify.post('/dvtoken', {
schema: {
body: {
type: 'object',
required: ['policyId'],
properties: {
policyId: { type: 'string' },
apiKey: { type: 'string' },
companyId: { type: 'string' },
flowParameters: { type: 'object' },
},
},
},
}, async function (request, reply) {
// Allow for apiKey and companyId overrides to come from front end, even though it's not encouraged.
// Optional chaining guards against null/undefined values for optional body properties.
const apiKey = request.body?.apiKey || process.env.BXI_API_KEY;
const companyId = request.body?.companyId || process.env.BXI_COMPANY_ID;
let body = {
policyId: request.body.policyId,
};
if (request.cookies?.['DV-ST']) {
body.global = {
sessionToken: request.cookies['DV-ST'],
};
}
if (request.body?.flowParameters) {
body.parameters = request.body.flowParameters;
}
const dvBaseUrl = `${process.env.BXI_API_URL}/`;
const dvSdkTokenBaseUrl = `${process.env.BXI_SDK_TOKEN_URL}/v1`;
const tokenRequest = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-SK-API-KEY': apiKey,
},
body: JSON.stringify(body),
};
const tokenResponse = await fetch(
`${dvSdkTokenBaseUrl}/company/${companyId}/sdktoken`,
tokenRequest
); // Endpoint is case sensitive in Davinci V2
const parsedResponse = await tokenResponse.json();
if (!parsedResponse?.success) {
request.log.error('An error occurred getting DaVinci token');
request.log.error({ parsedResponse }, 'Parsed Response');
return reply.code(500).send({
error: `An error occurred getting DaVinci token. See server logs for more details, code: ${parsedResponse?.httpResponseCode}, message: '${parsedResponse?.message}'.`,
});
}
request.log.info('Successfully retrieved sdktoken for DaVinci');
return reply.send({
token: parsedResponse.access_token,
companyId: companyId,
apiRoot: dvBaseUrl,
});
});
// Start the server
const start = async () => {
try {
const port = process.env.PORT || 3000;
await fastify.listen({ port, host: '0.0.0.0' });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
Getting the SDK token
When implementing a DaVinci application integration using the widget method, be aware that the POST <authPath>/<companyID>/davinci/policy/<davinciFlowPolicyID>/start request that invokes the flow takes an SDK token to authenticate. However, the call to get a DaVinci SDK token, GET <orchestratePath>/company/<companyID>/sdktoken, requires the application’s API key to authenticate.
|
The |
The following sample shows a server-side code snippet from a server.js file used to generate the DaVinci SDK token without exposing the application’s API key. You can use this in your server-side code if you already have a server-side component. It’s not necessary if you’re using the example described in the previous section.
|
The sample won’t work unless you add your region-specific information. Replace any instances of <region> with your regional top-level domain:
|
/************************
* DaVinci components
************************/
// Get a Widget sdkToken
function getDVToken(cb) {
const url = 'https://orchestrate-api.pingone.<region>/v1/company/${companyId}/sdktoken';
fetch(url, {
headers: {
"X-SK-API-KEY": <DV API Key>
},
method: "GET"
})
.then(res => res.json())
.then(data => cb(data))
.catch(err => console.log("Error: ", err));
}
Using the widget with a return URL
When using the widget with social providers, PingID, or other services requiring you to supply a return URL, add coding similar to this.
Retrieve the value for a continueToken.
/**
* Event listener for window.load()
* Listening for a query parameter of `continueToken`
*/
window.addEventListener('load', (event) => {
var urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('continueToken')) {
// flush parameter from window url
window.history.pushState({}, document.title, window.location.pathname);
continueWidget('policyId', 'authnflow', urlParams.get('continueToken'))
}
});
Invoke the widget and continue the flow using the continueToken.
|
The following example won’t work unless you add your region-specific information. Replace any instances of
|
/**
* Recreates an instance of the Widget placed on the page,
* and uses the provided 'continueToken' value instead of fetching a new one from DaVinci
* @param {string} policyId - The flow policy ID for the widget to run
* @param {string} divId - Location on the page to place the Widget
* @param {string} continueToken - Value of the 'continueToken' query parameter
*/
function continueWidget(policyId, divId, continueToken){
/**
* Creates an instance of the Widget with the following:
* @param {object} props - Properties for the Widget execution
* @param {object} props.config - Object containing the Widget configuration
* @param {string} props.config.method - Widget run method { "runFlow" | "continueFlow" }
* @param {string} props.config.apiRoot - URL of the DaVinci instance for this flow
* @param {string} props.config.accessToken - @param {string} continueToken
* @param {string} props.config.companyId - ID of the PingOne environment that contains the flow
* @param {string} props.config.policyId - Flow policy ID for the Widget to run
* @param {boolean} props.useModal - Present Widget as a modal, instead of embedded
* @param {requestCallback} props.successCallback - The callback that handles the Success response
* @param {requestCallback} props.errorCallback - The callback that handles the Error response
* @param {requestCallback} props.onCloseModal - The callback that handles the modal close response ('useModal' == true)
*/
var props= {
config: {
method: 'continueFlow',
apiRoot: 'https://auth.pingone.<region>/',
accessToken: continueToken,
companyId: <companyID>,
policyId: policyId`
},
useModal: false,
successCallback, errorCallback, onCloseModal
}
/*** Invoke DaVinci Widget ****/
davinci.skRenderScreen(document.getElementById(divId),props)
}
Limiting the cookies passed by the widget
By default, the widget passes to DaVinci all cookies that could be presented to the origin. There can be a large number of cookies, only some of which are likely to be used for your DaVinci flows. In some cases, this can cause a 431 Error: Request Header Fields Too Large. To avoid this possibility, you can set the originCookies property.
The originCookies property is optional and accepts a string array of cookie names. Only the cookies specified will be passed by the widget to DaVinci. Set this as you would any of the DaVinci properties.
|
The following example won’t work unless you add your region-specific information. Replace any instances of
|
var props= {
config: {
apiRoot: 'https://auth.pingone.<region>/',
companyId: <companyID>,
policyId: <policyId>,
originCookies: ["cookie-1", "cookie-2"]
}
}
To disable passing cookies from the widget, set originCookies to an empty array.