- Enable
context_access_enabled: truein your agent definition. - Add variables like
channelto thecontext_variablesin your agent definition file. - Reimport the agent.
- Include context variables in the JWT payload.
Copy
Ask AI
spec_version: v1
style: react
name: hello_agent
llm: groq/openai/gpt-oss-120b
description: 'Agent description'
instructions: |
You are a helpful agent that must answer user questions in a clean and concise manner. Be polite and do not perform harmful behavior.
collaborators: []
tools: []
context_access_enabled: true
context_variables:
- channel # Use it to get access to context variables on the embedded chat
createJWT
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 demonstration purposes. For production, use a longer duration
* such as 45 days (45 * 24 * 60 * 60 * 1000 milliseconds).
*/
const TIME_45_DAYS = 60; // Value for demonstration. Replace with: 45 * 24 * 60 * 60 * 1000 for production.
/**
* Create a signed JWT string for the Orchestrate embedded 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: "FHY1234DD5", // Example value. Replace with actual user ID.
// User payload: It will be encrypted with IBM's public key
// This data is sensitive and will only be readable by watsonx Orchestrate servers
// Optional
user_payload: {
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: "7100s", // Value for demonstration. The maximum expiry time is 7100 seconds.
});
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 demonstration)
// In production, you might want to use the full UUID for better uniqueness
anonymousID = `anon-${uuid().slice(0, 5)}`;
}
// Set or 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 demonstration, 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 embedded 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 embedded 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;
JavaScript
Copy
Ask AI
<script>
function getUserId() {
let embed_user_id = getCookie('embed_user_id');
if (!embed_user_id) {
embed_user_id = Math.trunc(Math.random() * 1000000);
setCookie('embed_user_id', embed_user_id);
}
return embed_user_id;
}
function getCookie(name) {
console.log('getCookie');
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
function setCookie(name, value) {
document.cookie = `${name}=${value}; path=/`;
}
function preSendHandler(event) {
if (event?.message?.content) {
event.message.content = event.message.content.toUpperCase();
}
}
function sendHandler(event) {
console.log('send event', event);
}
function feedbackHandler(event) {
console.log('feedback', event);
}
function preReceiveHandler(event) {
event?.content?.map((element) => {
element.type = 'date';
});
}
function receiveHandler(event) {
console.log('received event', event);
}
function userDefinedResponseHandler(event) {
console.log('userDefinedResponse event', event);
event.hostElement.innerHTML = `
<cds-code-snippet>
node -v Lorem ipsum dolor sit amet, consectetur adipisicing elit. Blanditiis,
veritatis voluptate id incidunt molestiae officia possimus, quasi itaque
alias, architecto hic, dicta fugit? Debitis delectus quidem explicabo vitae
laboriosam!
</cds-code-snippet>
<br><br>
<div style="background-color:orange;color:white;padding:10px;">
<p>${event.contentItem?.template || '[No message content]'}</p>
</div>`;
}
function onChatLoad(instance) {
instance.on('chatstarted', (instance) => {
window.wxoChatInstance = instance;
});
instance.on('pre:send', preSendHandler);
instance.on('send', sendHandler);
instance.on('pre:receive', preReceiveHandler);
instance.on('receive', receiveHandler);
instance.on('feedback', feedbackHandler);
instance.on('userDefinedResponse', userDefinedResponseHandler);
}
async function getIdentityToken() {
// This will make a call to your server to request a new JWT.
const result = await fetch(
"http://localhost:3000/createJWT?user_id=" + getUserId()
);
window.wxOConfiguration.token = await result.text();
}
window.wxOConfiguration = {
orchestrationID: "20250430-0912-2925-309a-35c6bef54760_20250430-0949-0287-00f2-33dc583100c9",
hostURL: "https://us-south.watson-orchestrate.cloud.ibm.com",
rootElementID: "root",
deploymentPlatform: "ibmcloud",
crn: "crn:v1:bluemix:public:watsonx-orchestrate:us-south:a/123-456::",
chatOptions: {
agentId: "852431a8-32dd-4925-8cc3-9ea3d3162726",
agentEnvironmentId: "5d769a04-9445-4768-a687-710d6e9a24cf",
onLoad: onChatLoad
},
};
getIdentityToken().then(() => {
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);
});
</script>

