You can integrate watsonx Orchestrate with any enterprise application that supports token exchange and Single Sign-On (SSO). This set up enables users to access third-party applications through watsonx embedded chat using their own credentials.

Architecture overview

The diagram explains how watsonx Orchestrate connects with the following components:

  • An identity provider (IdP), such as Microsoft Entra ID (Entra ID). The same approach applies to other identity providers that support token exchange and Single Sign-On (SSO).

  • A third-party enterprise application, like Workday.

  • An application that hosts the embedded chat

This flow uses OAuth 2.0 On-Behalf-Of (OBO) to exchange tokens and enable secure access to data.

Flow summary

  • User authentication: The user logs into the application that hosts the embedded chat using their own credentials. The IdP authenticates the user and returns an access token.

  • Token propagation through embedded chat: The application that embeds watsonx Orchestrate web chat, receives the access token from IdP and passes it to watsonx Orchestrate.

  • Exchange tokens:

    watsonx Orchestrate:

    • Requests a SAML assertion from the IdP using the access token.
    • Exchanges the SAML assertion for an access token from the enterprise application’s OAuth server.
  • API access: watsonx Orchestrate uses the enterprise app’s access token to call its API (for example, Workday API) and retrieve data.

  • Response: The API response is returned to the user through the embedded chat in the application.


Tutorial: Connecting watsonx Orchestrate to Workday using Entra ID

The tutorial guides you to configure Single Sign-On (SSO) for Workday using Entra ID and the OAuth 2.0 On-Behalf-Of (OBO) flow with watsonx Orchestrate. This integration lets you manage authentication centrally while giving users direct, secure access to Workday without separate logins.

Before you begin

Make sure you have:

  • The application or web page where the embedded chat is hosted registered with Entra ID and configured with the appropriate API permissions.
  • Client credentials for Workday API (such as client ID and secret).
  • Admin or Builder access to your watsonx Orchestrate instance.
  • The watsonx Orchestrate Agent Development Kit (ADK) installed and authenticated.

Steps for configuring SSO authentication between watsonx Orchestrate and Workday

  1. Verify SSO authentication between Entra ID and Workday

Make sure the SSO authentication is configured between Entra ID and Workday.

  1. Configure token exchange in watsonx Orchestrate

Use the watsonx Orchestrate ADK (Agent Development Kit) to set up the OAuth 2.0 OBO flow. This allows token exchange between Entra ID and Workday.

a. Set up the connection between Workday and watsonx Orchestrate.

  • You can either use a configuration file or CLI commands to set up the connection. b. Configure the connection to the identity provider

c. Set the Workday OAuth server credentials

d. Embed watsonx Orchestrate web chat to your application

e. Test the integration

Verify SSO authentication between Entra ID and Workday

Before configuring watsonx Orchestrate, verify the SSO authentication between Entra ID and Workday. Use CURL or Postman to run the commands.

1

Set your environment variables

Run the following commands in the terminal.

[BASH]
export CLIENTID=<entra_app_client_id>
export CLIENTSEC=<entra_app_client_secret>
export WD_TENANT=<entra_directory_tenant_id>
export SCOPE=http://www.workday.com/user_impersonation
export WD_CLIENTID=<workday_api_client_id>
export UNAME=<username_of_user>
export PWD=<password_of_user>

Replace <entra_app_client_id>, <entra_app_client_secret>, <entra_directory_tenant_id>, <workday_api_client_id>, <username_of_user> and <password_of_user> with the appropriate information.

2

Add permissions to access the Workday API

Before you can exchange tokens and access Workday APIs using Entra ID, you need to configure the permissions for your app in Microsoft Entra.

a. Open Microsoft Entra admin center and navigate to App registrations.

b. Select your application used for token exchange.

c. Go to API permissions and select Add a permission.

d. Choose the Workday API.

e. Select Delegated permissions and add the required scopes.

For detailed steps, see Configure an application to access a web API in the Microsoft documentation.

3

Get a login token from Entra ID

Use the following command to get a login token. The password grant type is used to simulate user logging in to Entra ID.

[BASH]
curl -H 'Content-Type: application/x-www-form-urlencoded' \
  https://login.microsoftonline.com/$WD_TENANT/oauth2/v2.0/token \
  -d "grant_type=password&client_id=$CLIENTID&client_secret=$CLIENTSEC&scope=api://$CLIENTID/default&username=$UNAME&password=$PWD"
4

Exchange the login token for a SAML assertion

Use the following command to get SAML assertion from Entra ID. The JWT bearer grant type is used to exchange the access token for a SAML assertion.

Replace the $TOKEN with the token you received in the Step 2.

[BASH]
curl -H 'Content-Type: application/x-www-form-urlencoded' \
  https://login.microsoftonline.com/$WD_TENANT/oauth2/v2.0/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&client_id=$CLIENTID&client_secret=$CLIENTSEC&assertion=$TOKEN&scope=$SCOPE&requested_token_use=on_behalf_of&requested_token_type=urn:ietf:params:oauth:token-type:saml2"
5

Exchange the SAML assertion for Workday access token

Run the following command to get SAML assertion. Replace the $SAML_ASSERT with the SAML assertion you received in the Step 3.

[BASH]
curl -X POST https://<workday_domain>/ccx/oauth2/<workday_tenant>/token \
  -H 'content-type: application/x-www-form-urlencoded' \
  -d 'grant_type=urn:ietf:params:oauth:grant-type:saml2-bearer&client_id=$WD_CLIENTID&assertion=$SAML_ASSERT'

Replace the placeholders with your app’s details.

ParameterDescription
<workday_domain>The domain of your Workday instance. This is where API requests are sent.
<workday_client>Your Workday tenant identifier.
$SAML_ASSERTThe SAML assertion token received from Entra ID. Used to request a Workday access token.
6

Trigger the Workday API

Use your access token recieved from Step 4 to call Workday API.

[BASH]
curl --location 'https://<workday_domain>/ccx/api/common/v1/<workday_tenant>/workers' \
  --header 'Authorization: Bearer <workday_access_token>'
7

Result

If the token is valid and the API is correctly configured:

  • Workday return a JSON response containing a list of workers for the specified tenant.

Configure token exchange in watsonx Orchestrate

1

Start Orchestrate ADK

Install the watsonx Orchestrate Developer Edition with the ADK and activate your watsonx Orchestrate instance. For detailed instructions, see Using your watsonx Orchestrate account and Activating an environment.

2

Set up the connection between Workday and watsonx Orchestrate

You can configure Workday connection in two ways: either import a configuration file or set up the connection directly in the CLI. Using a configuration file is the recommended approach.

Use a configuration file

a. Create a configuration file called as connections.yaml.

b. Insert the following code into the connections.yaml file.

[YAML]
        spec_version: v1
  kind: connections
  app_id: workdayapp
  environments:
    draft:
      kind: oauth_auth_on_behalf_of_flow
      type: member
      sso: true
      server_url: https://wd2-impl-services1.workday.com/ccx
      idp_config:
        header:
          content-type: application/x-www-form-urlencoded
        body:
          requested_token_use: on_behalf_of
          requested_token_type: urn:ietf:params:oauth:token-type:saml2
      app_config:
        header:
          content-type: application/x-www-form-urlencoded

    live:
      kind: oauth_auth_on_behalf_of_flow
      type: member
      sso: true
      server_url: https://wd2-impl-services1.workday.com/ccx
      idp_config:
        header:
          content-type: application/x-www-form-urlencoded
        body:
          requested_token_use: on_behalf_of
          requested_token_type: urn:ietf:params:oauth:token-type:saml2
      app_config:
        header:
          content-type: application/x-www-form-urlencoded

c. Run the commands to import the connections.yaml file. See Importing Connection using a Connection file for more information.

[BASH]
orchestrate connections import -f connections.yaml
orchestrate connections import -f identity-provider.yaml
Use the CLI commands

Follow the commands to set up the connection.

[BASH]
  orchestrate connections configure \
--app-id workdayapp \
--env live \
--type team \
--kind oauth_auth_on_behalf_of_flow \
--server-url https://example.workday.com/ccx/api/v1/tenant1

Replace <app-id> with the ID of your web page or application that hosts the embedded chat. The kind flag ensures that the connection uses the OBO flow.

3

Set the identity provider as Entra ID

[BASH]
orchestrate connections set-identity-provider \
  --app-id workdayapp \
  --env live \
  --url https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token \
  --client-id <entra-client-id> \
  --client-secret <entra-client-secret> \
  --scope http://www.workday.com/user_impersonation \
  --grant-type authorization_code

Replace {tenant-id}, <entra-client-id> and <entra-client-secret> with appropriate information.

4

Import your tool

Use the following command to import your tool to watsonx Orchestrate instance. See Associating a connection to a tool for more information.

