@thoughtspot/visual-embed-sdk
Version:
ThoughtSpot Embed SDK
438 lines • 15.4 kB
JavaScript
import { getAuthenticationToken } from './authToken';
import { getEmbedConfig } from './embed/embedConfig';
import { initMixpanel } from './mixpanel-service';
import { AuthType, EmbedEvent, } from './types';
import { getDOMNode, getRedirectUrl, getSSOMarker } from './utils';
import { EndPoints, fetchAuthPostService, fetchAuthService, fetchBasicAuthService, fetchLogoutService, } from './utils/authService';
import { isActiveService } from './utils/authService/tokenizedAuthService';
import { logger } from './utils/logger';
import { getSessionInfo, getPreauthInfo } from './utils/sessionInfoService';
import { ERROR_MESSAGE } from './errors';
import { resetAllCachedServices } from './utils/resetServices';
// eslint-disable-next-line import/no-mutable-exports
export let loggedInStatus = false;
// eslint-disable-next-line import/no-mutable-exports
export let samlAuthWindow = null;
// eslint-disable-next-line import/no-mutable-exports
export let samlCompletionPromise = null;
let releaseVersion = '';
export const SSO_REDIRECTION_MARKER_GUID = '5e16222e-ef02-43e9-9fbd-24226bf3ce5b';
/**
* Enum for auth failure types. This is the parameter passed to the listner
* of {@link AuthStatus.FAILURE}.
* @group Authentication / Init
*/
export var AuthFailureType;
(function (AuthFailureType) {
AuthFailureType["SDK"] = "SDK";
AuthFailureType["NO_COOKIE_ACCESS"] = "NO_COOKIE_ACCESS";
AuthFailureType["EXPIRY"] = "EXPIRY";
AuthFailureType["OTHER"] = "OTHER";
AuthFailureType["IDLE_SESSION_TIMEOUT"] = "IDLE_SESSION_TIMEOUT";
AuthFailureType["UNAUTHENTICATED_FAILURE"] = "UNAUTHENTICATED_FAILURE";
})(AuthFailureType || (AuthFailureType = {}));
/**
* Enum for auth status emitted by the emitter returned from {@link init}.
* @group Authentication / Init
*/
export var AuthStatus;
(function (AuthStatus) {
/**
* Emits when the SDK fails to authenticate
*/
AuthStatus["FAILURE"] = "FAILURE";
/**
* Emits when the SDK authenticates successfully
*/
AuthStatus["SDK_SUCCESS"] = "SDK_SUCCESS";
/**
* @hidden
* Emits when iframe is loaded and session info is available
*/
AuthStatus["SESSION_INFO_SUCCESS"] = "SESSION_INFO_SUCCESS";
/**
* Emits when the app sends an authentication success message
*/
AuthStatus["SUCCESS"] = "SUCCESS";
/**
* Emits when a user logs out
*/
AuthStatus["LOGOUT"] = "LOGOUT";
/**
* Emitted when inPopup is true in the SAMLRedirect flow and the
* popup is waiting to be triggered either programmatically
* or by the trigger button.
* @version SDK: 1.19.0
*/
AuthStatus["WAITING_FOR_POPUP"] = "WAITING_FOR_POPUP";
/**
* Emitted when the SAML popup is closed without authentication
*/
AuthStatus["SAML_POPUP_CLOSED_NO_AUTH"] = "SAML_POPUP_CLOSED_NO_AUTH";
})(AuthStatus || (AuthStatus = {}));
/**
* Events which can be triggered on the emitter returned from {@link init}.
* @group Authentication / Init
*/
export var AuthEvent;
(function (AuthEvent) {
/**
* Manually trigger the SSO popup. This is useful when
* authStatus is SAMLRedirect/OIDCRedirect and inPopup is set to true
*/
AuthEvent["TRIGGER_SSO_POPUP"] = "TRIGGER_SSO_POPUP";
})(AuthEvent || (AuthEvent = {}));
let authEE;
/**
*
*/
export function getAuthEE() {
return authEE;
}
/**
*
* @param eventEmitter
*/
export function setAuthEE(eventEmitter) {
authEE = eventEmitter;
}
/**
*
*/
export function notifyAuthSDKSuccess() {
if (!authEE) {
logger.error(ERROR_MESSAGE.SDK_NOT_INITIALIZED);
return;
}
authEE.emit(AuthStatus.SDK_SUCCESS);
}
/**
*
*/
export async function notifyAuthSuccess() {
if (!authEE) {
logger.error(ERROR_MESSAGE.SDK_NOT_INITIALIZED);
return;
}
try {
getPreauthInfo();
const sessionInfo = await getSessionInfo();
authEE.emit(AuthStatus.SUCCESS, sessionInfo);
}
catch (e) {
logger.error(ERROR_MESSAGE.SESSION_INFO_FAILED);
}
}
/**
*
* @param failureType
*/
export function notifyAuthFailure(failureType) {
if (!authEE) {
logger.error(ERROR_MESSAGE.SDK_NOT_INITIALIZED);
return;
}
authEE.emit(AuthStatus.FAILURE, failureType);
}
/**
*
*/
export function notifyLogout() {
if (!authEE) {
logger.error(ERROR_MESSAGE.SDK_NOT_INITIALIZED);
return;
}
authEE.emit(AuthStatus.LOGOUT);
}
/**
* Check if we are logged into the ThoughtSpot cluster
* @param thoughtSpotHost The ThoughtSpot cluster hostname or IP
*/
async function isLoggedIn(thoughtSpotHost) {
try {
const response = await isActiveService(thoughtSpotHost);
return response;
}
catch (e) {
return false;
}
}
/**
* Services to be called after the login is successful,
* This should be called after the cookie is set for cookie auth or
* after the token is set for cookieless.
* @return {Promise<void>}
* @example
* ```js
* await postLoginService();
* ```
* @version SDK: 1.28.3 | ThoughtSpot: *
*/
export async function postLoginService() {
try {
getPreauthInfo();
const sessionInfo = await getSessionInfo();
releaseVersion = sessionInfo.releaseVersion;
const embedConfig = getEmbedConfig();
if (!embedConfig.disableSDKTracking) {
initMixpanel(sessionInfo);
}
}
catch (e) {
logger.error('Post login services failed.', e.message, e);
}
}
/**
* Return releaseVersion if available
*/
export function getReleaseVersion() {
return releaseVersion;
}
/**
* Check if we are stuck at the SSO redirect URL
*/
function isAtSSORedirectUrl() {
return window.location.href.indexOf(getSSOMarker(SSO_REDIRECTION_MARKER_GUID)) >= 0;
}
/**
* Remove the SSO redirect URL marker
*/
function removeSSORedirectUrlMarker() {
// Note (sunny): This will leave a # around even if it was not in the URL
// to begin with. Trying to remove the hash by changing window.location will
// reload the page which we don't want. We'll live with adding an
// unnecessary hash to the parent page URL until we find any use case where
// that creates an issue.
// Replace any occurences of ?ssoMarker=guid or &ssoMarker=guid.
let updatedHash = window.location.hash.replace(`?${getSSOMarker(SSO_REDIRECTION_MARKER_GUID)}`, '');
updatedHash = updatedHash.replace(`&${getSSOMarker(SSO_REDIRECTION_MARKER_GUID)}`, '');
window.location.hash = updatedHash;
}
/**
* Perform token based authentication
* @param embedConfig The embed configuration
*/
export const doTokenAuth = async (embedConfig) => {
const { thoughtSpotHost, username, authEndpoint, getAuthToken, } = embedConfig;
if (!authEndpoint && !getAuthToken) {
throw new Error('Either auth endpoint or getAuthToken function must be provided');
}
loggedInStatus = await isLoggedIn(thoughtSpotHost);
if (!loggedInStatus) {
let authToken;
try {
authToken = await getAuthenticationToken(embedConfig);
}
catch (e) {
loggedInStatus = false;
throw e;
}
let resp;
try {
resp = await fetchAuthPostService(thoughtSpotHost, username, authToken);
}
catch (e) {
resp = await fetchAuthService(thoughtSpotHost, username, authToken);
}
// token login issues a 302 when successful
loggedInStatus = resp.ok || resp.type === 'opaqueredirect';
if (loggedInStatus && embedConfig.detectCookieAccessSlow) {
// When 3rd party cookie access is blocked, this will fail because
// cookies will not be sent with the call.
loggedInStatus = await isLoggedIn(thoughtSpotHost);
}
}
return loggedInStatus;
};
/**
* Validate embedConfig parameters required for cookielessTokenAuth
* @param embedConfig The embed configuration
*/
export const doCookielessTokenAuth = async (embedConfig) => {
const { authEndpoint, getAuthToken } = embedConfig;
if (!authEndpoint && !getAuthToken) {
throw new Error('Either auth endpoint or getAuthToken function must be provided');
}
let authSuccess = false;
try {
const authToken = await getAuthenticationToken(embedConfig);
if (authToken) {
authSuccess = true;
}
}
catch {
authSuccess = false;
}
return authSuccess;
};
/**
* Perform basic authentication to the ThoughtSpot cluster using the cluster
* credentials.
*
* Warning: This feature is primarily intended for developer testing. It is
* strongly advised not to use this authentication method in production.
* @param embedConfig The embed configuration
*/
export const doBasicAuth = async (embedConfig) => {
const { thoughtSpotHost, username, password } = embedConfig;
const loggedIn = await isLoggedIn(thoughtSpotHost);
if (!loggedIn) {
const response = await fetchBasicAuthService(thoughtSpotHost, username, password);
loggedInStatus = response.ok;
if (embedConfig.detectCookieAccessSlow) {
loggedInStatus = await isLoggedIn(thoughtSpotHost);
}
}
else {
loggedInStatus = true;
}
return loggedInStatus;
};
/**
*
* @param ssoURL
* @param triggerContainer
* @param triggerText
*/
async function samlPopupFlow(ssoURL, triggerContainer, triggerText) {
let popupClosedCheck;
const openPopup = () => {
if (samlAuthWindow === null || samlAuthWindow.closed) {
samlAuthWindow = window.open(ssoURL, '_blank', 'location=no,height=570,width=520,scrollbars=yes,status=yes');
if (samlAuthWindow) {
popupClosedCheck = setInterval(() => {
if (samlAuthWindow.closed) {
clearInterval(popupClosedCheck);
if (samlCompletionPromise && !samlCompletionResolved) {
authEE === null || authEE === void 0 ? void 0 : authEE.emit(AuthStatus.SAML_POPUP_CLOSED_NO_AUTH);
}
}
}, 500);
}
}
else {
samlAuthWindow.focus();
}
};
let samlCompletionResolved = false;
authEE === null || authEE === void 0 ? void 0 : authEE.emit(AuthStatus.WAITING_FOR_POPUP);
const containerEl = getDOMNode(triggerContainer);
if (containerEl) {
containerEl.innerHTML = '<button id="ts-auth-btn" class="ts-auth-btn" style="margin: auto;"></button>';
const authElem = document.getElementById('ts-auth-btn');
authElem.textContent = triggerText;
authElem.addEventListener('click', openPopup, { once: true });
}
samlCompletionPromise = samlCompletionPromise || new Promise((resolve, reject) => {
window.addEventListener('message', (e) => {
if (e.data.type === EmbedEvent.SAMLComplete) {
samlCompletionResolved = true;
if (popupClosedCheck) {
clearInterval(popupClosedCheck);
}
e.source.close();
resolve();
}
});
});
authEE === null || authEE === void 0 ? void 0 : authEE.once(AuthEvent.TRIGGER_SSO_POPUP, openPopup);
return samlCompletionPromise;
}
/**
* Perform SAML authentication
* @param embedConfig The embed configuration
* @param ssoEndPoint
*/
const doSSOAuth = async (embedConfig, ssoEndPoint) => {
const { thoughtSpotHost } = embedConfig;
const loggedIn = await isLoggedIn(thoughtSpotHost);
if (loggedIn) {
if (isAtSSORedirectUrl()) {
removeSSORedirectUrlMarker();
}
loggedInStatus = true;
return;
}
// we have already tried authentication and it did not succeed, restore
// the current URL to the original one and invoke the callback.
if (isAtSSORedirectUrl()) {
removeSSORedirectUrlMarker();
loggedInStatus = false;
return;
}
const ssoURL = `${thoughtSpotHost}${ssoEndPoint}`;
if (embedConfig.inPopup) {
await samlPopupFlow(ssoURL, embedConfig.authTriggerContainer, embedConfig.authTriggerText);
loggedInStatus = await isLoggedIn(thoughtSpotHost);
return;
}
window.location.href = ssoURL;
};
export const doSamlAuth = async (embedConfig) => {
const { thoughtSpotHost } = embedConfig;
// redirect for SSO, when the SSO authentication is done, this page will be
// loaded again and the same JS will execute again.
const ssoRedirectUrl = embedConfig.inPopup
? `${thoughtSpotHost}/v2/#/embed/saml-complete`
: getRedirectUrl(window.location.href, SSO_REDIRECTION_MARKER_GUID, embedConfig.redirectPath);
// bring back the page to the same URL
const ssoEndPoint = `${EndPoints.SAML_LOGIN_TEMPLATE(encodeURIComponent(ssoRedirectUrl))}`;
await doSSOAuth(embedConfig, ssoEndPoint);
return loggedInStatus;
};
export const doOIDCAuth = async (embedConfig) => {
const { thoughtSpotHost } = embedConfig;
// redirect for SSO, when the SSO authentication is done, this page will be
// loaded again and the same JS will execute again.
const ssoRedirectUrl = embedConfig.noRedirect || embedConfig.inPopup
? `${thoughtSpotHost}/v2/#/embed/saml-complete`
: getRedirectUrl(window.location.href, SSO_REDIRECTION_MARKER_GUID, embedConfig.redirectPath);
// bring back the page to the same URL
const baseEndpoint = `${EndPoints.OIDC_LOGIN_TEMPLATE(encodeURIComponent(ssoRedirectUrl))}`;
const ssoEndPoint = `${baseEndpoint}${baseEndpoint.includes('?') ? '&' : '?'}forceSAMLAutoRedirect=true`;
await doSSOAuth(embedConfig, ssoEndPoint);
return loggedInStatus;
};
export const logout = async (embedConfig) => {
const { thoughtSpotHost } = embedConfig;
await fetchLogoutService(thoughtSpotHost);
resetAllCachedServices();
const thoughtspotIframes = document.querySelectorAll("[data-ts-iframe='true']");
if (thoughtspotIframes === null || thoughtspotIframes === void 0 ? void 0 : thoughtspotIframes.length) {
thoughtspotIframes.forEach((el) => {
el.parentElement.innerHTML = embedConfig.loginFailedMessage;
});
}
loggedInStatus = false;
return loggedInStatus;
};
/**
* Perform authentication on the ThoughtSpot cluster
* @param embedConfig The embed configuration
*/
export const authenticate = async (embedConfig) => {
const { authType } = embedConfig;
switch (authType) {
case AuthType.SSO:
case AuthType.SAMLRedirect:
case AuthType.SAML:
return doSamlAuth(embedConfig);
case AuthType.OIDC:
case AuthType.OIDCRedirect:
return doOIDCAuth(embedConfig);
case AuthType.AuthServer:
case AuthType.TrustedAuthToken:
return doTokenAuth(embedConfig);
case AuthType.TrustedAuthTokenCookieless:
return doCookielessTokenAuth(embedConfig);
case AuthType.Basic:
return doBasicAuth(embedConfig);
default:
return Promise.resolve(true);
}
};
/**
* Check if we are authenticated to the ThoughtSpot cluster
*/
export const isAuthenticated = () => loggedInStatus;
//# sourceMappingURL=auth.js.map