UNPKG

purecloud-platform-client-v2

Version:

A JavaScript library to interface with the PureCloud Platform API

1,487 lines (1,347 loc) 51.9 kB
import Configuration from './configuration.js'; import DefaultHttpClient from './DefaultHttpClient.js'; import AbstractHttpClient from './AbstractHttpClient.js'; import HttpRequestOptions from './HttpRequestOptions.js'; import { default as qs } from 'qs'; /** * @module purecloud-platform-client-v2/ApiClient * @version 223.0.0 */ class ApiClient { /** * Singleton getter */ get instance() { return ApiClient.instance; } /** * Singleton setter */ set instance(value) { ApiClient.instance = value; } /** * Manages low level client-server communications, parameter marshalling, etc. There should not be any need for an * application to use this class directly - the *Api and model classes provide the public API for the service. The * contents of this file should be regarded as internal but are documented for completeness. * @alias module:purecloud-platform-client-v2/ApiClient * @class */ constructor() { /** * @description The default API client implementation. * @type {module:purecloud-platform-client-v2/ApiClient} */ if(!ApiClient.instance){ ApiClient.instance = this; } /** * Enumeration of collection format separator strategies. * @enum {String} * @readonly */ this.CollectionFormatEnum = { /** * Comma-separated values. Value: <code>csv</code> * @const */ CSV: ',', /** * Space-separated values. Value: <code>ssv</code> * @const */ SSV: ' ', /** * Tab-separated values. Value: <code>tsv</code> * @const */ TSV: '\t', /** * Pipe(|)-separated values. Value: <code>pipes</code> * @const */ PIPES: '|', /** * Native array. Value: <code>multi</code> * @const */ MULTI: 'multi' }; /** * @description Value is `true` if local storage exists. Otherwise, false. */ try { localStorage.setItem('purecloud_local_storage_test', 'purecloud_local_storage_test'); localStorage.removeItem('purecloud_local_storage_test'); this.hasLocalStorage = true; } catch(e) { this.hasLocalStorage = false; } /** * The authentication methods to be included for all API calls. * @type {Array.<String>} */ this.authentications = { 'Guest Chat JWT': {type: 'apiKey', 'in': 'header', name: 'Authorization'}, 'PureCloud OAuth': {type: 'oauth2'} }; /** * The default HTTP headers to be included for all API calls. * @type {Array.<String>} * @default {} */ this.defaultHeaders = {}; /** * The default HTTP timeout for all API calls. * @type {Number} * @default 16000 */ this.timeout = 16000; this.authData = {}; this.settingsPrefix = 'purecloud'; // Transparently request a new access token when it expires (Code Authorization only) this.refreshInProgress = false; this.httpClient; this.proxyAgent; this.config = new Configuration(); if (typeof(window) !== 'undefined') window.ApiClient = this; } /** * @description If set to `true`, the response object will contain additional information about the HTTP response. When `false` (default) only the body object will be returned. * @param {boolean} returnExtended - `true` to return extended responses */ setReturnExtendedResponses(returnExtended) { this.returnExtended = returnExtended; } /** * @description When `true`, persists the auth token to local storage to avoid a redirect to the login page on each page load. Defaults to `false`. * @param {boolean} doPersist - `true` to persist the auth token to local storage * @param {string} prefix - (Optional, default 'purecloud') The name prefix used for the local storage key */ setPersistSettings(doPersist, prefix) { this.persistSettings = doPersist; this.settingsPrefix = prefix ? prefix.replace(/\W+/g, '_') : 'purecloud'; } /** * @description Saves the auth token to local storage, if enabled. */ _saveSettings(opts) { try { this.authData.accessToken = opts.accessToken; this.authentications['PureCloud OAuth'].accessToken = opts.accessToken; if (opts.state) { this.authData.state = opts.state; } this.authData.error = opts.error; this.authData.error_description = opts.error_description; if (opts.tokenExpiryTime) { this.authData.tokenExpiryTime = opts.tokenExpiryTime; this.authData.tokenExpiryTimeString = opts.tokenExpiryTimeString; } // Don't save settings if we aren't supposed to be persisting them if (this.persistSettings !== true) return; // Ensure we can access local storage if (!this.hasLocalStorage) { return; } // Remove state from data so it's not persisted let tempData = JSON.parse(JSON.stringify(this.authData)); delete tempData.state; // Save updated auth data localStorage.setItem(`${this.settingsPrefix}_auth_data`, JSON.stringify(tempData)); } catch (e) { console.error(e); } } /** * @description Loads settings from local storage, if enabled. */ _loadSettings() { // Don't load settings if we aren't supposed to be persisting them if (this.persistSettings !== true) return; // Ensure we can access local storage if (!this.hasLocalStorage) { return; } // Load current auth data const tempState = this.authData.state; this.authData = localStorage.getItem(`${this.settingsPrefix}_auth_data`); if (!this.authData) this.authData = {}; else this.authData = JSON.parse(this.authData); if (this.authData.accessToken) this.setAccessToken(this.authData.accessToken); this.authData.state = tempState; } /** * @description Sets the environment used by the session * @param {string} environment - (Optional, default "mypurecloud.com") Environment the session use, e.g. mypurecloud.ie, mypurecloud.com.au, etc. */ setEnvironment(environment) { this.config.setEnvironment(environment); } /** * @description Sets the dynamic HttpClient used by the client * @param {object} httpClient - HttpClient to be injected */ setHttpClient(httpClient) { if (!(httpClient instanceof AbstractHttpClient)) { throw new Error("httpclient must be an instance of AbstractHttpClient. See DefaultltHttpClient for a prototype"); } this.httpClient = httpClient; } /** * @description Gets the HttpClient used by the client */ getHttpClient() { if (this.httpClient) { return this.httpClient; } else { this.httpClient = new DefaultHttpClient(this.timeout, this.proxyAgent); return this.httpClient; } } /** * @description Sets the certificate paths if MTLS authentication is needed * @param {string} certPath - path for certs * @param {string} keyPath - path for key * @param {string} caPath - path for public certs */ setMTLSCertificates(certPath, keyPath, caPath) { if (typeof window === 'undefined') { const agentOptions = {} if (certPath) { agentOptions.cert = require('fs').readFileSync(certPath); } if (keyPath) { agentOptions.key = require('fs').readFileSync(keyPath); } if (caPath) { agentOptions.ca = require('fs').readFileSync(caPath); } agentOptions.rejectUnauthorized = true this.proxyAgent = new require('https').Agent(agentOptions); const httpClient = this.getHttpClient(); httpClient.setHttpsAgent(this.proxyAgent); } else { throw new Error("MTLS authentication is managed by the Browser itself. MTLS certificates cannot be set via code on Browser."); } } /** * @description Sets preHook functions for the httpClient * @param {string} preHook - method definition for prehook */ setPreHook(preHook) { const httpClient = this.getHttpClient(); httpClient.setPreHook(preHook); } /** * @description Sets postHook functions for the httpClient * @param {string} postHook - method definition for posthook */ setPostHook(postHook) { const httpClient = this.getHttpClient(); httpClient.setPostHook(postHook); } /** * @description Sets the certificate content if MTLS authentication is needed * @param {string} certContent - content for certs * @param {string} keyContent - content for key * @param {string} caContent - content for public certs */ setMTLSContents(certContent, keyContent, caContent) { if (typeof window === 'undefined') { const agentOptions = {} if (certContent) { agentOptions.cert = certContent; } if (keyContent) { agentOptions.key = keyContent; } if (caContent) { agentOptions.ca = caContent; } agentOptions.rejectUnauthorized = true this.proxyAgent = new require('https').Agent(agentOptions); const httpClient = this.getHttpClient(); httpClient.setHttpsAgent(this.proxyAgent); } else { throw new Error("MTLS authentication is managed by the Browser itself. MTLS certificates cannot be set via code on Browser."); } } /** * @description Sets the gateway used by the session * @param {object} gateway - Gateway Configuration interface * @param {string} gateway.host - The address of the gateway. * @param {string} gateway.protocol - (optional) The protocol to use. It will default to "https" if the parameter is not defined or empty. * @param {string} gateway.port - (optional) The port to target. This parameter can be defined if a non default port is used and needs to be specified in the url (value must be greater than 0). * @param {string} gateway.path_params_login - (optional) An arbitrary string to be appended to the gateway url path for Login requests. * @param {string} gateway.path_params_api - (optional) An arbitrary string to be appended to the gateway url path for API requests. * @param {string} gateway.username - (optional) Not used at this stage (for a possible future use). * @param {string} gateway.password - (optional) Not used at this stage (for a possible future use). */ setGateway(gateway) { this.config.setGateway(gateway); } /** * @description Initiates the implicit grant login flow. Will attempt to load the token from local storage, if enabled. * @param {string} clientId - The client ID of an OAuth Implicit Grant client * @param {string} redirectUri - The redirect URI of the OAuth Implicit Grant client * @param {object} opts - (optional) Additional options * @param {string} opts.state - (optional) An arbitrary string to be passed back with the login response. Used for client apps to associate login responses with a request. * @param {string} opts.org - (optional) The organization name that would normally used when specifying an organization name when logging in. This is only used when a provider is also specified. * @param {string} opts.provider - (optional) Authentication provider to log in with e.g. okta, adfs, salesforce, onelogin. This is only used when an org is also specified. */ loginImplicitGrant(clientId, redirectUri, opts) { // Check for auth token in hash const hash = this._setValuesFromUrlHash(); this.clientId = clientId; this.redirectUri = redirectUri; if (!opts) opts = {}; return new Promise((resolve, reject) => { // Abort if org and provider are not set together if (opts.org && !opts.provider) { reject(new Error('opts.provider must be set if opts.org is set')); } else if (opts.provider && !opts.org) { reject(new Error('opts.org must be set if opts.provider is set')); } // Abort on auth error if (hash && hash.error) { hash.accessToken = undefined; this._saveSettings(hash); return reject(new Error(`[${hash.error}] ${hash.error_description}`)); } // Test token and proceed with login this._testTokenAccess() .then(() => { if (!this.authData.state && opts.state) this.authData.state = opts.state; resolve(this.authData); }) .catch((error) => { var query = { client_id: encodeURIComponent(this.clientId), redirect_uri: encodeURIComponent(this.redirectUri), response_type: 'token' }; if (opts.state) query.state = encodeURIComponent(opts.state); if (opts.org) query.org = encodeURIComponent(opts.org); if (opts.provider) query.provider = encodeURIComponent(opts.provider); var url = this._buildAuthUrl('oauth/authorize', query); window.location.replace(url); }); }); } /** * @description Initiates the client credentials login flow. Only available in node apps. * @param {string} clientId - The client ID of an OAuth Implicit Grant client * @param {string} clientSecret - The client secret of an OAuth Implicit Grant client */ loginClientCredentialsGrant(clientId, clientSecret) { this.clientId = clientId; var authHeader = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); var loginBasePath = this.config.getConfUrl('login', `https://login.${this.config.environment}`); return new Promise((resolve, reject) => { // Block browsers from using client credentials if (typeof window !== 'undefined') { reject(new Error('The client credentials grant is not supported in a browser.')); return; } const headers = { 'Authorization': `Basic ${authHeader}` } var requestOptions = new HttpRequestOptions(`${loginBasePath}/oauth/token`, `POST`, headers, null, 'grant_type=client_credentials', this.timeout); const httpClient = this.getHttpClient(); httpClient.request(requestOptions) .then((response) => { // Logging this.config.logger.log( 'trace', response.status, 'POST', `${loginBasePath}/oauth/token`, headers, response.headers, { grant_type: 'client_credentials' }, undefined ); this.config.logger.log( 'debug', response.status, 'POST', `${loginBasePath}/oauth/token`, headers, undefined, { grant_type: 'client_credentials' }, undefined ); // Save access token this.setAccessToken(response.data['access_token']); // Set expiry time this.authData.tokenExpiryTime = (new Date()).getTime() + (response.data['expires_in'] * 1000); this.authData.tokenExpiryTimeString = (new Date(this.authData.tokenExpiryTime)).toUTCString(); // Return auth data resolve(this.authData); }) .catch((error) => { // Log error if (error.response) { this.config.logger.log( 'error', error.response.status, 'POST', `${loginBasePath}/oauth/token`, headers, error.response.headers, { grant_type: 'client_credentials' }, error.response.data ); } reject(error); }); }); } /** * @description Initiates the Saml2Bearerflow. Only available in node apps. * @param {string} clientId - The client ID of an OAuth Implicit Grant client * @param {string} clientSecret - The client secret of an OAuth Implicit Grant client * @param {string} orgName - The orgName of an OAuth Implicit Grant client * @param {string} assertion - The saml2bearer assertion */ loginSaml2BearerGrant(clientId, clientSecret, orgName, assertion) { this.clientId = clientId; var loginBasePath = this.config.getConfUrl('login', `https://login.${this.config.environment}`); return new Promise((resolve, reject) => { if (typeof window !== 'undefined') { reject(new Error('The saml2bearer grant is not supported in a browser.')); return; } var encodedData = Buffer.from(clientId + ':' + clientSecret).toString('base64'); var request = this._formAuthRequest(encodedData, { grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer', orgName: orgName, assertion: assertion }); request.proxy = this.proxy; var bodyParam = { grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer', orgName: orgName, assertion: assertion, }; // Handle response request .then((response) => { // Logging this.config.logger.log( 'trace', response.status, 'POST', `${loginBasePath}/oauth/token`, request.headers, response.headers, bodyParam, undefined ); this.config.logger.log( 'debug', response.status, 'POST', `${loginBasePath}/oauth/token`, request.headers, undefined, bodyParam, undefined ); // Get access token from response var access_token = response.data.access_token; this.setAccessToken(access_token); this.authData.tokenExpiryTime = new Date().getTime() + response.data['expires_in'] * 1000; this.authData.tokenExpiryTimeString = new Date(this.authData.tokenExpiryTime).toUTCString(); // Return auth data resolve(this.authData); }) .catch((error) => { // Log error if (error.response) { this.config.logger.log( 'error', error.response.status, 'POST', `${loginBasePath}/oauth/token`, request.headers, error.response.headers, bodyParam, error.response.data ); } reject(error); }); }); } /** * @description Completes the PKCE Code Authorization. * @param {string} clientId - The client ID of an OAuth Code Authorization Grant client * @param {string} codeVerifier - code verifier used to generate the code challenge * @param {string} authCode - Authorization code * @param {string} redirectUri - Authorized redirect URI for your Code Authorization client */ authorizePKCEGrant(clientId, codeVerifier, authCode, redirectUri) { this.clientId = clientId; var loginBasePath = this.config.getConfUrl('login', `https://login.${this.config.environment}`); return new Promise((resolve, reject) => { var headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; var data = qs.stringify({ grant_type: 'authorization_code', code: authCode, code_verifier: codeVerifier, client_id: clientId, redirect_uri: redirectUri }); var requestOptions = new HttpRequestOptions(`${loginBasePath}/oauth/token`, `POST`, headers, null, data, this.timeout); const httpClient = this.getHttpClient(); var bodyParam = { grant_type: 'authorization_code', code: authCode, code_verifier: codeVerifier, client_id: clientId, redirect_uri: redirectUri, }; // Handle response httpClient.request(requestOptions) .then((response) => { // Logging this.config.logger.log( 'trace', response.status, 'POST', `${loginBasePath}/oauth/token`, requestOptions.headers, response.headers, bodyParam, undefined ); this.config.logger.log( 'debug', response.status, 'POST', `${loginBasePath}/oauth/token`, requestOptions.headers, undefined, bodyParam, undefined ); // Get access token from response var access_token = response.data.access_token; this.setAccessToken(access_token); this.authData.tokenExpiryTime = new Date().getTime() + response.data['expires_in'] * 1000; this.authData.tokenExpiryTimeString = new Date(this.authData.tokenExpiryTime).toUTCString(); // Return auth data resolve(this.authData); }) .catch((error) => { // Log error if (error.response) { this.config.logger.log( 'error', error.response.status, 'POST', `${loginBasePath}/oauth/token`, requestOptions.headers, error.response.headers, bodyParam, error.response.data ); } reject(error); }); }); } /** * @description Generate a random string used as PKCE Code Verifier - length = 43 to 128. * @param {number} nChar - code length */ generatePKCECodeVerifier(nChar) { if (nChar < 43 || nChar > 128) { throw new Error(`PKCE Code Verifier (length) must be between 43 and 128 characters`); } // Check for window if (typeof window === 'undefined') { try { const getRandomValues = require('crypto').getRandomValues; const unreservedCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~"; let randomString = Array.from(getRandomValues(new Uint32Array(nChar))) .map((x) => unreservedCharacters[x % unreservedCharacters.length]) .join(''); return randomString; } catch (err) { throw new Error(`Crypto module is missing/not supported.`); } } else { const unreservedCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~"; let randomString = Array.from(crypto.getRandomValues(new Uint32Array(nChar))) .map((x) => unreservedCharacters[x % unreservedCharacters.length]) .join(''); return randomString; } } /** * @description Compute Base64Url PKCE Code Challenge from Code Verifier. * @param {string} code - code verifier used to generate the code challenge */ computePKCECodeChallenge(code) { if (code.length < 43 || code.length > 128) { throw new Error(`PKCE Code Verifier (length) must be between 43 and 128 characters`); } // Check for window if (typeof window === 'undefined') { // nodejs try { const createHash = require('crypto').createHash; const utf8 = new TextEncoder().encode(code); return new Promise((resolve, reject) => { const hashHex = createHash('sha256').update(utf8).digest(); const hashBase64Url = Buffer.from(hashHex).toString('base64url'); resolve(hashBase64Url); }); } catch (err) { throw new Error(`Crypto module is missing/not supported.`); } } else { // browser const utf8 = new TextEncoder().encode(code); return new Promise((resolve, reject) => { window.crypto.subtle.digest("SHA-256", utf8).then((hashBuffer) => { const hashBase64 = Buffer.from(hashBuffer).toString('base64'); let hashBase64Url = hashBase64.replaceAll("+", "-").replaceAll("/", "_"); hashBase64Url = hashBase64Url.split("=")[0]; resolve(hashBase64Url); }) .catch((error) => { // Handle failure return reject(new Error(`Code Challenge Error ${error}`)); }); }); } } /** * @description Initiates the pkce auth code grant login flow. Will attempt to load the token from local storage, if enabled. * @param {string} clientId - The client ID of an OAuth Implicit Grant client * @param {string} redirectUri - The redirect URI of the OAuth Implicit Grant client * @param {object} opts - (optional) Additional options * @param {string} opts.state - (optional) An arbitrary string to be passed back with the login response. Used for client apps to associate login responses with a request. * @param {string} opts.org - (optional) The organization name that would normally used when specifying an organization name when logging in. This is only used when a provider is also specified. * @param {string} opts.provider - (optional) Authentication provider to log in with e.g. okta, adfs, salesforce, onelogin. This is only used when an org is also specified. * @param {string} codeVerifier - (optional) code verifier used to generate the code challenge */ loginPKCEGrant(clientId, redirectUri, opts, codeVerifier) { // Need Local Storage or non null codeVerifier as parameter if (!this.hasLocalStorage && !codeVerifier) { throw new Error(`loginPKCEGrant requires Local Storage or codeVerifier as input parameter`); } // Check for auth code in query const query = this._setValuesFromUrlQuery(); this.clientId = clientId; this.redirectUri = redirectUri; this.codeVerifier = codeVerifier; if (!opts) opts = {}; return new Promise((resolve, reject) => { // Abort if org and provider are not set together if (opts.org && !opts.provider) { reject(new Error('opts.provider must be set if opts.org is set')); } else if (opts.provider && !opts.org) { reject(new Error('opts.org must be set if opts.provider is set')); } // Abort on auth error if (query && query.error) { // remove codeVerifier from session storage if (this.hasLocalStorage) { sessionStorage.removeItem(`genesys_cloud_sdk_pkce_code_verifier`); } // reset access token if any was stored this._saveSettings({ accessToken: undefined }); return reject(new Error(`[${query.error}] ${query.error_description}`)); } // Get token on auth code if (query && query.code) { if (!this.codeVerifier) { // load codeVerifier from session storage if (this.hasLocalStorage) { this.codeVerifier = sessionStorage.getItem(`genesys_cloud_sdk_pkce_code_verifier`); } } this.authorizePKCEGrant(this.clientId, this.codeVerifier, query.code, this.redirectUri) .then(() => { // Do authenticated things this._testTokenAccess() .then(() => { if (!this.authData.state && query.state) this.authData.state = query.state; // remove codeVerifier from session storage if (this.hasLocalStorage) { sessionStorage.removeItem(`genesys_cloud_sdk_pkce_code_verifier`); } resolve(this.authData); }) .catch((error) => { // Handle failure response this._saveSettings({ accessToken: undefined}); // remove codeVerifier from session storage if (this.hasLocalStorage) { sessionStorage.removeItem(`genesys_cloud_sdk_pkce_code_verifier`); } return reject(new Error(`[${error.name}] ${error.msg}`)); }); }) .catch((error) => { // Handle failure response this._saveSettings({ accessToken: undefined}); // remove codeVerifier from session storage if (this.hasLocalStorage) { sessionStorage.removeItem(`genesys_cloud_sdk_pkce_code_verifier`); } return reject(new Error(`[${error.name}] ${error.msg}`)); }); } else { // Test token (if previously stored) and proceed with login this._testTokenAccess() .then(() => { if (!this.authData.state && opts.state) this.authData.state = opts.state; resolve(this.authData); }) .catch((error) => { if (!this.codeVerifier) { this.codeVerifier = this.generatePKCECodeVerifier(128); // save codeVerifier in session storage if (this.hasLocalStorage) { sessionStorage.setItem(`genesys_cloud_sdk_pkce_code_verifier`, this.codeVerifier); } } this.computePKCECodeChallenge(this.codeVerifier) .then((codeChallenge) => { var tokenQuery = { client_id: encodeURIComponent(this.clientId), redirect_uri: encodeURIComponent(this.redirectUri), code_challenge: encodeURIComponent(codeChallenge), response_type: 'code', code_challenge_method: 'S256' }; if (opts.state) tokenQuery.state = encodeURIComponent(opts.state); if (opts.org) tokenQuery.org = encodeURIComponent(opts.org); if (opts.provider) tokenQuery.provider = encodeURIComponent(opts.provider); var url = this._buildAuthUrl('oauth/authorize', tokenQuery); window.location.replace(url); }) .catch((err) => { return reject(new Error(`[${err.name}]`)); }); }); } }); } /** * @description Parses the URL Query, grabs the code, and clears the query param. If no code is found, no action is taken. */ _setValuesFromUrlQuery() { // Check for window if (!(typeof window !== 'undefined' && window.location.search)) return; // Process query string let query = {}; let queryParams = new URLSearchParams(window.location.search); let code = queryParams.get('code'); let error = queryParams.get('error'); let errorDescription = queryParams.get('error_description'); let state = queryParams.get('state'); // Check for error if (error) { query.error = error; if (errorDescription) { query.error_description = errorDescription; } return query; } // Everything goes in here because we only want to act if we found an access token if (code) { query.code = code; if (state) { query.state = state; } } // Remove code from URL // Credit: https://stackoverflow.com/questions/1397329/how-to-remove-the-hash-from-window-location-with-javascript-without-page-refresh/5298684#5298684 var scrollV, scrollH, loc = window.location; if ('replaceState' in history) { history.replaceState('', document.title, loc.pathname); } else { // Prevent scrolling by storing the page's current scroll offset scrollV = document.body.scrollTop; scrollH = document.body.scrollLeft; // Remove code history.pushState('', document.title, loc.pathname); // Restore the scroll offset, should be flicker free document.body.scrollTop = scrollV; document.body.scrollLeft = scrollH; } return query; } /** * @description Initiates the Code Authorization. Only available in node apps. * @param {string} clientId - The client ID of an OAuth Code Authorization Grant client * @param {string} clientSecret - The client secret of an OAuth Code Authorization Grant client * @param {string} authCode - Authorization code * @param {string} redirectUri - Authorized redirect URI for your Code Authorization client */ loginCodeAuthorizationGrant(clientId, clientSecret, authCode, redirectUri) { this.clientId = clientId; this.clientSecret = clientSecret; return new Promise((resolve, reject) => { if (typeof window !== 'undefined') { reject(new Error('The Code Authorization grant is not supported in a browser.')); return; } var encodedData = Buffer.from(clientId + ':' + clientSecret).toString('base64'); var request = this._formAuthRequest(encodedData, { grant_type: 'authorization_code', code: authCode, redirect_uri: redirectUri }); request.proxy = this.proxy; var bodyParam = { grant_type: 'authorization_code', code: authCode, redirect_uri: redirectUri, }; // Handle response this._handleCodeAuthorizationResponse(request, bodyParam, resolve, reject); }); } /** * @description Requests a new access token for Code Authorization. Only available in node apps. * @param {string} clientId - The client ID of an OAuth Code Authorization Grant client * @param {string} clientSecret - The client secret of an OAuth Code Authorization Grant client * @param {string} authCode - Authorization code * @param {string} redirectUri - Authorized redirect URI for your Code Authorization client */ refreshCodeAuthorizationGrant(clientId, clientSecret, refreshToken) { return new Promise((resolve, reject) => { if (typeof window !== 'undefined') { reject(new Error('The Code Authorization grant is not supported in a browser.')); return; } var encodedData = Buffer.from(clientId + ':' + clientSecret).toString('base64'); var request = this._formAuthRequest(encodedData, { grant_type: 'refresh_token' , refresh_token: refreshToken }); request.proxy = this.proxy; var bodyParam = { grant_type: 'refresh_token', refresh_token: refreshToken, }; // Handle response this._handleCodeAuthorizationResponse(request, bodyParam, resolve, reject); }); } /** * @description Handles the response for code auth requests * @param {object} request - Authorization request object * @param {object} bodyParam - Input body data for authorization request * @param {function} resolve - Promise resolve callback * @param {function} reject - Promise reject callback */ _handleCodeAuthorizationResponse(request, bodyParam, resolve, reject) { var loginBasePath = this.config.getConfUrl('login', `https://login.${this.config.environment}`); request .then((response) => { // Logging this.config.logger.log( 'trace', response.status, 'POST', `${loginBasePath}/oauth/token`, request.headers, response.headers, bodyParam, undefined ); this.config.logger.log( 'debug', response.status, 'POST', `${loginBasePath}/oauth/token`, request.headers, undefined, bodyParam, undefined ); // Get access token from response var access_token = response.data.access_token; var refresh_token = response.data.refresh_token; this.setAccessToken(access_token); this.authData.refreshToken = refresh_token; this.authData.tokenExpiryTime = new Date().getTime() + response.data['expires_in'] * 1000; this.authData.tokenExpiryTimeString = new Date(this.authData.tokenExpiryTime).toUTCString(); // Return auth data resolve(this.authData); }) .catch((error) => { // Log error if (error.response) { this.config.logger.log( 'error', error.response.status, 'POST', `${loginBasePath}/oauth/token`, request.headers, error.response.headers, bodyParam, error.response.data ); } reject(error); }); } /** * @description Utility function to create the request for auth requests * @param {string} encodedData - Base64 encoded client and clientSecret pair * @param {object} data - data to url form encode */ _formAuthRequest(encodedData, data) { var loginBasePath = this.config.getConfUrl('login', `https://login.${this.config.environment}`); var headers = { 'Authorization': 'Basic ' + encodedData, 'Content-Type': 'application/x-www-form-urlencoded' }; var requestOptions = new HttpRequestOptions(`${loginBasePath}/oauth/token`, `POST`, headers, null, qs.stringify(data), this.timeout); const httpClient = this.getHttpClient(); return httpClient.request(requestOptions); } /** * @description Handles an expired access token. Only available in node apps. * @param {string} statusCode - The status code of a request */ _handleExpiredAccessToken() { return new Promise((resolve, reject) => { if (typeof window !== 'undefined') { reject(new Error('This method is not supported in a browser.')); return; } if (!this.refreshInProgress) { this.refreshInProgress = true; this.refreshCodeAuthorizationGrant(this.clientId, this.clientSecret, this.authData.refreshToken) .then(() => { this.refreshInProgress = false; resolve(); }) .catch((err) => { // Handle failure response this.refreshInProgress = false; reject(err); }); } else { // Wait refresh_token_wait_max seconds for other thread to complete refresh this._sleep(this.config.refresh_token_wait_max) .then(() => { if (this.refreshInProgress) reject(new Error(`Token refresh took longer than ${this.config.refresh_token_wait_max} seconds`)); else resolve(); }); } }); } /** * @description Sleeps for a defined length * @param {int} millis - Length to sleep in milliseconds */ _sleep(millis) { return new Promise(resolve => setTimeout(resolve, millis)); } /** * @description Loads token from storage, if enabled, and checks to ensure it works. */ _testTokenAccess() { return new Promise((resolve, reject) => { // Load from storage this._loadSettings(); // Check if there is a token to test if (!this.authentications['PureCloud OAuth'].accessToken) { reject(new Error('Token is not set')); return; } // Test token this.callApi('/api/v2/tokens/me', 'GET', null, null, null, null, null, ['PureCloud OAuth'], ['application/json'], ['application/json']) .then(() => { resolve(); }) .catch((error) => { this._saveSettings({ accessToken: undefined }); reject(error); }); }); } /** * @description Parses the URL hash, grabs the access token, and clears the hash. If no access token is found, no action is taken. */ _setValuesFromUrlHash() { // Check for window if(!(typeof window !== 'undefined' && window.location.hash)) return; // Process hash string into object const hashRegex = new RegExp(`^#*(.+?)=(.+?)$`, 'i'); let hash = {}; window.location.hash.split('&').forEach((h) => { const match = hashRegex.exec(h); if (match) hash[match[1]] = decodeURIComponent(decodeURIComponent(match[2].replace(/\+/g, '%20'))); }); // Check for error if (hash.error) { return hash; } // Everything goes in here because we only want to act if we found an access token if (hash.access_token) { let opts = {}; if (hash.state) { opts.state = hash.state; } if (hash.expires_in) { opts.tokenExpiryTime = (new Date()).getTime() + (parseInt(hash.expires_in.replace(/\+/g, '%20')) * 1000); opts.tokenExpiryTimeString = (new Date(opts.tokenExpiryTime)).toUTCString(); } // Set access token opts.accessToken = hash.access_token.replace(/\+/g, '%20'); // Remove hash from URL // Credit: https://stackoverflow.com/questions/1397329/how-to-remove-the-hash-from-window-location-with-javascript-without-page-refresh/5298684#5298684 var scrollV, scrollH, loc = window.location; if ('replaceState' in history) { history.replaceState('', document.title, loc.pathname + loc.search); } else { // Prevent scrolling by storing the page's current scroll offset scrollV = document.body.scrollTop; scrollH = document.body.scrollLeft; // Remove hash loc.hash = ''; // Restore the scroll offset, should be flicker free document.body.scrollTop = scrollV; document.body.scrollLeft = scrollH; } this._saveSettings(opts); } } /** * @description Sets the access token to be used with requests * @param {string} token - The access token */ setAccessToken(token) { this._saveSettings({ accessToken: token }); } /** * @description Sets the storage key to use when persisting the access token * @param {string} storageKey - The storage key name */ setStorageKey(storageKey) { // Set storage key this.storageKey = storageKey; // Trigger storage of current token this.setAccessToken(this.authentications['PureCloud OAuth'].accessToken); } /** * @description Redirects the user to the PureCloud logout page */ logout(logoutRedirectUri) { if(this.hasLocalStorage) { this._saveSettings({ accessToken: undefined, state: undefined, tokenExpiryTime: undefined, tokenExpiryTimeString: undefined }); } var query = { client_id: encodeURIComponent(this.clientId) }; if (logoutRedirectUri) query['redirect_uri'] = encodeURI(logoutRedirectUri); var url = this._buildAuthUrl('logout', query); window.location.replace(url); } /** * @description Constructs a URL to the auth server * @param {string} path - The path for the URL * @param {object} query - An object of key/value pairs to use for querystring keys/values */ _buildAuthUrl(path, query) { if (!query) query = {}; var loginBasePath = this.config.getConfUrl('login', this.config.authUrl); return Object.keys(query).reduce((url, key) => !query[key] ? url : `${url}&${key}=${query[key]}`, `${loginBasePath}/${path}?`); } /** * Returns a string representation for an actual parameter. * @param param The actual parameter. * @returns {String} The string representation of <code>param</code>. */ paramToString(param) { if (!param) { return ''; } if (param instanceof Date) { return param.toJSON(); } if (param instanceof Boolean) { return param.toString().toLowerCase(); } return param.toString(); } /** * Returns query parameters serialized in the format needed for an axios request. * @param param The unserialized query parameters. * @returns {Object} The serialized representation the query parameters. */ serialize(obj) { var result = {}; for (var p in obj) { if (obj.hasOwnProperty(p) && obj[p] !== undefined) { result[encodeURIComponent(p)] = Array.isArray(obj[p]) ? obj[p].join(",") : this.paramToString(obj[p]); } } return result } /** * Adds headers onto an existing header object (may be empty) * @param existingHeaders The existing header object. * @param newHeaders New headers. * @returns {Object} The combination of all headers. */ addHeaders(existingHeaders, ...newHeaders) { if (existingHeaders) { existingHeaders = Object.assign(existingHeaders, ...newHeaders); } else { existingHeaders = Object.assign(...newHeaders); } return existingHeaders; } /** * Builds full URL by appending the given path to the base URL and replacing path parameter place-holders with parameter values. * NOTE: query parameters are not handled here. * @param {String} path The path to append to the base URL. * @param {Object} pathParams The parameter values to append. * @returns {String} The encoded path with parameter values substituted. */ buildUrl(path, pathParams) { if (!path.match(/^\//)) { path = `/${path}`; } var url = this.config.getConfUrl('api', this.config.basePath) + path; url = url.replace(/\{([\w-]+)\}/g, (fullMatch, key) => { var value; if (pathParams.hasOwnProperty(key)) { value = this.paramToString(pathParams[key]); } else { value = fullMatch; } return encodeURIComponent(value); }); return url; } /** * Checks whether the given content type represents JSON.<br> * JSON content type examples:<br> * <ul> * <li>application/json</li> * <li>application/json; charset=UTF8</li> * <li>APPLICATION/JSON</li> * </ul> * @param {String} contentType The MIME content type to check. * @returns {Boolean} <code>true</code> if <code>contentType</code> represents JSON, otherwise <code>false</code>. */ isJsonMime(contentType) { return Boolean(contentType && contentType.match(/^application\/json(;.*)?$/i)); } /** * Chooses a content type from the given array, with JSON preferred; i.e. return JSON if included, otherwise return the first. * @param {Array.<String>} contentTypes * @returns {String} The chosen content type, preferring JSON. */ jsonPreferredMime(contentTypes) { for (var i = 0; i < contentTypes.length; i++) { if (this.isJsonMime(contentTypes[i])) { return contentTypes[i]; } } return contentTypes[0]; } /** * Checks whether the given parameter value represents file-like content. * @param param The parameter to check. * @returns {Boolean} <code>true</code> if <code>param</code> represents a file. */ isFileParam(param) { // fs.ReadStream in Node.js (but not in runtime like browserify) if (typeof window === 'undefined' && typeof require === 'function' && require('fs') && param instanceof require('fs').ReadStream) { return true; } // Buffer in Node.js if (typeof Buffer === 'function' && param instanceof Buffer) { return true; } // Blob in browser if (typeof Blob === 'function' && param instanceof Blob) { return true; } // File in browser (it seems File object is also instance of Blob, but keep this for safe) if (typeof File === 'function' && param instanceof File) { return true; } return false; } /** * Normalizes parameter values: * <ul> * <li>remove nils</li> * <li>keep files and arrays</li> * <li>format to string with `paramToString` for other cases</li> * </ul> * @param {Object.<String, Object>} params The parameters as object properties. * @returns {Object.<String, Object>} normalized parameters. */ normalizeParams(params) { var newParams = {}; for (var key in params) { if (params.hasOwnProperty(key) && params[key] !== undefined) { var value = params[key]; if (this.isFileParam(value) || Array.isArray(value)) { newParams[key] = value; } else { newParams[key] = this.paramToString(value); } } } return newParams; } /** * Builds a string representation of an array-type actual parameter, according to the given collection format. * @param {Array} param An array parameter. * @param {module:purecloud-platform-client-v2/ApiClient.CollectionFormatEnum} collectionFormat The array element separator strategy. * @returns {String|Array} A string representation of the supplied collection, using the specified delimiter. Returns * <code>param</code> as is if <code>collectionFormat</code> is <code>multi</code>. */ buildCollectionParam(param, collectionFormat) { if (!param) return; if (!Array.isArray(param)) { param = [param] } switch (collectionFormat) { case 'csv': return param.map(this.paramToString).join(','); case 'ssv': return param.map(this.paramToString).join(' '); case 'tsv': return param.map(this.paramToString).join('\t'); case 'pipes': return param.map(this.paramToString).join('|'); case 'multi': // return the array directly as axios will handle it as expected return param.map(this.paramToString); default: throw new Error(`Unknown collection format: ${collectionFormat}`); } } /** * Applies authentication headers to the request. * @param {Object} request The axios request config object. * @param {Array.<String>} authNames An array of authentication method names. */ applyAuthToRequest(request, authNames) { authNames.forEach((authName) => { var auth = this.authentications[authName]; switch (auth.type) { case 'basic': if (auth.username || auth.password) { request.auth = { username: auth.username || '', password: auth.password || '' }; } break; case 'apiKey': if (auth.apiKey) { var data = {}; if (auth.apiKeyPrefix) { data[auth.name] = `${auth.apiKeyPrefix} ${auth.apiKey}`; } else { data[auth.name] = auth.apiKey; } if (auth['in'] === 'header') { request.headers = this.addHeaders(request.headers, data); } else { request.setParams(this.serialize(data)); } } break; case 'oauth2': if (auth.accessToken) { request.headers = this.addHeaders(request.headers, {'Authorization': `Bearer ${auth.accessToken}`}); } break; default: throw new Error(`Unknown authentication type: ${auth.type}`); } }); } /** * @description Sets the proxy agent axios will use for requests * @param {any} agent - The proxy agent */ setProxyAgent(agent) { this.proxyAgent = agent; const httpClient = this.getHttpClient(); httpClient.setHttpsAgent(this.proxyAgent); } /** * Invokes the REST service using the supplied settings and parameters. * @param {String} path The base URL to invoke. * @param {String} httpMethod The HTTP method to use. * @param {Object.<String, String>} pathParams A map of path parameters and their values. * @param {Object.<String, Object>} queryParams A map of query parameters and their values. * @param {Object.<String, Object>} headerParams A map of header parameters and their values. * @param {Object.<String, Object>} formParams A map of form parameters and their values. * @param {Object} bodyParam The value to pass as the request body. * @param {Array.<String>} authNames An array of authentication type names. * @param {Array.<String>} contentTypes An array of request MIME types. * @param {Array.<String>} accepts An array of acceptable response MIME types.types or the * constructor for a complex type. * @returns {Promise} A Promise object. */ callApi(path, httpMethod, pathParams, queryParams, headerParams, formParams, bodyParam, authNames, contentTypes, accepts) { return new Promise((resolve, reject) => { sendRequest(this); function sendRequest(that) { var url = that.buildUrl(path, pathParams); var request = new HttpRequestOptions(url, httpMethod, null, that.serialize(queryParams), null, that.timeout); // apply authentications that.applyAuthToRequest(request, authNames); // set header parameters const defaultHeaders = that.defaultHeaders; const normalizedHeaderParams = that.normalizeParams(headerParams); request.headers = that.addHeaders(request.headers, defaultHeaders, normalizedHeaderParams); var contentType = that.jsonPreferredMime(contentTypes); if (contentType) { request.headers['Content-Type'] = contentType; } else if (!request.headers['Content-Type']) { request.headers['Content-Type'] = 'application/json'; } if (contentType === 'application/x-www-form-urlencoded') { request.setData(that.normalizeParams(formParams)); } else if (contentType == 'multipart/form-data') { var _formParams = that.normalizeParams(formParams); for (var key in _formParams) { if (_formParams.hasOwnProperty(key)) { // Looks like axios handles files and forms the same way var formData = new FormData(); formData.set(key, _formParams[key]); request.setData(formData); } } } else if (bodyParam) { request.setData(bodyParam); } var accept = that.jsonPreferredMime(accepts); if (accept)