orchestrate tools import -k python -f <path to file> --app-id workdayapp 
5

Set the Workday OAuth credentials

Run the following commands to configure the system to use the Workday OAuth server.

[BASH]
orchestrate connections set-credentials \
  --app-id workdayapp \
  --env live \
  --client-id <workday-client-id> \
  --token-url https://msft.login.workday.com/ccx/oauth2/tenant1/token \
  --grant-type authorization_code

Make sure the client ID and token URL match those provided in your Workday API registration.

6

Add watsonx Orchestrate web chat to your application

a. After the configuration is complete, embed the watsonx Orchestrate web chat to your web page or application. For detailed instructions on embedding the web chat, refer to Enabling Secure Embed Flow.

b. To enable SSO, in addition to the embed instructions, complete the following steps:

  • Create a createJWT.js and an embed.html files in your watsonx Orchestrate repository.
  • Use the examples provided in the following tabs as templates for each file.
const fs = require('fs');
const RSA = require('node-rsa');
const crypto = require('crypto');
const jwtLib = require('jsonwebtoken');
const express = require('express');
const path = require('path');
const { v4: uuid } = require('uuid');

const router = express.Router();

// If you want to generate your own private/public key pair, you can use commands like the following.
//
// ssh-keygen -t rsa -b 4096 -m PEM -f example-jwtRS256.key
// openssl rsa -in example-jwtRS256.key -pubout -outform PEM -out example-jwtRS256.key.pub

// *** DO NOT USE THE PUBLIC AND PRIVATE KEYS FROM THIS EXAMPLE FOR PRODUCTION USE! ***

// This is your private key that you will keep on your server. This is used to sign the jwt. You will paste your public
// key into the appropriate field on the Security tab of the web chat settings page. IBM watsonx Assistant will use your
// public key to validate the signature on the jwt.
const PRIVATE_KEY = fs.readFileSync(path.join(__dirname, '../keys/<jwt-key>'));

// This is IBM's public key and can be found on the Security tab of the web chat settings page. This is only necessary
// if you wish to encrypt a user payload in your jwt. The code below will use this key to encrypt the user payload
// inside the JWT.
const IBM_PUBLIC_KEY = fs.readFileSync(path.join(__dirname, '../keys/ibmPublic.key.pub'));


// A time period of 45 days in milliseconds.
const TIME_45_DAYS = 1000 * 60 * 60 * 24 * 45;

