Skip to main content

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
Example:
<!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 configured

Security Architecture

The embedded chat uses RSA public-key cryptography to secure communication. The configuration involves two key pairs:
  1. IBM Key Pair
    • Generated by: watsonx Orchestrate service.
    • Public key: Shared with your application. Your application uses this key to encrypt the user_payload section of the JWT sent to watsonx Orchestrate.
    • Private key: Stored securely by watsonx Orchestrate.cUsed to decrypt the user_payload section of the JWT.
  2. 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.
  3. 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:
  • 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
When security is disabled:
  • 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
#!/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:
chmod +x wxO-embed-chat-security-tool.sh
4

Run the script

Run the script and follow the instructions to enable or disable security:
./wxO-embed-chat-security-tool.sh
After you configure security:
  1. The tool generates an IBM key pair via the API
  2. The tool generates a client key pair using OpenSSL
  3. Both public keys are configured in the service
  4. Security is enabled
All keys are saved in the wxo_security_config directory:
  • ibm_public_key.pem: IBM’s public key in PEM format
  • ibm_public_key.txt: IBM’s public key in single-line format
  • client_private_key.pem: Your private key (keep it secure!)
  • client_public_key.pem: Your public key in PEM format
  • client_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:
orchestrate channels webchat embed --agent-name=test_agent1
This outputs a <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
<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:
.
└── webchatProject/
    ├── keys/
    │   ├── example-jwtRS256.key
    │   ├── example-jwtRS256.key.pub
    │   └── ibmPublic.key.pub
    ├── routes/
    │   └── createJWT.js
    ├── static/
    │   └── index.html
    ├── server.js
    └── package.json
You can generate the example-jwtRS256 keys with the following commands:
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
And obtain the IBM public key by running the script provided in Enabling security. Use the following file contents as a reference for your implementation:
/**
 * 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: