UNPKG

@thoughtspot/visual-embed-sdk

Version:
438 lines 15.4 kB
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