/**
* Generates a signed JWT. The JWT used here will always be assigned a user ID using the given anonymous user ID. If
* the user is authenticated and we have session info, then info about the user will also be added to the JWT. We
* always use the anonymous user ID even if the user is authenticated because IBM watsonx Assistant doesn't allow you to
* change the user ID in the middle of a session.
*/
function createJWTString(anonymousUserID, sessionInfo) {
  // This is the content of the JWT. You would normally look up the user information from a user profile.
  const jwtContent = {
    // This is the subject of the JWT which will be the ID of the user.
    //
    // This user ID will be available under integrations.channel.private.user.id in dialog and
    // system_integrations.channel.private.user.id in actions.
    sub: anonymousUserID,
    // This object is optional and contains any data you wish to include as part of the JWT. This data will be
    // encrypted using IBM's public key so it will not be visible to your users. IBM watsonx Assistant will decrypt this
    // data and make it available to use in your assistant.
    woUserId: '<wo_user_id>',
    woTenantId: '<wo_tenant_id>',
    user_payload: {
      custom_message: 'This is a secret, encrypted message!!',
      name: 'Username',
      custom_user_id: Username_ID,
      sso_token: <sso_token>,
    },
  };

  // If the user is authenticated, then add the user's real info to the JWT.
  if (sessionInfo) {
    jwtContent.user_payload.name = sessionInfo.userName;
    jwtContent.user_payload.custom_user_id = sessionInfo.customUserID;
  }

  // const dataString = JSON.stringify(jwtContent.user_payload);


  if (jwtContent.user_payload) {
  // Example code snippet to encrypt sensitive data in JWT payload.
    // If there is a user payload, encrypt it using the IBM public key
    // and base64 format.
    const dataString = JSON.stringify(jwtContent.user_payload);
    const utf8Data = Buffer.from(dataString, 'utf-8'); // convert utf-8 to byte-stream
    
    // Encrypt the data
    const rsaKey = new RSA(IBM_PUBLIC_KEY);
    jwtContent.user_payload = rsaKey.encrypt(utf8Data, 'base64');
  }


    // // Encrypt the data
    // const encryptedBuffer = crypto.publicEncrypt(
    //   {
    //     key: IBM_PUBLIC_KEY,
    //     padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
    //     oaepHash: 'sha256'  // Specify OAEP padding with SHA256
    //   },
    //   Buffer.from(dataString, 'utf-8')
    // );

    // // Convert encrypted data to base64
    // jwtContent.user_payload = encryptedBuffer.toString('base64');
    console.log(jwtContent.user_payload)

    // Encrypt the user payload.
    // const rsaKey = new RSA(IBM_PUBLIC_KEY);
    // jwtContent.user_payload = rsaKey.encrypt(jwtContent.user_payload, 'base64');


    // Now sign the jwt content to make the actual jwt. We are giving this a very short expiration time (10 seconds)
    // to demonstrate the web chat capability of fetching a new token when it expires. In a production environment,
    // you would likely want to set this to a much higher value or leave it out entirely.
    const jwtString = jwtLib.sign(jwtContent, PRIVATE_KEY, {
      algorithm: 'RS256',
      expiresIn: '10000000s',
    });

    return jwtString;
  }

  /**
  * Gets or sets the anonymous user ID cookie. This will also ensure that an existing cookie is updated with a new 45
  * day expiration time.
  */
  function getOrSetAnonymousID(request, response) {
    let anonymousID = request.cookies['ANONYMOUS-USER-ID'];
    if (!anonymousID) {
      // If we don't already have an anonymous user ID, then create one. Normally you would want to use a full UUID,
      // but for the sake of this example we are going to shorten it to just five characters to make them easier to read.
      anonymousID = `anon-${uuid().substr(0, 5)}`;
    }

    // Here we set the value of the cookie and give it an expiration date of 45 days. We do this even if we already
    // have an ID to make sure that we update the expiration date to a new 45 days.
    response.cookie('ANONYMOUS-USER-ID', anonymousID, {
      expires: new Date(Date.now() + TIME_45_DAYS),
      httpOnly: true,
    });

    return anonymousID;
  }

  /**
  * Returns the session info for an authenticated user.
  */
  function getSessionInfo(request) {
    // Normally the cookie would contain a session token that we would use to look up the user's info from something
    // like a database. But for the sake of simplicity in this example the session cookie directly contains the user's
    // info.
    const sessionInfo = request.cookies.SESSION_INFO;
    if (sessionInfo) {
      return JSON.parse(sessionInfo);
    }
    return null;
  }

  /**
  * Handles the createJWT request.
  */
  function createJWT(request, response) {
    const anonymousUserID = getOrSetAnonymousID(request, response);
    const sessionInfo = getSessionInfo(request);
    response.send(createJWTString(anonymousUserID, sessionInfo));
  }

  router.get('/', createJWT);

  module.exports = router;

Replace the <wo_tenant_id> and <sso_token> with appropriate information.

ParameterDescription
<wo_tenant_id>Your watsonx Orchestrate tenant ID.
<sso_token>The SSO token obtained from the Entra ID.
Run the following commands to retrieve the SSO token
  curl --location 'https://login.microsoftonline.com/0195ea87-1839-4df0-9739-bf7eec6de925/oauth2/v2.0/token' \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --header 'Cookie: fpc=ArnSgeQ8FmNJkcInGVBWnw94mWYSAQAAAG0uod8OAAAA; stsservicecookie=estsfd; x-ms-gateway-slice=estsfd' \
    --data-urlencode 'grant_type=password' \
    --data-urlencode 'client_id=<client_id>' \
    --data-urlencode 'client_secret=<client_secret>' \
    --data-urlencode 'scope=api://<client_id>/default' \
    --data-urlencode '<username>' \
    --data-urlencode '<password>'
7

Test the integration

To test the integration, sign in and interact with Workday through embedded chat.

a. Log in to the web page or application hosting the embedded chat using your own credentials.

b. Launch the watsonx Orchestrate webchat widget.

c. Ask a Workday-related query in the webchat.

8

Result

If the SSO authentication is correctly configured:

  • Workday returns a JSON response with the requested data.

Troubleshooting

IssuePossible causeResolution
401 UnauthorizedInvalid SAML assertion or client credentialsCheck your app’s API client credentials
invalid_grantRedirect URI mismatchVerify your IDP app settings
No response in embedded chatChat not embedded correctlyVerify the script and app ID