/**
* 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;