Embedding the agent into the web chat
IBM watsonx Orchestrate allows you to embed intelligent agents directly into your web applications using the Embedded Chat feature. This integration supports secure communication, flexible UI customization, and advanced event handling to deliver rich conversational experiences.Why embed agents?
Embedding agents into your application provides:- Real-time interaction: Users engage with agents directly within your app.
- Custom UI integration: Match the agent’s look and feel to your brand.
- Secure communication: RSA encryption and JWT authentication protect sensitive data.
- Context-aware automation: Agents can access encrypted context variables to personalize responses.
- Scalable deployment: Embed across multiple environments using consistent configuration.
Prerequisites
Environments: draft vs. live
- Draft: Used for testing. Embed snippets target the draft variant.
- Live: Available after deployment. Embed snippets default to the live variant in production.
Application requirements
Your application must:- Include a server (local or cloud)
- Have an HTML element with ID
root - Place the embed script inside the
<body>tag - The page that embeds the web chat must use HTML’s strict mode (in clude the
<!DOCTYPE html>tag) - Enable security
Copy
Ask AI
<!DOCTYPE html>
<body>
<div id="root"></div>
<script src="path-to-embed-script.js"></script>
</body>
Security configuration
Security is enabled by default, but must be explicitly configured. The embedded chat will not function until security is properly configuredSecurity Architecture
The embedded chat uses RSA public-key cryptography to secure communication. The configuration involves two key pairs:-
IBM Key Pair
- Generated by: watsonx Orchestrate service.
- Public key: Shared with your application. Your application uses this key to encrypt the
user_payloadsection of the JWT sent to watsonx Orchestrate. - Private key: Stored securely by watsonx Orchestrate.cUsed to decrypt the
user_payloadsection of the JWT.
-
Client Key Pair
- Generated by: You (or a security configuration tool).
- Public key: Shared with watsonx Orchestrate. Used to verify that JWTs originate from your application.
- Private key: Remains with you and must be stored securely. Used to sign JWTs sent to watsonx Orchestrate.
-
JWT Authentication
- When security is enabled, your application must:
- Generate a JWT signed with your private key (Client Key Pair).
- Include the JWT in all requests to the Embedded Chat API. watsonx Orchestrate validates the token using your public key.
- When security is enabled, your application must:
- All requests to the Embedded Chat API must include a valid JWT token
- The token must be signed with your private key
- The watsonx Orchestrate service validates the token using your public key
- This prevents unauthorized access to your watsonx Orchestrate instance
- Requests to the Embedded Chat API do not require authentication
- Anyone with access to your web application where chat is Embedded can access your watsonx Orchestrate instance. In addition, your Watson Orchestrate instance allows anonymous authentication to a limited set of Apis, which is required to get your embed chat to work for anonymous users.
- This option should only be used for specific use cases where anonymous chat access is required.
- Ensure your watsonx Orchestrate instance in this case, does not provide access to sensitive data or access to tools configured with functional credentials that access sensitive data.
Enabling security
1
Prerequisites
- IBM watsonx Orchestrate instance
- API Key with administrative privileges
- Service Instance URL from your watsonx Orchestrate instance
- On macOS and Linux: OpenSSL installed on your system (for key generation)
- Python Installed on your system (for key extraction from APIs)
2
Get the watsonx Orchestrate security tool
Copy the following automated script to configure security:
wxO-embed-chat-security-tool.sh
Copy
Ask AI
#!/usr/bin/env bash
set -euo pipefail
# ============================================================
# IBM watsonx Orchestrate — Embedded Chat Security Helper (v4)
# Single Bash script for IBM Cloud / AWS / CPD.
#
# WHAT THIS DOES
# - Turns Embedded Chat security ON or OFF for a single instance.
# - When turning security ON:
# • Uses your CLIENT PUBLIC key (existing or newly generated).
# • Asks watsonx Orchestrate for an IBM PUBLIC key.
# • Saves the keys locally in ./keys and updates the instance config.
# - When turning security OFF:
# • Disables embedded chat security for that instance (anonymous access).
# - When viewing:
# • Only fetches and prints the current configuration (no changes).
#
# IMPORTANT:
# - By default, Embedded Chat security is ON but not configured.
# In that state, Embedded Chat will NOT work until keys are set,
# or security is explicitly disabled for anonymous access.
# - Fully configured security requires BOTH:
# • An IBM PUBLIC key generated by watsonx Orchestrate.
# • Your CLIENT PUBLIC key (from your own key pair).
#
# SUPPORTED INSTANCE URL FORMATS
# IBM Cloud / AWS:
# https://api.<host>/instances/<INSTANCE_ID>
#
# CPD (Cloud Pak for Data):
# https://<host>/orchestrate/<CPD_INSTANCE>/instances/<INSTANCE_ID>
#
# ARGUMENTS (optional, in order)
# 1) WXO_API_KEY (IBM Cloud / AWS only; ignored for CPD)
# 2) FULL_INSTANCE_API_URL (Service instance URL as above)
#
# ENVIRONMENT VARIABLES (optional)
# ACTION=enable|disable|view (single-shot, no main menu)
# IAM_URL=<explicit IAM base> (Override IAM URL for non-CPD flows)
# WXO_API_KEY (Same as arg 1, if not given)
# FULL_INSTANCE_API_URL (Same as arg 2, if not given)
# API_URL_WITH_INSTANCE (Legacy name for FULL_INSTANCE_API_URL)
# CPD_USERNAME / CPD_PASSWORD (For CPD login, otherwise prompted)
#
# CPD TOKEN:
# POST https://<host>/icp4d-api/v1/authorize { username, password }
#
# NON-CPD TOKEN:
# POST ${IAM_URL}/siusermgr/api/1.0/apikeys/token { apikey }
# ============================================================
# ===============================
# Colors / formatting (best effort)
# ===============================
if [[ -t 1 ]]; then
BOLD='\033[1m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
else
BOLD='' GREEN='' YELLOW='' RED='' BLUE='' NC=''
fi
# Keep original ACTION for non-interactive single-shot runs
NON_INTERACTIVE_ACTION="${ACTION:-}"
# ===============================
# Helpers
# ===============================
banner() {
echo "============================================================"
echo -e "${BOLD}$1${NC}"
echo "============================================================"
}
need_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo -e "${RED}❌ Required command '$1' not found. Please install it and retry.${NC}"
exit 1
fi
}
prompt_if_empty() {
local __varname="$1"
local __prompt="$2"
if [[ -z "${!__varname:-}" ]]; then
read -rp "$__prompt" "$__varname"
export "$__varname"
fi
}
prompt_secret_if_empty() {
local __varname="$1"
local __prompt="$2"
if [[ -z "${!__varname:-}" ]]; then
read -rs -p "$__prompt" "$__varname"
echo
export "$__varname"
fi
}
# Returns base URL with any leading 'api.' stripped from the host.
strip_leading_api() {
local in_url="$1"
if [[ "${in_url}" =~ ^(https?://)([^/]+)(/.*)?$ ]]; then
local scheme="${BASH_REMATCH[1]}"
local host="${BASH_REMATCH[2]}"
host="${host#api.}"
echo "${scheme}${host}"
return 0
fi
echo "${in_url}"
return 0
}
# Get scheme://host from a full URL
origin_from_url() {
local in_url="$1"
if [[ "${in_url}" =~ ^(https?://[^/]+) ]]; then
echo "${BASH_REMATCH[1]}"
return 0
fi
echo "${in_url}"
return 0
}
# Read a PEM file and return JSON string (jq -Rs .), normalize line endings
read_pem_as_json_string() {
local pem_path="$1"
if [[ ! -f "$pem_path" ]]; then
echo -e "${RED}❌ PEM file not found: $pem_path${NC}" >&2
return 1
fi
if [[ ! -r "$pem_path" ]]; then
echo -e "${RED}❌ PEM file not readable: $pem_path${NC}" >&2
return 1
fi
if grep -qE -- '-----BEGIN (RSA )?PUBLIC KEY-----' "$pem_path"; then
: # public key — good
elif grep -qE -- '-----BEGIN (RSA )?PRIVATE KEY-----' "$pem_path"; then
echo -e "${YELLOW}⚠️ You provided a PRIVATE key PEM. The API expects the PUBLIC key. Continuing…${NC}" >&2
else
echo -e "${YELLOW}⚠️ File lacks standard PUBLIC/PRIVATE KEY PEM header. Continuing…${NC}" >&2
fi
tr -d '\r' < "$pem_path" | sed '${/^$/d;}' | jq -Rs .
}
# Quick help for instance URL
show_instance_help() {
echo
echo -e "${BOLD}How to find your Service instance URL:${NC}"
echo "1. Open watsonx Orchestrate in your browser."
echo "2. Click the profile icon in the top-right corner."
echo "3. Choose \"Settings\" → \"API details\"."
echo "4. Copy the \"Service instance URL\", which looks like:"
echo -e " ${BLUE}https://api.us-south.watson-orchestrate.ibm.com/instances/20250807-1007-4445-5049-459a42144389${NC}"
echo
echo "Paste that full URL into this script when prompted."
echo
}
# Print a human-friendly summary of the /config payload
print_config_summary() {
local config_json="$1"
# Detect whether is_security_enabled field exists, and preserve true/false
local is_enabled
is_enabled="$(
echo "${config_json}" \
| jq -r 'if has("is_security_enabled") then (.is_security_enabled|tostring) else "__missing__" end' 2>/dev/null \
|| echo "__missing__"
)"
# Extract keys
local public_key client_public_key
public_key="$(echo "${config_json}" | jq -r '.public_key // ""')"
client_public_key="$(echo "${config_json}" | jq -r '.client_public_key // ""')"
local has_client="false"
local has_ibm="false"
[[ -n "${client_public_key}" ]] && has_client="true"
[[ -n "${public_key}" ]] && has_ibm="true"
if [[ "${is_enabled}" == "__missing__" || -z "${is_enabled}" ]]; then
echo -e "${YELLOW}Could not find is_security_enabled in the response. Raw config is shown above.${NC}"
return 0
fi
# =========================
# SECURITY DISABLED STATES
# =========================
if [[ "${is_enabled}" == "false" ]]; then
echo -e "Embedded chat security is: ${YELLOW}DISABLED${NC}"
if [[ "${has_client}" == "true" && "${has_ibm}" == "false" ]]; then
echo "Security is disabled, but a client public key is still configured."
echo "This may be a stale or incomplete setup."
echo
echo "To clean this up, you can:"
echo " • Replace it with the correct client public key, and when you turn security back ON,"
echo " generate a new IBM public key; or"
echo " • Clear the client public key if you intend to keep anonymous access."
echo
echo "When you enable security again, use the IBM public key to encrypt the user_payload"
echo "in your identity token, and sign the token with the private key that matches your"
echo "client public key."
echo
echo "Disabled security will allow anonymous users to access your embedded chat."
echo "Anyone who can access your website will be able to open the chat without authentication."
echo
echo "Only use disabled security if your use case explicitly requires anonymous access and your"
echo "watsonx Orchestrate instance does not expose sensitive data or tools configured with"
echo "functional credentials."
else
echo "Anyone who can access your website can open the embedded chat without authentication."
echo
echo "Only use disabled security when your use case explicitly requires anonymous access and your"
echo "watsonx Orchestrate instance does not expose sensitive data or tools configured with"
echo "functional credentials."
fi
return 0
fi
# If we got here and it's not "true", just show raw
if [[ "${is_enabled}" != "true" ]]; then
echo -e "${YELLOW}Could not interpret is_security_enabled=\"${is_enabled}\". Raw config is shown above.${NC}"
return 0
fi
# =========================
# SECURITY ENABLED STATES
# =========================
echo -e "Embedded chat security is: ${GREEN}ENABLED${NC}"
if [[ "${has_client}" == "true" && "${has_ibm}" == "true" ]]; then
# Fully configured: security ON + both keys present
echo -e "${GREEN}Embedded chat security is enabled and configured.${NC}"
echo "Security is enabled and both IBM and client key pairs are configured."
echo
echo "Update your embed script to pass a valid RS256-signed JWT using the private key"
echo "that matches your client public key, so watsonx Orchestrate can authenticate users"
echo "and allow embedded chat to work as expected."
elif [[ "${has_client}" == "true" && "${has_ibm}" == "false" ]]; then
# Security ON + client key present + IBM key missing
echo -e "${GREEN}Embedded chat security is enabled (encryption not configured).${NC}"
echo "A client public key is configured, but Encrypt sensitive information is not set up."
echo
echo "Generate an IBM public key so you can encrypt the user_payload in your identity token"
echo "and fully enable encrypted embedded chat security."
elif [[ "${has_client}" == "false" && "${has_ibm}" == "true" ]]; then
# Security ON + IBM key present + client key missing
echo -e "${YELLOW}Embedded chat security is enabled but the client public key is missing.${NC}"
echo "A client public key is required to validate your identity tokens."
echo
echo "Paste a valid client public key in the Chat user identity section and update"
echo "your configuration so watsonx Orchestrate can verify your RS256-signed JWTs."
elif [[ "${has_client}" == "false" && "${has_ibm}" == "false" ]]; then
# Security ON + no keys at all
echo -e "${YELLOW}Embedded chat security is enabled but not fully configured.${NC}"
echo "Security is currently turned on, but embedded chat will not work until both key"
echo "pairs are configured:"
echo " • An IBM public key generated by watsonx Orchestrate."
echo " • Your application’s client public key used to verify signed tokens."
echo
echo "To complete configuration, we recommend the following steps:"
echo " 1) Provide a valid client public key in the Chat user identity section."
echo " 2) Generate an IBM public key for Encrypt sensitive information."
echo " 3) Update your embed script to use the IBM public key to encrypt user_payload."
else
# Catch-all for any other unusual combination
echo -e "${YELLOW}Security is enabled, but the key configuration is incomplete or unusual.${NC}"
echo "Check that both of the following are set correctly:"
echo " • IBM public key (for encrypting user_payload)."
echo " • Client public key (for verifying your signed JWTs)."
fi
}
# ===============================
# Dependencies
# ===============================
need_cmd curl
need_cmd jq
need_cmd sed
need_cmd openssl
need_cmd tr
need_cmd grep
# ===============================
# Welcome + optional help
# ===============================
banner "IBM watsonx Orchestrate — Embedded Chat Security Helper (v4)"
echo "This helper manages embedded chat security for a single watsonx Orchestrate instance."
echo
echo "By default, embedded chat security is ON but not configured."
echo "In that state, embedded chat will not work until:"
echo " • both IBM and client key pairs are configured, or"
echo " • security is explicitly turned OFF to allow anonymous access."
echo
read -rp "Do you need help finding your Service instance URL? (y/N): " NEED_HELP
NEED_HELP="${NEED_HELP:-n}"
if [[ "${NEED_HELP}" == [Yy] ]]; then
show_instance_help
fi
# ===============================
# 1) Inputs (URL first), then creds
# ===============================
WXO_API_KEY="${1:-${WXO_API_KEY:-}}"
FULL_INSTANCE_API_URL="${2:-${FULL_INSTANCE_API_URL:-${API_URL_WITH_INSTANCE:-}}}"
if [[ -z "${FULL_INSTANCE_API_URL}" ]]; then
banner "watsonx Orchestrate Embedded Chat Security"
echo "Please provide the Service instance URL from the API details page:"
echo " • IBM Cloud / AWS:"
echo " https://api.<host>/instances/<INSTANCE_ID>"
echo " • CPD:"
echo " https://<host>/orchestrate/<CPD_INSTANCE>/instances/<INSTANCE_ID>"
echo
fi
prompt_if_empty FULL_INSTANCE_API_URL "Enter Service instance URL: "
# ===============================
# 2) Parse URL, detect CPD vs Non-CPD
# ===============================
IS_CPD="false"
API_ORIGIN="$(origin_from_url "${FULL_INSTANCE_API_URL}")"
# CPD pattern
if [[ "${FULL_INSTANCE_API_URL}" =~ ^(https?://[^/]+)/orchestrate/([^/]+)/instances/([^/?#]+) ]]; then
IS_CPD="true"
API_URL_BASE="${BASH_REMATCH[1]}/orchestrate/${BASH_REMATCH[2]}"
CPD_INSTANCE_NAME="${BASH_REMATCH[2]}"
WXO_INSTANCE_ID="${BASH_REMATCH[3]}"
# Non-CPD pattern
elif [[ "${FULL_INSTANCE_API_URL}" =~ ^(https?://[^/]+)/instances/([^/?#]+) ]]; then
IS_CPD="false"
API_URL_BASE="${BASH_REMATCH[1]}"
WXO_INSTANCE_ID="${BASH_REMATCH[2]}"
else
echo -e "${RED}❌ Invalid Service instance URL.${NC}"
echo " IBM Cloud / AWS example:"
echo " https://api.us-south.watson-orchestrate.ibm.com/instances/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
echo " CPD example:"
echo " https://cpd-cpd-instance-1.apps.example.com/orchestrate/cpd-instance-1/instances/1757991870301390"
echo "Got: ${FULL_INSTANCE_API_URL}"
exit 1
fi
# Heuristic: also mark CPD if host contains 'cpd' (defensive)
if [[ "${API_ORIGIN}" == *"cpd"* ]]; then
IS_CPD="true"
fi
banner "Inputs"
echo "WXO_INSTANCE_ID: ${WXO_INSTANCE_ID}"
echo "API_URL_BASE: ${API_URL_BASE}"
echo "API_ORIGIN: ${API_ORIGIN}"
echo "Detected Platform: $([[ "${IS_CPD}" == "true" ]] && echo "CPD" || echo "IBM Cloud / AWS")"
echo
# ===============================
# 3) Token acquisition (once)
# ===============================
if [[ "${IS_CPD}" == "true" ]]; then
# CPD: username + password -> token at <origin>/icp4d-api/v1/authorize
banner "Fetching CPD token"
CPD_USERNAME="${CPD_USERNAME:-}"
CPD_PASSWORD="${CPD_PASSWORD:-}"
prompt_if_empty CPD_USERNAME "Enter CPD Username: "
prompt_secret_if_empty CPD_PASSWORD "Enter CPD Password (hidden): "
WXO_TOKEN="$(
curl --fail -sS \
--request POST \
--url "${API_ORIGIN}/icp4d-api/v1/authorize" \
--header 'Content-Type: application/json' \
--header 'cache-control: no-cache' \
--data "{\"username\":\"${CPD_USERNAME}\",\"password\":\"${CPD_PASSWORD}\"}" --insecure \
| jq -r .token
)"
if [[ -z "${WXO_TOKEN}" || "${WXO_TOKEN}" == "null" ]]; then
echo -e "${RED}❌ Failed to fetch CPD token. Check your credentials and host.${NC}"
exit 1
fi
echo -e "${GREEN}✅ CPD token acquired.${NC}"
echo
else
# IBM Cloud / AWS: API key -> IAM token selection
if [[ -z "${WXO_API_KEY}" ]]; then
echo "IBM Cloud / AWS flow selected."
echo "You will be prompted for WXO_API_KEY (input hidden)."
fi
prompt_secret_if_empty WXO_API_KEY "Enter WXO_API_KEY (hidden): "
# Auto-select IAM_URL unless provided
if [[ -z "${IAM_URL:-}" ]]; then
API_URL_NOAPI="$(strip_leading_api "${API_URL_BASE}")"
case "${API_URL_NOAPI}" in
# IBM Cloud - PROD
https://au-syd.watson-orchestrate.cloud.ibm.com|\
https://jp-tok.watson-orchestrate.cloud.ibm.com|\
https://eu-de.watson-orchestrate.cloud.ibm.com|\
https://eu-gb.watson-orchestrate.cloud.ibm.com|\
https://us-south.watson-orchestrate.cloud.ibm.com|\
https://us-east.watson-orchestrate.cloud.ibm.com|\
https://ca-tor.watson-orchestrate.cloud.ibm.com)
IAM_URL="https://iam.platform.saas.ibm.com"
;;
# AWS - PROD
https://ap-southeast-1.dl.watson-orchestrate.ibm.com|\
https://ap-south-1.dl.watson-orchestrate.ibm.com|\
https://eu-central-1.dl.watson-orchestrate.ibm.com|\
https://dl.watson-orchestrate.ibm.com|\
https://cio.watson-orchestrate.ibm.com)
IAM_URL="https://iam.platform.saas.ibm.com"
;;
# GovCloud (mapped to PROD IAM)
https://origin-us-gov-east-1.watson-orchestrate.prep.ibmforusgov.com)
IAM_URL="https://iam.platform.saas.ibm.com"
;;
# STAGING / TEST
https://us-south.watson-orchestrate.test.cloud.ibm.com|\
https://perfus-south.watson-orchestrate.test.cloud.ibm.com|\
https://staging-wa.watson-orchestrate.ibm.com|\
https://wodlperf.watson-orchestrate.ibm.com|\
https://preprod.dl.watson-orchestrate.ibm.com)
IAM_URL="https://iam.platform.test.saas.ibm.com"
;;
# DEV
https://us-south.watson-orchestrate-dev.test.cloud.ibm.com|\
https://dev-wa.watson-orchestrate.ibm.com|\
https://dev-fedtest.watson-orchestrate.ibm.com)
IAM_URL="https://iam.platform.dev.saas.ibm.com"
;;
*)
echo -e "${YELLOW}⚠️ Unknown API host after normalization: ${API_URL_NOAPI}${NC}"
echo " Falling back to PROD IAM."
IAM_URL="https://iam.platform.saas.ibm.com"
;;
esac
export IAM_URL
banner "IAM URL selected"
echo "Normalized API host: ${API_URL_NOAPI}"
echo "IAM_URL: ${IAM_URL}"
echo
else
banner "IAM URL selected (from environment)"
echo "IAM_URL: ${IAM_URL}"
echo
fi
banner "Fetching IAM token"
WXO_TOKEN="$(
curl --fail -sS \
--request POST \
--url "${IAM_URL}/siusermgr/api/1.0/apikeys/token" \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data "{\"apikey\": \"${WXO_API_KEY}\"}" \
| jq -r .token
)"
if [[ -z "${WXO_TOKEN}" || "${WXO_TOKEN}" == "null" ]]; then
echo -e "${RED}❌ Failed to fetch IAM token. Check your WXO_API_KEY and IAM_URL.${NC}"
exit 1
fi
echo -e "${GREEN}✅ Token acquired.${NC}"
echo
fi
# ===============================
# 4) Endpoints (CPD vs Non-CPD)
# ===============================
if [[ "${IS_CPD}" == "true" ]]; then
CONFIG_ENDPOINT="${API_URL_BASE}/instances/${WXO_INSTANCE_ID}/v1/embed/secure/config"
GENKEY_ENDPOINT="${API_URL_BASE}/instances/${WXO_INSTANCE_ID}/v1/embed/secure/generate-key-pair"
else
CONFIG_ENDPOINT="${API_URL_BASE}/instances/${WXO_INSTANCE_ID}/v1/embed/secure/config"
GENKEY_ENDPOINT="${API_URL_BASE}/instances/${WXO_INSTANCE_ID}/v1/embed/secure/generate-key-pair"
fi
# ===============================
# 5) Common action runner (enable/disable/view + verify)
# ===============================
run_action() {
local action="$1"
if [[ "${action}" == "enable" ]]; then
banner "Turn ON embedded chat security and configure keys"
mkdir -p keys
echo "Choose how to provide the client public key:"
echo " 1) Use an existing PUBLIC key PEM (path on disk)"
echo " 2) Generate a NEW RSA 4096 key pair now"
read -rp "Enter 1 or 2: " key_choice
local CLIENT_PUBLIC_KEY_JSON=""
local KEY_CHOICE="${key_choice}"
case "$key_choice" in
1)
read -rp "Enter path to client PUBLIC key PEM (e.g., /path/to/jwtRS256.key.pub): " CLIENT_PUB_PEM_PATH
CLIENT_PUBLIC_KEY_JSON="$(read_pem_as_json_string "$CLIENT_PUB_PEM_PATH")"
;;
2)
echo "Generating client RSA key pair in ./keys ..."
openssl genrsa -out keys/example-jwtRS256.key 4096 >/dev/null 2>&1
openssl rsa -in keys/example-jwtRS256.key -pubout -out keys/example-jwtRS256.key.pub >/dev/null 2>&1
CLIENT_PUBLIC_KEY_JSON="$(read_pem_as_json_string "keys/example-jwtRS256.key.pub")"
echo "🔐 Generated client key files:"
echo " • keys/example-jwtRS256.key (client PRIVATE key — keep this secure)"
echo " • keys/example-jwtRS256.key.pub (client PUBLIC key)"
;;
*)
echo -e "${RED}❌ Invalid choice.${NC}"
return 1
;;
esac
if [[ -z "${CLIENT_PUBLIC_KEY_JSON:-}" || "${CLIENT_PUBLIC_KEY_JSON}" == "null" ]]; then
echo -e "${RED}❌ Could not prepare client PUBLIC key for payload.${NC}"
return 1
fi
echo "Requesting IBM public key from watsonx Orchestrate..."
curl --fail -sS \
--request POST \
--url "${GENKEY_ENDPOINT}" \
--header "Authorization: Bearer ${WXO_TOKEN}" --insecure \
| jq -r '.public_key' > keys/ibmPublic.key.pub
if [[ ! -s keys/ibmPublic.key.pub ]]; then
echo -e "${RED}❌ Failed to retrieve IBM public key.${NC}"
return 1
fi
local IBM_PUBLIC_KEY_JSON
IBM_PUBLIC_KEY_JSON="$(read_pem_as_json_string "keys/ibmPublic.key.pub")"
local CONFIG_PAYLOAD
CONFIG_PAYLOAD=$(
cat <<JSON
{
"public_key": ${IBM_PUBLIC_KEY_JSON},
"client_public_key": ${CLIENT_PUBLIC_KEY_JSON},
"is_security_enabled": true
}
JSON
)
echo "Updating embedded chat security configuration..."
local RESULT
RESULT="$(
curl --fail -sS \
--request POST \
--url "${CONFIG_ENDPOINT}" \
--header "Authorization: Bearer ${WXO_TOKEN}" \
--header 'Content-Type: application/json' \
--data "${CONFIG_PAYLOAD}" --insecure
)"
echo -e "${GREEN}✅ Embedded chat security has been turned ON and keys are configured (server response below).${NC}"
echo "Server response:"
echo "${RESULT}" | jq .
echo
echo "Saved IBM PUBLIC key to: keys/ibmPublic.key.pub"
if [[ "${KEY_CHOICE}" == "1" ]]; then
echo "Use this IBM public key to encrypt the user_payload in your identity token."
else
echo "Use:"
echo " • keys/example-jwtRS256.key (client PRIVATE key for signing JWTs)"
echo " • keys/example-jwtRS256.key.pub (client PUBLIC key configured on the instance)"
echo " • keys/ibmPublic.key.pub (IBM PUBLIC key for encrypting user_payload)"
fi
echo
elif [[ "${action}" == "disable" ]]; then
# For interactive runs (no NON_INTERACTIVE_ACTION), ask for confirmation
if [[ -z "${NON_INTERACTIVE_ACTION}" ]]; then
echo
echo -e "${RED}Disable embedded chat security?${NC}"
echo "Disabling security will allow anonymous users to access your embedded chat."
echo "Anyone who can access your website will be able to open the chat without authentication."
echo
echo "Only disable security if your use case explicitly requires anonymous access and your"
echo "watsonx Orchestrate instance does not expose sensitive data or tools configured with"
echo "functional credentials."
echo
read -rp "Type 'yes' to confirm disabling security: " CONFIRM_DISABLE
if [[ "${CONFIRM_DISABLE}" != "yes" ]]; then
echo "Operation cancelled. Security settings were not changed."
return 0
fi
fi
banner "Turn OFF embedded chat security (anonymous access)"
local DISABLE_PAYLOAD
DISABLE_PAYLOAD='{"is_security_enabled": false}'
echo "Updating embedded chat security configuration..."
local RESULT
RESULT="$(
curl --fail -sS \
--request POST \
--url "${CONFIG_ENDPOINT}" \
--header "Authorization: Bearer ${WXO_TOKEN}" \
--header 'Content-Type: application/json' \
--data "${DISABLE_PAYLOAD}" --insecure
)"
echo -e "${YELLOW}✅ Embedded chat security has been turned OFF for this instance.${NC}"
echo "Server response:"
echo "${RESULT}" | jq .
echo
else
banner "View current embedded chat security configuration (read-only)"
fi
# Common: GET /config and summary
banner "Check current configuration"
local VERIFY
VERIFY="$(
curl --fail -sS \
--request GET \
--url "${CONFIG_ENDPOINT}" \
--header "Authorization: Bearer ${WXO_TOKEN}" \
--header 'Accept: application/json' --insecure
)"
echo "Raw configuration from the service:"
echo "${VERIFY}" | jq .
echo
echo "Summary:"
print_config_summary "${VERIFY}"
echo
echo -e "${GREEN}Action completed.${NC}"
}
# ===============================
# 6) Non-interactive (ACTION set) or interactive main menu loop
# ===============================
if [[ -n "${NON_INTERACTIVE_ACTION}" ]]; then
# Single-shot mode for automation: just run the given action and exit.
run_action "${NON_INTERACTIVE_ACTION}"
exit 0
fi
# Interactive main menu loop
while true; do
echo
echo "What would you like to do for this instance?"
echo " 1) Turn ON embedded chat security and configure keys (recommended)"
echo " 2) Turn OFF embedded chat security and allow anonymous access"
echo " 3) View current embedded chat security configuration (no changes)"
echo " 4) Exit"
read -rp "Enter 1, 2, 3 or 4: " choice
case "$choice" in
1)
run_action "enable"
;;
2)
run_action "disable"
;;
3)
run_action "view"
;;
4)
echo "Exiting."
break
;;
*)
echo -e "${RED}❌ Invalid choice. Please enter 1, 2, 3 or 4.${NC}"
;;
esac
done
3
Change the script's permissions
On Unix-based systems (macOS and Linux), change the permissions to run the script:
Copy
Ask AI
chmod +x wxO-embed-chat-security-tool.sh
4
Run the script
Run the script and follow the instructions to enable or disable security:
Copy
Ask AI
./wxO-embed-chat-security-tool.sh
- The tool generates an IBM key pair via the API
- The tool generates a client key pair using OpenSSL
- Both public keys are configured in the service
- Security is enabled
wxo_security_config directory:
ibm_public_key.pem: IBM’s public key in PEM formatibm_public_key.txt: IBM’s public key in single-line formatclient_private_key.pem: Your private key (keep it secure!)client_public_key.pem: Your public key in PEM formatclient_public_key.txt: Your public key in single-line format
Anonymous Access
You may disable security for anonymous access, but only if:- No sensitive data is exposed.
- No tools with functional credentials are accessible.
How to embed
Use the CLI command to generate the embed script:Copy
Ask AI
orchestrate channels webchat embed --agent-name=test_agent1
<script> tag with configuration for your environment. You can add this script to an HTML page on your website.
The following is an example of the command’s output:
OUTPUT
Copy
Ask AI
<script>
window.wxOConfiguration = {
orchestrationID: "your-orgID_orchestrationID", // Adds control over chat display mode (e.g., fullscreen)
hostURL: "https://dl.watson-orchestrate.ibm.com", // or region-specific host
rootElementID: "root",
showLauncher: false,
deploymentPlatform: "ibmcloud", // Required for IBM Cloud embed, can be skipped for other embed scenarios
crn: "your-org-crn", // Required for IBM Cloud embed, can be skipped for other embed scenarios. For more information, see [FAQs](https://www.ibm.com/docs/en/watsonx/watson-orchestrate/base?topic=experience-faqs#how-can-i-know-my-crn-id)
chatOptions: {
agentId: "your-agent-id",
agentEnvironmentId: "your-agent-env-id",
},
layout: {
form: "fullscreen-overlay", // Options: float | custom | fullscreen-overlay - if not specified float is default
showOrchestrateHeader: true, // Optional: shows top agent header bar
width: "600px", // Optional: honored when form is float only
height: "600px", // Optional: honored when form is float only
},
};
setTimeout(function () {
const script = document.createElement("script");
script.src = `${window.wxOConfiguration.hostURL}/wxochat/wxoLoader.js?embed=true`;
script.addEventListener("load", function () {
wxoLoader.init();
});
document.head.appendChild(script);
}, 0);
</script>
Example implementation
The following is a minimal implementation example of a server and website running the web chat with security and authentication configured. This implementation uses NodeJS with the Express web application framework. The project uses the following folder structure:Copy
Ask AI
.
└── webchatProject/
├── keys/
│ ├── example-jwtRS256.key
│ ├── example-jwtRS256.key.pub
│ └── ibmPublic.key.pub
├── routes/
│ └── createJWT.js
├── static/
│ └── index.html
├── server.js
└── package.json
example-jwtRS256 keys with the following commands:
Copy
Ask AI
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
Copy
Ask AI
/**
* JWT Creation Service for watsonx Orchestrate Secure Embed Chat
*
* This module handles the server-side creation of JSON Web Tokens (JWTs) for secure
* authentication with IBM watsonx Orchestrate embed chat. It demonstrates:
*
* 1. RS256 JWT signing using your private key
* 2. Optional user payload encryption using IBM's public key
* 3. Cookie-based anonymous user tracking
* 4. Session-based user information enrichment
*
* Security Notes:
* - Never expose your private key to the client side
* - Always generate JWTs server-side only
* - Use HTTPS in production environments
* - Adjust token expiration times based on your security requirements
*/
const fs = require("fs");
const path = require("path");
const express = require("express");
const { v4: uuid } = require("uuid");
const jwtLib = require("jsonwebtoken");
const NodeRSA = require("node-rsa");
const router = express.Router();
/**
* Load your server-side RSA private key (PEM format)
*
* This key is used to sign JWTs with the RS256 algorithm. The corresponding public key
* must be uploaded to your watsonx Orchestrate instance security settings.
*
* IMPORTANT: Keep this file secure and never expose it to clients!
*
* Generate a new key pair using either method:
*
* Method 1 - Using ssh-keygen:
* 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
*
* Method 2 - Using openssl directly:
* openssl genrsa -out example-jwtRS256.key 4096
* openssl rsa -in example-jwtRS256.key -pubout -out example-jwtRS256.key.pub
*/
const PRIVATE_KEY_PATH = path.join(__dirname, "../keys/example-jwtRS256.key");
if (!fs.existsSync(PRIVATE_KEY_PATH)) {
throw new Error(`Private key not found at ${PRIVATE_KEY_PATH}`);
}
const PRIVATE_KEY = fs.readFileSync(PRIVATE_KEY_PATH);
/**
* Load IBM's RSA public key (PEM format)
*
* This key is provided by IBM watsonx Orchestrate and is used to encrypt the user_payload
* field in the JWT. This ensures that sensitive user information cannot be read by clients,
* as only IBM's servers can decrypt it.
*
* You can obtain this key by:
* 1. Running the wxo-embed-security-v4.sh script (recommended)
* 2. Calling the generate-key-pair API endpoint manually
*
* The encrypted payload will be decrypted server-side by watsonx Orchestrate.
*/
const IBM_PUBLIC_KEY_PATH = path.join(__dirname, "../keys/ibmPublic.key.pub");
if (!fs.existsSync(IBM_PUBLIC_KEY_PATH)) {
throw new Error(`IBM public key not found at ${IBM_PUBLIC_KEY_PATH}`);
}
const IBM_PUBLIC_KEY = fs.readFileSync(IBM_PUBLIC_KEY_PATH);
/**
* Cookie lifetime configuration
*
* This defines how long the anonymous user ID cookie will persist.
* Currently set to 60ms for demo purposes - in production, use a longer duration
* such as 45 days (45 * 24 * 60 * 60 * 1000 milliseconds).
*/
const TIME_45_DAYS = 60; // Demo value - replace with: 45 * 24 * 60 * 60 * 1000 for production
/**
* Create a signed JWT string for the wxO embed chat client
*
* This function constructs a JWT with the following structure:
*
* @param {string} anonymousUserID - A stable identifier for anonymous users (from cookie)
* @param {object|null} sessionInfo - Optional authenticated user session data
*
* JWT Claims:
* - sub: Subject (user identifier) - should be a stable, unique ID for the user
* - user_payload: Encrypted user data that will be decrypted by watsonx Orchestrate
* - name: User's display name
* - custom_message: Any custom message or metadata
* - custom_user_id: Your application's internal user ID
* - sso_token: Single sign-on token if applicable
* - context: Additional context data accessible by the agent
* - wxo_clientID: Your client/organization identifier
* - wxo_name: User's name for display in chat
* - wxo_role: User's role (e.g., Admin, User, Guest)
*
* @returns {string} A signed JWT token string
*/
function createJWTString(anonymousUserID, sessionInfo) {
// Base JWT claims structure
// Customize these fields based on your application's requirements
const jwtContent = {
// Subject: Unique identifier for the user
// In production, use a real user ID from your authentication system
sub: "FHYU235DD5", // Example value - replace with actual user ID
// User payload: Will be encrypted with IBM's public key
// This data is sensitive and will only be readable by watsonx Orchestrate servers
user_payload: {
name: "Anonymous",
custom_message: "Encrypted message",
custom_user_id: "",
sso_token: "sso_token",
},
// Context: Additional metadata accessible by the agent
// This data is NOT encrypted and can be read by the client
context: {
wxo_clientID: "865511", // Your client/organization ID
wxo_name: "Ava", // Display name in chat
wxo_role: "Admin", // User role
},
};
// Enrich the JWT with authenticated session data if available
// In production, this would come from your authentication system
if (sessionInfo) {
jwtContent.user_payload.name = sessionInfo.userName;
jwtContent.user_payload.custom_user_id = sessionInfo.customUserID;
}
// Encrypt the user_payload using IBM's RSA public key
// This ensures sensitive user data cannot be read by clients
// Only watsonx Orchestrate servers can decrypt this data
if (jwtContent.user_payload) {
const rsaKey = new NodeRSA(IBM_PUBLIC_KEY);
const dataString = JSON.stringify(jwtContent.user_payload);
const utf8Data = Buffer.from(dataString, "utf-8");
// Encrypt and encode as base64 string
jwtContent.user_payload = rsaKey.encrypt(utf8Data, "base64");
}
// Sign the JWT using RS256 algorithm with your private key
// The token expiration should be set based on your security requirements
// Common values: "1h" (1 hour), "6h" (6 hours), "1d" (1 day)
const jwtString = jwtLib.sign(jwtContent, PRIVATE_KEY, {
algorithm: "RS256",
expiresIn: "10000000s", // Demo value - use shorter duration in production (e.g., "1h")
});
return jwtString;
}
/**
* Retrieve or create a stable anonymous user ID stored in a cookie
*
* This function ensures that anonymous users maintain a consistent identity across
* page refreshes and sessions. The ID is stored in a secure HTTP-only cookie.
*
* Benefits:
* - Prevents user identity from changing mid-session
* - Enables conversation continuity for anonymous users
* - Provides basic user tracking without requiring authentication
*
* @param {object} request - Express request object
* @param {object} response - Express response object
* @returns {string} The anonymous user ID
*/
function getOrSetAnonymousID(request, response) {
// Check if an anonymous ID already exists in cookies
let anonymousID = request.cookies["ANONYMOUS-USER-ID"];
// Generate a new ID if none exists
if (!anonymousID) {
// Create a short, readable ID using UUID (first 5 characters for demo)
// In production, you might want to use the full UUID for better uniqueness
anonymousID = `anon-${uuid().slice(0, 5)}`;
}
// Set/refresh the cookie with each request to maintain the session
response.cookie("ANONYMOUS-USER-ID", anonymousID, {
expires: new Date(Date.now() + TIME_45_DAYS),
httpOnly: true, // Prevents client-side JavaScript from accessing the cookie (security)
sameSite: "Lax", // Provides CSRF protection while allowing normal navigation
secure: false, // Set to true in production when using HTTPS
});
return anonymousID;
}
/**
* Parse authenticated session information from cookies
*
* This function retrieves user session data if the user is authenticated.
* In a production application, you would:
* 1. Verify the session token/cookie
* 2. Fetch user information from your database or identity provider
* 3. Validate user permissions
*
* For this demo, we simply parse a JSON string from a cookie.
*
* @param {object} request - Express request object
* @returns {object|null} Session info object or null if not authenticated
*/
function getSessionInfo(request) {
const sessionInfo = request.cookies?.SESSION_INFO;
if (!sessionInfo) return null;
try {
// Parse the JSON session data
return JSON.parse(sessionInfo);
} catch {
// Return null if parsing fails (invalid JSON)
return null;
}
}
/**
* Express route handler for JWT creation
*
* This endpoint is called by the client to obtain a fresh JWT token.
* The token is required for secure authentication with watsonx Orchestrate embed chat.
*
* Flow:
* 1. Retrieve or create an anonymous user ID (stored in cookie)
* 2. Check for authenticated session information
* 3. Generate a signed JWT with user data
* 4. Return the JWT as plain text response
*
* The client will include this JWT in the wxOConfiguration.token field
* when initializing the embed chat.
*
* @param {object} request - Express request object
* @param {object} response - Express response object
*/
function createJWT(request, response) {
// Ensure we have a stable user ID (anonymous or authenticated)
const anonymousUserID = getOrSetAnonymousID(request, response);
// Get authenticated session data if available
const sessionInfo = getSessionInfo(request);
// Create and sign the JWT
const token = createJWTString(anonymousUserID, sessionInfo);
// Return the JWT as plain text
response.send(token);
}
// Define the GET endpoint that returns a signed JWT string
// This endpoint is called by the client before initializing the chat
router.get("/", createJWT);
module.exports = router;
API Integration
You can also integrate your agent with external applications by using the ADK’s provided agent completions API. These APIs allow agents to be shared across multiple watsonx Orchestrate instances:- Orchestrate Native Runs API: For long-duration workflows. API Documentation:
- Chat Completions Compatibility Layer: OpenAI-compatible for easy integration. API Documentation:

